import { describe, it, expect } from 'vitest'; import { parseSpeckContent, parseSpeckFile } from '../src/parser.js'; import { generateWASM } from '../src/generators/wasm.js'; describe('SpeckDL Parser', () => { describe('parseSpeckContent', () => { it('should parse a minimal speck definition with name', () => { const content = `speck Minimal { input: Nat output: Bool }`; const ast = parseSpeckContent(content); expect(ast.specks).toHaveLength(1); expect(ast.specks[0].name).toBe('Minimal'); }); it('should parse multiple speck definitions in one file', () => { const content = `speck First { input: Nat output: Bool } speck Second { input: String output: Nat }`; const ast = parseSpeckContent(content); expect(ast.specks).toHaveLength(2); expect(ast.specks[0].name).toBe('First'); expect(ast.specks[1].name).toBe('Second'); }); it('should return empty specks array for empty content', () => { const ast = parseSpeckContent(''); expect(ast.specks).toHaveLength(0); }); it('should return empty specks array for comment-only content', () => { const ast = parseSpeckContent('// This is a comment\n/* block comment */\n'); expect(ast.specks).toHaveLength(0); }); it('should skip comments inside speck blocks', () => { const content = `speck WithComments { // This is a comment input: Nat /* block comment */ output: String }`; const ast = parseSpeckContent(content); expect(ast.specks).toHaveLength(1); expect(ast.specks[0].members).toHaveLength(2); }); it('should parse speck name with hyphenated names', () => { const content = `speck My-Service-Handler { input: Nat output: Bool }`; const ast = parseSpeckContent(content); expect(ast.specks).toHaveLength(1); expect(ast.specks[0].name).toBe('My-Service-Handler'); }); }); describe('member parsing - inputs and outputs', () => { it('should parse input with primitive type', () => { const content = `speck Test { input: Nat }`; const ast = parseSpeckContent(content); const members = ast.specks[0].members; expect(members).toHaveLength(1); expect(members[0].type).toBe('input'); if (members[0].type === 'input') { expect(members[0].typeExpr.type).toBe('primitive'); expect(members[0].typeExpr.name).toBe('Nat'); } }); it('should parse output with primitive type', () => { const content = `speck Test { output: Bool }`; const ast = parseSpeckContent(content); expect(ast.specks[0].members[0].type).toBe('output'); }); it('should parse input with record type', () => { const content = `speck Test { input: { name: String, age: Nat } }`; const ast = parseSpeckContent(content); const input = ast.specks[0].members[0]; expect(input.type).toBe('input'); if (input.type === 'input') { expect(input.typeExpr.type).toBe('record'); expect(input.typeExpr.fields).toHaveLength(2); expect(input.typeExpr.fields![0].name).toBe('name'); expect(input.typeExpr.fields![1].name).toBe('age'); } }); it('should parse input with list type', () => { const content = `speck Test { input: [Nat] }`; const ast = parseSpeckContent(content); const input = ast.specks[0].members[0]; if (input.type === 'input') { expect(input.typeExpr.type).toBe('list'); expect(input.typeExpr.elementType?.type).toBe('primitive'); expect(input.typeExpr.elementType?.name).toBe('Nat'); } }); it('should parse all supported primitive types', () => { const primitives = ['Nat', 'Int', 'Real', 'Bool', 'String', 'Bytes']; for (const prim of primitives) { const content = `speck Test { input: ${prim} }`; const ast = parseSpeckContent(content); const input = ast.specks[0].members[0]; if (input.type === 'input') { expect(input.typeExpr.name).toBe(prim); } } }); }); describe('member parsing - constraints', () => { it('should parse simple constraints', () => { const content = `speck Test { input: Nat constraint: x > 0 constraint: y <= 100 }`; const ast = parseSpeckContent(content); const constraints = ast.specks[0].members.filter(m => m.type === 'constraint'); expect(constraints).toHaveLength(2); if (constraints[0].type === 'constraint') { expect(constraints[0].expr).toBe('x > 0'); } if (constraints[1].type === 'constraint') { expect(constraints[1].expr).toBe('y <= 100'); } }); it('should parse complex constraint expressions', () => { const content = `speck Test { input: Nat constraint: implies(safe, volume >= minDose / drugConcentration) }`; const ast = parseSpeckContent(content); const constraint = ast.specks[0].members.find(m => m.type === 'constraint'); expect(constraint).toBeDefined(); if (constraint?.type === 'constraint') { expect(constraint.expr).toContain('implies'); expect(constraint.expr).toContain('volume'); } }); }); describe('member parsing - verify', () => { it('should parse verify statements', () => { const content = `speck Test { input: Nat verify: always(implies(attempt > 1, delay > 0)) }`; const ast = parseSpeckContent(content); const verify = ast.specks[0].members.find(m => m.type === 'verify'); expect(verify).toBeDefined(); if (verify?.type === 'verify') { expect(verify.temporalExpr).toContain('always'); expect(verify.temporalExpr).toContain('implies'); } }); }); describe('member parsing - review', () => { it('should parse manual review', () => { const content = `speck Test { input: Nat review: manual }`; const ast = parseSpeckContent(content); const review = ast.specks[0].members.find(m => m.type === 'review'); expect(review).toBeDefined(); if (review?.type === 'review') { expect(review.kind).toBe('manual'); } }); it('should parse auto review', () => { const content = `speck Test { input: Nat review: auto }`; const ast = parseSpeckContent(content); const review = ast.specks[0].members.find(m => m.type === 'review'); if (review?.type === 'review') { expect(review.kind).toBe('auto'); } }); it('should parse hybrid review', () => { const content = `speck Test { input: Nat review: hybrid }`; const ast = parseSpeckContent(content); const review = ast.specks[0].members.find(m => m.type === 'review'); if (review?.type === 'review') { expect(review.kind).toBe('hybrid'); } }); }); describe('member parsing - derives and satisfies', () => { it('should parse derives from with via rationale', () => { const content = `speck Test { input: Nat derives from ParentSpeck via "specialization" }`; const ast = parseSpeckContent(content); const derives = ast.specks[0].members.find(m => m.type === 'derives'); expect(derives).toBeDefined(); if (derives?.type === 'derives') { expect(derives.from).toBe('ParentSpeck'); expect(derives.via).toBe('specialization'); } }); it('should parse derives from without via', () => { const content = `speck Test { input: Nat derives from BaseComponent }`; const ast = parseSpeckContent(content); const derives = ast.specks[0].members.find(m => m.type === 'derives'); if (derives?.type === 'derives') { expect(derives.from).toBe('BaseComponent'); expect(derives.via).toBeUndefined(); } }); it('should parse satisfies with clause', () => { const content = `speck Test { input: Nat satisfies REQ-01 clause "3.1.2" }`; const ast = parseSpeckContent(content); const satisfies = ast.specks[0].members.find(m => m.type === 'satisfies'); expect(satisfies).toBeDefined(); if (satisfies?.type === 'satisfies') { expect(satisfies.requirement).toBe('REQ-01'); expect(satisfies.clause).toBe('3.1.2'); } }); it('should parse satisfies without clause', () => { const content = `speck Test { input: Nat satisfies RESILIENCE-REQ-02 }`; const ast = parseSpeckContent(content); const satisfies = ast.specks[0].members.find(m => m.type === 'satisfies'); if (satisfies?.type === 'satisfies') { expect(satisfies.requirement).toBe('RESILIENCE-REQ-02'); expect(satisfies.clause).toBeUndefined(); } }); }); describe('member parsing - author', () => { it('should parse author with name and email', () => { const content = `speck Test { input: Nat author: "Scott Scoble" <"scott@scoble.me"> }`; const ast = parseSpeckContent(content); const author = ast.specks[0].members.find(m => m.type === 'author'); expect(author).toBeDefined(); if (author?.type === 'author') { expect(author.name).toBe('Scott Scoble'); expect(author.email).toBe('scott@scoble.me'); } }); }); describe('member parsing - source', () => { it('should parse source with kind and ref', () => { const content = `speck Test { input: Nat source: architecture_review ref "AR-2024-0215" }`; const ast = parseSpeckContent(content); const source = ast.specks[0].members.find(m => m.type === 'source'); expect(source).toBeDefined(); if (source?.type === 'source') { expect(source.kind).toBe('architecture_review'); expect(source.ref).toBe('AR-2024-0215'); } }); it('should parse source without ref', () => { const content = `speck Test { input: Nat source: meeting }`; const ast = parseSpeckContent(content); const source = ast.specks[0].members.find(m => m.type === 'source'); if (source?.type === 'source') { expect(source.kind).toBe('meeting'); } }); }); describe('member parsing - events', () => { it('should parse event definitions', () => { const content = `speck Test { input: Nat event RetryAttempted { attemptNumber: Nat, delay: Real } }`; const ast = parseSpeckContent(content); const event = ast.specks[0].members.find(m => m.type === 'event'); expect(event).toBeDefined(); }); }); describe('member parsing - bom', () => { it('should parse bom declaration', () => { const content = `speck Test { input: Nat bom { compiler: "speckl-compile" version "0.2.0" license: "MIT" } }`; const ast = parseSpeckContent(content); const bom = ast.specks[0].members.find(m => m.type === 'bom'); expect(bom).toBeDefined(); }); }); describe('real-world examples', () => { it('should parse RetryHandler-style speck with full annotations', () => { const content = `speck RetryHandler { bom { compiler: "speckl-compile" version "0.2.0" solver: "z3" version "4.12.5" } provenance { design_decision: "ADR-0021" } derives from FaultHandler via "specialization" satisfies RESILIENCE-REQ-01 clause "3.1.2" author: "Scott Scoble" <"scott@scoble.me"> source: architecture_review ref "AR-2024-0215" review: manual input: { operation: Operation, maxRetries: Nat } output: { success: Bool } constraint: maxRetries <= 10 constraint: maxRetries >= 0 verify: always(implies(attempt > 1, delay > 0)) }`; const ast = parseSpeckContent(content); expect(ast.specks).toHaveLength(1); const speck = ast.specks[0]; expect(speck.name).toBe('RetryHandler'); // Should parse all member types const types = speck.members.map(m => m.type); expect(types).toContain('bom'); expect(types).toContain('provenance'); expect(types).toContain('derives'); expect(types).toContain('satisfies'); expect(types).toContain('author'); expect(types).toContain('source'); expect(types).toContain('review'); expect(types).toContain('input'); expect(types).toContain('output'); expect(types).toContain('constraint'); expect(types).toContain('verify'); }); }); describe('member parsing - state machine', () => { it('should parse state block with primitive variables', () => { const content = `speck ToggleSwitch { state: { on: Bool := false toggleCount: Nat := 0 } }`; const ast = parseSpeckContent(content); const state = ast.specks[0].members.find(m => m.type === 'state'); expect(state).toBeDefined(); if (state?.type === 'state') { expect(state.variables).toHaveLength(2); expect(state.variables[0].name).toBe('on'); expect(state.variables[0].typeExpr.type).toBe('primitive'); expect(state.variables[0].typeExpr.name).toBe('Bool'); expect(state.variables[0].defaultInit).toBe('false'); expect(state.variables[1].name).toBe('toggleCount'); expect(state.variables[1].defaultInit).toBe('0'); } }); it('should parse state block with Set and Map types', () => { const content = `speck AccountLedger { state: { balances: Map participants: Set } }`; const ast = parseSpeckContent(content); const state = ast.specks[0].members.find(m => m.type === 'state'); if (state?.type === 'state') { expect(state.variables).toHaveLength(2); expect(state.variables[0].typeExpr.type).toBe('map'); expect(state.variables[1].typeExpr.type).toBe('set'); } }); it('should parse init block', () => { const content = `speck Test { state: { x: Nat } init: { x := 0 } }`; const ast = parseSpeckContent(content); const init = ast.specks[0].members.find(m => m.type === 'init'); expect(init).toBeDefined(); if (init?.type === 'init') { expect(init.assignments).toHaveLength(1); expect(init.assignments[0].name).toBe('x'); expect(init.assignments[0].expr).toBe('0'); } }); it('should parse action without parameters', () => { const content = `speck Test { action Toggle() { x := x + 1 } }`; const ast = parseSpeckContent(content); const action = ast.specks[0].members.find(m => m.type === 'action'); expect(action).toBeDefined(); if (action?.type === 'action') { expect(action.name).toBe('Toggle'); expect(action.params).toHaveLength(0); } }); it('should parse action with parameters', () => { const content = `speck Test { action Transfer(from: Nat, to: Nat, amount: Real) { require from in balances balances[from] := balances[from] - amount } }`; const ast = parseSpeckContent(content); const action = ast.specks[0].members.find(m => m.type === 'action'); if (action?.type === 'action') { expect(action.name).toBe('Transfer'); expect(action.params).toHaveLength(3); expect(action.params[0].name).toBe('from'); expect(action.params[0].type.type).toBe('primitive'); expect(action.params[0].type.name).toBe('Nat'); } }); it('should parse full TigerBeetle speck from file', () => { const ast = parseSpeckFile('./__tests__/tigerbeetle.speck'); expect(ast.specks).toHaveLength(1); const speck = ast.specks[0]; expect(speck.name).toBe('TigerBeetle'); const state = speck.members.find(m => m.type === 'state'); const init = speck.members.find(m => m.type === 'init'); const actions = speck.members.filter(m => m.type === 'action'); const events = speck.members.filter(m => m.type === 'event'); const constraints = speck.members.filter(m => m.type === 'constraint'); const verifies = speck.members.filter(m => m.type === 'verify'); expect(state).toBeDefined(); expect(init).toBeDefined(); expect(actions).toHaveLength(4); expect(events).toHaveLength(3); expect(constraints).toHaveLength(2); expect(verifies).toHaveLength(2); if (state?.type === 'state') { expect(state.variables).toHaveLength(4); expect(state.variables[0].name).toBe('nextId'); expect(state.variables[1].typeExpr.type).toBe('map'); expect(state.variables[2].typeExpr.type).toBe('set'); } if (init?.type === 'init') { expect(init.assignments).toHaveLength(4); } const createAccount = actions.find(a => a.type === 'action' && a.name === 'CreateAccount'); expect(createAccount).toBeDefined(); if (createAccount?.type === 'action') { expect(createAccount.params).toHaveLength(1); expect(createAccount.statements.length).toBeGreaterThan(0); } const transfer = actions.find(a => a.type === 'action' && a.name === 'Transfer'); expect(transfer).toBeDefined(); if (transfer?.type === 'action') { expect(transfer.params).toHaveLength(3); } }); }); describe('WASM Codegen', () => { it('should generate WAT for TigerBeetle with all exports', () => { const ast = parseSpeckFile('./__tests__/tigerbeetle.speck'); const fs = require('fs'); const os = require('os'); const path = require('path'); const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'speckl-wasm-test-')); generateWASM(ast, tmpDir); const watPath = path.join(tmpDir, 'TigerBeetle.wat'); expect(fs.existsSync(watPath)).toBe(true); const wat = fs.readFileSync(watPath, 'utf-8'); // Module structure expect(wat).toContain('(module'); expect(wat).toContain('(memory (export "memory")'); // Host imports expect(wat).toContain('(import "speckl" "map_create"'); expect(wat).toContain('(import "speckl" "set_create"'); // State globals expect(wat).toContain('(global $nextId'); expect(wat).toContain('(global $accounts'); expect(wat).toContain('(global $transfers'); expect(wat).toContain('(global $ledgerOpen'); // init export expect(wat).toContain('(func $init (export "init")'); // Action exports expect(wat).toContain('(func $CreateAccount (export "CreateAccount")'); expect(wat).toContain('(func $Transfer (export "Transfer")'); expect(wat).toContain('(func $CloseAccount (export "CloseAccount")'); expect(wat).toContain('(func $CloseLedger (export "CloseLedger")'); // getState export expect(wat).toContain('(func $getState (export "getState")'); // Constraint stub expect(wat).toContain('(func $checkConstraints (export "checkConstraints")'); // Scalar init values expect(wat).toContain('(i32.const 1)'); // nextId default expect(wat).toContain('(call $map_create)'); expect(wat).toContain('(call $set_create)'); fs.rmSync(tmpDir, { recursive: true }); }); it('should emit scalar guard expressions as WASM comparison', () => { const ast = parseSpeckFile('./__tests__/tigerbeetle.speck'); const fs = require('fs'); const os = require('os'); const path = require('path'); const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'speckl-wasm-test-')); generateWASM(ast, tmpDir); const wat = fs.readFileSync(path.join(tmpDir, 'TigerBeetle.wat'), 'utf-8'); // ledgerOpen == true should compile to i32.eq expect(wat).toContain('(i32.eq (global.get $ledgerOpen) (i32.const 1))'); // nextId + 1 should compile to i32.add expect(wat).toContain('(i32.add (global.get $nextId) (i32.const 1))'); fs.rmSync(tmpDir, { recursive: true }); }); }); });