@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.
@@ -1,16 +1,12 @@
1
- // Standard library functions for Vibe scripts
2
- // Import with: import { uuid } from "system"
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 and CANNOT be imported from "system".
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 direct script use, import functions from "system" instead.
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 all file, search, directory, utility, and system tools.
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
+ }
@@ -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('library functions still require import', () => {
208
- test('uuid requires import from system', async () => {
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
+ });