@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
|
@@ -1,16 +1,12 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// System module - re-exports utility functions for convenience
|
|
2
|
+
// Prefer importing from "system/utils" directly.
|
|
3
|
+
//
|
|
4
|
+
// Import with: import { uuid, random, now } from "system"
|
|
5
|
+
// Or directly: import { uuid, random, now } from "system/utils"
|
|
3
6
|
//
|
|
4
|
-
// These are TypeScript functions that can be called directly from Vibe scripts.
|
|
5
7
|
// For AI tools, use: import { allTools } from "system/tools"
|
|
6
8
|
//
|
|
7
9
|
// NOTE: print() and env() are auto-imported core functions.
|
|
8
|
-
// They are available everywhere without import
|
|
10
|
+
// They are available everywhere without import.
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
* Generate a UUID v4.
|
|
12
|
-
* @returns A new UUID string
|
|
13
|
-
*/
|
|
14
|
-
export function uuid(): string {
|
|
15
|
-
return crypto.randomUUID();
|
|
16
|
-
}
|
|
12
|
+
export { uuid, now, random, jsonParse, jsonStringify } from './utils';
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Import with: import { allTools, readFile, writeFile, ... } from "system/tools"
|
|
3
3
|
//
|
|
4
4
|
// These are tools that AI models can use via the tools parameter.
|
|
5
|
-
// For
|
|
5
|
+
// For utility functions (uuid, random, now, etc.), use: import { ... } from "system/utils"
|
|
6
6
|
|
|
7
7
|
import type { VibeToolValue, ToolContext } from '../../tools/types';
|
|
8
8
|
import { validatePathInSandbox } from '../../tools/security';
|
|
@@ -386,117 +386,6 @@ export const dirExists: VibeToolValue = {
|
|
|
386
386
|
},
|
|
387
387
|
};
|
|
388
388
|
|
|
389
|
-
// =============================================================================
|
|
390
|
-
// Utility Tools
|
|
391
|
-
// =============================================================================
|
|
392
|
-
|
|
393
|
-
export const env: VibeToolValue = {
|
|
394
|
-
__vibeTool: true,
|
|
395
|
-
name: 'env',
|
|
396
|
-
schema: {
|
|
397
|
-
name: 'env',
|
|
398
|
-
description: 'Get an environment variable.',
|
|
399
|
-
parameters: [
|
|
400
|
-
{ name: 'name', type: { type: 'string' }, description: 'The environment variable name', required: true },
|
|
401
|
-
{ name: 'defaultValue', type: { type: 'string' }, description: 'Default value if not set', required: false },
|
|
402
|
-
],
|
|
403
|
-
returns: { type: 'string' },
|
|
404
|
-
},
|
|
405
|
-
executor: async (args: Record<string, unknown>) => {
|
|
406
|
-
const name = args.name as string;
|
|
407
|
-
const defaultValue = args.defaultValue as string | undefined;
|
|
408
|
-
return process.env[name] ?? defaultValue ?? '';
|
|
409
|
-
},
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
export const now: VibeToolValue = {
|
|
413
|
-
__vibeTool: true,
|
|
414
|
-
name: 'now',
|
|
415
|
-
schema: {
|
|
416
|
-
name: 'now',
|
|
417
|
-
description: 'Get the current timestamp in milliseconds.',
|
|
418
|
-
parameters: [],
|
|
419
|
-
returns: { type: 'number' },
|
|
420
|
-
},
|
|
421
|
-
executor: async () => {
|
|
422
|
-
return Date.now();
|
|
423
|
-
},
|
|
424
|
-
};
|
|
425
|
-
|
|
426
|
-
export const jsonParse: VibeToolValue = {
|
|
427
|
-
__vibeTool: true,
|
|
428
|
-
name: 'jsonParse',
|
|
429
|
-
schema: {
|
|
430
|
-
name: 'jsonParse',
|
|
431
|
-
description: 'Parse a JSON string into an object.',
|
|
432
|
-
parameters: [
|
|
433
|
-
{ name: 'text', type: { type: 'string' }, description: 'The JSON string to parse', required: true },
|
|
434
|
-
],
|
|
435
|
-
returns: { type: 'object', additionalProperties: true },
|
|
436
|
-
},
|
|
437
|
-
executor: async (args: Record<string, unknown>) => {
|
|
438
|
-
const text = args.text as string;
|
|
439
|
-
return JSON.parse(text);
|
|
440
|
-
},
|
|
441
|
-
};
|
|
442
|
-
|
|
443
|
-
export const jsonStringify: VibeToolValue = {
|
|
444
|
-
__vibeTool: true,
|
|
445
|
-
name: 'jsonStringify',
|
|
446
|
-
schema: {
|
|
447
|
-
name: 'jsonStringify',
|
|
448
|
-
description: 'Convert an object to a JSON string.',
|
|
449
|
-
parameters: [
|
|
450
|
-
{ name: 'value', type: { type: 'object', additionalProperties: true }, description: 'The value to stringify', required: true },
|
|
451
|
-
{ name: 'pretty', type: { type: 'boolean' }, description: 'Whether to format with indentation', required: false },
|
|
452
|
-
],
|
|
453
|
-
returns: { type: 'string' },
|
|
454
|
-
},
|
|
455
|
-
executor: async (args: Record<string, unknown>) => {
|
|
456
|
-
const value = args.value;
|
|
457
|
-
const pretty = args.pretty as boolean | undefined;
|
|
458
|
-
return pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value);
|
|
459
|
-
},
|
|
460
|
-
};
|
|
461
|
-
|
|
462
|
-
export const random: VibeToolValue = {
|
|
463
|
-
__vibeTool: true,
|
|
464
|
-
name: 'random',
|
|
465
|
-
schema: {
|
|
466
|
-
name: 'random',
|
|
467
|
-
description: 'Generate a random number. Without arguments, returns 0-1. With min/max, returns integer in range.',
|
|
468
|
-
parameters: [
|
|
469
|
-
{ name: 'min', type: { type: 'number' }, description: 'Minimum value (inclusive)', required: false },
|
|
470
|
-
{ name: 'max', type: { type: 'number' }, description: 'Maximum value (inclusive)', required: false },
|
|
471
|
-
],
|
|
472
|
-
returns: { type: 'number' },
|
|
473
|
-
},
|
|
474
|
-
executor: async (args: Record<string, unknown>) => {
|
|
475
|
-
const min = args.min as number | undefined;
|
|
476
|
-
const max = args.max as number | undefined;
|
|
477
|
-
|
|
478
|
-
if (min !== undefined && max !== undefined) {
|
|
479
|
-
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
return Math.random();
|
|
483
|
-
},
|
|
484
|
-
};
|
|
485
|
-
|
|
486
|
-
export const uuid: VibeToolValue = {
|
|
487
|
-
__vibeTool: true,
|
|
488
|
-
name: 'uuid',
|
|
489
|
-
schema: {
|
|
490
|
-
name: 'uuid',
|
|
491
|
-
description: 'Generate a UUID v4.',
|
|
492
|
-
parameters: [],
|
|
493
|
-
returns: { type: 'string' },
|
|
494
|
-
},
|
|
495
|
-
executor: async () => {
|
|
496
|
-
return crypto.randomUUID();
|
|
497
|
-
},
|
|
498
|
-
};
|
|
499
|
-
|
|
500
389
|
// =============================================================================
|
|
501
390
|
// System Tools
|
|
502
391
|
// =============================================================================
|
|
@@ -659,8 +548,8 @@ console.log('__VIBE_RESULT__' + JSON.stringify(__result));
|
|
|
659
548
|
// =============================================================================
|
|
660
549
|
|
|
661
550
|
/**
|
|
662
|
-
* All tools - the complete set of standard tools.
|
|
663
|
-
* Includes
|
|
551
|
+
* All tools - the complete set of standard AI tools.
|
|
552
|
+
* Includes file, search, directory, and system tools.
|
|
664
553
|
*/
|
|
665
554
|
export const allTools: VibeToolValue[] = [
|
|
666
555
|
// File tools
|
|
@@ -669,8 +558,6 @@ export const allTools: VibeToolValue[] = [
|
|
|
669
558
|
glob, grep,
|
|
670
559
|
// Directory tools
|
|
671
560
|
mkdir, dirExists,
|
|
672
|
-
// Utility tools
|
|
673
|
-
env, now, jsonParse, jsonStringify, random, uuid,
|
|
674
561
|
// System tools
|
|
675
562
|
bash, runCode,
|
|
676
563
|
];
|
|
@@ -686,8 +573,6 @@ export const readonlyTools: VibeToolValue[] = [
|
|
|
686
573
|
glob, grep,
|
|
687
574
|
// Directory tools (read-only)
|
|
688
575
|
dirExists,
|
|
689
|
-
// Utility tools
|
|
690
|
-
env, now, jsonParse, jsonStringify, random, uuid,
|
|
691
576
|
];
|
|
692
577
|
|
|
693
578
|
/**
|
|
@@ -701,7 +586,5 @@ export const safeTools: VibeToolValue[] = [
|
|
|
701
586
|
glob, grep,
|
|
702
587
|
// Directory tools
|
|
703
588
|
mkdir, dirExists,
|
|
704
|
-
// Utility tools
|
|
705
|
-
env, now, jsonParse, jsonStringify, random, uuid,
|
|
706
589
|
];
|
|
707
590
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// System utility functions for Vibe scripts
|
|
2
|
+
// Import with: import { uuid, now, random, jsonParse, jsonStringify } from "system/utils"
|
|
3
|
+
//
|
|
4
|
+
// These are utility functions that can be called directly from Vibe scripts.
|
|
5
|
+
// For AI tools, use: import { readFile, writeFile, ... } from "system/tools"
|
|
6
|
+
//
|
|
7
|
+
// NOTE: print() and env() are auto-imported core functions.
|
|
8
|
+
// They are available everywhere without import.
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate a UUID v4.
|
|
12
|
+
* @returns A new UUID string
|
|
13
|
+
*/
|
|
14
|
+
export function uuid(): string {
|
|
15
|
+
return crypto.randomUUID();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get the current timestamp in milliseconds.
|
|
20
|
+
* @returns Unix timestamp in milliseconds
|
|
21
|
+
*/
|
|
22
|
+
export function now(): number {
|
|
23
|
+
return Date.now();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generate a random number.
|
|
28
|
+
* Without arguments, returns a float between 0 and 1.
|
|
29
|
+
* With min/max, returns an integer in the range [min, max] inclusive.
|
|
30
|
+
* @param min - Minimum value (inclusive)
|
|
31
|
+
* @param max - Maximum value (inclusive)
|
|
32
|
+
* @returns Random number
|
|
33
|
+
*/
|
|
34
|
+
export function random(min?: number, max?: number): number {
|
|
35
|
+
if (min !== undefined && max !== undefined) {
|
|
36
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
37
|
+
}
|
|
38
|
+
return Math.random();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse a JSON string into an object.
|
|
43
|
+
* @param text - The JSON string to parse
|
|
44
|
+
* @returns Parsed object
|
|
45
|
+
*/
|
|
46
|
+
export function jsonParse(text: string): unknown {
|
|
47
|
+
return JSON.parse(text);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Convert a value to a JSON string.
|
|
52
|
+
* @param value - The value to stringify
|
|
53
|
+
* @param pretty - Whether to format with indentation
|
|
54
|
+
* @returns JSON string
|
|
55
|
+
*/
|
|
56
|
+
export function jsonStringify(value: unknown, pretty?: boolean): string {
|
|
57
|
+
return pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value);
|
|
58
|
+
}
|
package/src/runtime/step.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
execStatement,
|
|
11
11
|
execStatements,
|
|
12
12
|
execReturnValue,
|
|
13
|
+
execThrowError,
|
|
13
14
|
execIfBranch,
|
|
14
15
|
execEnterBlock,
|
|
15
16
|
execExitBlock,
|
|
@@ -317,6 +318,9 @@ function executeInstruction(state: RuntimeState, instruction: Instruction): Runt
|
|
|
317
318
|
case 'return_value':
|
|
318
319
|
return execReturnValue(state);
|
|
319
320
|
|
|
321
|
+
case 'throw_error':
|
|
322
|
+
return execThrowError(state, instruction.location);
|
|
323
|
+
|
|
320
324
|
case 'enter_block':
|
|
321
325
|
return execEnterBlock(state, instruction.savedKeys);
|
|
322
326
|
|
|
@@ -176,22 +176,22 @@ if true {
|
|
|
176
176
|
});
|
|
177
177
|
|
|
178
178
|
describe('core functions cannot be imported', () => {
|
|
179
|
-
test('importing env from system fails', async () => {
|
|
179
|
+
test('importing env from system/utils fails (env is core function)', async () => {
|
|
180
180
|
const ast = parse(`
|
|
181
|
-
import { env } from "system"
|
|
181
|
+
import { env } from "system/utils"
|
|
182
182
|
let x = 1
|
|
183
183
|
`);
|
|
184
184
|
const runtime = new Runtime(ast, createMockProvider());
|
|
185
|
-
await expect(runtime.run()).rejects.toThrow("'env' is not exported from 'system'");
|
|
185
|
+
await expect(runtime.run()).rejects.toThrow("'env' is not exported from 'system/utils'");
|
|
186
186
|
});
|
|
187
187
|
|
|
188
|
-
test('importing print from system fails', async () => {
|
|
188
|
+
test('importing print from system/utils fails (print is core function)', async () => {
|
|
189
189
|
const ast = parse(`
|
|
190
|
-
import { print } from "system"
|
|
190
|
+
import { print } from "system/utils"
|
|
191
191
|
let x = 1
|
|
192
192
|
`);
|
|
193
193
|
const runtime = new Runtime(ast, createMockProvider());
|
|
194
|
-
await expect(runtime.run()).rejects.toThrow("'print' is not exported from 'system'");
|
|
194
|
+
await expect(runtime.run()).rejects.toThrow("'print' is not exported from 'system/utils'");
|
|
195
195
|
});
|
|
196
196
|
|
|
197
197
|
test('importing from system/core is blocked', async () => {
|
|
@@ -202,10 +202,19 @@ let x = 1
|
|
|
202
202
|
const runtime = new Runtime(ast, createMockProvider());
|
|
203
203
|
await expect(runtime.run()).rejects.toThrow("'system/core' cannot be imported");
|
|
204
204
|
});
|
|
205
|
+
|
|
206
|
+
test('importing from bare "system" fails (not a valid module)', async () => {
|
|
207
|
+
const ast = parse(`
|
|
208
|
+
import { uuid } from "system"
|
|
209
|
+
let x = 1
|
|
210
|
+
`);
|
|
211
|
+
const runtime = new Runtime(ast, createMockProvider());
|
|
212
|
+
await expect(runtime.run()).rejects.toThrow("Unknown system module: 'system'");
|
|
213
|
+
});
|
|
205
214
|
});
|
|
206
215
|
|
|
207
|
-
describe('
|
|
208
|
-
test('uuid requires import
|
|
216
|
+
describe('utility functions require import from system/utils', () => {
|
|
217
|
+
test('uuid requires import', async () => {
|
|
209
218
|
const ast = parse(`
|
|
210
219
|
let id = uuid()
|
|
211
220
|
`);
|
|
@@ -213,9 +222,9 @@ let id = uuid()
|
|
|
213
222
|
await expect(runtime.run()).rejects.toThrow("'uuid' is not defined");
|
|
214
223
|
});
|
|
215
224
|
|
|
216
|
-
test('uuid works when imported from system', async () => {
|
|
225
|
+
test('uuid works when imported from system/utils', async () => {
|
|
217
226
|
const ast = parse(`
|
|
218
|
-
import { uuid } from "system"
|
|
227
|
+
import { uuid } from "system/utils"
|
|
219
228
|
let id = uuid()
|
|
220
229
|
`);
|
|
221
230
|
const runtime = new Runtime(ast, createMockProvider());
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// Tests for throw statement - throw "message" returns immediately with error value
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from 'bun:test';
|
|
4
|
+
import { parse } from '../../parser/parse';
|
|
5
|
+
import { Runtime } from '../index';
|
|
6
|
+
import type { AIProvider, AIResponse } from '../types';
|
|
7
|
+
import { isVibeValue } from '../types';
|
|
8
|
+
|
|
9
|
+
// Mock provider for tests that don't need AI
|
|
10
|
+
function createMockProvider(): AIProvider {
|
|
11
|
+
return {
|
|
12
|
+
async chat(): Promise<AIResponse> {
|
|
13
|
+
return { content: 'mock response', toolCalls: [] };
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('Throw Statement', () => {
|
|
19
|
+
test('throw with string literal creates error value', async () => {
|
|
20
|
+
const ast = parse(`
|
|
21
|
+
function fail(): text {
|
|
22
|
+
throw "Something went wrong"
|
|
23
|
+
return "never reached"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let result = fail()
|
|
27
|
+
`);
|
|
28
|
+
const runtime = new Runtime(ast, createMockProvider());
|
|
29
|
+
await runtime.run();
|
|
30
|
+
|
|
31
|
+
const result = runtime.getRawValue('result');
|
|
32
|
+
expect(isVibeValue(result)).toBe(true);
|
|
33
|
+
if (isVibeValue(result)) {
|
|
34
|
+
expect(result.err).toBe(true);
|
|
35
|
+
expect(result.errDetails?.message).toBe('Something went wrong');
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('throw returns immediately from function', async () => {
|
|
40
|
+
const ast = parse(`
|
|
41
|
+
let sideEffect = 0
|
|
42
|
+
|
|
43
|
+
function testThrow(): number {
|
|
44
|
+
sideEffect = 1
|
|
45
|
+
throw "error"
|
|
46
|
+
sideEffect = 2
|
|
47
|
+
return 42
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let result = testThrow()
|
|
51
|
+
`);
|
|
52
|
+
const runtime = new Runtime(ast, createMockProvider());
|
|
53
|
+
await runtime.run();
|
|
54
|
+
|
|
55
|
+
// Side effect should only be 1 (before throw)
|
|
56
|
+
expect(runtime.getValue('sideEffect')).toBe(1);
|
|
57
|
+
|
|
58
|
+
const result = runtime.getRawValue('result');
|
|
59
|
+
expect(isVibeValue(result)).toBe(true);
|
|
60
|
+
if (isVibeValue(result)) {
|
|
61
|
+
expect(result.err).toBe(true);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('throw with expression evaluates message', async () => {
|
|
66
|
+
const ast = parse(`
|
|
67
|
+
function divide(a: number, b: number): number {
|
|
68
|
+
if b == 0 {
|
|
69
|
+
throw "Cannot divide " + a + " by zero"
|
|
70
|
+
}
|
|
71
|
+
return a / b
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let result = divide(10, 0)
|
|
75
|
+
`);
|
|
76
|
+
const runtime = new Runtime(ast, createMockProvider());
|
|
77
|
+
await runtime.run();
|
|
78
|
+
|
|
79
|
+
const result = runtime.getRawValue('result');
|
|
80
|
+
expect(isVibeValue(result)).toBe(true);
|
|
81
|
+
if (isVibeValue(result)) {
|
|
82
|
+
expect(result.err).toBe(true);
|
|
83
|
+
expect(result.errDetails?.message).toBe('Cannot divide 10 by zero');
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('successful function call returns normal value', async () => {
|
|
88
|
+
const ast = parse(`
|
|
89
|
+
function divide(a: number, b: number): number {
|
|
90
|
+
if b == 0 {
|
|
91
|
+
throw "Division by zero"
|
|
92
|
+
}
|
|
93
|
+
return a / b
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let result = divide(10, 2)
|
|
97
|
+
`);
|
|
98
|
+
const runtime = new Runtime(ast, createMockProvider());
|
|
99
|
+
await runtime.run();
|
|
100
|
+
|
|
101
|
+
const result = runtime.getRawValue('result');
|
|
102
|
+
expect(isVibeValue(result)).toBe(true);
|
|
103
|
+
if (isVibeValue(result)) {
|
|
104
|
+
expect(result.err).toBe(false);
|
|
105
|
+
expect(result.value).toBe(5);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('error propagates through expressions', async () => {
|
|
110
|
+
const ast = parse(`
|
|
111
|
+
function fail(): number {
|
|
112
|
+
throw "error"
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let a = fail()
|
|
116
|
+
let b = a + 10
|
|
117
|
+
`);
|
|
118
|
+
const runtime = new Runtime(ast, createMockProvider());
|
|
119
|
+
await runtime.run();
|
|
120
|
+
|
|
121
|
+
const a = runtime.getRawValue('a');
|
|
122
|
+
const b = runtime.getRawValue('b');
|
|
123
|
+
|
|
124
|
+
expect(isVibeValue(a)).toBe(true);
|
|
125
|
+
expect(isVibeValue(b)).toBe(true);
|
|
126
|
+
|
|
127
|
+
if (isVibeValue(a) && isVibeValue(b)) {
|
|
128
|
+
expect(a.err).toBe(true);
|
|
129
|
+
expect(b.err).toBe(true); // Error propagates
|
|
130
|
+
expect(b.errDetails?.message).toBe('error');
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('caller can check for error with .err', async () => {
|
|
135
|
+
const ast = parse(`
|
|
136
|
+
function mayFail(shouldFail: boolean): text {
|
|
137
|
+
if shouldFail {
|
|
138
|
+
throw "Failed!"
|
|
139
|
+
}
|
|
140
|
+
return "Success"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let result1 = mayFail(true)
|
|
144
|
+
let hasError1 = result1.err
|
|
145
|
+
|
|
146
|
+
let result2 = mayFail(false)
|
|
147
|
+
let hasError2 = result2.err
|
|
148
|
+
`);
|
|
149
|
+
const runtime = new Runtime(ast, createMockProvider());
|
|
150
|
+
await runtime.run();
|
|
151
|
+
|
|
152
|
+
expect(runtime.getValue('hasError1')).toBe(true);
|
|
153
|
+
expect(runtime.getValue('hasError2')).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('throw at top level completes with error', async () => {
|
|
157
|
+
const ast = parse(`
|
|
158
|
+
let x = 1
|
|
159
|
+
throw "Top level error"
|
|
160
|
+
let y = 2
|
|
161
|
+
`);
|
|
162
|
+
const runtime = new Runtime(ast, createMockProvider());
|
|
163
|
+
await runtime.run();
|
|
164
|
+
|
|
165
|
+
expect(runtime.getValue('x')).toBe(1);
|
|
166
|
+
// y should not be set because throw exits
|
|
167
|
+
expect(runtime.getValue('y')).toBeUndefined();
|
|
168
|
+
|
|
169
|
+
// lastResult should be the error
|
|
170
|
+
const lastResult = runtime.getState().lastResult;
|
|
171
|
+
expect(isVibeValue(lastResult)).toBe(true);
|
|
172
|
+
if (isVibeValue(lastResult)) {
|
|
173
|
+
expect(lastResult.err).toBe(true);
|
|
174
|
+
expect(lastResult.errDetails?.message).toBe('Top level error');
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('throw in nested function unwinds correctly', async () => {
|
|
179
|
+
const ast = parse(`
|
|
180
|
+
function inner(): number {
|
|
181
|
+
throw "inner error"
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function outer(): number {
|
|
185
|
+
let x = inner()
|
|
186
|
+
return x + 1
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let result = outer()
|
|
190
|
+
`);
|
|
191
|
+
const runtime = new Runtime(ast, createMockProvider());
|
|
192
|
+
await runtime.run();
|
|
193
|
+
|
|
194
|
+
const result = runtime.getRawValue('result');
|
|
195
|
+
expect(isVibeValue(result)).toBe(true);
|
|
196
|
+
if (isVibeValue(result)) {
|
|
197
|
+
expect(result.err).toBe(true);
|
|
198
|
+
expect(result.errDetails?.message).toBe('inner error');
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('throw with variable message', async () => {
|
|
203
|
+
const ast = parse(`
|
|
204
|
+
function failWith(msg: text): text {
|
|
205
|
+
throw msg
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let result = failWith("custom error message")
|
|
209
|
+
`);
|
|
210
|
+
const runtime = new Runtime(ast, createMockProvider());
|
|
211
|
+
await runtime.run();
|
|
212
|
+
|
|
213
|
+
const result = runtime.getRawValue('result');
|
|
214
|
+
expect(isVibeValue(result)).toBe(true);
|
|
215
|
+
if (isVibeValue(result)) {
|
|
216
|
+
expect(result.err).toBe(true);
|
|
217
|
+
expect(result.errDetails?.message).toBe('custom error message');
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
});
|