Some checks failed
Deploy Landing Page to Storj / deploy (push) Failing after 3s
- Add __tests__/e2e-wasm.test.ts: full pipeline test - Parse Counter speck → generate WAT → assemble with wabt → instantiate → run - Verify init, action calls, guard pass/fail, state mutations - TigerBeetle WAT validation test (wabt parse + validate + toBinary) - Fix wasm.ts bugs found by E2E: - Guard condition inverted: added i32.eqz to negate comparison - Missing local declarations: locals now inserted before body - Uncompiled guards left stack values: now emit comments only (no i32.const 1) - Install wabt npm package for WAT→WASM assembly in tests - 41/41 tests passing (39 original + 2 E2E) Milestone: Speckl produces runnable WebAssembly from specs.
203 lines
7 KiB
TypeScript
203 lines
7 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { parseSpeckContent } from '../src/parser.js';
|
|
import { generateWASM } from '../src/generators/wasm.js';
|
|
import { createHostImports, bindMemory, readScalarState } from '../src/runtime/wasm-host.js';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import os from 'os';
|
|
|
|
// Dynamic import wabt since it's a CommonJS module
|
|
async function loadWabt() {
|
|
const wabt = await import('wabt');
|
|
return wabt.default();
|
|
}
|
|
|
|
describe('SpeckDL WASM E2E', () => {
|
|
it('should compile Counter speck to WAT, assemble to WASM, and run in Node.js', async () => {
|
|
// A minimal scalar-only speck that we can fully compile and run
|
|
// NOTE: Parser requires braces on the SAME line as block keywords
|
|
const speckSource = `speck Counter {
|
|
author: "Speckl Compiler v0.3"
|
|
source: document ref "WASM-E2E"
|
|
review: automated
|
|
|
|
state: {
|
|
count: Nat := 0
|
|
maxCount: Nat := 10
|
|
}
|
|
|
|
init: {
|
|
count := 0
|
|
maxCount := 10
|
|
}
|
|
|
|
action Increment() {
|
|
require count < maxCount
|
|
count := count + 1
|
|
}
|
|
|
|
action Decrement() {
|
|
require count > 0
|
|
count := count - 1
|
|
}
|
|
|
|
action Reset() {
|
|
count := 0
|
|
}
|
|
|
|
action GetCount() {
|
|
return count
|
|
}
|
|
}`;
|
|
|
|
// 1. Parse
|
|
const ast = parseSpeckContent(speckSource);
|
|
expect(ast.specks).toHaveLength(1);
|
|
expect(ast.specks[0].name).toBe('Counter');
|
|
|
|
// 2. Generate WAT to temp dir
|
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'speckl-wasm-e2e-'));
|
|
generateWASM(ast, tmpDir);
|
|
|
|
const watPath = path.join(tmpDir, 'Counter.wat');
|
|
expect(fs.existsSync(watPath)).toBe(true);
|
|
|
|
const watContent = fs.readFileSync(watPath, 'utf-8');
|
|
expect(watContent).toContain('(module');
|
|
expect(watContent).toContain('(export "init")');
|
|
expect(watContent).toContain('(export "Increment")');
|
|
expect(watContent).toContain('(export "Decrement")');
|
|
expect(watContent).toContain('(export "Reset")');
|
|
expect(watContent).toContain('(export "GetCount")');
|
|
expect(watContent).toContain('(export "getState")');
|
|
expect(watContent).toContain('(export "memory")');
|
|
|
|
// 3. Assemble WAT → WASM using wabt
|
|
const wabtModule = await loadWabt();
|
|
const wasmModule = wabtModule.parseWat('Counter.wat', watContent);
|
|
wasmModule.resolveNames();
|
|
wasmModule.validate({ features: {} });
|
|
const binaryOutput = wasmModule.toBinary({ log: false, write_debug_names: false });
|
|
const wasmBytes = binaryOutput.buffer;
|
|
|
|
expect(wasmBytes.length).toBeGreaterThan(0);
|
|
expect(wasmBytes.length).toBeGreaterThan(50); // Not just empty header
|
|
|
|
// 4. Instantiate with host runtime
|
|
const hostState = {
|
|
maps: new Map(),
|
|
sets: new Map(),
|
|
nextHandle: 1,
|
|
memory: null as WebAssembly.Memory | null,
|
|
};
|
|
const imports = { speckl: createHostImports(hostState) };
|
|
|
|
const wasm = await WebAssembly.instantiate(wasmBytes, imports);
|
|
const memory = wasm.instance.exports.memory as WebAssembly.Memory;
|
|
bindMemory(hostState, memory);
|
|
|
|
// 5. Verify exports
|
|
expect(typeof wasm.instance.exports.init).toBe('function');
|
|
expect(typeof wasm.instance.exports.Increment).toBe('function');
|
|
expect(typeof wasm.instance.exports.Decrement).toBe('function');
|
|
expect(typeof wasm.instance.exports.Reset).toBe('function');
|
|
expect(typeof wasm.instance.exports.GetCount).toBe('function');
|
|
expect(typeof wasm.instance.exports.getState).toBe('function');
|
|
expect(memory).toBeInstanceOf(WebAssembly.Memory);
|
|
|
|
// 6. Run init
|
|
(wasm.instance.exports.init as Function)();
|
|
|
|
// 7. Verify initial state via getState
|
|
const stateResult = (wasm.instance.exports.getState as Function)();
|
|
// getState returns i32 i32 (ptr, len)
|
|
expect(stateResult).toBeDefined();
|
|
|
|
// Read memory directly to verify state
|
|
const scalarState = readScalarState(memory, ['count', 'maxCount']);
|
|
expect(scalarState.count).toBe(0);
|
|
expect(scalarState.maxCount).toBe(10);
|
|
|
|
// 8. Run Increment 3 times, calling getState after each to sync memory
|
|
for (let i = 0; i < 3; i++) {
|
|
const ret = (wasm.instance.exports.Increment as Function)();
|
|
expect(ret).toBe(1); // Success
|
|
(wasm.instance.exports.getState as Function)(); // Sync globals to memory
|
|
}
|
|
|
|
const afterIncrement = readScalarState(memory, ['count', 'maxCount']);
|
|
expect(afterIncrement.count).toBe(3);
|
|
expect(afterIncrement.maxCount).toBe(10);
|
|
|
|
// 9. Run Decrement once
|
|
const decRet = (wasm.instance.exports.Decrement as Function)();
|
|
expect(decRet).toBe(1); // Success
|
|
(wasm.instance.exports.getState as Function)();
|
|
|
|
const afterDecrement = readScalarState(memory, ['count', 'maxCount']);
|
|
expect(afterDecrement.count).toBe(2);
|
|
|
|
// 10. Run GetCount — should return current count (global, not memory)
|
|
const getRet = (wasm.instance.exports.GetCount as Function)();
|
|
expect(getRet).toBe(2);
|
|
|
|
// 11. Run Reset
|
|
const resetRet = (wasm.instance.exports.Reset as Function)();
|
|
expect(resetRet).toBe(1); // Success
|
|
(wasm.instance.exports.getState as Function)();
|
|
|
|
const afterReset = readScalarState(memory, ['count', 'maxCount']);
|
|
expect(afterReset.count).toBe(0);
|
|
|
|
// 12. Guard test: Decrement at 0 should fail
|
|
const guardFail = (wasm.instance.exports.Decrement as Function)();
|
|
expect(guardFail).toBe(0); // Guard failed → return 0
|
|
|
|
// 13. Guard test: Increment at maxCount should fail
|
|
// First increment to max
|
|
for (let i = 0; i < 10; i++) {
|
|
(wasm.instance.exports.Increment as Function)();
|
|
}
|
|
(wasm.instance.exports.getState as Function)();
|
|
const atMax = readScalarState(memory, ['count', 'maxCount']);
|
|
expect(atMax.count).toBe(10);
|
|
|
|
const maxGuardFail = (wasm.instance.exports.Increment as Function)();
|
|
expect(maxGuardFail).toBe(0); // Guard failed → return 0
|
|
|
|
// Cleanup
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
}, 30000); // 30s timeout for wabt compilation
|
|
|
|
it('should compile TigerBeetle speck to valid WAT that wabt accepts', async () => {
|
|
// Load the full TigerBeetle speck from file
|
|
const speckPath = path.join(__dirname, 'tigerbeetle.speck');
|
|
const speckSource = fs.readFileSync(speckPath, 'utf-8');
|
|
|
|
const ast = parseSpeckContent(speckSource);
|
|
expect(ast.specks).toHaveLength(1);
|
|
expect(ast.specks[0].name).toBe('TigerBeetle');
|
|
|
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'speckl-wasm-e2e-tb-'));
|
|
generateWASM(ast, tmpDir);
|
|
|
|
const watPath = path.join(tmpDir, 'TigerBeetle.wat');
|
|
expect(fs.existsSync(watPath)).toBe(true);
|
|
|
|
const watContent = fs.readFileSync(watPath, 'utf-8');
|
|
|
|
// Validate with wabt
|
|
const wabtModule = await loadWabt();
|
|
const wasmModule = wabtModule.parseWat('TigerBeetle.wat', watContent);
|
|
|
|
// These should not throw
|
|
wasmModule.resolveNames();
|
|
wasmModule.validate({ features: {} });
|
|
const binaryOutput = wasmModule.toBinary({ log: false, write_debug_names: false });
|
|
|
|
expect(binaryOutput.buffer.length).toBeGreaterThan(100);
|
|
|
|
// Cleanup
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
}, 30000);
|
|
});
|