@vibe-lang/runtime 0.2.9 → 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 +6 -0
- package/src/parser/parse.ts +117 -1
- package/src/parser/test/errors/model-declaration.test.ts +12 -1
- 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/visitor.ts +9 -0
- 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 +3 -120
- 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) },
|
|
@@ -444,6 +445,11 @@ class VibeParser extends CstParser {
|
|
|
444
445
|
this.CONSUME(T.Break);
|
|
445
446
|
});
|
|
446
447
|
|
|
448
|
+
private throwStatement = this.RULE('throwStatement', () => {
|
|
449
|
+
this.CONSUME(T.Throw);
|
|
450
|
+
this.SUBRULE(this.expression); // Error message is required
|
|
451
|
+
});
|
|
452
|
+
|
|
447
453
|
private ifStatement = this.RULE('ifStatement', () => {
|
|
448
454
|
this.CONSUME(T.If);
|
|
449
455
|
this.SUBRULE(this.expression);
|
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) */
|
|
@@ -20,6 +20,18 @@ function improveErrorMessage(error: IRecognitionException): string {
|
|
|
20
20
|
const currentToken = error.token;
|
|
21
21
|
const message = error.message;
|
|
22
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
|
+
|
|
23
35
|
// Missing type annotation for function/tool parameter
|
|
24
36
|
// Detected: in "parameter" or "toolParameter" rule, expected Colon, previous token is Identifier
|
|
25
37
|
if (
|
|
@@ -53,6 +65,103 @@ function improveErrorMessage(error: IRecognitionException): string {
|
|
|
53
65
|
return message;
|
|
54
66
|
}
|
|
55
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
|
+
|
|
56
165
|
/**
|
|
57
166
|
* Parse a Vibe source code string into an AST
|
|
58
167
|
*/
|
|
@@ -63,6 +172,13 @@ export function parse(source: string, options?: ParseOptions): Program {
|
|
|
63
172
|
// Tokenize
|
|
64
173
|
const tokens = tokenize(source);
|
|
65
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
|
+
|
|
66
182
|
// Parse to CST
|
|
67
183
|
vibeParser.input = tokens;
|
|
68
184
|
const cst = vibeParser.program();
|
|
@@ -133,7 +133,18 @@ model myModel = {
|
|
|
133
133
|
name: "test"
|
|
134
134
|
apiKey: "key"
|
|
135
135
|
}
|
|
136
|
-
`)).toThrow();
|
|
136
|
+
`)).toThrow(/[Mm]issing comma/);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('export model missing comma separator', () => {
|
|
140
|
+
expect(() => parse(`
|
|
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',
|
|
146
|
+
}
|
|
147
|
+
`)).toThrow(/[Mm]issing comma/);
|
|
137
148
|
});
|
|
138
149
|
|
|
139
150
|
test('properties with double comma', () => {
|
|
@@ -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
|
// ============================================================================
|
package/src/parser/visitor.ts
CHANGED
|
@@ -61,6 +61,7 @@ class VibeAstVisitor extends BaseVibeVisitor {
|
|
|
61
61
|
if (ctx.toolDeclaration) return this.visit(ctx.toolDeclaration);
|
|
62
62
|
if (ctx.returnStatement) return this.visit(ctx.returnStatement);
|
|
63
63
|
if (ctx.breakStatement) return this.visit(ctx.breakStatement);
|
|
64
|
+
if (ctx.throwStatement) return this.visit(ctx.throwStatement);
|
|
64
65
|
if (ctx.ifStatement) return this.visit(ctx.ifStatement);
|
|
65
66
|
if (ctx.forInStatement) return this.visit(ctx.forInStatement);
|
|
66
67
|
if (ctx.whileStatement) return this.visit(ctx.whileStatement);
|
|
@@ -409,6 +410,14 @@ class VibeAstVisitor extends BaseVibeVisitor {
|
|
|
409
410
|
return { type: 'BreakStatement', location: tokenLocation(ctx.Break[0]) };
|
|
410
411
|
}
|
|
411
412
|
|
|
413
|
+
throwStatement(ctx: { Throw: IToken[]; expression: CstNode[] }): AST.ThrowStatement {
|
|
414
|
+
return {
|
|
415
|
+
type: 'ThrowStatement',
|
|
416
|
+
message: this.visit(ctx.expression),
|
|
417
|
+
location: tokenLocation(ctx.Throw[0]),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
412
421
|
ifStatement(ctx: { If: IToken[]; expression: CstNode[]; blockStatement: CstNode[]; ifStatement?: CstNode[] }): AST.IfStatement {
|
|
413
422
|
const alternate = ctx.ifStatement ? this.visit(ctx.ifStatement) : ctx.blockStatement.length > 1 ? this.visit(ctx.blockStatement[1]) : null;
|
|
414
423
|
return { type: 'IfStatement', condition: this.visit(ctx.expression), consequent: this.visit(ctx.blockStatement[0]), alternate, location: tokenLocation(ctx.If[0]) };
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import * as AST from '../../ast';
|
|
4
4
|
import type { SourceLocation } from '../../errors';
|
|
5
5
|
import type { RuntimeState, VibeValue } from '../types';
|
|
6
|
-
import { createVibeValue, resolveValue, isVibeValue } from '../types';
|
|
6
|
+
import { createVibeValue, createVibeError, resolveValue, isVibeValue } from '../types';
|
|
7
7
|
import { currentFrame } from '../state';
|
|
8
8
|
import { requireBoolean, validateAndCoerce } from '../validation';
|
|
9
9
|
import { execDeclareVar } from './variables';
|
|
@@ -441,6 +441,53 @@ export function execReturnValue(state: RuntimeState): RuntimeState {
|
|
|
441
441
|
return { ...state, callStack: newCallStack, instructionStack: newInstructionStack, lastResult: validatedReturnValue };
|
|
442
442
|
}
|
|
443
443
|
|
|
444
|
+
/**
|
|
445
|
+
* Throw statement - evaluate message and throw error.
|
|
446
|
+
*/
|
|
447
|
+
export function execThrowStatement(state: RuntimeState, stmt: AST.ThrowStatement): RuntimeState {
|
|
448
|
+
return {
|
|
449
|
+
...state,
|
|
450
|
+
instructionStack: [
|
|
451
|
+
{ op: 'exec_expression', expr: stmt.message, location: stmt.message.location },
|
|
452
|
+
{ op: 'throw_error', location: stmt.location },
|
|
453
|
+
...state.instructionStack,
|
|
454
|
+
],
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Throw error - create error value and unwind to function boundary.
|
|
460
|
+
* Uses lastResult as the error message.
|
|
461
|
+
*/
|
|
462
|
+
export function execThrowError(state: RuntimeState, location: SourceLocation): RuntimeState {
|
|
463
|
+
// Get the error message from lastResult
|
|
464
|
+
const messageValue = resolveValue(state.lastResult);
|
|
465
|
+
const message = typeof messageValue === 'string' ? messageValue : String(messageValue);
|
|
466
|
+
|
|
467
|
+
// Create error VibeValue
|
|
468
|
+
const errorValue = createVibeError(message, location);
|
|
469
|
+
|
|
470
|
+
// Check if we're at top level (only main frame) or in a function
|
|
471
|
+
const isTopLevel = state.callStack.length === 1;
|
|
472
|
+
|
|
473
|
+
if (isTopLevel) {
|
|
474
|
+
// At top level - complete with error but keep the frame for variable access
|
|
475
|
+
return { ...state, status: 'completed', instructionStack: [], lastResult: errorValue };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// In a function - unwind like return: pop frame and skip to after pop_frame instruction
|
|
479
|
+
const newCallStack = state.callStack.slice(0, -1);
|
|
480
|
+
|
|
481
|
+
// Find and skip past the pop_frame instruction
|
|
482
|
+
let newInstructionStack = state.instructionStack;
|
|
483
|
+
const popFrameIndex = newInstructionStack.findIndex((i) => i.op === 'pop_frame');
|
|
484
|
+
if (popFrameIndex !== -1) {
|
|
485
|
+
newInstructionStack = newInstructionStack.slice(popFrameIndex + 1);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return { ...state, callStack: newCallStack, instructionStack: newInstructionStack, lastResult: errorValue };
|
|
489
|
+
}
|
|
490
|
+
|
|
444
491
|
/**
|
|
445
492
|
* Execute statements at index - sequential statement execution.
|
|
446
493
|
*/
|
|
@@ -598,6 +645,9 @@ export function execStatement(state: RuntimeState, stmt: AST.Statement): Runtime
|
|
|
598
645
|
case 'BreakStatement':
|
|
599
646
|
return execBreakStatement(state, stmt);
|
|
600
647
|
|
|
648
|
+
case 'ThrowStatement':
|
|
649
|
+
return execThrowStatement(state, stmt);
|
|
650
|
+
|
|
601
651
|
default:
|
|
602
652
|
throw new Error(`Unknown statement type: ${(stmt as AST.Statement).type}`);
|
|
603
653
|
}
|
package/src/runtime/index.ts
CHANGED
|
@@ -185,13 +185,12 @@ export class Runtime {
|
|
|
185
185
|
return resolveValue(variable?.value);
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
// Get raw
|
|
188
|
+
// Get raw VibeValue wrapper (for testing error state, toolCalls, etc.)
|
|
189
189
|
getRawValue(name: string): unknown {
|
|
190
190
|
const frame = this.state.callStack[this.state.callStack.length - 1];
|
|
191
191
|
if (!frame) return undefined;
|
|
192
192
|
|
|
193
|
-
|
|
194
|
-
return variable?.value;
|
|
193
|
+
return frame.locals[name];
|
|
195
194
|
}
|
|
196
195
|
|
|
197
196
|
// Get all AI interactions (for debugging)
|
package/src/runtime/modules.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { resolve, dirname, join } from 'path';
|
|
|
9
9
|
|
|
10
10
|
// Map system module names to their implementation files
|
|
11
11
|
const SYSTEM_MODULES: Record<string, string> = {
|
|
12
|
-
'system': join(__dirname, 'stdlib', 'index.ts'),
|
|
12
|
+
'system/utils': join(__dirname, 'stdlib', 'utils', 'index.ts'),
|
|
13
13
|
'system/tools': join(__dirname, 'stdlib', 'tools', 'index.ts'),
|
|
14
14
|
};
|
|
15
15
|
|