import { describe, it, expect } from 'vitest'; import { parse } from './parser'; import type { BinaryExpr, ExprStmt, FnDecl, LetStmt } from './ast'; describe('parser — declarations', () => { it('parses a let binding with initializer', () => { const { program, errors } = parse('let x = 1 + 2;'); expect(errors).toEqual([]); expect(program.stmts).toHaveLength(1); const let_ = program.stmts[0] as LetStmt; expect(let_.kind).toBe('Let'); expect(let_.name).toBe('x'); expect(let_.init?.kind).toBe('Binary'); }); it('parses a const binding', () => { const { program, errors } = parse('const PI = 3.14;'); expect(errors).toEqual([]); expect(program.stmts[0]).toMatchObject({ kind: 'Const', name: 'PI' }); }); it('parses fn declarations with parameters', () => { const { program, errors } = parse('fn process(order, user) { order.total }'); expect(errors).toEqual([]); const fn = program.stmts[0] as FnDecl; expect(fn.kind).toBe('FnDecl'); expect(fn.name).toBe('process'); expect(fn.params.map((p) => p.name)).toEqual(['order', 'user']); expect(fn.body.stmts).toHaveLength(1); }); }); describe('parser — expressions', () => { it('respects binary precedence (* before +)', () => { const { program } = parse('let a = 1 + 2 * 3;'); const e = (program.stmts[0] as LetStmt).init as BinaryExpr; expect(e.kind).toBe('Binary'); expect(e.op).toBe('+'); const right = e.right as BinaryExpr; expect(right.op).toBe('*'); }); it('parses method chains (member + call + index)', () => { const { program, errors } = parse('let x = ctx.request.body["k"];'); expect(errors).toEqual([]); const init = (program.stmts[0] as LetStmt).init!; expect(init.kind).toBe('Index'); }); it('parses log::info("hi") as Call(Member(Ident(log), "info"), ["hi"])', () => { const { program, errors } = parse('log::info("hi");'); expect(errors).toEqual([]); const stmt = program.stmts[0] as ExprStmt; expect(stmt.expr.kind).toBe('Call'); }); it('parses object-map literal #{} with keys', () => { const { program, errors } = parse('let o = #{ a: 1, b: 2 };'); expect(errors).toEqual([]); const init = (program.stmts[0] as LetStmt).init!; expect(init.kind).toBe('ObjectMap'); if (init.kind === 'ObjectMap') { expect(init.entries.map((e) => e.key)).toEqual(['a', 'b']); } }); it('parses array literals', () => { const { program, errors } = parse('let xs = [1, 2, 3];'); expect(errors).toEqual([]); expect((program.stmts[0] as LetStmt).init!.kind).toBe('Array'); }); it('parses if-as-expression for let RHS', () => { const { program, errors } = parse('let x = if true { 1 } else { 2 };'); expect(errors).toEqual([]); expect((program.stmts[0] as LetStmt).init!.kind).toBe('IfExpr'); }); }); describe('parser — control flow', () => { it('parses while, for, loop', () => { const { errors: e1 } = parse('while true { break; }'); const { errors: e2 } = parse('for x in [1, 2] { x }'); const { errors: e3 } = parse('loop { break; }'); expect(e1).toEqual([]); expect(e2).toEqual([]); expect(e3).toEqual([]); }); it('parses if / else if / else chains', () => { const { program, errors } = parse(` if a { 1 } else if b { 2 } else { 3 } `); expect(errors).toEqual([]); const stmt = program.stmts[0] as ExprStmt; expect(stmt.expr.kind).toBe('IfExpr'); }); it('parses try / catch with binding', () => { const { errors } = parse('try { foo(); } catch (e) { log::error(e); }'); expect(errors).toEqual([]); }); }); describe('parser — error tolerance', () => { it('is lenient about missing semicolons between statements', () => { // The parser accepts implicit statement separation so completions // remain useful while the user is still typing. Both bindings // should land in the program regardless of the missing `;`. const { program } = parse('let x = 1 let y = 2;'); const names = program.stmts.flatMap((s) => (s.kind === 'Let' ? [s.name] : [])); expect(names).toContain('x'); expect(names).toContain('y'); }); it('does not loop forever on garbage', () => { const { errors } = parse('@@@ ### }}}'); expect(errors.length).toBeGreaterThan(0); }); it('recovers after a bad statement and parses the next one', () => { const { program } = parse('let = ; let y = 2;'); const y = program.stmts.find((s) => s.kind === 'Let' && s.name === 'y'); expect(y).toBeDefined(); }); });