@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-lang/runtime",
3
- "version": "0.2.9",
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) },
@@ -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);
@@ -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
- 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
  // ============================================================================
@@ -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
  }
@@ -185,13 +185,12 @@ export class Runtime {
185
185
  return resolveValue(variable?.value);
186
186
  }
187
187
 
188
- // Get raw value including VibeValue wrapper if present
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
- const variable = frame.locals[name];
194
- return variable?.value;
193
+ return frame.locals[name];
195
194
  }
196
195
 
197
196
  // Get all AI interactions (for debugging)
@@ -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