@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.
- package/package.json +1 -1
- package/src/ast/index.ts +6 -0
- package/src/lexer/index.ts +2 -0
- package/src/parser/index.ts +14 -0
- package/src/parser/parse.ts +138 -1
- package/src/parser/test/errors/missing-tokens.test.ts +25 -0
- package/src/parser/test/errors/model-declaration.test.ts +14 -8
- package/src/parser/test/errors/unclosed-delimiters.test.ts +200 -34
- package/src/parser/test/errors/unexpected-tokens.test.ts +15 -1
- package/src/parser/test/literals.test.ts +29 -0
- package/src/parser/test/model-declaration.test.ts +14 -0
- package/src/parser/visitor.ts +9 -0
- package/src/runtime/async/dependencies.ts +8 -1
- package/src/runtime/async/test/dependencies.test.ts +27 -1
- package/src/runtime/exec/statements.ts +51 -1
- package/src/runtime/index.ts +2 -3
- package/src/runtime/modules.ts +1 -1
- package/src/runtime/stdlib/index.ts +7 -11
- package/src/runtime/stdlib/tools/index.ts +5 -122
- package/src/runtime/stdlib/utils/index.ts +58 -0
- package/src/runtime/step.ts +4 -0
- package/src/runtime/test/core-functions.test.ts +19 -10
- package/src/runtime/test/throw.test.ts +220 -0
- package/src/runtime/test/tool-execution.test.ts +30 -30
- package/src/runtime/types.ts +4 -1
- package/src/runtime/validation.ts +6 -0
- package/src/semantic/analyzer-context.ts +2 -0
- package/src/semantic/analyzer-visitors.ts +149 -2
- package/src/semantic/analyzer.ts +1 -0
- package/src/semantic/test/fixtures/exports.vibe +25 -0
- package/src/semantic/test/function-return-check.test.ts +215 -0
- package/src/semantic/test/imports.test.ts +66 -2
- package/src/semantic/test/prompt-validation.test.ts +44 -0
package/package.json
CHANGED
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;
|
package/src/lexer/index.ts
CHANGED
|
@@ -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,
|
package/src/parser/index.ts
CHANGED
|
@@ -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
|
|
package/src/parser/parse.ts
CHANGED
|
@@ -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('
|
|
139
|
+
test('export model missing comma separator', () => {
|
|
140
140
|
expect(() => parse(`
|
|
141
|
-
model
|
|
142
|
-
name:
|
|
143
|
-
|
|
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('
|
|
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
|
-
|
|
10
|
+
test('unclosed block statement reports location of opening brace', () => {
|
|
11
|
+
try {
|
|
12
|
+
parse(`
|
|
11
13
|
{
|
|
12
14
|
let x = "hello"
|
|
13
|
-
`)
|
|
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
|
-
|
|
26
|
+
test('unclosed function body reports location of opening brace', () => {
|
|
27
|
+
try {
|
|
28
|
+
parse(`
|
|
18
29
|
function foo() {
|
|
19
30
|
return "hello"
|
|
20
|
-
`)
|
|
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
|
-
|
|
42
|
+
test('unclosed if block reports location of opening brace', () => {
|
|
43
|
+
try {
|
|
44
|
+
parse(`
|
|
25
45
|
if true {
|
|
26
46
|
let x = "yes"
|
|
27
|
-
`)
|
|
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
|
-
|
|
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
|
-
`)
|
|
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
|
-
|
|
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
|
-
`)
|
|
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
|
-
|
|
97
|
+
test('unclosed function call reports location of opening paren', () => {
|
|
98
|
+
try {
|
|
99
|
+
parse(`
|
|
54
100
|
foo("hello"
|
|
55
|
-
`)
|
|
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
|
-
|
|
111
|
+
test('unclosed function params reports location of opening paren', () => {
|
|
112
|
+
try {
|
|
113
|
+
parse(`
|
|
60
114
|
function greet(name, age
|
|
61
|
-
`)
|
|
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
|
-
|
|
125
|
+
test('unclosed grouped expression reports location', () => {
|
|
126
|
+
try {
|
|
127
|
+
parse(`
|
|
66
128
|
let x = ("hello"
|
|
67
|
-
`)
|
|
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
|
|
71
|
-
|
|
139
|
+
test('nested unclosed parens reports innermost', () => {
|
|
140
|
+
try {
|
|
141
|
+
parse(`
|
|
72
142
|
outer(inner("deep"
|
|
73
|
-
`)
|
|
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
|
-
//
|
|
200
|
+
// Mismatched delimiters
|
|
106
201
|
// ============================================================================
|
|
107
202
|
|
|
108
|
-
test('
|
|
109
|
-
|
|
203
|
+
test('mismatched closing brace vs paren', () => {
|
|
204
|
+
try {
|
|
205
|
+
parse(`
|
|
110
206
|
function test() {
|
|
111
207
|
foo(
|
|
112
208
|
}
|
|
113
|
-
`)
|
|
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
|
});
|