@vibe-lang/runtime 0.2.8 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/package.json +1 -1
  2. package/src/ast/index.ts +6 -0
  3. package/src/lexer/index.ts +2 -0
  4. package/src/parser/index.ts +14 -0
  5. package/src/parser/parse.ts +138 -1
  6. package/src/parser/test/errors/missing-tokens.test.ts +25 -0
  7. package/src/parser/test/errors/model-declaration.test.ts +14 -8
  8. package/src/parser/test/errors/unclosed-delimiters.test.ts +200 -34
  9. package/src/parser/test/errors/unexpected-tokens.test.ts +15 -1
  10. package/src/parser/test/literals.test.ts +29 -0
  11. package/src/parser/test/model-declaration.test.ts +14 -0
  12. package/src/parser/visitor.ts +9 -0
  13. package/src/runtime/async/dependencies.ts +8 -1
  14. package/src/runtime/async/test/dependencies.test.ts +27 -1
  15. package/src/runtime/exec/statements.ts +51 -1
  16. package/src/runtime/index.ts +2 -3
  17. package/src/runtime/modules.ts +1 -1
  18. package/src/runtime/stdlib/index.ts +7 -11
  19. package/src/runtime/stdlib/tools/index.ts +5 -122
  20. package/src/runtime/stdlib/utils/index.ts +58 -0
  21. package/src/runtime/step.ts +4 -0
  22. package/src/runtime/test/core-functions.test.ts +19 -10
  23. package/src/runtime/test/throw.test.ts +220 -0
  24. package/src/runtime/test/tool-execution.test.ts +30 -30
  25. package/src/runtime/types.ts +4 -1
  26. package/src/runtime/validation.ts +6 -0
  27. package/src/semantic/analyzer-context.ts +2 -0
  28. package/src/semantic/analyzer-visitors.ts +149 -2
  29. package/src/semantic/analyzer.ts +1 -0
  30. package/src/semantic/test/fixtures/exports.vibe +25 -0
  31. package/src/semantic/test/function-return-check.test.ts +215 -0
  32. package/src/semantic/test/imports.test.ts +66 -2
  33. package/src/semantic/test/prompt-validation.test.ts +44 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-lang/runtime",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "Vibe language runtime and CLI",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
package/src/ast/index.ts CHANGED
@@ -64,6 +64,7 @@ export type Statement =
64
64
  | ToolDeclaration
65
65
  | ReturnStatement
66
66
  | BreakStatement
67
+ | ThrowStatement
67
68
  | IfStatement
68
69
  | ForInStatement
69
70
  | WhileStatement
@@ -181,6 +182,11 @@ export interface BreakStatement extends BaseNode {
181
182
  type: 'BreakStatement';
182
183
  }
183
184
 
185
+ export interface ThrowStatement extends BaseNode {
186
+ type: 'ThrowStatement';
187
+ message: Expression; // The error message expression
188
+ }
189
+
184
190
  export interface IfStatement extends BaseNode {
185
191
  type: 'IfStatement';
186
192
  condition: Expression;
@@ -46,6 +46,7 @@ export const Function = token({ name: 'Function', pattern: /function/, longer_al
46
46
  export const Tool = token({ name: 'Tool', pattern: /tool/, longer_alt: Identifier });
47
47
  export const Return = token({ name: 'Return', pattern: /return/, longer_alt: Identifier });
48
48
  export const Break = token({ name: 'Break', pattern: /break/, longer_alt: Identifier });
49
+ export const Throw = token({ name: 'Throw', pattern: /throw/, longer_alt: Identifier });
49
50
  export const If = token({ name: 'If', pattern: /if/, longer_alt: Identifier });
50
51
  export const Else = token({ name: 'Else', pattern: /else/, longer_alt: Identifier });
51
52
  export const While = token({ name: 'While', pattern: /while/, longer_alt: Identifier });
@@ -255,6 +256,7 @@ export const allTokens = [
255
256
  Tool,
256
257
  Return,
257
258
  Break,
259
+ Throw,
258
260
  If,
259
261
  Else,
260
262
  While,
@@ -38,6 +38,7 @@ class VibeParser extends CstParser {
38
38
  { ALT: () => this.SUBRULE(this.toolDeclaration) },
39
39
  { ALT: () => this.SUBRULE(this.returnStatement) },
40
40
  { ALT: () => this.SUBRULE(this.breakStatement) },
41
+ { ALT: () => this.SUBRULE(this.throwStatement) },
41
42
  { ALT: () => this.SUBRULE(this.ifStatement) },
42
43
  { ALT: () => this.SUBRULE(this.forInStatement) },
43
44
  { ALT: () => this.SUBRULE(this.whileStatement) },
@@ -316,6 +317,10 @@ class VibeParser extends CstParser {
316
317
  this.CONSUME(T.Comma);
317
318
  this.SUBRULE2(this.property);
318
319
  });
320
+ // Allow trailing comma
321
+ this.OPTION(() => {
322
+ this.CONSUME2(T.Comma);
323
+ });
319
324
  });
320
325
 
321
326
  private property = this.RULE('property', () => {
@@ -440,6 +445,11 @@ class VibeParser extends CstParser {
440
445
  this.CONSUME(T.Break);
441
446
  });
442
447
 
448
+ private throwStatement = this.RULE('throwStatement', () => {
449
+ this.CONSUME(T.Throw);
450
+ this.SUBRULE(this.expression); // Error message is required
451
+ });
452
+
443
453
  private ifStatement = this.RULE('ifStatement', () => {
444
454
  this.CONSUME(T.If);
445
455
  this.SUBRULE(this.expression);
@@ -782,6 +792,10 @@ class VibeParser extends CstParser {
782
792
  this.CONSUME(T.Comma);
783
793
  this.SUBRULE2(this.expression);
784
794
  });
795
+ // Allow trailing comma
796
+ this.OPTION(() => {
797
+ this.CONSUME2(T.Comma);
798
+ });
785
799
  });
786
800
  }
787
801
 
@@ -4,7 +4,7 @@ import { vibeAstVisitor } from './visitor';
4
4
  import { setCurrentFile } from './visitor/helpers';
5
5
  import { ParserError } from '../errors';
6
6
  import type { Program } from '../ast';
7
- import type { IRecognitionException } from 'chevrotain';
7
+ import type { IRecognitionException, IToken } from 'chevrotain';
8
8
 
9
9
  export interface ParseOptions {
10
10
  /** File path to include in source locations (for error reporting) */
@@ -17,8 +17,21 @@ export interface ParseOptions {
17
17
  function improveErrorMessage(error: IRecognitionException): string {
18
18
  const ruleStack = error.context?.ruleStack ?? [];
19
19
  const previousToken = error.previousToken;
20
+ const currentToken = error.token;
20
21
  const message = error.message;
21
22
 
23
+ // Reserved word used where identifier expected
24
+ // Detected: message mentions "Identifier" and current token is a keyword/type token (not Identifier)
25
+ if (message.includes('Identifier') && currentToken?.image && currentToken?.tokenType?.name) {
26
+ const tokenTypeName = currentToken.tokenType.name;
27
+ // If parser expected Identifier but got a keyword/type token, it's a reserved word
28
+ if (tokenTypeName !== 'Identifier') {
29
+ const isType = tokenTypeName.endsWith('Type');
30
+ const kind = isType ? 'reserved type name' : 'reserved keyword';
31
+ return `Invalid identifier '${currentToken.image}' - '${currentToken.image}' is a ${kind}`;
32
+ }
33
+ }
34
+
22
35
  // Missing type annotation for function/tool parameter
23
36
  // Detected: in "parameter" or "toolParameter" rule, expected Colon, previous token is Identifier
24
37
  if (
@@ -29,9 +42,126 @@ function improveErrorMessage(error: IRecognitionException): string {
29
42
  return `Missing type annotation for parameter '${previousToken.image}'`;
30
43
  }
31
44
 
45
+ // Missing comma between properties in object/model declaration
46
+ // Detected: in objectLiteral/objectLiteralExpr, expected RBrace, found Identifier
47
+ if (
48
+ (ruleStack.includes('objectLiteral') || ruleStack.includes('objectLiteralExpr')) &&
49
+ message.includes('RBrace') &&
50
+ currentToken?.tokenType?.name === 'Identifier'
51
+ ) {
52
+ return `Missing comma between properties. Add ',' after the previous property`;
53
+ }
54
+
55
+ // Missing comma between elements in array
56
+ // Detected: in arrayLiteral, expected RBracket, found something else
57
+ if (
58
+ ruleStack.includes('arrayLiteral') &&
59
+ message.includes('RBracket') &&
60
+ currentToken?.tokenType?.name !== 'RBracket'
61
+ ) {
62
+ return `Missing comma between array elements. Add ',' after the previous element`;
63
+ }
64
+
32
65
  return message;
33
66
  }
34
67
 
68
+ interface DelimiterInfo {
69
+ type: 'brace' | 'paren' | 'bracket';
70
+ token: IToken;
71
+ line: number;
72
+ column: number;
73
+ }
74
+
75
+ /**
76
+ * Check for unclosed or mismatched delimiters before parsing.
77
+ * Returns a ParserError if there's a delimiter issue, null otherwise.
78
+ */
79
+ function checkDelimiters(tokens: IToken[], source: string, file?: string): ParserError | null {
80
+ const stack: DelimiterInfo[] = [];
81
+
82
+ const delimiterPairs: Record<string, { open: 'brace' | 'paren' | 'bracket'; close: 'brace' | 'paren' | 'bracket' }> = {
83
+ LBrace: { open: 'brace', close: 'brace' },
84
+ RBrace: { open: 'brace', close: 'brace' },
85
+ LParen: { open: 'paren', close: 'paren' },
86
+ RParen: { open: 'paren', close: 'paren' },
87
+ LBracket: { open: 'bracket', close: 'bracket' },
88
+ RBracket: { open: 'bracket', close: 'bracket' },
89
+ };
90
+
91
+ const delimiterNames: Record<string, string> = {
92
+ brace: 'brace',
93
+ paren: 'parenthesis',
94
+ bracket: 'bracket',
95
+ };
96
+
97
+ const openingChars: Record<string, string> = {
98
+ brace: '{',
99
+ paren: '(',
100
+ bracket: '[',
101
+ };
102
+
103
+ const closingChars: Record<string, string> = {
104
+ brace: '}',
105
+ paren: ')',
106
+ bracket: ']',
107
+ };
108
+
109
+ for (const token of tokens) {
110
+ const tokenName = token.tokenType.name;
111
+
112
+ // Opening delimiters
113
+ if (tokenName === 'LBrace' || tokenName === 'LParen' || tokenName === 'LBracket') {
114
+ const type = delimiterPairs[tokenName].open;
115
+ stack.push({
116
+ type,
117
+ token,
118
+ line: token.startLine ?? 1,
119
+ column: token.startColumn ?? 1,
120
+ });
121
+ }
122
+ // Closing delimiters
123
+ else if (tokenName === 'RBrace' || tokenName === 'RParen' || tokenName === 'RBracket') {
124
+ const expectedType = delimiterPairs[tokenName].close;
125
+
126
+ if (stack.length === 0) {
127
+ // Unmatched closing delimiter
128
+ return new ParserError(
129
+ `Unmatched closing ${delimiterNames[expectedType]} '${closingChars[expectedType]}'`,
130
+ token.image,
131
+ { line: token.startLine ?? 1, column: token.startColumn ?? 1, file },
132
+ source
133
+ );
134
+ }
135
+
136
+ const top = stack.pop()!;
137
+ if (top.type !== expectedType) {
138
+ // Mismatched delimiter - report at the closing delimiter but mention the opening
139
+ return new ParserError(
140
+ `Mismatched delimiters: expected closing ${delimiterNames[top.type]} '${closingChars[top.type]}' to match '${openingChars[top.type]}' at line ${top.line}, but found '${closingChars[expectedType]}'`,
141
+ token.image,
142
+ { line: token.startLine ?? 1, column: token.startColumn ?? 1, file },
143
+ source
144
+ );
145
+ }
146
+ }
147
+ }
148
+
149
+ // Check for unclosed delimiters
150
+ if (stack.length > 0) {
151
+ // Report the first unclosed delimiter (deepest nesting level that's unclosed)
152
+ // We want to report the innermost unclosed one, which is the last on the stack
153
+ const unclosed = stack[stack.length - 1];
154
+ return new ParserError(
155
+ `Unclosed ${delimiterNames[unclosed.type]} '${openingChars[unclosed.type]}' - missing '${closingChars[unclosed.type]}'`,
156
+ openingChars[unclosed.type],
157
+ { line: unclosed.line, column: unclosed.column, file },
158
+ source
159
+ );
160
+ }
161
+
162
+ return null;
163
+ }
164
+
35
165
  /**
36
166
  * Parse a Vibe source code string into an AST
37
167
  */
@@ -42,6 +172,13 @@ export function parse(source: string, options?: ParseOptions): Program {
42
172
  // Tokenize
43
173
  const tokens = tokenize(source);
44
174
 
175
+ // Check for unclosed/mismatched delimiters first
176
+ // This provides better error messages than the parser for delimiter issues
177
+ const delimiterError = checkDelimiters(tokens, source, options?.file);
178
+ if (delimiterError) {
179
+ throw delimiterError;
180
+ }
181
+
45
182
  // Parse to CST
46
183
  vibeParser.input = tokens;
47
184
  const cst = vibeParser.program();
@@ -157,4 +157,29 @@ foo(
157
157
  greet("hello"
158
158
  `)).toThrow();
159
159
  });
160
+
161
+ // ============================================================================
162
+ // missing comma errors
163
+ // ============================================================================
164
+
165
+ test('object literal missing comma between properties', () => {
166
+ expect(() => parse(`let x = { a: 1 b: 2 }`)).toThrow(
167
+ "Missing comma between properties"
168
+ );
169
+ });
170
+
171
+ test('model declaration missing comma between properties', () => {
172
+ expect(() => parse(`
173
+ model m = {
174
+ name: "test"
175
+ apiKey: "key"
176
+ }
177
+ `)).toThrow("Missing comma between properties");
178
+ });
179
+
180
+ test('array literal missing comma between elements', () => {
181
+ expect(() => parse(`let arr = [1 2 3]`)).toThrow(
182
+ "Missing comma between array elements"
183
+ );
184
+ });
160
185
  });
@@ -133,26 +133,32 @@ model myModel = {
133
133
  name: "test"
134
134
  apiKey: "key"
135
135
  }
136
- `)).toThrow();
136
+ `)).toThrow(/[Mm]issing comma/);
137
137
  });
138
138
 
139
- test('properties with double comma', () => {
139
+ test('export model missing comma separator', () => {
140
140
  expect(() => parse(`
141
- model myModel = {
142
- name: "test",,
143
- apiKey: "key"
141
+ export model gemini3Flash = {
142
+ name: 'gemini-flash-3',
143
+ provider: 'google',
144
+ apiKey: env('GOOGLE_API_KEY')
145
+ url: 'https://api.google.com/v1',
144
146
  }
145
- `)).toThrow();
147
+ `)).toThrow(/[Mm]issing comma/);
146
148
  });
147
149
 
148
- test('trailing comma without next property', () => {
150
+ test('properties with double comma', () => {
149
151
  expect(() => parse(`
150
152
  model myModel = {
151
- name: "test",
153
+ name: "test",,
154
+ apiKey: "key"
152
155
  }
153
156
  `)).toThrow();
154
157
  });
155
158
 
159
+ // Note: trailing comma IS now allowed (test removed)
160
+ // model myModel = { name: "test", } is valid
161
+
156
162
  // ============================================================================
157
163
  // Model keyword misuse
158
164
  // ============================================================================
@@ -1,80 +1,175 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
2
  import { parse } from '../../parse';
3
+ import { ParserError } from '../../../errors';
3
4
 
4
5
  describe('Syntax Errors - Unclosed Delimiters', () => {
5
6
  // ============================================================================
6
- // Unclosed braces
7
+ // Unclosed braces - with location verification
7
8
  // ============================================================================
8
9
 
9
- test('unclosed block statement', () => {
10
- expect(() => parse(`
10
+ test('unclosed block statement reports location of opening brace', () => {
11
+ try {
12
+ parse(`
11
13
  {
12
14
  let x = "hello"
13
- `)).toThrow();
15
+ `);
16
+ expect.unreachable('Should have thrown');
17
+ } catch (e) {
18
+ expect(e).toBeInstanceOf(ParserError);
19
+ const err = e as ParserError;
20
+ expect(err.message).toContain("Unclosed brace '{'");
21
+ expect(err.location?.line).toBe(2); // Line where { is
22
+ expect(err.location?.column).toBe(1);
23
+ }
14
24
  });
15
25
 
16
- test('unclosed function body', () => {
17
- expect(() => parse(`
26
+ test('unclosed function body reports location of opening brace', () => {
27
+ try {
28
+ parse(`
18
29
  function foo() {
19
30
  return "hello"
20
- `)).toThrow();
31
+ `);
32
+ expect.unreachable('Should have thrown');
33
+ } catch (e) {
34
+ expect(e).toBeInstanceOf(ParserError);
35
+ const err = e as ParserError;
36
+ expect(err.message).toContain("Unclosed brace '{'");
37
+ expect(err.location?.line).toBe(2); // Line where function { is
38
+ expect(err.location?.column).toBe(16); // Column where { is
39
+ }
21
40
  });
22
41
 
23
- test('unclosed if block', () => {
24
- expect(() => parse(`
42
+ test('unclosed if block reports location of opening brace', () => {
43
+ try {
44
+ parse(`
25
45
  if true {
26
46
  let x = "yes"
27
- `)).toThrow();
47
+ `);
48
+ expect.unreachable('Should have thrown');
49
+ } catch (e) {
50
+ expect(e).toBeInstanceOf(ParserError);
51
+ const err = e as ParserError;
52
+ expect(err.message).toContain("Unclosed brace '{'");
53
+ expect(err.location?.line).toBe(2);
54
+ }
28
55
  });
29
56
 
30
- test('unclosed else block', () => {
31
- expect(() => parse(`
57
+ test('unclosed else block reports location of opening brace', () => {
58
+ try {
59
+ parse(`
32
60
  if true {
33
61
  let x = "yes"
34
62
  } else {
35
63
  let y = "no"
36
- `)).toThrow();
64
+ `);
65
+ expect.unreachable('Should have thrown');
66
+ } catch (e) {
67
+ expect(e).toBeInstanceOf(ParserError);
68
+ const err = e as ParserError;
69
+ expect(err.message).toContain("Unclosed brace '{'");
70
+ expect(err.location?.line).toBe(4); // Line where else { is
71
+ }
37
72
  });
38
73
 
39
- test('nested unclosed braces', () => {
40
- expect(() => parse(`
74
+ test('nested unclosed braces reports innermost unclosed', () => {
75
+ try {
76
+ parse(`
41
77
  function outer() {
42
78
  if true {
43
79
  let x = "nested"
44
80
  }
45
- `)).toThrow();
81
+ `);
82
+ expect.unreachable('Should have thrown');
83
+ } catch (e) {
84
+ expect(e).toBeInstanceOf(ParserError);
85
+ const err = e as ParserError;
86
+ // The innermost unclosed brace is the 'if' block's brace
87
+ // But actually the outer function brace is unclosed since only one } appears
88
+ expect(err.message).toContain("Unclosed brace '{'");
89
+ expect(err.location?.line).toBe(2); // The function's { is unclosed
90
+ }
46
91
  });
47
92
 
48
93
  // ============================================================================
49
- // Unclosed parentheses
94
+ // Unclosed parentheses - with location verification
50
95
  // ============================================================================
51
96
 
52
- test('unclosed function call', () => {
53
- expect(() => parse(`
97
+ test('unclosed function call reports location of opening paren', () => {
98
+ try {
99
+ parse(`
54
100
  foo("hello"
55
- `)).toThrow();
101
+ `);
102
+ expect.unreachable('Should have thrown');
103
+ } catch (e) {
104
+ expect(e).toBeInstanceOf(ParserError);
105
+ const err = e as ParserError;
106
+ expect(err.message).toContain("Unclosed parenthesis '('");
107
+ expect(err.location?.line).toBe(2); // Line where ( is
108
+ }
56
109
  });
57
110
 
58
- test('unclosed function params', () => {
59
- expect(() => parse(`
111
+ test('unclosed function params reports location of opening paren', () => {
112
+ try {
113
+ parse(`
60
114
  function greet(name, age
61
- `)).toThrow();
115
+ `);
116
+ expect.unreachable('Should have thrown');
117
+ } catch (e) {
118
+ expect(e).toBeInstanceOf(ParserError);
119
+ const err = e as ParserError;
120
+ expect(err.message).toContain("Unclosed parenthesis '('");
121
+ expect(err.location?.line).toBe(2);
122
+ }
62
123
  });
63
124
 
64
- test('unclosed grouped expression', () => {
65
- expect(() => parse(`
125
+ test('unclosed grouped expression reports location', () => {
126
+ try {
127
+ parse(`
66
128
  let x = ("hello"
67
- `)).toThrow();
129
+ `);
130
+ expect.unreachable('Should have thrown');
131
+ } catch (e) {
132
+ expect(e).toBeInstanceOf(ParserError);
133
+ const err = e as ParserError;
134
+ expect(err.message).toContain("Unclosed parenthesis '('");
135
+ expect(err.location?.line).toBe(2);
136
+ }
68
137
  });
69
138
 
70
- test('nested unclosed parens in call', () => {
71
- expect(() => parse(`
139
+ test('nested unclosed parens reports innermost', () => {
140
+ try {
141
+ parse(`
72
142
  outer(inner("deep"
73
- `)).toThrow();
143
+ `);
144
+ expect.unreachable('Should have thrown');
145
+ } catch (e) {
146
+ expect(e).toBeInstanceOf(ParserError);
147
+ const err = e as ParserError;
148
+ // inner( is the innermost unclosed
149
+ expect(err.message).toContain("Unclosed parenthesis '('");
150
+ }
151
+ });
152
+
153
+ // ============================================================================
154
+ // Unclosed brackets - with location verification
155
+ // ============================================================================
156
+
157
+ test('unclosed array literal reports location', () => {
158
+ try {
159
+ parse(`
160
+ let arr = [1, 2, 3
161
+ `);
162
+ expect.unreachable('Should have thrown');
163
+ } catch (e) {
164
+ expect(e).toBeInstanceOf(ParserError);
165
+ const err = e as ParserError;
166
+ expect(err.message).toContain("Unclosed bracket '['");
167
+ expect(err.location?.line).toBe(2);
168
+ }
74
169
  });
75
170
 
76
171
  // ============================================================================
77
- // Unclosed strings
172
+ // Unclosed strings (handled by lexer, not delimiter checker)
78
173
  // ============================================================================
79
174
 
80
175
  test('unclosed double quote string', () => {
@@ -102,15 +197,86 @@ let x = vibe "what is 2+2?
102
197
  });
103
198
 
104
199
  // ============================================================================
105
- // Mixed unclosed delimiters
200
+ // Mismatched delimiters
106
201
  // ============================================================================
107
202
 
108
- test('unclosed brace and paren', () => {
109
- expect(() => parse(`
203
+ test('mismatched closing brace vs paren', () => {
204
+ try {
205
+ parse(`
110
206
  function test() {
111
207
  foo(
112
208
  }
113
- `)).toThrow();
209
+ `);
210
+ expect.unreachable('Should have thrown');
211
+ } catch (e) {
212
+ expect(e).toBeInstanceOf(ParserError);
213
+ const err = e as ParserError;
214
+ expect(err.message).toContain("Mismatched delimiters");
215
+ expect(err.message).toContain("expected closing parenthesis");
216
+ }
217
+ });
218
+
219
+ test('unmatched closing brace', () => {
220
+ try {
221
+ parse(`
222
+ let x = 1
223
+ }
224
+ `);
225
+ expect.unreachable('Should have thrown');
226
+ } catch (e) {
227
+ expect(e).toBeInstanceOf(ParserError);
228
+ const err = e as ParserError;
229
+ expect(err.message).toContain("Unmatched closing brace");
230
+ expect(err.location?.line).toBe(3);
231
+ }
232
+ });
233
+
234
+ test('unmatched closing paren', () => {
235
+ try {
236
+ parse(`
237
+ let x = 1)
238
+ `);
239
+ expect.unreachable('Should have thrown');
240
+ } catch (e) {
241
+ expect(e).toBeInstanceOf(ParserError);
242
+ const err = e as ParserError;
243
+ expect(err.message).toContain("Unmatched closing parenthesis");
244
+ }
245
+ });
246
+
247
+ test('unmatched closing bracket', () => {
248
+ try {
249
+ parse(`
250
+ let x = 1]
251
+ `);
252
+ expect.unreachable('Should have thrown');
253
+ } catch (e) {
254
+ expect(e).toBeInstanceOf(ParserError);
255
+ const err = e as ParserError;
256
+ expect(err.message).toContain("Unmatched closing bracket");
257
+ }
258
+ });
259
+
260
+ // ============================================================================
261
+ // While loop specific (from user's example)
262
+ // ============================================================================
263
+
264
+ test('unclosed while loop reports location of opening brace', () => {
265
+ try {
266
+ parse(`
267
+ let keepGoing = true
268
+ let count = 0
269
+
270
+ while keepGoing {
271
+ count = count + 1
272
+ `);
273
+ expect.unreachable('Should have thrown');
274
+ } catch (e) {
275
+ expect(e).toBeInstanceOf(ParserError);
276
+ const err = e as ParserError;
277
+ expect(err.message).toContain("Unclosed brace '{'");
278
+ expect(err.location?.line).toBe(5); // Line where while { is
279
+ }
114
280
  });
115
281
 
116
282
  test('unclosed string inside unclosed block', () => {
@@ -31,7 +31,21 @@ function() {
31
31
  function let() {
32
32
  return "hello"
33
33
  }
34
- `)).toThrow();
34
+ `)).toThrow(/reserved keyword/);
35
+ });
36
+
37
+ test('function with type name as name', () => {
38
+ expect(() => parse(`
39
+ function text(x: text, y: number): text {
40
+ return x + y
41
+ }
42
+ `)).toThrow(/reserved type name/);
43
+ });
44
+
45
+ test('variable with keyword as name', () => {
46
+ expect(() => parse(`
47
+ let return = 5
48
+ `)).toThrow(/reserved keyword/);
35
49
  });
36
50
 
37
51
  // ============================================================================
@@ -401,4 +401,33 @@ describe('Parser - For-In Statement', () => {
401
401
  const inner = outer.body.body[0];
402
402
  expect(inner.type).toBe('ForInStatement');
403
403
  });
404
+
405
+ // Trailing commas
406
+ test('object literal with trailing comma', () => {
407
+ const ast = parse('let x = { a: 1, b: 2, }');
408
+ const obj = (ast.body[0] as any).initializer;
409
+ expect(obj.type).toBe('ObjectLiteral');
410
+ expect(obj.properties).toHaveLength(2);
411
+ });
412
+
413
+ test('array literal with trailing comma', () => {
414
+ const ast = parse('let x = [1, 2, 3,]');
415
+ const arr = (ast.body[0] as any).initializer;
416
+ expect(arr.type).toBe('ArrayLiteral');
417
+ expect(arr.elements).toHaveLength(3);
418
+ });
419
+
420
+ test('single element object with trailing comma', () => {
421
+ const ast = parse('let x = { a: 1, }');
422
+ const obj = (ast.body[0] as any).initializer;
423
+ expect(obj.type).toBe('ObjectLiteral');
424
+ expect(obj.properties).toHaveLength(1);
425
+ });
426
+
427
+ test('single element array with trailing comma', () => {
428
+ const ast = parse('let x = [1,]');
429
+ const arr = (ast.body[0] as any).initializer;
430
+ expect(arr.type).toBe('ArrayLiteral');
431
+ expect(arr.elements).toHaveLength(1);
432
+ });
404
433
  });