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); });