@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
|
@@ -110,6 +110,20 @@ model myModel = {
|
|
|
110
110
|
expect(model.config.apiKey.value).toBe('sk-test');
|
|
111
111
|
expect(model.config.url.value).toBe('https://api.openai.com');
|
|
112
112
|
});
|
|
113
|
+
|
|
114
|
+
test('model with trailing comma', () => {
|
|
115
|
+
const ast = parse(`
|
|
116
|
+
model myModel = {
|
|
117
|
+
name: "gpt-4",
|
|
118
|
+
apiKey: "sk-test",
|
|
119
|
+
url: "https://api.openai.com",
|
|
120
|
+
}
|
|
121
|
+
`);
|
|
122
|
+
expect(ast.body).toHaveLength(1);
|
|
123
|
+
const model = ast.body[0] as any;
|
|
124
|
+
expect(model.type).toBe('ModelDeclaration');
|
|
125
|
+
expect(model.config.providedFields).toEqual(['name', 'apiKey', 'url']);
|
|
126
|
+
});
|
|
113
127
|
});
|
|
114
128
|
|
|
115
129
|
describe('Syntax Errors - Model Declaration', () => {
|
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]) };
|
|
@@ -83,8 +83,15 @@ export function getReferencedVariables(expr: AST.Expression): string[] {
|
|
|
83
83
|
}
|
|
84
84
|
break;
|
|
85
85
|
|
|
86
|
-
// Literals don't reference variables
|
|
87
86
|
case 'StringLiteral':
|
|
87
|
+
// Extract variables from {varName} and !{varName} interpolation in strings
|
|
88
|
+
const stringMatches = node.value.matchAll(/!?\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g);
|
|
89
|
+
for (const match of stringMatches) {
|
|
90
|
+
variables.push(match[1]);
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
|
|
94
|
+
// These literals don't reference variables
|
|
88
95
|
case 'NumberLiteral':
|
|
89
96
|
case 'BooleanLiteral':
|
|
90
97
|
case 'NullLiteral':
|
|
@@ -22,11 +22,37 @@ describe('Async Dependency Detection', () => {
|
|
|
22
22
|
expect(getReferencedVariables(expr)).toEqual(['myVar']);
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
test('extracts no variables from string literal', () => {
|
|
25
|
+
test('extracts no variables from plain string literal', () => {
|
|
26
26
|
const expr = parseExpr('"hello"');
|
|
27
27
|
expect(getReferencedVariables(expr)).toEqual([]);
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
test('extracts variables from string literal with {var} interpolation', () => {
|
|
31
|
+
const expr = parseExpr('"Hello {name}!"');
|
|
32
|
+
const vars = getReferencedVariables(expr);
|
|
33
|
+
expect(vars).toContain('name');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('extracts variables from string literal with !{var} expansion', () => {
|
|
37
|
+
const expr = parseExpr('"Process this: !{data}"');
|
|
38
|
+
const vars = getReferencedVariables(expr);
|
|
39
|
+
expect(vars).toContain('data');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('extracts multiple variables from string literal interpolation', () => {
|
|
43
|
+
const expr = parseExpr('"Hello {greeting} {name}, your data is !{info}"');
|
|
44
|
+
const vars = getReferencedVariables(expr);
|
|
45
|
+
expect(vars).toContain('greeting');
|
|
46
|
+
expect(vars).toContain('name');
|
|
47
|
+
expect(vars).toContain('info');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('extracts variables from single-quoted string with interpolation', () => {
|
|
51
|
+
const expr = parseExpr("'User: {user}'");
|
|
52
|
+
const vars = getReferencedVariables(expr);
|
|
53
|
+
expect(vars).toContain('user');
|
|
54
|
+
});
|
|
55
|
+
|
|
30
56
|
test('extracts no variables from number literal', () => {
|
|
31
57
|
const expr = parseExpr('42');
|
|
32
58
|
expect(getReferencedVariables(expr)).toEqual([]);
|
|
@@ -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
|
|
|
@@ -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
|
// =============================================================================
|
|
@@ -567,12 +456,12 @@ export const runCode: VibeToolValue = {
|
|
|
567
456
|
schema: {
|
|
568
457
|
name: 'runCode',
|
|
569
458
|
description:
|
|
570
|
-
'Execute TypeScript
|
|
459
|
+
'Execute TypeScript code in a sandboxed subprocess. IMPORTANT: Only write TypeScript code, never Python or other languages. ' +
|
|
571
460
|
'All scope variables are automatically available as local variables. ' +
|
|
572
461
|
'Use `return value` to pass results back. Bun APIs are available. ' +
|
|
573
462
|
'Each execution creates a unique folder in .vibe-cache/ for intermediate files.',
|
|
574
463
|
parameters: [
|
|
575
|
-
{ name: 'code', type: { type: 'string' }, description: 'TypeScript
|
|
464
|
+
{ name: 'code', type: { type: 'string' }, description: 'TypeScript code to execute (not Python or other languages)', required: true },
|
|
576
465
|
{ name: 'scope', type: { type: 'object', additionalProperties: true }, description: 'Variables to make available in the code', required: false },
|
|
577
466
|
{ name: 'timeout', type: { type: 'number' }, description: 'Timeout in milliseconds (default: 30000)', required: false },
|
|
578
467
|
],
|
|
@@ -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());
|