From dea69eef9c06cfc18a46d2e3f2b3dd4df0c47099 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Sat, 18 Oct 2025 15:34:13 -0500 Subject: [PATCH 01/21] fix: group compound assignment with other binary operators This was breaking operator precedence by ignoring compound assignment. --- apps/parser/generated/cst-types.ts | 1 - apps/parser/src/lexer.ts | 6 ++++- apps/parser/src/parser.ts | 27 +++++----------------- apps/parser/src/visitors/printers/json.ts | 10 ++++++-- apps/parser/src/visitors/printers/paren.ts | 5 +++- apps/parser/src/visitors/printers/xml.ts | 8 +++++-- 6 files changed, 29 insertions(+), 28 deletions(-) diff --git a/apps/parser/generated/cst-types.ts b/apps/parser/generated/cst-types.ts index 89c0d6c..ca719ae 100644 --- a/apps/parser/generated/cst-types.ts +++ b/apps/parser/generated/cst-types.ts @@ -78,7 +78,6 @@ export interface ExpressionCstNode extends CstNode { export type ExpressionCstChildren = { value: ValueCstNode[]; PostFix?: IToken[]; - CmpAsgn?: IToken[]; BinOp?: IToken[]; expression?: ExpressionCstNode[]; }; diff --git a/apps/parser/src/lexer.ts b/apps/parser/src/lexer.ts index c6fc219..afad766 100644 --- a/apps/parser/src/lexer.ts +++ b/apps/parser/src/lexer.ts @@ -103,7 +103,11 @@ export const binopTokens: TokenType[] = [ IN, ]; /* Compound Assignment Tokens */ -export const CmpAsgn: TokenType = createToken({ name: 'CmpAsgn', pattern: Lexer.NA }); +export const CmpAsgn: TokenType = createToken({ + name: 'CmpAsgn', + pattern: Lexer.NA, + categories: BinOp, +}); export const PL_EQU: TokenType = createToken({ name: 'PL_EQU', pattern: '+=', diff --git a/apps/parser/src/parser.ts b/apps/parser/src/parser.ts index faaeec6..631aa66 100644 --- a/apps/parser/src/parser.ts +++ b/apps/parser/src/parser.ts @@ -130,27 +130,12 @@ export class EncodeParser extends CstParser { private expression = this.RULE('expression', () => { this.SUBRULE(this.value); - this.OR([ - { - ALT: () => this.CONSUME(Tokens.PostFix), - }, - { - ALT: () => { - this.OPTION(() => { - this.OR2([ - { - ALT: () => this.CONSUME(Tokens.CmpAsgn), - }, - { - ALT: () => this.CONSUME(Tokens.BinOp), - }, - ]); - this.SUBRULE(this.expression); - // TODO reorder based on precedence - }); - }, - }, - ]); + this.OPTION(() => this.CONSUME(Tokens.PostFix)); + + this.OPTION1(() => { + this.CONSUME(Tokens.BinOp); // Compound assignment is categorized as a Binary operation by the lexer now + this.SUBRULE(this.expression); + }); }); private value = this.RULE('value', () => { diff --git a/apps/parser/src/visitors/printers/json.ts b/apps/parser/src/visitors/printers/json.ts index 822f323..7e14428 100644 --- a/apps/parser/src/visitors/printers/json.ts +++ b/apps/parser/src/visitors/printers/json.ts @@ -158,10 +158,16 @@ export class JSONPrinter extends BasePrinter implements ICstNodeVisitor 'tokenType' in e[0]) as IToken[] | undefined; + const op = expr.BinOp; + const pf = expr.PostFix; this.tree(`"expression":${this.pretty}{`, indent); - this.tree(`"op":${this.pretty}"${op?.[0].image ?? ''}"`, indent + 1, true); + if (op) { + this.tree(`"op":${this.pretty}"${op[0].image}"`, indent + 1, true); + } this.value(expr.value[0].children, indent + 1, !!expr.expression); + if (pf) { + this.tree(`"postfix":${this.pretty}"${pf[0].image}"`, indent + 1, true); + } if (expr.expression) { this.expression(expr.expression[0].children, indent + 1); } diff --git a/apps/parser/src/visitors/printers/paren.ts b/apps/parser/src/visitors/printers/paren.ts index 74883ea..c40dd05 100644 --- a/apps/parser/src/visitors/printers/paren.ts +++ b/apps/parser/src/visitors/printers/paren.ts @@ -136,13 +136,16 @@ export class ParenPrinter extends BasePrinter implements ICstNodeVisitor 'tokenType' in e[0]) as IToken[] | undefined; + const op = expr.BinOp; if (op) { this.tree(`(${op[0].image}`, indent); } else { this.tree('(', indent); } this.value(expr.value[0].children, indent + 2); + if (expr.PostFix) { + this.tree(expr.PostFix[0].image, indent); + } if (expr.expression) { this.expression(expr.expression[0].children, indent + 2); } diff --git a/apps/parser/src/visitors/printers/xml.ts b/apps/parser/src/visitors/printers/xml.ts index 53a2ede..ad059ad 100644 --- a/apps/parser/src/visitors/printers/xml.ts +++ b/apps/parser/src/visitors/printers/xml.ts @@ -143,8 +143,12 @@ export class XMLPrinter extends BasePrinter implements ICstNodeVisitor 'tokenType' in e[0]) as IToken[] | undefined; - this.tree(``, indent); + const op = expr.BinOp; + const pf = expr.PostFix; + this.tree( + ``, + indent, + ); this.value(expr.value[0].children, indent + 2); if (expr.expression) { this.expression(expr.expression[0].children, indent + 2); From 9766826bc9c89605790351f547e0eb5527546980 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Sat, 18 Oct 2025 15:42:24 -0500 Subject: [PATCH 02/21] fix: add missing operator in to precedence --- apps/parser/src/visitors/precedence.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/parser/src/visitors/precedence.ts b/apps/parser/src/visitors/precedence.ts index 77701a2..783a139 100644 --- a/apps/parser/src/visitors/precedence.ts +++ b/apps/parser/src/visitors/precedence.ts @@ -21,7 +21,7 @@ enum Prec { Mult = 0, // * / % Add, // + - Shift, // << >> >>> - Order, // < > <= >= + Relation, // < > <= >= in Equal, // == != BinXor, // ^ BinAnd, // & @@ -48,7 +48,8 @@ function tok2Prec(tok: TokenType) { case Tokens.GT: case Tokens.LE: case Tokens.GE: - return Prec.Order; + case Tokens.IN: + return Prec.Relation; case Tokens.EE: case Tokens.NE: return Prec.Equal; From 2d9045d5e40ded70c2bebb29353d1b7e88eda87d Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Sun, 16 Nov 2025 16:38:33 -0600 Subject: [PATCH 03/21] tests(comments): use Deno native deep compare This allows for better diagnostics with larger tests --- apps/parser/test/integration/comments.test.ts | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/apps/parser/test/integration/comments.test.ts b/apps/parser/test/integration/comments.test.ts index af1dad2..dc72707 100644 --- a/apps/parser/test/integration/comments.test.ts +++ b/apps/parser/test/integration/comments.test.ts @@ -25,7 +25,7 @@ Deno.test('Comment parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - assertEquals(afterReorder, JSON.stringify({ file: {} })); + assertEquals(JSON.parse(afterReorder), { file: {} }); assert(!parserOutput.statement, 'No output should be generated'); }); @@ -52,7 +52,7 @@ Deno.test('Comment parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - assertEquals(afterReorder, JSON.stringify({ file: {} })); + assertEquals(JSON.parse(afterReorder), { file: {} }); assert(!parserOutput.statement, 'No output should be generated'); }); @@ -71,27 +71,23 @@ Deno.test('Comment parsing #integration', async (t) => { assert(!!parserOutput.statement); - assertEquals( - afterReorder, - JSON.stringify({ - file: { - statements: [ - { - type: 'declaration', - declaration: { - image: 'str', - expression: { - op: '', - value: { - constant: "'/*****/ //'", - }, + assertEquals(JSON.parse(afterReorder), { + file: { + statements: [ + { + type: 'declaration', + declaration: { + image: 'str', + expression: { + value: { + constant: "'/*****/ //'", }, }, }, - ], - }, - }), - ); + }, + ], + }, + }); assertEquals(parserOutput.statement.length, 1, 'One statement should be generated'); }); From 35effd5eb5438c7ce5bbc026fcd1bf087c604dcf Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Sun, 16 Nov 2025 17:10:17 -0600 Subject: [PATCH 04/21] fix(parser): missing required parens for while --- apps/parser/generated/cst-types.ts | 4 +- apps/parser/generated/syntax-diagrams.html | 1167 ++++++++++---------- apps/parser/src/parser.ts | 2 + 3 files changed, 579 insertions(+), 594 deletions(-) diff --git a/apps/parser/generated/cst-types.ts b/apps/parser/generated/cst-types.ts index ca719ae..2601430 100644 --- a/apps/parser/generated/cst-types.ts +++ b/apps/parser/generated/cst-types.ts @@ -29,6 +29,8 @@ export type StatementCstChildren = { body?: BodyCstNode[]; DO?: IToken[]; WHILE?: IToken[]; + LPAREN?: IToken[]; + RPAREN?: IToken[]; FINALLY?: IToken[]; }; @@ -106,9 +108,9 @@ export type ConstantCstChildren = { STRING?: IToken[]; BOOL?: IToken[]; BIN?: IToken[]; - INT?: IToken[]; CMPX?: IToken[]; REAL?: IToken[]; + INT?: IToken[]; }; export interface TypeCstNode extends CstNode { diff --git a/apps/parser/generated/syntax-diagrams.html b/apps/parser/generated/syntax-diagrams.html index 111d915..4cae9a7 100644 --- a/apps/parser/generated/syntax-diagrams.html +++ b/apps/parser/generated/syntax-diagrams.html @@ -1,694 +1,675 @@ + - - - - - + + + + + + -
+
diff --git a/apps/parser/src/parser.ts b/apps/parser/src/parser.ts index 631aa66..246009d 100644 --- a/apps/parser/src/parser.ts +++ b/apps/parser/src/parser.ts @@ -68,7 +68,9 @@ export class EncodeParser extends CstParser { }); this.CONSUME(Tokens.WHILE); + this.CONSUME(Tokens.LPAREN); this.SUBRULE3(this.expression); + this.CONSUME(Tokens.RPAREN); this.OR2([ { ALT: () => { From 4794d763f67e304799c3b53f907b029603794c66 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Sun, 16 Nov 2025 17:20:49 -0600 Subject: [PATCH 05/21] tests(control-flow): test structure --- .../test/integration/control-flow.test.ts | 270 ++++++++++++++++-- 1 file changed, 247 insertions(+), 23 deletions(-) diff --git a/apps/parser/test/integration/control-flow.test.ts b/apps/parser/test/integration/control-flow.test.ts index 9247b13..455d30d 100644 --- a/apps/parser/test/integration/control-flow.test.ts +++ b/apps/parser/test/integration/control-flow.test.ts @@ -9,12 +9,12 @@ Deno.test('Control flow parsing #integration', async (t) => { const precedenceHandler = new TestSubject.PrecedenceHandler(); - const printer = new TestSubject.ParenPrinter(); + const printer = new TestSubject.JSONPrinter(false, null, 0); const typeAnalyzer = new TestSubject.TypeAnalyzer(); await t.step('simple if statement', () => { - const { parserOutput, typeOutput } = performParsingTestCase({ + const { parserOutput, typeOutput, afterReorder } = performParsingTestCase({ code: ['let a = 0;', 'if (a > 1) {', '', '}', ''].join('\n'), parser, @@ -28,12 +28,45 @@ Deno.test('Control flow parsing #integration', async (t) => { assert(parserOutput.statement); assertGreater(parserOutput.statement.length, 0, 'Statements should be generated'); + assertEquals(JSON.parse(afterReorder), { + file: { + statements: [ + { + type: 'declaration', + declaration: { + image: 'a', + expression: { + value: { + constant: '0', + }, + }, + }, + }, + { + type: 'if', + expression: { + op: '>', + value: { + id: 'a', + }, + expression: { + value: { + constant: '1', + }, + }, + }, + body: {}, + }, + ], + }, + }); + assertEquals(typeOutput.warnings, 0, 'TypeAnalyzer should not report any warnings'); assertEquals(typeOutput.errors, 0, 'TypeAnalyzer should not report any errors'); }); await t.step('incorrect variable access', () => { - const { parserOutput, typeOutput } = performParsingTestCase({ + const { parserOutput, typeOutput, afterReorder } = performParsingTestCase({ code: [ 'let a = 0;', 'if (1) {', @@ -59,12 +92,114 @@ Deno.test('Control flow parsing #integration', async (t) => { assert(parserOutput.statement); assertGreater(parserOutput.statement.length, 0, 'Statements should be generated'); + assertEquals(JSON.parse(afterReorder), { + file: { + statements: [ + { + type: 'declaration', + declaration: { + image: 'a', + expression: { + value: { + constant: '0', + }, + }, + }, + }, + { + type: 'if', + expression: { + value: { + constant: '1', + }, + }, + body: { + statements: [ + { + type: 'declaration', + declaration: { + image: 'b', + expression: { + value: { + constant: '20', + }, + }, + }, + }, + ], + }, + elif: [ + { + declaration: { + image: 'b', + expression: { + value: { + constant: '10', + }, + }, + }, + body: { + statements: [ + { + type: 'expression', + expression: { + op: '=', + value: { + id: 'a', + }, + expression: { + value: { + id: 'b', + }, + }, + }, + }, + { + type: 'declaration', + declaration: { + image: 'a', + expression: { + value: { + constant: '25', + }, + }, + }, + }, + ], + }, + }, + ], + else: { + body: { + statements: [ + { + type: 'expression', + expression: { + op: '=', + value: { + id: 'b', + }, + expression: { + value: { + constant: '2', + }, + }, + }, + }, + ], + }, + }, + }, + ], + }, + }); + assertEquals(typeOutput.warnings, 1, 'TypeAnalyzer should report a warning'); assertEquals(typeOutput.errors, 1, 'TypeAnalyzer should report an error'); }); await t.step('simple do-while loop', () => { - const { parserOutput, typeOutput } = performParsingTestCase({ + const { parserOutput, typeOutput, afterReorder } = performParsingTestCase({ code: ['do {}', 'while (a < 3); // maybe the ; should be replaced by an empty body???'].join( '\n', ), @@ -80,12 +215,37 @@ Deno.test('Control flow parsing #integration', async (t) => { assert(parserOutput.statement); assertGreater(parserOutput.statement.length, 0, 'Statements should be generated'); + assertEquals(JSON.parse(afterReorder), { + file: { + statements: [ + { + type: 'while', + do: { + body: {}, + }, + expression: { + op: '<', + value: { + id: 'a', + }, + expression: { + value: { + constant: '3', + }, + }, + }, + body: {}, + }, + ], + }, + }); + assertEquals(typeOutput.warnings, 0, 'TypeAnalyzer should not report any warnings'); assertEquals(typeOutput.errors, 1, 'TypeAnalyzer should report an error'); }); await t.step('incorrect variable access in while-finally block', () => { - const { parserOutput, typeOutput } = performParsingTestCase({ + const { parserOutput, typeOutput, afterReorder } = performParsingTestCase({ code: [ 'while (b > 4) {', ' let c = 1;', @@ -108,26 +268,90 @@ Deno.test('Control flow parsing #integration', async (t) => { assert(parserOutput.statement); assertGreater(parserOutput.statement.length, 0, 'Statements should be generated'); - assertEquals(typeOutput.warnings, 0, 'TypeAnalyzer should not report any warnings'); - assertEquals(typeOutput.errors, 3, 'TypeAnalyzer should report 3 errors'); - }); - - await t.step('simple do-while loop', () => { - const { parserOutput, typeOutput } = performParsingTestCase({ - code: 'do {} while(true) {}', - - parser, - precedenceHandler, - printer, - typeAnalyzer, + assertEquals(JSON.parse(afterReorder), { + file: { + statements: [ + { + type: 'while', + expression: { + op: '>', + value: { + id: 'b', + }, + expression: { + value: { + constant: '4', + }, + }, + }, + body: { + statements: [ + { + type: 'declaration', + declaration: { + image: 'c', + expression: { + value: { + constant: '1', + }, + }, + }, + }, + { + type: 'if', + expression: { + value: { + id: 'a', + }, + }, + body: { + statements: [ + { + type: 'continue', + }, + ], + }, + }, + ], + }, + finally: { + body: { + statements: [ + { + type: 'return', + expression: { + op: '+', + value: { + nested: { + expression: { + op: '+', + value: { + constant: '1', + }, + expression: { + value: { + constant: '2', + }, + }, + }, + }, + }, + expression: { + value: { + id: 'c', + }, + }, + }, + }, + ], + }, + }, + }, + ], + }, }); - assertEquals(parser.errors.length, 0, 'Parser should not error'); - - assert(parserOutput.statement); - assertGreater(parserOutput.statement.length, 0, 'Statements should be generated'); - assertEquals(typeOutput.warnings, 0, 'TypeAnalyzer should not report any warnings'); - assertEquals(typeOutput.errors, 0, 'TypeAnalyzer should not report any errors'); + assertEquals(typeOutput.errors, 3, 'TypeAnalyzer should report 3 errors'); }); }); From 6027fc5d7652483299fdfef4724fcd9466f28714 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Thu, 20 Nov 2025 09:41:54 -0600 Subject: [PATCH 06/21] feat(tests): validation functions --- apps/parser/src/validate.ts | 369 ++++++++++++++++++++++++++++++++++++ 1 file changed, 369 insertions(+) create mode 100644 apps/parser/src/validate.ts diff --git a/apps/parser/src/validate.ts b/apps/parser/src/validate.ts new file mode 100644 index 0000000..9349c81 --- /dev/null +++ b/apps/parser/src/validate.ts @@ -0,0 +1,369 @@ +import { assert, assertEquals, assertGreater } from '@std/assert'; +import type { + BodyCstChildren, + ConstantCstChildren, + DeclarationCstChildren, + ExpressionCstChildren, + FileCstChildren, + IfPredBodyCstChildren, + StatementCstChildren, + StatementCstNode, + TypeCstChildren, + ValueCstChildren, +} from '@/generated/cst-types.ts'; + +export type ValidationFunction = + | typeof file + | typeof statement + | typeof ifPredBody + | typeof declaration + | typeof body + | typeof expression + | typeof value + | typeof constant + | typeof type; + +export function file(node: FileCstChildren, args?: Parameters[1] | null) { + if (args?.length) { + assert(node.statement); + } + if (args === null) { + assert(!node.statement?.length); + } + if (node.statement && args !== null) { + statement_list(node.statement, args); + } +} + +function statement_list(statements: StatementCstNode[], args?: T[]) { + if (args) { + assertEquals(statements.length, args.length); + } else { + assertGreater(statements.length, 0); + } + for (let i = 0; i < statements.length; i++) { + statement(statements[i].children, args?.[i]); + } +} + +type BodyStatement = ['body', Body | undefined]; +type IfStatement = [ + 'if', + // 0 = if, 1-n = elif + (Parameters[1] | false | undefined)[] | undefined, + Body | false | undefined, // else +]; +type WhileStatement = [ + 'while', + Body | false | undefined, // do + Parameters[1] | undefined, // while + Body | false | undefined, // while-body + Body | false | undefined, // finally-body +]; +type Statement = Parameters[1]; + +export function statement( + stmt: StatementCstChildren, + args?: + | ['declaration', Parameters[1] | undefined] + | ['break'] + | ['continue'] + | ['return', Parameters[1] | false | undefined] + | IfStatement + | WhileStatement + | BodyStatement + | ['expression', Parameters[1] | undefined] + | false + | undefined, +) { + if (args === false) { + assertEquals(Object.keys(stmt).length, 1); + assert(stmt.SEMI); + } else if (stmt.LET && stmt.declaration) { + if (args) { + assertEquals(args[0], 'declaration'); + } + declaration( + stmt.declaration[0].children, + (args?.[1] as Parameters[1]) || undefined, + ); + } else if (stmt.BREAK) { + if (args) { + assertEquals(args[0], 'break'); + } + assertEquals(stmt.BREAK[0].image, 'break'); + } else if (stmt.CONTINUE) { + if (args) { + assertEquals(args[0], 'continue'); + } + assertEquals(stmt.CONTINUE[0].image, 'continue'); + } else if (stmt.RETURN) { + if (args) { + assertEquals(args[0], 'return'); + } + assertEquals(stmt.RETURN[0].image, 'return'); + assert(args?.[1] === undefined || (args[1] ? stmt.expression : !stmt.expression)); + if (stmt.expression) { + assertEquals(stmt.expression.length, 1); + expression( + stmt.expression[0].children, + (args?.[1] as Parameters[1]) || undefined, + ); + } + } else if (stmt.IF && stmt.ifPredBody) { + if (args) { + assertEquals(args[0], 'if'); + } + const [_, p, e] = args ?? []; + let bodyCount = 0; + const predBody = stmt.ifPredBody; + // biome-ignore lint/complexity/useOptionalChain: p could be false without being nullish + if (p && p.length) { + assertEquals(predBody.length, p.length); + } + ifPredBody( + predBody[bodyCount++].children, + (p && (p[0] as Parameters[1])) || undefined, + ); + if (stmt.ELIF) { + stmt.ELIF.forEach(() => { + ifPredBody( + predBody[bodyCount].children, + (p && (p[bodyCount] as Parameters[1])) || undefined, + ); + bodyCount++; + }); + } + assert(e === undefined || (e ? stmt.body : !stmt.body)); + if (stmt.ELSE && stmt.body) { + body(stmt.body[0].children, e as Body); + } + } else if (stmt.WHILE && stmt.expression) { + if (args) { + assertEquals(args[0], 'while'); + } + const [_, d, we, wb, f] = args || []; + let bodyCount = 0; + assert(d === undefined || (d ? stmt.DO : !stmt.DO)); + if (stmt.DO) { + assert(stmt.body); + assert(stmt.body[bodyCount]); + body(stmt.body[bodyCount++].children, d as Body); + } + expression(stmt.expression[0].children, we as Parameters[1]); + assert(wb === undefined || (wb ? !stmt.SEMI : stmt.SEMI)); + if (!stmt.SEMI) { + assert(stmt.body); + assert(stmt.body[bodyCount]); + body(stmt.body[bodyCount++].children, wb as Body); + } + assert(f === undefined || (f ? stmt.FINALLY : !stmt.FINALLY)); + if (stmt.FINALLY) { + assert(stmt.body); + assert(stmt.body[bodyCount]); + body(stmt.body[bodyCount++].children, f as Body); + } + } else if (stmt.body) { + if (args) { + assertEquals(args[0], 'body'); + } + body(stmt.body[0].children, args?.[1] as Body); + } else if (stmt.expression) { + if (args) { + assertEquals(args[0], 'expression'); + } + expression(stmt.expression[0].children, args?.[1] as Parameters[1]); + } else if (stmt.SEMI) { + assert(!args); + } else { + throw new Error(`Validation: unhandled statement type!\n${JSON.stringify(stmt, null, 2)}`); + } +} + +export function ifPredBody( + predBody: IfPredBodyCstChildren, + args?: + | [ + 'declaration', + Parameters[1] | undefined, + Parameters[1] | undefined, + ] + | ['expression', Parameters[1] | undefined, Body | undefined], +) { + if (predBody.LET && predBody.declaration) { + if (args) { + assertEquals(args[0], 'declaration'); + } + declaration( + predBody.declaration[0].children, + args?.[1] as Parameters[1] | undefined, + ); + } else if (predBody.expression) { + if (args) { + assertEquals(args[0], 'expression'); + } + expression( + predBody.expression[0].children, + args?.[1] as Parameters[1] | undefined, + ); + } else { + throw new Error(`Validation: unhandled ifPredBody type!\n${JSON.stringify(predBody, null, 2)}`); + } + + body(predBody.body[0].children, args?.[2]); +} + +export function declaration( + decl: DeclarationCstChildren, + args?: [ + string | undefined, + Parameters[1] | false | undefined, + Parameters[1] | false | undefined, + ], +) { + const [id, t, e] = args ?? []; + assertEquals(decl.ID.length, 1); + if (id) { + assertEquals(decl.ID[0].image, id); + } else { + assertGreater(decl.ID[0].image.length, 0); + } + assert(t === undefined || (t ? decl.type : !decl.type)); + if (decl.type) { + assertEquals(decl.type.length, 1); + type(decl.type[0].children, t || undefined); + } + assert(e === undefined || (e ? decl.expression : !decl.expression)); + if (decl.expression) { + assertEquals(decl.expression.length, 1); + expression(decl.expression[0].children, e || undefined); + } +} + +type Body = Parameters[1] | null; + +export function body(node: BodyCstChildren, args?: T) { + assertEquals(node.LCURLY?.at(0)?.image, '{'); + if (args?.length) { + assert(node.statement); + } + if (args === null) { + assert(!node.statement?.length); + } + if (node.statement && args !== null) { + statement_list(node.statement, args); + } + assertEquals(node.RCURLY?.at(0)?.image, '}'); +} + +export function expression< + T extends + | [ + string | false | undefined, // operator + Parameters[1] | undefined, + string | false | undefined, // postfix operator + T | false | undefined, + ] + | undefined, +>(expr: ExpressionCstChildren, args?: T) { + const [op, val, pf, rhs] = args ?? []; + assert(op === undefined || (op ? expr.BinOp : !expr.BinOp)); + if (expr.BinOp) { + assertEquals(expr.BinOp.length, 1); + if (op) { + assertEquals(expr.BinOp[0].image, op); + } else { + assertGreater(expr.BinOp[0].image.length, 0); + } + } + assert(expr.value?.at(0)?.children); + value(expr.value[0].children, val); + assert(pf === undefined || (pf ? expr.PostFix : !expr.PostFix)); + if (expr.PostFix) { + assertEquals(expr.PostFix.length, 1); + if (pf) { + assertEquals(expr.PostFix[0].image, pf); + } else { + assertGreater(expr.PostFix[0].image.length, 0); + } + } + assert(rhs === undefined || (rhs ? expr.expression : !expr.expression)); + if (expr.expression) { + assertEquals(expr.expression.length, 1); + expression(expr.expression[0].children, rhs || undefined); + } +} + +type NestedValue = ['nested', Parameters[1]]; + +export function value< + T extends + | NestedValue + | ['constant', Parameters[1]] + | ['id', string | undefined] + | ['prefix', string | undefined, T] + | undefined, +>(val: ValueCstChildren, args?: T) { + if (val.expression) { + if (args) { + assertEquals(args[0], 'nested'); + } + assertEquals(val.LPAREN?.at(0)?.image, '('); + expression(val.expression[0].children, args?.at(1) as Parameters[1]); + assertEquals(val.RPAREN?.at(0)?.image, ')'); + } else if (val.constant) { + if (args) { + assertEquals(args[0], 'constant'); + } + constant(val.constant[0].children, args?.at(1) as Parameters[1]); + } else if (val.ID) { + assertEquals(val.ID.length, 1); + if (args) { + assertEquals(args[0], 'id'); + } + if (args?.[1]) { + assertEquals(val.ID[0].image, args[1]); + } else { + assertGreater(val.ID[0].image.length, 0); + } + } else if (val.value) { + assert(val.UnOp); + assertEquals(val.UnOp.length, 1); + if (args) { + assertEquals(args[0], 'prefix'); + } + if (args?.[1]) { + assertEquals(val.UnOp[0].image, args[1]); + } else { + assertGreater(val.UnOp[0].image.length, 0); + } + value(val.value[0].children, args?.at(2) as T); + } else { + throw new Error(`Validation: unhandled value type!\n${JSON.stringify(val, null, 2)}`); + } +} + +export function constant(c: ConstantCstChildren, args?: [keyof ConstantCstChildren, string]) { + assert(c.BIN || c.BOOL || c.CMPX || c.INT || c.REAL || c.STRING); + assertEquals(Object.values(c).length, 1); + if (args?.at(0)) { + assert(c[args[0]]); + assertEquals(c[args[0]]?.length, 1); + if (args.at(1)) { + assertEquals(c[args[0]]?.[0]?.image, args[1]); + } + } else { + assertEquals(Object.values(c)[0].length, 1); + assertGreater(Object.values(c)[0][0].image.length, 0); + } +} + +export function type(t: TypeCstChildren, args?: [string]) { + assert(t.BASIC_TYPE); + assertEquals(t.BASIC_TYPE.length, 1); + if (args?.at(0)) { + assertEquals(t.BASIC_TYPE[0].image, args[0]); + } else { + assertGreater(t.BASIC_TYPE[0].image.length, 0); + } +} From 520fb55cd9b0cdeaa684a1fb39b830542277c724 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Thu, 20 Nov 2025 10:00:26 -0600 Subject: [PATCH 07/21] refactor(validate): remove dependency on Parameters as much as feasible --- apps/parser/src/validate.ts | 113 +++++++++++++----------------------- 1 file changed, 41 insertions(+), 72 deletions(-) diff --git a/apps/parser/src/validate.ts b/apps/parser/src/validate.ts index 9349c81..5f4ecee 100644 --- a/apps/parser/src/validate.ts +++ b/apps/parser/src/validate.ts @@ -23,7 +23,7 @@ export type ValidationFunction = | typeof constant | typeof type; -export function file(node: FileCstChildren, args?: Parameters[1] | null) { +export function file(node: FileCstChildren, args?: Statement[] | null) { if (args?.length) { assert(node.statement); } @@ -35,7 +35,7 @@ export function file(node: FileCstChildren, args?: Parameters(statements: StatementCstNode[], args?: T[]) { +function statement_list(statements: StatementCstNode[], args?: Statement[]) { if (args) { assertEquals(statements.length, args.length); } else { @@ -50,32 +50,28 @@ type BodyStatement = ['body', Body | undefined]; type IfStatement = [ 'if', // 0 = if, 1-n = elif - (Parameters[1] | false | undefined)[] | undefined, + (IfPredBody | false | undefined)[] | undefined, Body | false | undefined, // else ]; type WhileStatement = [ 'while', Body | false | undefined, // do - Parameters[1] | undefined, // while + Expression | undefined, // while Body | false | undefined, // while-body Body | false | undefined, // finally-body ]; -type Statement = Parameters[1]; -export function statement( - stmt: StatementCstChildren, - args?: - | ['declaration', Parameters[1] | undefined] - | ['break'] - | ['continue'] - | ['return', Parameters[1] | false | undefined] - | IfStatement - | WhileStatement - | BodyStatement - | ['expression', Parameters[1] | undefined] - | false - | undefined, -) { +type Statement = + | ['declaration', Declaration | undefined] + | ['break'] + | ['continue'] + | ['return', Expression | false | undefined] + | IfStatement + | WhileStatement + | BodyStatement + | ['expression', Expression | undefined] + | false; +export function statement(stmt: StatementCstChildren, args?: Statement) { if (args === false) { assertEquals(Object.keys(stmt).length, 1); assert(stmt.SEMI); @@ -83,10 +79,7 @@ export function statement( if (args) { assertEquals(args[0], 'declaration'); } - declaration( - stmt.declaration[0].children, - (args?.[1] as Parameters[1]) || undefined, - ); + declaration(stmt.declaration[0].children, (args?.[1] as Declaration) || undefined); } else if (stmt.BREAK) { if (args) { assertEquals(args[0], 'break'); @@ -105,10 +98,7 @@ export function statement( assert(args?.[1] === undefined || (args[1] ? stmt.expression : !stmt.expression)); if (stmt.expression) { assertEquals(stmt.expression.length, 1); - expression( - stmt.expression[0].children, - (args?.[1] as Parameters[1]) || undefined, - ); + expression(stmt.expression[0].children, (args?.[1] as Expression) || undefined); } } else if (stmt.IF && stmt.ifPredBody) { if (args) { @@ -121,16 +111,10 @@ export function statement( if (p && p.length) { assertEquals(predBody.length, p.length); } - ifPredBody( - predBody[bodyCount++].children, - (p && (p[0] as Parameters[1])) || undefined, - ); + ifPredBody(predBody[bodyCount++].children, (p && (p[0] as IfPredBody)) || undefined); if (stmt.ELIF) { stmt.ELIF.forEach(() => { - ifPredBody( - predBody[bodyCount].children, - (p && (p[bodyCount] as Parameters[1])) || undefined, - ); + ifPredBody(predBody[bodyCount].children, (p && (p[bodyCount] as IfPredBody)) || undefined); bodyCount++; }); } @@ -150,7 +134,7 @@ export function statement( assert(stmt.body[bodyCount]); body(stmt.body[bodyCount++].children, d as Body); } - expression(stmt.expression[0].children, we as Parameters[1]); + expression(stmt.expression[0].children, we as Expression); assert(wb === undefined || (wb ? !stmt.SEMI : stmt.SEMI)); if (!stmt.SEMI) { assert(stmt.body); @@ -172,7 +156,7 @@ export function statement( if (args) { assertEquals(args[0], 'expression'); } - expression(stmt.expression[0].children, args?.[1] as Parameters[1]); + expression(stmt.expression[0].children, args?.[1] as Expression); } else if (stmt.SEMI) { assert(!args); } else { @@ -180,32 +164,20 @@ export function statement( } } -export function ifPredBody( - predBody: IfPredBodyCstChildren, - args?: - | [ - 'declaration', - Parameters[1] | undefined, - Parameters[1] | undefined, - ] - | ['expression', Parameters[1] | undefined, Body | undefined], -) { +type IfPredBody = + | ['declaration', Declaration | undefined, Body | undefined] + | ['expression', Expression | undefined, Body | undefined]; +export function ifPredBody(predBody: IfPredBodyCstChildren, args?: IfPredBody) { if (predBody.LET && predBody.declaration) { if (args) { assertEquals(args[0], 'declaration'); } - declaration( - predBody.declaration[0].children, - args?.[1] as Parameters[1] | undefined, - ); + declaration(predBody.declaration[0].children, args?.[1] as Declaration | undefined); } else if (predBody.expression) { if (args) { assertEquals(args[0], 'expression'); } - expression( - predBody.expression[0].children, - args?.[1] as Parameters[1] | undefined, - ); + expression(predBody.expression[0].children, args?.[1] as Expression | undefined); } else { throw new Error(`Validation: unhandled ifPredBody type!\n${JSON.stringify(predBody, null, 2)}`); } @@ -213,14 +185,8 @@ export function ifPredBody( body(predBody.body[0].children, args?.[2]); } -export function declaration( - decl: DeclarationCstChildren, - args?: [ - string | undefined, - Parameters[1] | false | undefined, - Parameters[1] | false | undefined, - ], -) { +type Declaration = [string | undefined, Type | false | undefined, Expression | false | undefined]; +export function declaration(decl: DeclarationCstChildren, args?: Declaration) { const [id, t, e] = args ?? []; assertEquals(decl.ID.length, 1); if (id) { @@ -240,8 +206,7 @@ export function declaration( } } -type Body = Parameters[1] | null; - +type Body = Statement[] | null; export function body(node: BodyCstChildren, args?: T) { assertEquals(node.LCURLY?.at(0)?.image, '{'); if (args?.length) { @@ -256,11 +221,12 @@ export function body(node: BodyCstChildren, args?: T) { assertEquals(node.RCURLY?.at(0)?.image, '}'); } +type Expression = Parameters[1]; export function expression< T extends | [ string | false | undefined, // operator - Parameters[1] | undefined, + Value | undefined, string | false | undefined, // postfix operator T | false | undefined, ] @@ -294,12 +260,13 @@ export function expression< } } -type NestedValue = ['nested', Parameters[1]]; +type NestedValue = ['nested', Expression]; +type Value = Parameters[1]; export function value< T extends | NestedValue - | ['constant', Parameters[1]] + | ['constant', Constant] | ['id', string | undefined] | ['prefix', string | undefined, T] | undefined, @@ -309,13 +276,13 @@ export function value< assertEquals(args[0], 'nested'); } assertEquals(val.LPAREN?.at(0)?.image, '('); - expression(val.expression[0].children, args?.at(1) as Parameters[1]); + expression(val.expression[0].children, args?.at(1) as Expression); assertEquals(val.RPAREN?.at(0)?.image, ')'); } else if (val.constant) { if (args) { assertEquals(args[0], 'constant'); } - constant(val.constant[0].children, args?.at(1) as Parameters[1]); + constant(val.constant[0].children, args?.at(1) as Constant); } else if (val.ID) { assertEquals(val.ID.length, 1); if (args) { @@ -343,7 +310,8 @@ export function value< } } -export function constant(c: ConstantCstChildren, args?: [keyof ConstantCstChildren, string]) { +type Constant = [keyof ConstantCstChildren, string]; +export function constant(c: ConstantCstChildren, args?: Constant) { assert(c.BIN || c.BOOL || c.CMPX || c.INT || c.REAL || c.STRING); assertEquals(Object.values(c).length, 1); if (args?.at(0)) { @@ -358,7 +326,8 @@ export function constant(c: ConstantCstChildren, args?: [keyof ConstantCstChildr } } -export function type(t: TypeCstChildren, args?: [string]) { +type Type = [string]; +export function type(t: TypeCstChildren, args?: Type) { assert(t.BASIC_TYPE); assertEquals(t.BASIC_TYPE.length, 1); if (args?.at(0)) { From a896ff8ed5c1a1dd9f21e16a6ec586f258b79105 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Thu, 20 Nov 2025 20:44:20 -0600 Subject: [PATCH 08/21] feat(validate): add error messages --- apps/parser/src/validate.ts | 237 ++++++++++++++++++++++++++---------- 1 file changed, 172 insertions(+), 65 deletions(-) diff --git a/apps/parser/src/validate.ts b/apps/parser/src/validate.ts index 5f4ecee..780b733 100644 --- a/apps/parser/src/validate.ts +++ b/apps/parser/src/validate.ts @@ -25,10 +25,13 @@ export type ValidationFunction = export function file(node: FileCstChildren, args?: Statement[] | null) { if (args?.length) { - assert(node.statement); + assert(node.statement, `File: expected 1+ statements but received ${node.statement?.length}`); } if (args === null) { - assert(!node.statement?.length); + assert( + !node.statement?.length, + `File: expected 0 statements but received ${node.statement?.length}`, + ); } if (node.statement && args !== null) { statement_list(node.statement, args); @@ -37,9 +40,17 @@ export function file(node: FileCstChildren, args?: Statement[] | null) { function statement_list(statements: StatementCstNode[], args?: Statement[]) { if (args) { - assertEquals(statements.length, args.length); + assertEquals( + statements.length, + args.length, + `Statement List: expected ${args.length} statements but received ${statements.length}`, + ); } else { - assertGreater(statements.length, 0); + assertGreater( + statements.length, + 0, + `Statement List: expected 1+ statements but received ${statements.length}`, + ); } for (let i = 0; i < statements.length; i++) { statement(statements[i].children, args?.[i]); @@ -73,43 +84,56 @@ type Statement = | false; export function statement(stmt: StatementCstChildren, args?: Statement) { if (args === false) { - assertEquals(Object.keys(stmt).length, 1); - assert(stmt.SEMI); + assert( + Object.keys(stmt).length === 1 && stmt.SEMI, + `Statement: expected SEMI but received ${Object.keys(stmt)}`, + ); } else if (stmt.LET && stmt.declaration) { if (args) { - assertEquals(args[0], 'declaration'); + assertEquals( + args[0], + 'declaration', + `Statement: expected ${args[0]} but received declaration`, + ); } declaration(stmt.declaration[0].children, (args?.[1] as Declaration) || undefined); } else if (stmt.BREAK) { if (args) { - assertEquals(args[0], 'break'); + assertEquals(args[0], 'break', `Statement: expected ${args[0]} but received break`); } assertEquals(stmt.BREAK[0].image, 'break'); } else if (stmt.CONTINUE) { if (args) { - assertEquals(args[0], 'continue'); + assertEquals(args[0], 'continue', `Statement: expected ${args[0]} but received continue`); } assertEquals(stmt.CONTINUE[0].image, 'continue'); } else if (stmt.RETURN) { if (args) { - assertEquals(args[0], 'return'); + assertEquals(args[0], 'return', `Statement: expected ${args[0]} but received return`); } assertEquals(stmt.RETURN[0].image, 'return'); - assert(args?.[1] === undefined || (args[1] ? stmt.expression : !stmt.expression)); + assert( + args?.[1] === undefined || (args[1] ? stmt.expression : !stmt.expression), + `Statement > return: expected ${!!args?.[1]} but received ${!!stmt.expression}`, + ); if (stmt.expression) { assertEquals(stmt.expression.length, 1); expression(stmt.expression[0].children, (args?.[1] as Expression) || undefined); } } else if (stmt.IF && stmt.ifPredBody) { if (args) { - assertEquals(args[0], 'if'); + assertEquals(args[0], 'if', `Statement: expected ${args[0]} but received if`); } const [_, p, e] = args ?? []; let bodyCount = 0; const predBody = stmt.ifPredBody; // biome-ignore lint/complexity/useOptionalChain: p could be false without being nullish if (p && p.length) { - assertEquals(predBody.length, p.length); + assertEquals( + predBody.length, + p.length, + `Statement: expected ${p.length} if-preds but received ${predBody.length}`, + ); } ifPredBody(predBody[bodyCount++].children, (p && (p[0] as IfPredBody)) || undefined); if (stmt.ELIF) { @@ -118,47 +142,63 @@ export function statement(stmt: StatementCstChildren, args?: Statement) { bodyCount++; }); } - assert(e === undefined || (e ? stmt.body : !stmt.body)); + assert( + e === undefined || (e ? stmt.body : !stmt.body), + `Statement > else: expected ${!!e} but received ${!!stmt.body}`, + ); if (stmt.ELSE && stmt.body) { body(stmt.body[0].children, e as Body); } } else if (stmt.WHILE && stmt.expression) { if (args) { - assertEquals(args[0], 'while'); + assertEquals(args[0], 'while', `Statement: expected ${args[0]} but received while`); } const [_, d, we, wb, f] = args || []; let bodyCount = 0; - assert(d === undefined || (d ? stmt.DO : !stmt.DO)); + assert( + d === undefined || (d ? stmt.DO : !stmt.DO), + `Statement > do: expected ${!!d} but received ${!!stmt.DO}`, + ); if (stmt.DO) { - assert(stmt.body); - assert(stmt.body[bodyCount]); + assert( + stmt.body?.[bodyCount], + `Statement > do: expected body but received ${stmt.body?.[bodyCount]}`, + ); body(stmt.body[bodyCount++].children, d as Body); } expression(stmt.expression[0].children, we as Expression); - assert(wb === undefined || (wb ? !stmt.SEMI : stmt.SEMI)); + assert( + wb === undefined || (wb ? !stmt.SEMI : stmt.SEMI), + `Statement > while: expected ${!!wb} but received ${!stmt.SEMI}`, + ); if (!stmt.SEMI) { - assert(stmt.body); - assert(stmt.body[bodyCount]); + assert( + stmt.body?.[bodyCount], + `Statement > while: expected body but received ${stmt.body?.[bodyCount]}`, + ); body(stmt.body[bodyCount++].children, wb as Body); } - assert(f === undefined || (f ? stmt.FINALLY : !stmt.FINALLY)); + assert( + f === undefined || (f ? stmt.FINALLY : !stmt.FINALLY), + `Statement > finally: expected ${!!f} but received ${!!stmt.FINALLY}`, + ); if (stmt.FINALLY) { - assert(stmt.body); - assert(stmt.body[bodyCount]); + assert( + stmt.body?.[bodyCount], + `Statement > finally: expected body but received ${stmt.body?.[bodyCount]}`, + ); body(stmt.body[bodyCount++].children, f as Body); } } else if (stmt.body) { if (args) { - assertEquals(args[0], 'body'); + assertEquals(args[0], 'body', `Statement: expected ${args[0]} but received body`); } body(stmt.body[0].children, args?.[1] as Body); } else if (stmt.expression) { if (args) { - assertEquals(args[0], 'expression'); + assertEquals(args[0], 'expression', `Statement: expected ${args[0]} but received expression`); } expression(stmt.expression[0].children, args?.[1] as Expression); - } else if (stmt.SEMI) { - assert(!args); } else { throw new Error(`Validation: unhandled statement type!\n${JSON.stringify(stmt, null, 2)}`); } @@ -170,12 +210,20 @@ type IfPredBody = export function ifPredBody(predBody: IfPredBodyCstChildren, args?: IfPredBody) { if (predBody.LET && predBody.declaration) { if (args) { - assertEquals(args[0], 'declaration'); + assertEquals( + args[0], + 'declaration', + `IfPredBody: expected ${args[0]} but received declaration`, + ); } declaration(predBody.declaration[0].children, args?.[1] as Declaration | undefined); } else if (predBody.expression) { if (args) { - assertEquals(args[0], 'expression'); + assertEquals( + args[0], + 'expression', + `IfPredBody: expected ${args[0]} but received expression`, + ); } expression(predBody.expression[0].children, args?.[1] as Expression | undefined); } else { @@ -190,16 +238,26 @@ export function declaration(decl: DeclarationCstChildren, args?: Declaration) { const [id, t, e] = args ?? []; assertEquals(decl.ID.length, 1); if (id) { - assertEquals(decl.ID[0].image, id); + assertEquals( + decl.ID[0].image, + id, + `Declaration: expected ${id} but received ${decl.ID[0].image}`, + ); } else { assertGreater(decl.ID[0].image.length, 0); } - assert(t === undefined || (t ? decl.type : !decl.type)); + assert( + t === undefined || (t ? decl.type : !decl.type), + `Declaration > type: expected ${!!t} but received ${!!decl.type}`, + ); if (decl.type) { assertEquals(decl.type.length, 1); type(decl.type[0].children, t || undefined); } - assert(e === undefined || (e ? decl.expression : !decl.expression)); + assert( + e === undefined || (e ? decl.expression : !decl.expression), + `Declaration > expression: expected ${!!e} but received ${!!decl.expression}`, + ); if (decl.expression) { assertEquals(decl.expression.length, 1); expression(decl.expression[0].children, e || undefined); @@ -208,17 +266,20 @@ export function declaration(decl: DeclarationCstChildren, args?: Declaration) { type Body = Statement[] | null; export function body(node: BodyCstChildren, args?: T) { - assertEquals(node.LCURLY?.at(0)?.image, '{'); + assertEquals(node.LCURLY?.at(0)?.image, '{', 'Body: missing {'); if (args?.length) { - assert(node.statement); + assert(node.statement, `Body: expected 1+ statements but received ${node.statement?.length}`); } if (args === null) { - assert(!node.statement?.length); + assert( + !node.statement?.length, + `Body: expected 0 statements but received ${node.statement?.length}`, + ); } if (node.statement && args !== null) { statement_list(node.statement, args); } - assertEquals(node.RCURLY?.at(0)?.image, '}'); + assertEquals(node.RCURLY?.at(0)?.image, '}', 'Body: missing }'); } type Expression = Parameters[1]; @@ -233,27 +294,44 @@ export function expression< | undefined, >(expr: ExpressionCstChildren, args?: T) { const [op, val, pf, rhs] = args ?? []; - assert(op === undefined || (op ? expr.BinOp : !expr.BinOp)); + assert( + op === undefined || (op ? expr.BinOp : !expr.BinOp), + `Expression > BinOp: expected ${!!op} but received ${!!expr.BinOp}`, + ); if (expr.BinOp) { assertEquals(expr.BinOp.length, 1); if (op) { - assertEquals(expr.BinOp[0].image, op); + assertEquals( + expr.BinOp[0].image, + op, + `Expression > BinOp: expected ${op} but received ${expr.BinOp[0].image}`, + ); } else { assertGreater(expr.BinOp[0].image.length, 0); } } assert(expr.value?.at(0)?.children); value(expr.value[0].children, val); - assert(pf === undefined || (pf ? expr.PostFix : !expr.PostFix)); + assert( + pf === undefined || (pf ? expr.PostFix : !expr.PostFix), + `Expression > PostFix: expected ${!!pf} but received ${!!expr.PostFix}`, + ); if (expr.PostFix) { assertEquals(expr.PostFix.length, 1); if (pf) { - assertEquals(expr.PostFix[0].image, pf); + assertEquals( + expr.PostFix[0].image, + pf, + `Expression > PostFix: expected ${pf} but received ${expr.PostFix[0].image}`, + ); } else { assertGreater(expr.PostFix[0].image.length, 0); } } - assert(rhs === undefined || (rhs ? expr.expression : !expr.expression)); + assert( + rhs === undefined || (rhs ? expr.expression : !expr.expression), + `Expression > rhs: expected ${!!rhs} but received ${!!expr.expression}`, + ); if (expr.expression) { assertEquals(expr.expression.length, 1); expression(expr.expression[0].children, rhs || undefined); @@ -273,36 +351,47 @@ export function value< >(val: ValueCstChildren, args?: T) { if (val.expression) { if (args) { - assertEquals(args[0], 'nested'); + assertEquals(args[0], 'nested', `Value: expected ${args[0]} but received nested`); } - assertEquals(val.LPAREN?.at(0)?.image, '('); + assertEquals(val.LPAREN?.at(0)?.image, '(', 'Value: missing ('); expression(val.expression[0].children, args?.at(1) as Expression); - assertEquals(val.RPAREN?.at(0)?.image, ')'); + assertEquals(val.RPAREN?.at(0)?.image, ')', 'Value: missing )'); } else if (val.constant) { if (args) { - assertEquals(args[0], 'constant'); + assertEquals(args[0], 'constant', `Value: expected ${args[0]} but received constant`); } constant(val.constant[0].children, args?.at(1) as Constant); } else if (val.ID) { assertEquals(val.ID.length, 1); if (args) { - assertEquals(args[0], 'id'); + assertEquals(args[0], 'id', `Value: expected ${args[0]} but received id`); } if (args?.[1]) { - assertEquals(val.ID[0].image, args[1]); + assertEquals( + val.ID[0].image, + args[1], + `Value > id: expected ${args[1]} but received ${val.ID[0].image}`, + ); } else { assertGreater(val.ID[0].image.length, 0); } } else if (val.value) { - assert(val.UnOp); - assertEquals(val.UnOp.length, 1); + assertEquals( + val.UnOp?.length, + 1, + `Value > prefix: expected 1 prefix but received ${val.UnOp?.length}`, + ); if (args) { - assertEquals(args[0], 'prefix'); + assertEquals(args[0], 'prefix', `Value: expected ${args[0]} but received prefix`); } if (args?.[1]) { - assertEquals(val.UnOp[0].image, args[1]); + assertEquals( + val.UnOp?.[0].image, + args[1], + `Value prefix: expected ${args[1]} but received ${val.UnOp?.[0].image}`, + ); } else { - assertGreater(val.UnOp[0].image.length, 0); + assertGreater(val.UnOp?.[0].image.length, 0); } value(val.value[0].children, args?.at(2) as T); } else { @@ -312,13 +401,24 @@ export function value< type Constant = [keyof ConstantCstChildren, string]; export function constant(c: ConstantCstChildren, args?: Constant) { - assert(c.BIN || c.BOOL || c.CMPX || c.INT || c.REAL || c.STRING); - assertEquals(Object.values(c).length, 1); - if (args?.at(0)) { - assert(c[args[0]]); - assertEquals(c[args[0]]?.length, 1); - if (args.at(1)) { - assertEquals(c[args[0]]?.[0]?.image, args[1]); + assert( + c.BIN || c.BOOL || c.CMPX || c.INT || c.REAL || c.STRING, + `Constant: unexpected literal type ${Object.keys(c)}`, + ); + assertEquals( + Object.keys(c).length, + 1, + `Constant: expected 1 literal but received ${Object.keys(c).length}`, + ); + if (args?.[0]) { + assertEquals( + c[args[0]]?.length, + 1, + `Constant: expected ${args[0]} but received ${Object.keys(c)}`, + ); + if (args[1]) { + const literal = c[args[0]]?.[0]?.image; + assertEquals(literal, args[1], `Constant: expected ${args[1]} but received ${literal}`); } } else { assertEquals(Object.values(c)[0].length, 1); @@ -326,12 +426,19 @@ export function constant(c: ConstantCstChildren, args?: Constant) { } } -type Type = [string]; +type Type = string; export function type(t: TypeCstChildren, args?: Type) { - assert(t.BASIC_TYPE); - assertEquals(t.BASIC_TYPE.length, 1); - if (args?.at(0)) { - assertEquals(t.BASIC_TYPE[0].image, args[0]); + assertEquals( + t.BASIC_TYPE?.length, + 1, + `Type: expected 1 type but received ${t.BASIC_TYPE?.length}`, + ); + if (args) { + assertEquals( + t.BASIC_TYPE[0].image, + args, + `Type: expected ${args} but received ${t.BASIC_TYPE[0].image}`, + ); } else { assertGreater(t.BASIC_TYPE[0].image.length, 0); } From 4321dbcb5453a328dd9fc8fbf3dee98afcd5bcc4 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Thu, 20 Nov 2025 20:48:12 -0600 Subject: [PATCH 09/21] test(comments): use validate --- apps/parser/mod.ts | 1 + apps/parser/test/integration/comments.test.ts | 43 ++++++------------- 2 files changed, 13 insertions(+), 31 deletions(-) diff --git a/apps/parser/mod.ts b/apps/parser/mod.ts index d6106ad..9763781 100644 --- a/apps/parser/mod.ts +++ b/apps/parser/mod.ts @@ -2,4 +2,5 @@ export * from './src/globals.ts'; export * from './src/lexer.ts'; export * from './src/logging.ts'; export * from './src/parser.ts'; +export * as v from './src/validate.ts'; export * from './src/visitors/mod.ts'; diff --git a/apps/parser/test/integration/comments.test.ts b/apps/parser/test/integration/comments.test.ts index dc72707..5df9d7e 100644 --- a/apps/parser/test/integration/comments.test.ts +++ b/apps/parser/test/integration/comments.test.ts @@ -1,5 +1,5 @@ import * as TestSubject from '@encode/parser/lib'; -import { assert, assertEquals } from '@std/assert'; +import { assertEquals } from '@std/assert'; import { performParsingTestCase, useGlobalSettings } from '@/test/utils/mod.ts'; Deno.test('Comment parsing #integration', async (t) => { @@ -14,7 +14,7 @@ Deno.test('Comment parsing #integration', async (t) => { const typeAnalyzer = new TestSubject.TypeAnalyzer(); await t.step('line comment', () => { - const { parserOutput, afterReorder } = performParsingTestCase({ + const { parserOutput } = performParsingTestCase({ code: '// line comment', parser, @@ -25,13 +25,11 @@ Deno.test('Comment parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - assertEquals(JSON.parse(afterReorder), { file: {} }); - - assert(!parserOutput.statement, 'No output should be generated'); + TestSubject.v.file(parserOutput, null); }); await t.step('collapsed multiline comment', () => { - const { parserOutput, afterReorder } = performParsingTestCase({ + const { parserOutput } = performParsingTestCase({ code: [ '/**/ // collapsed multiline comment', '/*****************', @@ -52,13 +50,11 @@ Deno.test('Comment parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - assertEquals(JSON.parse(afterReorder), { file: {} }); - - assert(!parserOutput.statement, 'No output should be generated'); + TestSubject.v.file(parserOutput, null); }); await t.step('comments embedded in a string', () => { - const { parserOutput, afterReorder } = performParsingTestCase({ + const { parserOutput } = performParsingTestCase({ code: "let str = '/*****/ //'; // comments embedded in a string", parser, @@ -69,26 +65,11 @@ Deno.test('Comment parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - assert(!!parserOutput.statement); - - assertEquals(JSON.parse(afterReorder), { - file: { - statements: [ - { - type: 'declaration', - declaration: { - image: 'str', - expression: { - value: { - constant: "'/*****/ //'", - }, - }, - }, - }, - ], - }, - }); - - assertEquals(parserOutput.statement.length, 1, 'One statement should be generated'); + TestSubject.v.file(parserOutput, [ + [ + 'declaration', + ['str', false, [false, ['constant', ['STRING', "'/*****/ //'"]], false, false]], + ], + ]); }); }); From c9d02f5b29814cd92f4fb0ea014bdf5cc1850a1e Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Thu, 20 Nov 2025 21:04:24 -0600 Subject: [PATCH 10/21] refactor(validate): reorder expression, use named constants --- apps/parser/src/validate.ts | 123 +++++++++--------- apps/parser/test/integration/comments.test.ts | 9 +- 2 files changed, 69 insertions(+), 63 deletions(-) diff --git a/apps/parser/src/validate.ts b/apps/parser/src/validate.ts index 780b733..fe05670 100644 --- a/apps/parser/src/validate.ts +++ b/apps/parser/src/validate.ts @@ -12,6 +12,11 @@ import type { ValueCstChildren, } from '@/generated/cst-types.ts'; +export const skip = undefined; +type Skip = typeof skip; +export const none = false; +type None = typeof none; + export type ValidationFunction = | typeof file | typeof statement @@ -57,33 +62,33 @@ function statement_list(statements: StatementCstNode[], args?: Statement[]) { } } -type BodyStatement = ['body', Body | undefined]; +type BodyStatement = ['body', Body | Skip]; type IfStatement = [ 'if', // 0 = if, 1-n = elif - (IfPredBody | false | undefined)[] | undefined, - Body | false | undefined, // else + (IfPredBody | None | Skip)[] | Skip, + Body | None | Skip, // else ]; type WhileStatement = [ 'while', - Body | false | undefined, // do - Expression | undefined, // while - Body | false | undefined, // while-body - Body | false | undefined, // finally-body + Body | None | Skip, // do + Expression | Skip, // while + Body | None | Skip, // while-body + Body | None | Skip, // finally-body ]; type Statement = - | ['declaration', Declaration | undefined] + | ['declaration', Declaration | Skip] | ['break'] | ['continue'] - | ['return', Expression | false | undefined] + | ['return', Expression | None | Skip] | IfStatement | WhileStatement | BodyStatement - | ['expression', Expression | undefined] - | false; + | ['expression', Expression | Skip] + | None; export function statement(stmt: StatementCstChildren, args?: Statement) { - if (args === false) { + if (args === none) { assert( Object.keys(stmt).length === 1 && stmt.SEMI, `Statement: expected SEMI but received ${Object.keys(stmt)}`, @@ -96,7 +101,7 @@ export function statement(stmt: StatementCstChildren, args?: Statement) { `Statement: expected ${args[0]} but received declaration`, ); } - declaration(stmt.declaration[0].children, (args?.[1] as Declaration) || undefined); + declaration(stmt.declaration[0].children, (args?.[1] as Declaration) || skip); } else if (stmt.BREAK) { if (args) { assertEquals(args[0], 'break', `Statement: expected ${args[0]} but received break`); @@ -113,12 +118,12 @@ export function statement(stmt: StatementCstChildren, args?: Statement) { } assertEquals(stmt.RETURN[0].image, 'return'); assert( - args?.[1] === undefined || (args[1] ? stmt.expression : !stmt.expression), + args?.[1] === skip || (args[1] ? stmt.expression : !stmt.expression), `Statement > return: expected ${!!args?.[1]} but received ${!!stmt.expression}`, ); if (stmt.expression) { assertEquals(stmt.expression.length, 1); - expression(stmt.expression[0].children, (args?.[1] as Expression) || undefined); + expression(stmt.expression[0].children, (args?.[1] as Expression) || skip); } } else if (stmt.IF && stmt.ifPredBody) { if (args) { @@ -127,7 +132,7 @@ export function statement(stmt: StatementCstChildren, args?: Statement) { const [_, p, e] = args ?? []; let bodyCount = 0; const predBody = stmt.ifPredBody; - // biome-ignore lint/complexity/useOptionalChain: p could be false without being nullish + // biome-ignore lint/complexity/useOptionalChain: p could be None without being nullish if (p && p.length) { assertEquals( predBody.length, @@ -135,15 +140,15 @@ export function statement(stmt: StatementCstChildren, args?: Statement) { `Statement: expected ${p.length} if-preds but received ${predBody.length}`, ); } - ifPredBody(predBody[bodyCount++].children, (p && (p[0] as IfPredBody)) || undefined); + ifPredBody(predBody[bodyCount++].children, (p && (p[0] as IfPredBody)) || skip); if (stmt.ELIF) { stmt.ELIF.forEach(() => { - ifPredBody(predBody[bodyCount].children, (p && (p[bodyCount] as IfPredBody)) || undefined); + ifPredBody(predBody[bodyCount].children, (p && (p[bodyCount] as IfPredBody)) || skip); bodyCount++; }); } assert( - e === undefined || (e ? stmt.body : !stmt.body), + e === skip || (e ? stmt.body : !stmt.body), `Statement > else: expected ${!!e} but received ${!!stmt.body}`, ); if (stmt.ELSE && stmt.body) { @@ -156,7 +161,7 @@ export function statement(stmt: StatementCstChildren, args?: Statement) { const [_, d, we, wb, f] = args || []; let bodyCount = 0; assert( - d === undefined || (d ? stmt.DO : !stmt.DO), + d === skip || (d ? stmt.DO : !stmt.DO), `Statement > do: expected ${!!d} but received ${!!stmt.DO}`, ); if (stmt.DO) { @@ -168,7 +173,7 @@ export function statement(stmt: StatementCstChildren, args?: Statement) { } expression(stmt.expression[0].children, we as Expression); assert( - wb === undefined || (wb ? !stmt.SEMI : stmt.SEMI), + wb === skip || (wb ? !stmt.SEMI : stmt.SEMI), `Statement > while: expected ${!!wb} but received ${!stmt.SEMI}`, ); if (!stmt.SEMI) { @@ -179,7 +184,7 @@ export function statement(stmt: StatementCstChildren, args?: Statement) { body(stmt.body[bodyCount++].children, wb as Body); } assert( - f === undefined || (f ? stmt.FINALLY : !stmt.FINALLY), + f === skip || (f ? stmt.FINALLY : !stmt.FINALLY), `Statement > finally: expected ${!!f} but received ${!!stmt.FINALLY}`, ); if (stmt.FINALLY) { @@ -205,8 +210,8 @@ export function statement(stmt: StatementCstChildren, args?: Statement) { } type IfPredBody = - | ['declaration', Declaration | undefined, Body | undefined] - | ['expression', Expression | undefined, Body | undefined]; + | ['declaration', Declaration | Skip, Body | Skip] + | ['expression', Expression | Skip, Body | Skip]; export function ifPredBody(predBody: IfPredBodyCstChildren, args?: IfPredBody) { if (predBody.LET && predBody.declaration) { if (args) { @@ -216,7 +221,7 @@ export function ifPredBody(predBody: IfPredBodyCstChildren, args?: IfPredBody) { `IfPredBody: expected ${args[0]} but received declaration`, ); } - declaration(predBody.declaration[0].children, args?.[1] as Declaration | undefined); + declaration(predBody.declaration[0].children, args?.[1] as Declaration | Skip); } else if (predBody.expression) { if (args) { assertEquals( @@ -225,7 +230,7 @@ export function ifPredBody(predBody: IfPredBodyCstChildren, args?: IfPredBody) { `IfPredBody: expected ${args[0]} but received expression`, ); } - expression(predBody.expression[0].children, args?.[1] as Expression | undefined); + expression(predBody.expression[0].children, args?.[1] as Expression | Skip); } else { throw new Error(`Validation: unhandled ifPredBody type!\n${JSON.stringify(predBody, null, 2)}`); } @@ -233,7 +238,7 @@ export function ifPredBody(predBody: IfPredBodyCstChildren, args?: IfPredBody) { body(predBody.body[0].children, args?.[2]); } -type Declaration = [string | undefined, Type | false | undefined, Expression | false | undefined]; +type Declaration = [string | Skip, Type | None | Skip, Expression | None | Skip]; export function declaration(decl: DeclarationCstChildren, args?: Declaration) { const [id, t, e] = args ?? []; assertEquals(decl.ID.length, 1); @@ -247,20 +252,20 @@ export function declaration(decl: DeclarationCstChildren, args?: Declaration) { assertGreater(decl.ID[0].image.length, 0); } assert( - t === undefined || (t ? decl.type : !decl.type), + t === skip || (t ? decl.type : !decl.type), `Declaration > type: expected ${!!t} but received ${!!decl.type}`, ); if (decl.type) { assertEquals(decl.type.length, 1); - type(decl.type[0].children, t || undefined); + type(decl.type[0].children, t || skip); } assert( - e === undefined || (e ? decl.expression : !decl.expression), + e === skip || (e ? decl.expression : !decl.expression), `Declaration > expression: expected ${!!e} but received ${!!decl.expression}`, ); if (decl.expression) { assertEquals(decl.expression.length, 1); - expression(decl.expression[0].children, e || undefined); + expression(decl.expression[0].children, e || skip); } } @@ -286,34 +291,18 @@ type Expression = Parameters[1]; export function expression< T extends | [ - string | false | undefined, // operator - Value | undefined, - string | false | undefined, // postfix operator - T | false | undefined, + Value | Skip, + string | None | Skip, // postfix operator + string | None | Skip, // operator + T | None | Skip, ] - | undefined, + | Skip, >(expr: ExpressionCstChildren, args?: T) { - const [op, val, pf, rhs] = args ?? []; - assert( - op === undefined || (op ? expr.BinOp : !expr.BinOp), - `Expression > BinOp: expected ${!!op} but received ${!!expr.BinOp}`, - ); - if (expr.BinOp) { - assertEquals(expr.BinOp.length, 1); - if (op) { - assertEquals( - expr.BinOp[0].image, - op, - `Expression > BinOp: expected ${op} but received ${expr.BinOp[0].image}`, - ); - } else { - assertGreater(expr.BinOp[0].image.length, 0); - } - } + const [val, pf, op, rhs] = args ?? []; assert(expr.value?.at(0)?.children); value(expr.value[0].children, val); assert( - pf === undefined || (pf ? expr.PostFix : !expr.PostFix), + pf === skip || (pf ? expr.PostFix : !expr.PostFix), `Expression > PostFix: expected ${!!pf} but received ${!!expr.PostFix}`, ); if (expr.PostFix) { @@ -329,12 +318,28 @@ export function expression< } } assert( - rhs === undefined || (rhs ? expr.expression : !expr.expression), + op === skip || (op ? expr.BinOp : !expr.BinOp), + `Expression > BinOp: expected ${!!op} but received ${!!expr.BinOp}`, + ); + if (expr.BinOp) { + assertEquals(expr.BinOp.length, 1); + if (op) { + assertEquals( + expr.BinOp[0].image, + op, + `Expression > BinOp: expected ${op} but received ${expr.BinOp[0].image}`, + ); + } else { + assertGreater(expr.BinOp[0].image.length, 0); + } + } + assert( + rhs === skip || (rhs ? expr.expression : !expr.expression), `Expression > rhs: expected ${!!rhs} but received ${!!expr.expression}`, ); if (expr.expression) { assertEquals(expr.expression.length, 1); - expression(expr.expression[0].children, rhs || undefined); + expression(expr.expression[0].children, rhs || skip); } } @@ -345,9 +350,9 @@ export function value< T extends | NestedValue | ['constant', Constant] - | ['id', string | undefined] - | ['prefix', string | undefined, T] - | undefined, + | ['id', string | Skip] + | ['prefix', string | Skip, T] + | Skip, >(val: ValueCstChildren, args?: T) { if (val.expression) { if (args) { diff --git a/apps/parser/test/integration/comments.test.ts b/apps/parser/test/integration/comments.test.ts index 5df9d7e..091dca2 100644 --- a/apps/parser/test/integration/comments.test.ts +++ b/apps/parser/test/integration/comments.test.ts @@ -1,4 +1,5 @@ import * as TestSubject from '@encode/parser/lib'; +import { v } from '@encode/parser/lib'; import { assertEquals } from '@std/assert'; import { performParsingTestCase, useGlobalSettings } from '@/test/utils/mod.ts'; @@ -25,7 +26,7 @@ Deno.test('Comment parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - TestSubject.v.file(parserOutput, null); + v.file(parserOutput, null); }); await t.step('collapsed multiline comment', () => { @@ -50,7 +51,7 @@ Deno.test('Comment parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - TestSubject.v.file(parserOutput, null); + v.file(parserOutput, null); }); await t.step('comments embedded in a string', () => { @@ -65,10 +66,10 @@ Deno.test('Comment parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - TestSubject.v.file(parserOutput, [ + v.file(parserOutput, [ [ 'declaration', - ['str', false, [false, ['constant', ['STRING', "'/*****/ //'"]], false, false]], + ['str', v.none, [['constant', ['STRING', "'/*****/ //'"]], v.none, v.none, v.none]], ], ]); }); From bac571d65118032c0f2274c2228af85960d5195e Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Thu, 20 Nov 2025 21:07:42 -0600 Subject: [PATCH 11/21] refactor(validate): use v.none consistently --- apps/parser/src/validate.ts | 18 ++++++++++-------- apps/parser/test/integration/comments.test.ts | 4 ++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/apps/parser/src/validate.ts b/apps/parser/src/validate.ts index fe05670..ec70a8d 100644 --- a/apps/parser/src/validate.ts +++ b/apps/parser/src/validate.ts @@ -28,17 +28,18 @@ export type ValidationFunction = | typeof constant | typeof type; -export function file(node: FileCstChildren, args?: Statement[] | null) { - if (args?.length) { +export function file(node: FileCstChildren, args?: Statement[] | None) { + // biome-ignore lint/complexity/useOptionalChain: args could be false without being nullish + if (args && args.length) { assert(node.statement, `File: expected 1+ statements but received ${node.statement?.length}`); } - if (args === null) { + if (args === false) { assert( !node.statement?.length, `File: expected 0 statements but received ${node.statement?.length}`, ); } - if (node.statement && args !== null) { + if (node.statement && args !== false) { statement_list(node.statement, args); } } @@ -269,19 +270,20 @@ export function declaration(decl: DeclarationCstChildren, args?: Declaration) { } } -type Body = Statement[] | null; +type Body = Statement[] | None; export function body(node: BodyCstChildren, args?: T) { assertEquals(node.LCURLY?.at(0)?.image, '{', 'Body: missing {'); - if (args?.length) { + // biome-ignore lint/complexity/useOptionalChain: args could be false without being nullish + if (args && args.length) { assert(node.statement, `Body: expected 1+ statements but received ${node.statement?.length}`); } - if (args === null) { + if (args === false) { assert( !node.statement?.length, `Body: expected 0 statements but received ${node.statement?.length}`, ); } - if (node.statement && args !== null) { + if (node.statement && args !== false) { statement_list(node.statement, args); } assertEquals(node.RCURLY?.at(0)?.image, '}', 'Body: missing }'); diff --git a/apps/parser/test/integration/comments.test.ts b/apps/parser/test/integration/comments.test.ts index 091dca2..0cfead6 100644 --- a/apps/parser/test/integration/comments.test.ts +++ b/apps/parser/test/integration/comments.test.ts @@ -26,7 +26,7 @@ Deno.test('Comment parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - v.file(parserOutput, null); + v.file(parserOutput, v.none); }); await t.step('collapsed multiline comment', () => { @@ -51,7 +51,7 @@ Deno.test('Comment parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - v.file(parserOutput, null); + v.file(parserOutput, v.none); }); await t.step('comments embedded in a string', () => { From c9df1863b64500e2f10ea49384afbffa6e88c793 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Fri, 21 Nov 2025 13:06:30 -0600 Subject: [PATCH 12/21] chore(validate): export types --- apps/parser/src/validate.ts | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/apps/parser/src/validate.ts b/apps/parser/src/validate.ts index ec70a8d..e6da2fd 100644 --- a/apps/parser/src/validate.ts +++ b/apps/parser/src/validate.ts @@ -63,14 +63,14 @@ function statement_list(statements: StatementCstNode[], args?: Statement[]) { } } -type BodyStatement = ['body', Body | Skip]; -type IfStatement = [ +export type BodyStatement = ['body', Body | Skip]; +export type IfStatement = [ 'if', // 0 = if, 1-n = elif (IfPredBody | None | Skip)[] | Skip, Body | None | Skip, // else ]; -type WhileStatement = [ +export type WhileStatement = [ 'while', Body | None | Skip, // do Expression | Skip, // while @@ -78,7 +78,7 @@ type WhileStatement = [ Body | None | Skip, // finally-body ]; -type Statement = +export type Statement = | ['declaration', Declaration | Skip] | ['break'] | ['continue'] @@ -87,7 +87,8 @@ type Statement = | WhileStatement | BodyStatement | ['expression', Expression | Skip] - | None; + | None + | Skip; export function statement(stmt: StatementCstChildren, args?: Statement) { if (args === none) { assert( @@ -210,9 +211,10 @@ export function statement(stmt: StatementCstChildren, args?: Statement) { } } -type IfPredBody = +export type IfPredBody = | ['declaration', Declaration | Skip, Body | Skip] - | ['expression', Expression | Skip, Body | Skip]; + | ['expression', Expression | Skip, Body | Skip] + | Skip; export function ifPredBody(predBody: IfPredBodyCstChildren, args?: IfPredBody) { if (predBody.LET && predBody.declaration) { if (args) { @@ -239,7 +241,7 @@ export function ifPredBody(predBody: IfPredBodyCstChildren, args?: IfPredBody) { body(predBody.body[0].children, args?.[2]); } -type Declaration = [string | Skip, Type | None | Skip, Expression | None | Skip]; +export type Declaration = [string | Skip, Type | None | Skip, Expression | None | Skip] | Skip; export function declaration(decl: DeclarationCstChildren, args?: Declaration) { const [id, t, e] = args ?? []; assertEquals(decl.ID.length, 1); @@ -270,7 +272,7 @@ export function declaration(decl: DeclarationCstChildren, args?: Declaration) { } } -type Body = Statement[] | None; +export type Body = Statement[] | None | Skip; export function body(node: BodyCstChildren, args?: T) { assertEquals(node.LCURLY?.at(0)?.image, '{', 'Body: missing {'); // biome-ignore lint/complexity/useOptionalChain: args could be false without being nullish @@ -289,7 +291,7 @@ export function body(node: BodyCstChildren, args?: T) { assertEquals(node.RCURLY?.at(0)?.image, '}', 'Body: missing }'); } -type Expression = Parameters[1]; +export type Expression = Parameters[1]; export function expression< T extends | [ @@ -347,7 +349,7 @@ export function expression< type NestedValue = ['nested', Expression]; -type Value = Parameters[1]; +export type Value = Parameters[1]; export function value< T extends | NestedValue @@ -406,7 +408,7 @@ export function value< } } -type Constant = [keyof ConstantCstChildren, string]; +export type Constant = [keyof ConstantCstChildren, string] | Skip; export function constant(c: ConstantCstChildren, args?: Constant) { assert( c.BIN || c.BOOL || c.CMPX || c.INT || c.REAL || c.STRING, @@ -433,7 +435,7 @@ export function constant(c: ConstantCstChildren, args?: Constant) { } } -type Type = string; +export type Type = string | Skip; export function type(t: TypeCstChildren, args?: Type) { assertEquals( t.BASIC_TYPE?.length, From c6310694355cae1048adbb163e42c1fd6f81e75f Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Fri, 21 Nov 2025 13:06:55 -0600 Subject: [PATCH 13/21] refactor(validate): shorten required args for expression --- apps/parser/src/validate.ts | 13 +++++++++---- apps/parser/test/integration/comments.test.ts | 5 +---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/parser/src/validate.ts b/apps/parser/src/validate.ts index e6da2fd..d077a2b 100644 --- a/apps/parser/src/validate.ts +++ b/apps/parser/src/validate.ts @@ -294,11 +294,16 @@ export function body(node: BodyCstChildren, args?: T) { export type Expression = Parameters[1]; export function expression< T extends + | [Value | Skip] + | [ + Value | Skip, + string | Skip, // postfix operator + ] | [ Value | Skip, string | None | Skip, // postfix operator - string | None | Skip, // operator - T | None | Skip, + string | Skip, // operator + T | Skip, ] | Skip, >(expr: ExpressionCstChildren, args?: T) { @@ -338,8 +343,8 @@ export function expression< } } assert( - rhs === skip || (rhs ? expr.expression : !expr.expression), - `Expression > rhs: expected ${!!rhs} but received ${!!expr.expression}`, + (op === skip && rhs === skip) || (rhs ? expr.expression : !expr.expression), + `Expression > rhs: expected ${!!(op || rhs)} but received ${!!expr.expression}`, ); if (expr.expression) { assertEquals(expr.expression.length, 1); diff --git a/apps/parser/test/integration/comments.test.ts b/apps/parser/test/integration/comments.test.ts index 0cfead6..a6145d5 100644 --- a/apps/parser/test/integration/comments.test.ts +++ b/apps/parser/test/integration/comments.test.ts @@ -67,10 +67,7 @@ Deno.test('Comment parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); v.file(parserOutput, [ - [ - 'declaration', - ['str', v.none, [['constant', ['STRING', "'/*****/ //'"]], v.none, v.none, v.none]], - ], + ['declaration', ['str', v.none, [['constant', ['STRING', "'/*****/ //'"]]]]], ]); }); }); From dd32a3386d386ee7c1b24a750ee1dbf44c86d87d Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Fri, 21 Nov 2025 13:50:06 -0600 Subject: [PATCH 14/21] fix(validate): remove unneeded parentheses check --- apps/parser/src/validate.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/parser/src/validate.ts b/apps/parser/src/validate.ts index d077a2b..11cabc7 100644 --- a/apps/parser/src/validate.ts +++ b/apps/parser/src/validate.ts @@ -367,9 +367,7 @@ export function value< if (args) { assertEquals(args[0], 'nested', `Value: expected ${args[0]} but received nested`); } - assertEquals(val.LPAREN?.at(0)?.image, '(', 'Value: missing ('); expression(val.expression[0].children, args?.at(1) as Expression); - assertEquals(val.RPAREN?.at(0)?.image, ')', 'Value: missing )'); } else if (val.constant) { if (args) { assertEquals(args[0], 'constant', `Value: expected ${args[0]} but received constant`); From 7d6da35235e8e04b5f5805423a480fe3d546910f Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Fri, 21 Nov 2025 13:50:48 -0600 Subject: [PATCH 15/21] test(control-flow): use validation --- .../test/integration/control-flow.test.ts | 297 +++--------------- 1 file changed, 42 insertions(+), 255 deletions(-) diff --git a/apps/parser/test/integration/control-flow.test.ts b/apps/parser/test/integration/control-flow.test.ts index 455d30d..f09afcc 100644 --- a/apps/parser/test/integration/control-flow.test.ts +++ b/apps/parser/test/integration/control-flow.test.ts @@ -1,5 +1,6 @@ import * as TestSubject from '@encode/parser/lib'; -import { assert, assertEquals, assertGreater } from '@std/assert'; +import { v } from '@encode/parser/lib'; +import { assertEquals } from '@std/assert'; import { performParsingTestCase, useGlobalSettings } from '@/test/utils/mod.ts'; Deno.test('Control flow parsing #integration', async (t) => { @@ -14,7 +15,7 @@ Deno.test('Control flow parsing #integration', async (t) => { const typeAnalyzer = new TestSubject.TypeAnalyzer(); await t.step('simple if statement', () => { - const { parserOutput, typeOutput, afterReorder } = performParsingTestCase({ + const { parserOutput, typeOutput } = performParsingTestCase({ code: ['let a = 0;', 'if (a > 1) {', '', '}', ''].join('\n'), parser, @@ -25,48 +26,21 @@ Deno.test('Control flow parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - assert(parserOutput.statement); - assertGreater(parserOutput.statement.length, 0, 'Statements should be generated'); - - assertEquals(JSON.parse(afterReorder), { - file: { - statements: [ - { - type: 'declaration', - declaration: { - image: 'a', - expression: { - value: { - constant: '0', - }, - }, - }, - }, - { - type: 'if', - expression: { - op: '>', - value: { - id: 'a', - }, - expression: { - value: { - constant: '1', - }, - }, - }, - body: {}, - }, - ], - }, - }); + v.file(parserOutput, [ + ['declaration', ['a', v.none, [['constant', ['INT', '0']]]]], + [ + 'if', + [['expression', [['id', 'a'], v.none, '>', [['constant', ['INT', '1']]]], v.none]], + v.none, + ], + ]); assertEquals(typeOutput.warnings, 0, 'TypeAnalyzer should not report any warnings'); assertEquals(typeOutput.errors, 0, 'TypeAnalyzer should not report any errors'); }); await t.step('incorrect variable access', () => { - const { parserOutput, typeOutput, afterReorder } = performParsingTestCase({ + const { parserOutput, typeOutput } = performParsingTestCase({ code: [ 'let a = 0;', 'if (1) {', @@ -89,117 +63,17 @@ Deno.test('Control flow parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - assert(parserOutput.statement); - assertGreater(parserOutput.statement.length, 0, 'Statements should be generated'); - - assertEquals(JSON.parse(afterReorder), { - file: { - statements: [ - { - type: 'declaration', - declaration: { - image: 'a', - expression: { - value: { - constant: '0', - }, - }, - }, - }, - { - type: 'if', - expression: { - value: { - constant: '1', - }, - }, - body: { - statements: [ - { - type: 'declaration', - declaration: { - image: 'b', - expression: { - value: { - constant: '20', - }, - }, - }, - }, - ], - }, - elif: [ - { - declaration: { - image: 'b', - expression: { - value: { - constant: '10', - }, - }, - }, - body: { - statements: [ - { - type: 'expression', - expression: { - op: '=', - value: { - id: 'a', - }, - expression: { - value: { - id: 'b', - }, - }, - }, - }, - { - type: 'declaration', - declaration: { - image: 'a', - expression: { - value: { - constant: '25', - }, - }, - }, - }, - ], - }, - }, - ], - else: { - body: { - statements: [ - { - type: 'expression', - expression: { - op: '=', - value: { - id: 'b', - }, - expression: { - value: { - constant: '2', - }, - }, - }, - }, - ], - }, - }, - }, - ], - }, - }); + v.file(parserOutput, [ + ['declaration', ['a', v.none, [['constant', ['INT', '0']]]]], + ['if', v.skip, [['expression', [['id', 'b'], v.none, '=', [['constant', ['INT', '2']]]]]]], + ]); assertEquals(typeOutput.warnings, 1, 'TypeAnalyzer should report a warning'); assertEquals(typeOutput.errors, 1, 'TypeAnalyzer should report an error'); }); await t.step('simple do-while loop', () => { - const { parserOutput, typeOutput, afterReorder } = performParsingTestCase({ + const { parserOutput, typeOutput } = performParsingTestCase({ code: ['do {}', 'while (a < 3); // maybe the ; should be replaced by an empty body???'].join( '\n', ), @@ -212,40 +86,16 @@ Deno.test('Control flow parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - assert(parserOutput.statement); - assertGreater(parserOutput.statement.length, 0, 'Statements should be generated'); - - assertEquals(JSON.parse(afterReorder), { - file: { - statements: [ - { - type: 'while', - do: { - body: {}, - }, - expression: { - op: '<', - value: { - id: 'a', - }, - expression: { - value: { - constant: '3', - }, - }, - }, - body: {}, - }, - ], - }, - }); + v.file(parserOutput, [ + ['while', [], [['id', 'a'], v.none, '<', [['constant', ['INT', '3']]]], v.none, v.none], + ]); assertEquals(typeOutput.warnings, 0, 'TypeAnalyzer should not report any warnings'); assertEquals(typeOutput.errors, 1, 'TypeAnalyzer should report an error'); }); await t.step('incorrect variable access in while-finally block', () => { - const { parserOutput, typeOutput, afterReorder } = performParsingTestCase({ + const { parserOutput, typeOutput } = performParsingTestCase({ code: [ 'while (b > 4) {', ' let c = 1;', @@ -265,91 +115,28 @@ Deno.test('Control flow parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - assert(parserOutput.statement); - assertGreater(parserOutput.statement.length, 0, 'Statements should be generated'); - - assertEquals(JSON.parse(afterReorder), { - file: { - statements: [ - { - type: 'while', - expression: { - op: '>', - value: { - id: 'b', - }, - expression: { - value: { - constant: '4', - }, - }, - }, - body: { - statements: [ - { - type: 'declaration', - declaration: { - image: 'c', - expression: { - value: { - constant: '1', - }, - }, - }, - }, - { - type: 'if', - expression: { - value: { - id: 'a', - }, - }, - body: { - statements: [ - { - type: 'continue', - }, - ], - }, - }, - ], - }, - finally: { - body: { - statements: [ - { - type: 'return', - expression: { - op: '+', - value: { - nested: { - expression: { - op: '+', - value: { - constant: '1', - }, - expression: { - value: { - constant: '2', - }, - }, - }, - }, - }, - expression: { - value: { - id: 'c', - }, - }, - }, - }, - ], - }, - }, - }, + v.file(parserOutput, [ + [ + 'while', + v.none, + [['id', 'b'], v.none, '>', [['constant', ['INT', '4']]]], + [ + ['declaration', ['c', v.none, [['constant', ['INT', '1']]]]], + ['if', [['expression', [['id', 'a']], [['continue']]]], v.none], ], - }, - }); + [ + [ + 'return', + [ + ['nested', [['constant', ['INT', '1']], v.none, '+', [['constant', ['INT', '2']]]]], + v.none, + '+', + [['id', 'c']], + ], + ], + ], + ], + ]); assertEquals(typeOutput.warnings, 0, 'TypeAnalyzer should not report any warnings'); assertEquals(typeOutput.errors, 3, 'TypeAnalyzer should report 3 errors'); From 5bcb62c0400942b92369de31db5b8c5584d826a4 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Fri, 21 Nov 2025 14:01:50 -0600 Subject: [PATCH 16/21] test(data-types): use validate --- .../test/integration/data-types.test.ts | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/apps/parser/test/integration/data-types.test.ts b/apps/parser/test/integration/data-types.test.ts index 5aba759..f26882f 100644 --- a/apps/parser/test/integration/data-types.test.ts +++ b/apps/parser/test/integration/data-types.test.ts @@ -1,5 +1,6 @@ import * as TestSubject from '@encode/parser/lib'; -import { assert, assertEquals } from '@std/assert'; +import { v } from '@encode/parser/lib'; +import { assertEquals } from '@std/assert'; import { performParsingTestCase, useGlobalSettings } from '@/test/utils/mod.ts'; Deno.test('Data type parsing #integration', async (t) => { @@ -25,7 +26,7 @@ Deno.test('Data type parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - assert(parserOutput.statement, 'Some output should be generated'); + v.file(parserOutput, [['declaration', ['real', v.none, [['constant', ['REAL', '1.0']]]]]]); }); await t.step('integer literal', () => { @@ -40,7 +41,7 @@ Deno.test('Data type parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - assert(parserOutput.statement, 'Some output should be generated'); + v.file(parserOutput, [['declaration', ['integer', v.none, [['constant', ['INT', '21']]]]]]); }); await t.step('string literal', () => { @@ -55,7 +56,9 @@ Deno.test('Data type parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - assert(parserOutput.statement, 'Some output should be generated'); + v.file(parserOutput, [ + ['declaration', ['str', v.none, [['constant', ['STRING', "'Hello, World!'"]]]]], + ]); }); await t.step('boolean literal', () => { @@ -70,7 +73,7 @@ Deno.test('Data type parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - assert(parserOutput.statement, 'Some output should be generated'); + v.file(parserOutput, [['declaration', ['flag', v.none, [['constant', ['BOOL', 'true']]]]]]); }); await t.step('bit literal', () => { @@ -85,7 +88,7 @@ Deno.test('Data type parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - assert(parserOutput.statement, 'Some output should be generated'); + v.file(parserOutput, [['declaration', ['bits', v.none, [['constant', ['BIN', '0xff']]]]]]); }); await t.step('complex number literal', () => { @@ -100,6 +103,15 @@ Deno.test('Data type parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - assert(parserOutput.statement, 'Some output should be generated'); + v.file(parserOutput, [ + [ + 'declaration', + [ + 'imag', + v.none, + [['constant', ['REAL', '1.0']], v.none, '+', [['constant', ['CMPX', '2.0i']]]], + ], + ], + ]); }); }); From 4988afd3cd495e198b2809a30ff7d3b3dd5f9820 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Fri, 21 Nov 2025 14:11:05 -0600 Subject: [PATCH 17/21] fix(validate): correctly check if pred length --- apps/parser/src/validate.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/parser/src/validate.ts b/apps/parser/src/validate.ts index 11cabc7..046be03 100644 --- a/apps/parser/src/validate.ts +++ b/apps/parser/src/validate.ts @@ -134,8 +134,7 @@ export function statement(stmt: StatementCstChildren, args?: Statement) { const [_, p, e] = args ?? []; let bodyCount = 0; const predBody = stmt.ifPredBody; - // biome-ignore lint/complexity/useOptionalChain: p could be None without being nullish - if (p && p.length) { + if (p) { assertEquals( predBody.length, p.length, From dbf7550ef7a65fd42a57aa3c35d30f588949602f Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Mon, 24 Nov 2025 13:40:05 -0600 Subject: [PATCH 18/21] test(keywords): use validate --- apps/parser/test/integration/keywords.test.ts | 41 ++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/apps/parser/test/integration/keywords.test.ts b/apps/parser/test/integration/keywords.test.ts index 911914b..37c5b6b 100644 --- a/apps/parser/test/integration/keywords.test.ts +++ b/apps/parser/test/integration/keywords.test.ts @@ -1,4 +1,5 @@ import * as TestSubject from '@encode/parser/lib'; +import { v } from '@encode/parser/lib'; import { assert } from '@std/assert'; import { performParsingTestCase, useGlobalSettings } from '@/test/utils/mod.ts'; @@ -34,7 +35,25 @@ Deno.test('Keyword parsing #integration', async (t) => { assert(parser.errors.length === 0, 'Parser should not error'); - assert(parserOutput.statement, 'Parser should generate statements'); + v.file(parserOutput, [ + ['declaration', ['lettuce', v.none, [['constant', ['INT', '1']]]]], + [ + 'if', + [ + [ + 'expression', + [['id', 'lettuce']], + [['declaration', ['spiffy', v.none, [['constant', ['INT', '2']]]]]], + ], + [ + 'expression', + [['id', 'lettuce']], + [['declaration', ['elifShmelif', v.none, [['constant', ['INT', '3']]]]]], + ], + ], + [['declaration', ['elsevier', v.none, [['constant', ['INT', '4']]]]]], + ], + ]); }); await t.step('false positive keyword snippets', async (t) => { @@ -50,7 +69,9 @@ Deno.test('Keyword parsing #integration', async (t) => { assert(parser.errors.length === 0, 'Parser should not error'); - assert(parserOutput.statement, 'Parser should generate statements'); + v.file(parserOutput, [ + ['declaration', ['coffeebreak', v.none, [['constant', ['INT', '8']]]]], + ]); }); await t.step('continue', () => { @@ -65,7 +86,9 @@ Deno.test('Keyword parsing #integration', async (t) => { assert(parser.errors.length === 0, 'Parser should not error'); - assert(parserOutput.statement, 'Parser should generate statements'); + v.file(parserOutput, [ + ['declaration', ['dareIcontinue', v.none, [['constant', ['INT', '9']]]]], + ]); }); await t.step('return', () => { @@ -80,7 +103,10 @@ Deno.test('Keyword parsing #integration', async (t) => { assert(parser.errors.length === 0, 'Parser should not error'); - assert(parserOutput.statement, 'Parser should generate statements'); + v.file(parserOutput, [ + ['declaration', ['returnOfTheJedi', v.none, [['constant', ['INT', '10']]]]], + ['return', [['id', 'OfTheJedi']]], + ]); }); await t.step('and, or, & not', () => { @@ -95,7 +121,10 @@ Deno.test('Keyword parsing #integration', async (t) => { assert(parser.errors.length === 0, 'Parser should not error'); - assert(parserOutput.statement, 'Parser should generate statements'); + v.file(parserOutput, [ + ['declaration', ['andor', v.none, [['constant', ['INT', '11']]]]], + ['declaration', ['notInNottingham', v.none, [['prefix', 'not', ['id', 'andor']]]]], + ]); }); await t.step('in', () => { @@ -110,7 +139,7 @@ Deno.test('Keyword parsing #integration', async (t) => { assert(parser.errors.length === 0, 'Parser should not error'); - assert(parserOutput.statement, 'Parser should generate statements'); + v.file(parserOutput, [['declaration', ['spinach', v.none, [['constant', ['INT', '13']]]]]]); }); }); }); From e670b464987e89a0715bafaec84497dfa7f9e2ec Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Mon, 24 Nov 2025 13:47:48 -0600 Subject: [PATCH 19/21] test(expressions): use validate --- .../test/integration/expressions.test.ts | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/apps/parser/test/integration/expressions.test.ts b/apps/parser/test/integration/expressions.test.ts index 4384c56..5d9932a 100644 --- a/apps/parser/test/integration/expressions.test.ts +++ b/apps/parser/test/integration/expressions.test.ts @@ -1,5 +1,6 @@ import * as TestSubject from '@encode/parser/lib'; -import { assert, assertEquals, assertGreater } from '@std/assert'; +import { v } from '@encode/parser/lib'; +import { assert, assertEquals } from '@std/assert'; import { performParsingTestCase, useGlobalSettings } from '@/test/utils/mod.ts'; Deno.test('Expression parsing #integration', async (t) => { @@ -25,8 +26,21 @@ Deno.test('Expression parsing #integration', async (t) => { assert(parser.errors.length === 0, 'Parser should not error'); - assert(parserOutput.statement); - assertGreater(parserOutput.statement.length, 0, 'Statements should be generated'); + v.file(parserOutput, [ + [ + 'declaration', + [ + 'a', + v.none, + [ + ['nested', [['constant', ['INT', '1']], v.none, '*', [['constant', ['INT', '2']]]]], + v.none, + '+', + [['constant', ['INT', '3']]], + ], + ], + ], + ]); assertEquals(precOutput, 1, 'Expression should be reordered'); }); From 50863c870783f1ca1dcdd10612f4dcfe035067ee Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Tue, 25 Nov 2025 10:10:11 -0600 Subject: [PATCH 20/21] chore(parser): create type for indexed function call --- apps/parser/src/parser.ts | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/apps/parser/src/parser.ts b/apps/parser/src/parser.ts index 246009d..bb7b88b 100644 --- a/apps/parser/src/parser.ts +++ b/apps/parser/src/parser.ts @@ -11,7 +11,7 @@ export class EncodeParser extends CstParser { this.MANY(() => this.SUBRULE(this.statement)); }); - private statement = this.RULE('statement', () => { + public statement: ParserMethod<[], CstNode> = this.RULE('statement', () => { this.OR([ { ALT: () => { @@ -94,7 +94,7 @@ export class EncodeParser extends CstParser { ]); }); - private ifPredBody = this.RULE('ifPredBody', () => { + public ifPredBody: ParserMethod<[], CstNode> = this.RULE('ifPredBody', () => { this.CONSUME(Tokens.LPAREN); this.OR([ { @@ -112,13 +112,13 @@ export class EncodeParser extends CstParser { this.SUBRULE(this.body); }); - private body = this.RULE('body', () => { + public body: ParserMethod<[], CstNode> = this.RULE('body', () => { this.CONSUME(Tokens.LCURLY); this.MANY(() => this.SUBRULE(this.statement)); this.CONSUME(Tokens.RCURLY); }); - private declaration = this.RULE('declaration', () => { + public declaration: ParserMethod<[], CstNode> = this.RULE('declaration', () => { this.CONSUME(Tokens.ID); this.OPTION(() => { this.CONSUME(Tokens.COLON); @@ -130,7 +130,7 @@ export class EncodeParser extends CstParser { }); }); - private expression = this.RULE('expression', () => { + public expression: ParserMethod<[], CstNode> = this.RULE('expression', () => { this.SUBRULE(this.value); this.OPTION(() => this.CONSUME(Tokens.PostFix)); @@ -140,7 +140,7 @@ export class EncodeParser extends CstParser { }); }); - private value = this.RULE('value', () => { + public value: ParserMethod<[], CstNode> = this.RULE('value', () => { this.OR([ { ALT: () => { @@ -164,13 +164,19 @@ export class EncodeParser extends CstParser { ]); }); - private constant = this.RULE('constant', () => + public constant: ParserMethod<[], CstNode> = this.RULE('constant', () => this.OR(Tokens.literals.map((t) => ({ ALT: () => this.CONSUME(t) }))), ); - private type = this.RULE('type', () => this.CONSUME(Tokens.BASIC_TYPE)); + public type: ParserMethod<[], CstNode> = this.RULE('type', () => this.CONSUME(Tokens.BASIC_TYPE)); } +export type EncodeRule = { + [Property in keyof EncodeParser]: EncodeParser[Property] extends ParserMethod<[], CstNode> + ? Property + : never; +}[keyof EncodeParser]; + export const parser: EncodeParser = new EncodeParser(); export const BaseCstVisitor: ReturnType = parser.getBaseCstVisitorConstructor(); From caa52b0530b629291eb57e16a16ccf8523ad97c5 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Tue, 25 Nov 2025 10:46:24 -0600 Subject: [PATCH 21/21] feat(tests): use startAt parameter TODO: create dependent type for output --- apps/parser/src/validate.ts | 123 ++++++++++-------- apps/parser/test/integration/comments.test.ts | 12 +- .../test/integration/control-flow.test.ts | 43 +++--- .../test/integration/data-types.test.ts | 44 +++++-- .../test/integration/expressions.test.ts | 18 +-- apps/parser/test/integration/keywords.test.ts | 25 ++-- apps/parser/test/utils/mod.ts | 13 +- 7 files changed, 166 insertions(+), 112 deletions(-) diff --git a/apps/parser/src/validate.ts b/apps/parser/src/validate.ts index 046be03..09853dc 100644 --- a/apps/parser/src/validate.ts +++ b/apps/parser/src/validate.ts @@ -1,15 +1,14 @@ import { assert, assertEquals, assertGreater } from '@std/assert'; import type { - BodyCstChildren, - ConstantCstChildren, - DeclarationCstChildren, - ExpressionCstChildren, - FileCstChildren, - IfPredBodyCstChildren, - StatementCstChildren, + BodyCstNode, + ConstantCstNode, + DeclarationCstNode, + ExpressionCstNode, + FileCstNode, + IfPredBodyCstNode, StatementCstNode, - TypeCstChildren, - ValueCstChildren, + TypeCstNode, + ValueCstNode, } from '@/generated/cst-types.ts'; export const skip = undefined; @@ -28,19 +27,21 @@ export type ValidationFunction = | typeof constant | typeof type; -export function file(node: FileCstChildren, args?: Statement[] | None) { +export function file(node: FileCstNode, args?: Statement[] | None) { + assertEquals(node.name, 'file'); + const file = node.children; // biome-ignore lint/complexity/useOptionalChain: args could be false without being nullish if (args && args.length) { - assert(node.statement, `File: expected 1+ statements but received ${node.statement?.length}`); + assert(file.statement, `File: expected 1+ statements but received ${file.statement?.length}`); } if (args === false) { assert( - !node.statement?.length, - `File: expected 0 statements but received ${node.statement?.length}`, + !file.statement?.length, + `File: expected 0 statements but received ${file.statement?.length}`, ); } - if (node.statement && args !== false) { - statement_list(node.statement, args); + if (file.statement && args !== false) { + statement_list(file.statement, args); } } @@ -59,7 +60,7 @@ function statement_list(statements: StatementCstNode[], args?: Statement[]) { ); } for (let i = 0; i < statements.length; i++) { - statement(statements[i].children, args?.[i]); + statement(statements[i], args?.[i]); } } @@ -89,7 +90,9 @@ export type Statement = | ['expression', Expression | Skip] | None | Skip; -export function statement(stmt: StatementCstChildren, args?: Statement) { +export function statement(node: StatementCstNode, args?: Statement) { + assertEquals(node.name, 'statement'); + const stmt = node.children; if (args === none) { assert( Object.keys(stmt).length === 1 && stmt.SEMI, @@ -103,7 +106,7 @@ export function statement(stmt: StatementCstChildren, args?: Statement) { `Statement: expected ${args[0]} but received declaration`, ); } - declaration(stmt.declaration[0].children, (args?.[1] as Declaration) || skip); + declaration(stmt.declaration[0], (args?.[1] as Declaration) || skip); } else if (stmt.BREAK) { if (args) { assertEquals(args[0], 'break', `Statement: expected ${args[0]} but received break`); @@ -125,7 +128,7 @@ export function statement(stmt: StatementCstChildren, args?: Statement) { ); if (stmt.expression) { assertEquals(stmt.expression.length, 1); - expression(stmt.expression[0].children, (args?.[1] as Expression) || skip); + expression(stmt.expression[0], (args?.[1] as Expression) || skip); } } else if (stmt.IF && stmt.ifPredBody) { if (args) { @@ -141,10 +144,10 @@ export function statement(stmt: StatementCstChildren, args?: Statement) { `Statement: expected ${p.length} if-preds but received ${predBody.length}`, ); } - ifPredBody(predBody[bodyCount++].children, (p && (p[0] as IfPredBody)) || skip); + ifPredBody(predBody[bodyCount++], (p && (p[0] as IfPredBody)) || skip); if (stmt.ELIF) { stmt.ELIF.forEach(() => { - ifPredBody(predBody[bodyCount].children, (p && (p[bodyCount] as IfPredBody)) || skip); + ifPredBody(predBody[bodyCount], (p && (p[bodyCount] as IfPredBody)) || skip); bodyCount++; }); } @@ -153,7 +156,7 @@ export function statement(stmt: StatementCstChildren, args?: Statement) { `Statement > else: expected ${!!e} but received ${!!stmt.body}`, ); if (stmt.ELSE && stmt.body) { - body(stmt.body[0].children, e as Body); + body(stmt.body[0], e as Body); } } else if (stmt.WHILE && stmt.expression) { if (args) { @@ -170,9 +173,9 @@ export function statement(stmt: StatementCstChildren, args?: Statement) { stmt.body?.[bodyCount], `Statement > do: expected body but received ${stmt.body?.[bodyCount]}`, ); - body(stmt.body[bodyCount++].children, d as Body); + body(stmt.body[bodyCount++], d as Body); } - expression(stmt.expression[0].children, we as Expression); + expression(stmt.expression[0], we as Expression); assert( wb === skip || (wb ? !stmt.SEMI : stmt.SEMI), `Statement > while: expected ${!!wb} but received ${!stmt.SEMI}`, @@ -182,7 +185,7 @@ export function statement(stmt: StatementCstChildren, args?: Statement) { stmt.body?.[bodyCount], `Statement > while: expected body but received ${stmt.body?.[bodyCount]}`, ); - body(stmt.body[bodyCount++].children, wb as Body); + body(stmt.body[bodyCount++], wb as Body); } assert( f === skip || (f ? stmt.FINALLY : !stmt.FINALLY), @@ -193,18 +196,18 @@ export function statement(stmt: StatementCstChildren, args?: Statement) { stmt.body?.[bodyCount], `Statement > finally: expected body but received ${stmt.body?.[bodyCount]}`, ); - body(stmt.body[bodyCount++].children, f as Body); + body(stmt.body[bodyCount++], f as Body); } } else if (stmt.body) { if (args) { assertEquals(args[0], 'body', `Statement: expected ${args[0]} but received body`); } - body(stmt.body[0].children, args?.[1] as Body); + body(stmt.body[0], args?.[1] as Body); } else if (stmt.expression) { if (args) { assertEquals(args[0], 'expression', `Statement: expected ${args[0]} but received expression`); } - expression(stmt.expression[0].children, args?.[1] as Expression); + expression(stmt.expression[0], args?.[1] as Expression); } else { throw new Error(`Validation: unhandled statement type!\n${JSON.stringify(stmt, null, 2)}`); } @@ -214,7 +217,9 @@ export type IfPredBody = | ['declaration', Declaration | Skip, Body | Skip] | ['expression', Expression | Skip, Body | Skip] | Skip; -export function ifPredBody(predBody: IfPredBodyCstChildren, args?: IfPredBody) { +export function ifPredBody(node: IfPredBodyCstNode, args?: IfPredBody) { + assertEquals(node.name, 'ifPredBody'); + const predBody = node.children; if (predBody.LET && predBody.declaration) { if (args) { assertEquals( @@ -223,7 +228,7 @@ export function ifPredBody(predBody: IfPredBodyCstChildren, args?: IfPredBody) { `IfPredBody: expected ${args[0]} but received declaration`, ); } - declaration(predBody.declaration[0].children, args?.[1] as Declaration | Skip); + declaration(predBody.declaration[0], args?.[1] as Declaration | Skip); } else if (predBody.expression) { if (args) { assertEquals( @@ -232,16 +237,18 @@ export function ifPredBody(predBody: IfPredBodyCstChildren, args?: IfPredBody) { `IfPredBody: expected ${args[0]} but received expression`, ); } - expression(predBody.expression[0].children, args?.[1] as Expression | Skip); + expression(predBody.expression[0], args?.[1] as Expression | Skip); } else { throw new Error(`Validation: unhandled ifPredBody type!\n${JSON.stringify(predBody, null, 2)}`); } - body(predBody.body[0].children, args?.[2]); + body(predBody.body[0], args?.[2]); } export type Declaration = [string | Skip, Type | None | Skip, Expression | None | Skip] | Skip; -export function declaration(decl: DeclarationCstChildren, args?: Declaration) { +export function declaration(node: DeclarationCstNode, args?: Declaration) { + assertEquals(node.name, 'declaration'); + const decl = node.children; const [id, t, e] = args ?? []; assertEquals(decl.ID.length, 1); if (id) { @@ -259,7 +266,7 @@ export function declaration(decl: DeclarationCstChildren, args?: Declaration) { ); if (decl.type) { assertEquals(decl.type.length, 1); - type(decl.type[0].children, t || skip); + type(decl.type[0], t || skip); } assert( e === skip || (e ? decl.expression : !decl.expression), @@ -267,27 +274,29 @@ export function declaration(decl: DeclarationCstChildren, args?: Declaration) { ); if (decl.expression) { assertEquals(decl.expression.length, 1); - expression(decl.expression[0].children, e || skip); + expression(decl.expression[0], e || skip); } } export type Body = Statement[] | None | Skip; -export function body(node: BodyCstChildren, args?: T) { - assertEquals(node.LCURLY?.at(0)?.image, '{', 'Body: missing {'); +export function body(node: BodyCstNode, args?: T) { + assertEquals(node.name, 'body'); + const body = node.children; + assertEquals(body.LCURLY?.at(0)?.image, '{', 'Body: missing {'); // biome-ignore lint/complexity/useOptionalChain: args could be false without being nullish if (args && args.length) { - assert(node.statement, `Body: expected 1+ statements but received ${node.statement?.length}`); + assert(body.statement, `Body: expected 1+ statements but received ${body.statement?.length}`); } if (args === false) { assert( - !node.statement?.length, - `Body: expected 0 statements but received ${node.statement?.length}`, + !body.statement?.length, + `Body: expected 0 statements but received ${body.statement?.length}`, ); } - if (node.statement && args !== false) { - statement_list(node.statement, args); + if (body.statement && args !== false) { + statement_list(body.statement, args); } - assertEquals(node.RCURLY?.at(0)?.image, '}', 'Body: missing }'); + assertEquals(body.RCURLY?.at(0)?.image, '}', 'Body: missing }'); } export type Expression = Parameters[1]; @@ -305,10 +314,12 @@ export function expression< T | Skip, ] | Skip, ->(expr: ExpressionCstChildren, args?: T) { +>(node: ExpressionCstNode, args?: T) { + assertEquals(node.name, 'expression'); + const expr = node.children; const [val, pf, op, rhs] = args ?? []; assert(expr.value?.at(0)?.children); - value(expr.value[0].children, val); + value(expr.value[0], val); assert( pf === skip || (pf ? expr.PostFix : !expr.PostFix), `Expression > PostFix: expected ${!!pf} but received ${!!expr.PostFix}`, @@ -347,7 +358,7 @@ export function expression< ); if (expr.expression) { assertEquals(expr.expression.length, 1); - expression(expr.expression[0].children, rhs || skip); + expression(expr.expression[0], rhs || skip); } } @@ -361,17 +372,19 @@ export function value< | ['id', string | Skip] | ['prefix', string | Skip, T] | Skip, ->(val: ValueCstChildren, args?: T) { +>(node: ValueCstNode, args?: T) { + assertEquals(node.name, 'value'); + const val = node.children; if (val.expression) { if (args) { assertEquals(args[0], 'nested', `Value: expected ${args[0]} but received nested`); } - expression(val.expression[0].children, args?.at(1) as Expression); + expression(val.expression[0], args?.at(1) as Expression); } else if (val.constant) { if (args) { assertEquals(args[0], 'constant', `Value: expected ${args[0]} but received constant`); } - constant(val.constant[0].children, args?.at(1) as Constant); + constant(val.constant[0], args?.at(1) as Constant); } else if (val.ID) { assertEquals(val.ID.length, 1); if (args) { @@ -404,14 +417,16 @@ export function value< } else { assertGreater(val.UnOp?.[0].image.length, 0); } - value(val.value[0].children, args?.at(2) as T); + value(val.value[0], args?.at(2) as T); } else { throw new Error(`Validation: unhandled value type!\n${JSON.stringify(val, null, 2)}`); } } -export type Constant = [keyof ConstantCstChildren, string] | Skip; -export function constant(c: ConstantCstChildren, args?: Constant) { +export type Constant = [keyof ConstantCstNode['children'], string] | Skip; +export function constant(node: ConstantCstNode, args?: Constant) { + assertEquals(node.name, 'constant'); + const c = node.children; assert( c.BIN || c.BOOL || c.CMPX || c.INT || c.REAL || c.STRING, `Constant: unexpected literal type ${Object.keys(c)}`, @@ -438,7 +453,9 @@ export function constant(c: ConstantCstChildren, args?: Constant) { } export type Type = string | Skip; -export function type(t: TypeCstChildren, args?: Type) { +export function type(node: TypeCstNode, args?: Type) { + assertEquals(node.name, 'type'); + const t = node.children; assertEquals( t.BASIC_TYPE?.length, 1, diff --git a/apps/parser/test/integration/comments.test.ts b/apps/parser/test/integration/comments.test.ts index a6145d5..0953ae0 100644 --- a/apps/parser/test/integration/comments.test.ts +++ b/apps/parser/test/integration/comments.test.ts @@ -2,6 +2,7 @@ import * as TestSubject from '@encode/parser/lib'; import { v } from '@encode/parser/lib'; import { assertEquals } from '@std/assert'; import { performParsingTestCase, useGlobalSettings } from '@/test/utils/mod.ts'; +import type { FileCstNode, StatementCstNode } from '../../generated/cst-types.ts'; Deno.test('Comment parsing #integration', async (t) => { using _globalSettings = useGlobalSettings({ debugTrees: true }); @@ -26,7 +27,8 @@ Deno.test('Comment parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - v.file(parserOutput, v.none); + // TODO use dependent types so cast is unnecessary + v.file(parserOutput as FileCstNode, v.none); }); await t.step('collapsed multiline comment', () => { @@ -51,7 +53,7 @@ Deno.test('Comment parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - v.file(parserOutput, v.none); + v.file(parserOutput as FileCstNode, v.none); }); await t.step('comments embedded in a string', () => { @@ -59,6 +61,7 @@ Deno.test('Comment parsing #integration', async (t) => { code: "let str = '/*****/ //'; // comments embedded in a string", parser, + startAt: 'statement', precedenceHandler, printer, typeAnalyzer, @@ -66,8 +69,9 @@ Deno.test('Comment parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - v.file(parserOutput, [ - ['declaration', ['str', v.none, [['constant', ['STRING', "'/*****/ //'"]]]]], + v.statement(parserOutput as StatementCstNode, [ + 'declaration', + ['str', v.none, [['constant', ['STRING', "'/*****/ //'"]]]], ]); }); }); diff --git a/apps/parser/test/integration/control-flow.test.ts b/apps/parser/test/integration/control-flow.test.ts index f09afcc..aa3edc1 100644 --- a/apps/parser/test/integration/control-flow.test.ts +++ b/apps/parser/test/integration/control-flow.test.ts @@ -2,6 +2,7 @@ import * as TestSubject from '@encode/parser/lib'; import { v } from '@encode/parser/lib'; import { assertEquals } from '@std/assert'; import { performParsingTestCase, useGlobalSettings } from '@/test/utils/mod.ts'; +import type { FileCstNode, StatementCstNode } from '../../generated/cst-types.ts'; Deno.test('Control flow parsing #integration', async (t) => { using _globalSettings = useGlobalSettings({ debugTrees: true }); @@ -26,7 +27,7 @@ Deno.test('Control flow parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - v.file(parserOutput, [ + v.file(parserOutput as FileCstNode, [ ['declaration', ['a', v.none, [['constant', ['INT', '0']]]]], [ 'if', @@ -63,7 +64,7 @@ Deno.test('Control flow parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - v.file(parserOutput, [ + v.file(parserOutput as FileCstNode, [ ['declaration', ['a', v.none, [['constant', ['INT', '0']]]]], ['if', v.skip, [['expression', [['id', 'b'], v.none, '=', [['constant', ['INT', '2']]]]]]], ]); @@ -79,6 +80,7 @@ Deno.test('Control flow parsing #integration', async (t) => { ), parser, + startAt: 'statement', precedenceHandler, printer, typeAnalyzer, @@ -86,8 +88,12 @@ Deno.test('Control flow parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - v.file(parserOutput, [ - ['while', [], [['id', 'a'], v.none, '<', [['constant', ['INT', '3']]]], v.none, v.none], + v.statement(parserOutput as StatementCstNode, [ + 'while', + [], + [['id', 'a'], v.none, '<', [['constant', ['INT', '3']]]], + v.none, + v.none, ]); assertEquals(typeOutput.warnings, 0, 'TypeAnalyzer should not report any warnings'); @@ -108,6 +114,7 @@ Deno.test('Control flow parsing #integration', async (t) => { ].join('\n'), parser, + startAt: 'statement', precedenceHandler, printer, typeAnalyzer, @@ -115,24 +122,22 @@ Deno.test('Control flow parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - v.file(parserOutput, [ + v.statement(parserOutput as StatementCstNode, [ + 'while', + v.none, + [['id', 'b'], v.none, '>', [['constant', ['INT', '4']]]], + [ + ['declaration', ['c', v.none, [['constant', ['INT', '1']]]]], + ['if', [['expression', [['id', 'a']], [['continue']]]], v.none], + ], [ - 'while', - v.none, - [['id', 'b'], v.none, '>', [['constant', ['INT', '4']]]], - [ - ['declaration', ['c', v.none, [['constant', ['INT', '1']]]]], - ['if', [['expression', [['id', 'a']], [['continue']]]], v.none], - ], [ + 'return', [ - 'return', - [ - ['nested', [['constant', ['INT', '1']], v.none, '+', [['constant', ['INT', '2']]]]], - v.none, - '+', - [['id', 'c']], - ], + ['nested', [['constant', ['INT', '1']], v.none, '+', [['constant', ['INT', '2']]]]], + v.none, + '+', + [['id', 'c']], ], ], ], diff --git a/apps/parser/test/integration/data-types.test.ts b/apps/parser/test/integration/data-types.test.ts index f26882f..34293fa 100644 --- a/apps/parser/test/integration/data-types.test.ts +++ b/apps/parser/test/integration/data-types.test.ts @@ -2,6 +2,7 @@ import * as TestSubject from '@encode/parser/lib'; import { v } from '@encode/parser/lib'; import { assertEquals } from '@std/assert'; import { performParsingTestCase, useGlobalSettings } from '@/test/utils/mod.ts'; +import type { StatementCstNode } from '../../generated/cst-types.ts'; Deno.test('Data type parsing #integration', async (t) => { using _globalSettings = useGlobalSettings({ debugTrees: true }); @@ -19,6 +20,7 @@ Deno.test('Data type parsing #integration', async (t) => { code: 'let real = 1.0;', parser, + startAt: 'statement', precedenceHandler, printer, typeAnalyzer, @@ -26,7 +28,10 @@ Deno.test('Data type parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - v.file(parserOutput, [['declaration', ['real', v.none, [['constant', ['REAL', '1.0']]]]]]); + v.statement(parserOutput as StatementCstNode, [ + 'declaration', + ['real', v.none, [['constant', ['REAL', '1.0']]]], + ]); }); await t.step('integer literal', () => { @@ -34,6 +39,7 @@ Deno.test('Data type parsing #integration', async (t) => { code: 'let integer = 21;', parser, + startAt: 'statement', precedenceHandler, printer, typeAnalyzer, @@ -41,7 +47,10 @@ Deno.test('Data type parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - v.file(parserOutput, [['declaration', ['integer', v.none, [['constant', ['INT', '21']]]]]]); + v.statement(parserOutput as StatementCstNode, [ + 'declaration', + ['integer', v.none, [['constant', ['INT', '21']]]], + ]); }); await t.step('string literal', () => { @@ -49,6 +58,7 @@ Deno.test('Data type parsing #integration', async (t) => { code: "let str = 'Hello, World!';", parser, + startAt: 'statement', precedenceHandler, printer, typeAnalyzer, @@ -56,8 +66,9 @@ Deno.test('Data type parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - v.file(parserOutput, [ - ['declaration', ['str', v.none, [['constant', ['STRING', "'Hello, World!'"]]]]], + v.statement(parserOutput as StatementCstNode, [ + 'declaration', + ['str', v.none, [['constant', ['STRING', "'Hello, World!'"]]]], ]); }); @@ -66,6 +77,7 @@ Deno.test('Data type parsing #integration', async (t) => { code: 'let flag = true;', parser, + startAt: 'statement', precedenceHandler, printer, typeAnalyzer, @@ -73,7 +85,10 @@ Deno.test('Data type parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - v.file(parserOutput, [['declaration', ['flag', v.none, [['constant', ['BOOL', 'true']]]]]]); + v.statement(parserOutput as StatementCstNode, [ + 'declaration', + ['flag', v.none, [['constant', ['BOOL', 'true']]]], + ]); }); await t.step('bit literal', () => { @@ -81,6 +96,7 @@ Deno.test('Data type parsing #integration', async (t) => { code: 'let bits = 0xff;', parser, + startAt: 'statement', precedenceHandler, printer, typeAnalyzer, @@ -88,7 +104,10 @@ Deno.test('Data type parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - v.file(parserOutput, [['declaration', ['bits', v.none, [['constant', ['BIN', '0xff']]]]]]); + v.statement(parserOutput as StatementCstNode, [ + 'declaration', + ['bits', v.none, [['constant', ['BIN', '0xff']]]], + ]); }); await t.step('complex number literal', () => { @@ -96,6 +115,7 @@ Deno.test('Data type parsing #integration', async (t) => { code: 'let imag = 1.0 + 2.0i;', parser, + startAt: 'statement', precedenceHandler, printer, typeAnalyzer, @@ -103,14 +123,12 @@ Deno.test('Data type parsing #integration', async (t) => { assertEquals(parser.errors.length, 0, 'Parser should not error'); - v.file(parserOutput, [ + v.statement(parserOutput as StatementCstNode, [ + 'declaration', [ - 'declaration', - [ - 'imag', - v.none, - [['constant', ['REAL', '1.0']], v.none, '+', [['constant', ['CMPX', '2.0i']]]], - ], + 'imag', + v.none, + [['constant', ['REAL', '1.0']], v.none, '+', [['constant', ['CMPX', '2.0i']]]], ], ]); }); diff --git a/apps/parser/test/integration/expressions.test.ts b/apps/parser/test/integration/expressions.test.ts index 5d9932a..c155c1e 100644 --- a/apps/parser/test/integration/expressions.test.ts +++ b/apps/parser/test/integration/expressions.test.ts @@ -2,6 +2,7 @@ import * as TestSubject from '@encode/parser/lib'; import { v } from '@encode/parser/lib'; import { assert, assertEquals } from '@std/assert'; import { performParsingTestCase, useGlobalSettings } from '@/test/utils/mod.ts'; +import type { StatementCstNode } from '../../generated/cst-types.ts'; Deno.test('Expression parsing #integration', async (t) => { using _globalSettings = useGlobalSettings({ debugTrees: true }); @@ -19,6 +20,7 @@ Deno.test('Expression parsing #integration', async (t) => { code: 'let a = 1 * 2 + 3;', parser, + startAt: 'statement', precedenceHandler, printer, typeAnalyzer, @@ -26,18 +28,16 @@ Deno.test('Expression parsing #integration', async (t) => { assert(parser.errors.length === 0, 'Parser should not error'); - v.file(parserOutput, [ + v.statement(parserOutput as StatementCstNode, [ + 'declaration', [ - 'declaration', + 'a', + v.none, [ - 'a', + ['nested', [['constant', ['INT', '1']], v.none, '*', [['constant', ['INT', '2']]]]], v.none, - [ - ['nested', [['constant', ['INT', '1']], v.none, '*', [['constant', ['INT', '2']]]]], - v.none, - '+', - [['constant', ['INT', '3']]], - ], + '+', + [['constant', ['INT', '3']]], ], ], ]); diff --git a/apps/parser/test/integration/keywords.test.ts b/apps/parser/test/integration/keywords.test.ts index 37c5b6b..31ca793 100644 --- a/apps/parser/test/integration/keywords.test.ts +++ b/apps/parser/test/integration/keywords.test.ts @@ -2,6 +2,7 @@ import * as TestSubject from '@encode/parser/lib'; import { v } from '@encode/parser/lib'; import { assert } from '@std/assert'; import { performParsingTestCase, useGlobalSettings } from '@/test/utils/mod.ts'; +import type { FileCstNode, StatementCstNode } from '../../generated/cst-types.ts'; Deno.test('Keyword parsing #integration', async (t) => { using _globalSettings = useGlobalSettings({ debugTrees: true }); @@ -35,7 +36,7 @@ Deno.test('Keyword parsing #integration', async (t) => { assert(parser.errors.length === 0, 'Parser should not error'); - v.file(parserOutput, [ + v.file(parserOutput as FileCstNode, [ ['declaration', ['lettuce', v.none, [['constant', ['INT', '1']]]]], [ 'if', @@ -62,6 +63,7 @@ Deno.test('Keyword parsing #integration', async (t) => { code: 'let coffeebreak = 8; // break', parser, + startAt: 'statement', precedenceHandler, printer, typeAnalyzer, @@ -69,8 +71,9 @@ Deno.test('Keyword parsing #integration', async (t) => { assert(parser.errors.length === 0, 'Parser should not error'); - v.file(parserOutput, [ - ['declaration', ['coffeebreak', v.none, [['constant', ['INT', '8']]]]], + v.statement(parserOutput as StatementCstNode, [ + 'declaration', + ['coffeebreak', v.none, [['constant', ['INT', '8']]]], ]); }); @@ -79,6 +82,7 @@ Deno.test('Keyword parsing #integration', async (t) => { code: 'let dareIcontinue = 9; // continue', parser, + startAt: 'statement', precedenceHandler, printer, typeAnalyzer, @@ -86,8 +90,9 @@ Deno.test('Keyword parsing #integration', async (t) => { assert(parser.errors.length === 0, 'Parser should not error'); - v.file(parserOutput, [ - ['declaration', ['dareIcontinue', v.none, [['constant', ['INT', '9']]]]], + v.statement(parserOutput as StatementCstNode, [ + 'declaration', + ['dareIcontinue', v.none, [['constant', ['INT', '9']]]], ]); }); @@ -103,7 +108,7 @@ Deno.test('Keyword parsing #integration', async (t) => { assert(parser.errors.length === 0, 'Parser should not error'); - v.file(parserOutput, [ + v.file(parserOutput as FileCstNode, [ ['declaration', ['returnOfTheJedi', v.none, [['constant', ['INT', '10']]]]], ['return', [['id', 'OfTheJedi']]], ]); @@ -121,7 +126,7 @@ Deno.test('Keyword parsing #integration', async (t) => { assert(parser.errors.length === 0, 'Parser should not error'); - v.file(parserOutput, [ + v.file(parserOutput as FileCstNode, [ ['declaration', ['andor', v.none, [['constant', ['INT', '11']]]]], ['declaration', ['notInNottingham', v.none, [['prefix', 'not', ['id', 'andor']]]]], ]); @@ -132,6 +137,7 @@ Deno.test('Keyword parsing #integration', async (t) => { code: 'let spinach = 13; // in', parser, + startAt: 'statement', precedenceHandler, printer, typeAnalyzer, @@ -139,7 +145,10 @@ Deno.test('Keyword parsing #integration', async (t) => { assert(parser.errors.length === 0, 'Parser should not error'); - v.file(parserOutput, [['declaration', ['spinach', v.none, [['constant', ['INT', '13']]]]]]); + v.statement(parserOutput as StatementCstNode, [ + 'declaration', + ['spinach', v.none, [['constant', ['INT', '13']]]], + ]); }); }); }); diff --git a/apps/parser/test/utils/mod.ts b/apps/parser/test/utils/mod.ts index 1e6589e..ccbf328 100644 --- a/apps/parser/test/utils/mod.ts +++ b/apps/parser/test/utils/mod.ts @@ -3,12 +3,12 @@ import { debug, EncodeLexer, type EncodeParser, + type EncodeRule, Globals, type PrecedenceHandler, type TypeAnalyzer, } from '@encode/parser/lib'; -import type { ILexingResult } from 'chevrotain'; -import type { FileCstChildren } from '@/generated/cst-types.ts'; +import type { CstNode, ILexingResult } from 'chevrotain'; export interface TestCaseParameters { /** @@ -17,6 +17,7 @@ export interface TestCaseParameters { * **Note:** We choose not to instantiate this ourselves in case we want to inject something else, e.g. a shim or an experimental impl */ parser: EncodeParser; + startAt?: EncodeRule; /** * The parser to use for parsing Encode code. * @@ -43,7 +44,7 @@ export interface TestCaseParameters { export interface TestCaseOutputs { lexingResult: ILexingResult; - parserOutput: FileCstChildren; + parserOutput: CstNode; beforeReorder: string; afterReorder: string; precOutput: number; @@ -64,11 +65,11 @@ export interface TestCaseOutputs { * @returns the results of executing the test procedure to be examined by assertions */ export function performParsingTestCase(params: TestCaseParameters): TestCaseOutputs { - const { code, parser, printer, typeAnalyzer, precedenceHandler } = params; + const { code, parser, startAt = 'file', printer, typeAnalyzer, precedenceHandler } = params; const lexingResult = EncodeLexer.tokenize(code); parser.input = lexingResult.tokens; - const parserOutput = parser.file(); + const parserOutput = parser[startAt](); // cache printer.output const printerOutput = printer.output; @@ -101,7 +102,7 @@ export function performParsingTestCase(params: TestCaseParameters): TestCaseOutp const testCaseOutputs: TestCaseOutputs = { lexingResult, - parserOutput: parserOutput.children, + parserOutput, beforeReorder, afterReorder, precOutput: precedenceHandler.reordered,