@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
|
@@ -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
|
+
});
|
|
@@ -177,9 +177,9 @@ let x = 1
|
|
|
177
177
|
expect(runtime.getValue('x')).toBe(1);
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
-
test('uuid function works when imported from system', async () => {
|
|
180
|
+
test('uuid function works when imported from system/utils', async () => {
|
|
181
181
|
const ast = parse(`
|
|
182
|
-
import { uuid } from "system"
|
|
182
|
+
import { uuid } from "system/utils"
|
|
183
183
|
let id = uuid()
|
|
184
184
|
`);
|
|
185
185
|
const runtime = new Runtime(ast, createMockProvider());
|
|
@@ -190,41 +190,41 @@ let id = uuid()
|
|
|
190
190
|
});
|
|
191
191
|
|
|
192
192
|
// ============================================================================
|
|
193
|
-
//
|
|
193
|
+
// Utility functions can be called directly from system/utils
|
|
194
194
|
// ============================================================================
|
|
195
195
|
|
|
196
|
-
test('
|
|
196
|
+
test('now can be called directly from system/utils', async () => {
|
|
197
197
|
const ast = parse(`
|
|
198
|
-
import { now } from "system/
|
|
198
|
+
import { now } from "system/utils"
|
|
199
199
|
let timestamp = now()
|
|
200
200
|
`);
|
|
201
201
|
const runtime = new Runtime(ast, createMockProvider());
|
|
202
|
-
await
|
|
203
|
-
|
|
204
|
-
);
|
|
202
|
+
await runtime.run();
|
|
203
|
+
const timestamp = runtime.getValue('timestamp');
|
|
204
|
+
expect(typeof timestamp).toBe('number');
|
|
205
|
+
expect(timestamp).toBeGreaterThan(0);
|
|
205
206
|
});
|
|
206
207
|
|
|
207
|
-
test('jsonParse
|
|
208
|
+
test('jsonParse can be called directly from system/utils', async () => {
|
|
208
209
|
const ast = parse(`
|
|
209
|
-
import { jsonParse } from "system/
|
|
210
|
+
import { jsonParse } from "system/utils"
|
|
210
211
|
let parsed = jsonParse('{"key": "value"}')
|
|
212
|
+
let val = ts(parsed) { return parsed.key }
|
|
211
213
|
`);
|
|
212
214
|
const runtime = new Runtime(ast, createMockProvider());
|
|
213
|
-
await
|
|
214
|
-
|
|
215
|
-
);
|
|
215
|
+
await runtime.run();
|
|
216
|
+
expect(runtime.getValue('val')).toBe('value');
|
|
216
217
|
});
|
|
217
218
|
|
|
218
|
-
test('jsonStringify
|
|
219
|
+
test('jsonStringify can be called directly from system/utils', async () => {
|
|
219
220
|
const ast = parse(`
|
|
220
|
-
import { jsonStringify } from "system/
|
|
221
|
+
import { jsonStringify } from "system/utils"
|
|
221
222
|
let obj:json = {name: "test"}
|
|
222
223
|
let str = jsonStringify(obj)
|
|
223
224
|
`);
|
|
224
225
|
const runtime = new Runtime(ast, createMockProvider());
|
|
225
|
-
await
|
|
226
|
-
|
|
227
|
-
);
|
|
226
|
+
await runtime.run();
|
|
227
|
+
expect(runtime.getValue('str')).toBe('{"name":"test"}');
|
|
228
228
|
});
|
|
229
229
|
|
|
230
230
|
test('allTools array can be imported from system/tools', async () => {
|
|
@@ -235,7 +235,7 @@ let toolCount = ts(allTools) { return allTools.length }
|
|
|
235
235
|
const runtime = new Runtime(ast, createMockProvider());
|
|
236
236
|
await runtime.run();
|
|
237
237
|
const toolCount = runtime.getValue('toolCount');
|
|
238
|
-
expect(toolCount).toBe(
|
|
238
|
+
expect(toolCount).toBe(13); // File, search, directory, and system tools for AI
|
|
239
239
|
});
|
|
240
240
|
});
|
|
241
241
|
|
|
@@ -332,9 +332,9 @@ let x = 1
|
|
|
332
332
|
const runtime = new Runtime(ast, createMockProvider());
|
|
333
333
|
await runtime.run();
|
|
334
334
|
|
|
335
|
-
// Verify model has all
|
|
335
|
+
// Verify model has all 13 AI tools (utility functions now in system/utils)
|
|
336
336
|
const model = runtime.getValue('m') as { tools?: unknown[] };
|
|
337
|
-
expect(model.tools).toHaveLength(
|
|
337
|
+
expect(model.tools).toHaveLength(13);
|
|
338
338
|
});
|
|
339
339
|
|
|
340
340
|
test('model without tools parameter has undefined tools', async () => {
|
|
@@ -355,7 +355,7 @@ let x = 1
|
|
|
355
355
|
});
|
|
356
356
|
|
|
357
357
|
describe('Runtime - Tool Bundles', () => {
|
|
358
|
-
test('allTools contains all
|
|
358
|
+
test('allTools contains all 13 AI tools including bash and runCode', async () => {
|
|
359
359
|
const ast = parse(`
|
|
360
360
|
import { allTools } from "system/tools"
|
|
361
361
|
let count = ts(allTools) { return allTools.length }
|
|
@@ -364,7 +364,7 @@ let names = ts(allTools) { return allTools.map(t => t.name).sort() }
|
|
|
364
364
|
const runtime = new Runtime(ast, createMockProvider());
|
|
365
365
|
await runtime.run();
|
|
366
366
|
|
|
367
|
-
expect(runtime.getValue('count')).toBe(
|
|
367
|
+
expect(runtime.getValue('count')).toBe(13);
|
|
368
368
|
const names = runtime.getValue('names') as string[];
|
|
369
369
|
expect(names).toContain('bash');
|
|
370
370
|
expect(names).toContain('runCode');
|
|
@@ -381,7 +381,7 @@ let names = ts(readonlyTools) { return readonlyTools.map(t => t.name).sort() }
|
|
|
381
381
|
const runtime = new Runtime(ast, createMockProvider());
|
|
382
382
|
await runtime.run();
|
|
383
383
|
|
|
384
|
-
expect(runtime.getValue('count')).toBe(
|
|
384
|
+
expect(runtime.getValue('count')).toBe(6);
|
|
385
385
|
const names = runtime.getValue('names') as string[];
|
|
386
386
|
// Should include read-only tools
|
|
387
387
|
expect(names).toContain('readFile');
|
|
@@ -409,7 +409,7 @@ let names = ts(safeTools) { return safeTools.map(t => t.name).sort() }
|
|
|
409
409
|
const runtime = new Runtime(ast, createMockProvider());
|
|
410
410
|
await runtime.run();
|
|
411
411
|
|
|
412
|
-
expect(runtime.getValue('count')).toBe(
|
|
412
|
+
expect(runtime.getValue('count')).toBe(11);
|
|
413
413
|
const names = runtime.getValue('names') as string[];
|
|
414
414
|
// Should NOT include bash or runCode
|
|
415
415
|
expect(names).not.toContain('bash');
|
|
@@ -430,7 +430,7 @@ let hasRunCode = ts(runCode) { return runCode.name === "runCode" }
|
|
|
430
430
|
const runtime = new Runtime(ast, createMockProvider());
|
|
431
431
|
await runtime.run();
|
|
432
432
|
|
|
433
|
-
expect(runtime.getValue('readCount')).toBe(
|
|
433
|
+
expect(runtime.getValue('readCount')).toBe(6);
|
|
434
434
|
expect(runtime.getValue('hasBash')).toBe(true);
|
|
435
435
|
expect(runtime.getValue('hasRunCode')).toBe(true);
|
|
436
436
|
});
|
|
@@ -445,8 +445,8 @@ let names = ts(combined) { return combined.map(t => t.name) }
|
|
|
445
445
|
const runtime = new Runtime(ast, createMockProvider());
|
|
446
446
|
await runtime.run();
|
|
447
447
|
|
|
448
|
-
// readonlyTools (
|
|
449
|
-
expect(runtime.getValue('count')).toBe(
|
|
448
|
+
// readonlyTools (6) + [bash] (1) = 7
|
|
449
|
+
expect(runtime.getValue('count')).toBe(7);
|
|
450
450
|
const names = runtime.getValue('names') as string[];
|
|
451
451
|
expect(names).toContain('readFile');
|
|
452
452
|
expect(names).toContain('bash');
|
|
@@ -461,7 +461,7 @@ let count = ts(custom) { return custom.length }
|
|
|
461
461
|
const runtime = new Runtime(ast, createMockProvider());
|
|
462
462
|
await runtime.run();
|
|
463
463
|
|
|
464
|
-
// readonlyTools (
|
|
465
|
-
expect(runtime.getValue('count')).toBe(
|
|
464
|
+
// readonlyTools (6) + bash (1) + runCode (1) = 8
|
|
465
|
+
expect(runtime.getValue('count')).toBe(8);
|
|
466
466
|
});
|
|
467
467
|
});
|
package/src/runtime/types.ts
CHANGED
|
@@ -794,4 +794,7 @@ export type Instruction =
|
|
|
794
794
|
| { op: 'destructure_assign'; fields: ExpectedField[]; isConst: boolean; location: SourceLocation }
|
|
795
795
|
|
|
796
796
|
// Break loop - exit innermost loop with cleanup
|
|
797
|
-
| { op: 'break_loop'; savedKeys: string[]; contextMode?: ContextMode; label?: string; entryIndex: number; scopeType: 'for' | 'while'; location: SourceLocation }
|
|
797
|
+
| { op: 'break_loop'; savedKeys: string[]; contextMode?: ContextMode; label?: string; entryIndex: number; scopeType: 'for' | 'while'; location: SourceLocation }
|
|
798
|
+
|
|
799
|
+
// Throw error - unwind to function boundary and return error value
|
|
800
|
+
| { op: 'throw_error'; location: SourceLocation };
|
|
@@ -24,6 +24,12 @@ export function validateAndCoerce(
|
|
|
24
24
|
location?: SourceLocation,
|
|
25
25
|
source?: 'ai' | 'user'
|
|
26
26
|
): { value: unknown; inferredType: VibeType } {
|
|
27
|
+
// If value is an error VibeValue, pass it through without validation
|
|
28
|
+
// Errors should propagate unchanged regardless of type annotation
|
|
29
|
+
if (isVibeValue(value) && value.err) {
|
|
30
|
+
return { value, inferredType: type };
|
|
31
|
+
}
|
|
32
|
+
|
|
27
33
|
// Resolve VibeValue unless this is a direct AI result to an UNTYPED variable
|
|
28
34
|
// (source === 'ai' means the value came directly from an AI call)
|
|
29
35
|
// For typed variables, always resolve so type validation can work on the primitive value
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { SourceLocation } from '../errors';
|
|
8
8
|
import type { SymbolTable, SymbolKind } from './symbol-table';
|
|
9
9
|
import type { TsFunctionSignature } from './ts-signatures';
|
|
10
|
+
import type { VibeType } from '../ast';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Context interface passed to analyzer helper functions.
|
|
@@ -59,4 +60,5 @@ export interface AnalyzerState {
|
|
|
59
60
|
inFunction: boolean;
|
|
60
61
|
atTopLevel: boolean;
|
|
61
62
|
loopDepth: number;
|
|
63
|
+
currentFunctionReturnType: VibeType; // Track return type for prompt context validation
|
|
62
64
|
}
|
|
@@ -10,6 +10,8 @@ import { isValidType } from './types';
|
|
|
10
10
|
import { isCoreFunction } from '../runtime/stdlib/core';
|
|
11
11
|
import { extractFunctionSignature } from './ts-signatures';
|
|
12
12
|
import { resolve, dirname } from 'path';
|
|
13
|
+
import { existsSync, readFileSync } from 'fs';
|
|
14
|
+
import { parse } from '../parser/parse';
|
|
13
15
|
import {
|
|
14
16
|
validateModelConfig,
|
|
15
17
|
validateToolDeclaration,
|
|
@@ -169,7 +171,15 @@ export function createVisitors(
|
|
|
169
171
|
if (!state.inFunction) {
|
|
170
172
|
ctx.error('return outside of function', node.location);
|
|
171
173
|
}
|
|
172
|
-
if (node.value)
|
|
174
|
+
if (node.value) {
|
|
175
|
+
// If returning from a prompt-typed function, validate string literals as prompt context
|
|
176
|
+
const isPromptReturn = state.currentFunctionReturnType === 'prompt';
|
|
177
|
+
if (isPromptReturn && (node.value.type === 'StringLiteral' || node.value.type === 'TemplateLiteral')) {
|
|
178
|
+
validateStringInterpolation(ctx, node.value.value, true, node.value.location);
|
|
179
|
+
} else {
|
|
180
|
+
visitExpression(node.value);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
173
183
|
break;
|
|
174
184
|
|
|
175
185
|
case 'BreakStatement':
|
|
@@ -178,6 +188,11 @@ export function createVisitors(
|
|
|
178
188
|
}
|
|
179
189
|
break;
|
|
180
190
|
|
|
191
|
+
case 'ThrowStatement':
|
|
192
|
+
// Visit the message expression for any nested checks
|
|
193
|
+
visitExpression(node.message);
|
|
194
|
+
break;
|
|
195
|
+
|
|
181
196
|
case 'IfStatement':
|
|
182
197
|
visitExpression(node.condition);
|
|
183
198
|
validateConditionType(ctx, node.condition, 'if', getExprType);
|
|
@@ -233,6 +248,7 @@ export function createVisitors(
|
|
|
233
248
|
returnType: node.returnType,
|
|
234
249
|
});
|
|
235
250
|
validateToolDeclaration(ctx, node);
|
|
251
|
+
visitTool(node);
|
|
236
252
|
break;
|
|
237
253
|
|
|
238
254
|
case 'AsyncStatement':
|
|
@@ -471,9 +487,12 @@ export function createVisitors(
|
|
|
471
487
|
return;
|
|
472
488
|
}
|
|
473
489
|
|
|
490
|
+
// Check for system module imports
|
|
491
|
+
const isSystemModule = node.source === 'system/utils' || node.source === 'system/tools';
|
|
474
492
|
const isToolImport = node.source === 'system/tools';
|
|
475
493
|
|
|
476
|
-
|
|
494
|
+
// Extract TypeScript signatures for non-system TS imports
|
|
495
|
+
if (node.sourceType === 'ts' && ctx.basePath && !isSystemModule) {
|
|
477
496
|
const sourcePath = resolve(dirname(ctx.basePath), node.source);
|
|
478
497
|
for (const spec of node.specifiers) {
|
|
479
498
|
try {
|
|
@@ -487,6 +506,12 @@ export function createVisitors(
|
|
|
487
506
|
}
|
|
488
507
|
}
|
|
489
508
|
|
|
509
|
+
// Validate Vibe imports - check that imported symbols exist in source file
|
|
510
|
+
let exportedNames: Set<string> | null = null;
|
|
511
|
+
if (node.sourceType === 'vibe' && ctx.basePath) {
|
|
512
|
+
exportedNames = getExportedNamesFromVibeFile(ctx.basePath, node.source);
|
|
513
|
+
}
|
|
514
|
+
|
|
490
515
|
for (const spec of node.specifiers) {
|
|
491
516
|
const existing = ctx.symbols.lookup(spec.local);
|
|
492
517
|
if (existing) {
|
|
@@ -502,6 +527,14 @@ export function createVisitors(
|
|
|
502
527
|
);
|
|
503
528
|
}
|
|
504
529
|
} else {
|
|
530
|
+
// Validate that imported name exists in source file (for Vibe imports)
|
|
531
|
+
if (exportedNames !== null && !exportedNames.has(spec.imported)) {
|
|
532
|
+
ctx.error(
|
|
533
|
+
`'${spec.imported}' is not exported from '${node.source}'`,
|
|
534
|
+
node.location
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
505
538
|
// Tool bundles (allTools, readonlyTools, safeTools) are imports, individual tools are 'tool' kind
|
|
506
539
|
const toolBundles = ['allTools', 'readonlyTools', 'safeTools'];
|
|
507
540
|
const importKind = isToolImport && !toolBundles.includes(spec.local) ? 'tool' : 'import';
|
|
@@ -510,9 +543,88 @@ export function createVisitors(
|
|
|
510
543
|
}
|
|
511
544
|
}
|
|
512
545
|
|
|
546
|
+
/**
|
|
547
|
+
* Get the set of exported names from a Vibe source file
|
|
548
|
+
* Returns null if the file cannot be read/parsed
|
|
549
|
+
*/
|
|
550
|
+
function getExportedNamesFromVibeFile(basePath: string, importSource: string): Set<string> | null {
|
|
551
|
+
try {
|
|
552
|
+
const sourcePath = resolve(dirname(basePath), importSource);
|
|
553
|
+
if (!existsSync(sourcePath)) {
|
|
554
|
+
return null; // File doesn't exist - let runtime handle this error
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const sourceContent = readFileSync(sourcePath, 'utf-8');
|
|
558
|
+
const sourceAst = parse(sourceContent, { file: sourcePath });
|
|
559
|
+
|
|
560
|
+
const exportedNames = new Set<string>();
|
|
561
|
+
for (const statement of sourceAst.body) {
|
|
562
|
+
if (statement.type === 'ExportDeclaration') {
|
|
563
|
+
const decl = statement.declaration;
|
|
564
|
+
if ('name' in decl) {
|
|
565
|
+
exportedNames.add(decl.name);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return exportedNames;
|
|
571
|
+
} catch {
|
|
572
|
+
return null; // Parse error or other issue - skip validation
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Check if a statement always returns or throws on all code paths.
|
|
578
|
+
* Used to verify functions with return types exit properly.
|
|
579
|
+
*
|
|
580
|
+
* Note: In Vibe, if a function/tool body ends with an expression statement,
|
|
581
|
+
* that expression's value is implicitly returned.
|
|
582
|
+
*/
|
|
583
|
+
function alwaysReturnsOrThrows(stmt: AST.Statement, isLastInBlock: boolean = false): boolean {
|
|
584
|
+
switch (stmt.type) {
|
|
585
|
+
case 'ReturnStatement':
|
|
586
|
+
case 'ThrowStatement':
|
|
587
|
+
return true;
|
|
588
|
+
|
|
589
|
+
case 'ExpressionStatement':
|
|
590
|
+
// An expression at the end of a function/tool body is an implicit return
|
|
591
|
+
return isLastInBlock;
|
|
592
|
+
|
|
593
|
+
case 'IfStatement':
|
|
594
|
+
// Must have else branch and both branches must return
|
|
595
|
+
if (!stmt.alternate) {
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
return alwaysReturnsOrThrows(stmt.consequent, isLastInBlock) &&
|
|
599
|
+
alwaysReturnsOrThrows(stmt.alternate, isLastInBlock);
|
|
600
|
+
|
|
601
|
+
case 'BlockStatement':
|
|
602
|
+
// Check if any statement in the block guarantees return/throw
|
|
603
|
+
// For the last statement, also consider implicit returns
|
|
604
|
+
for (let i = 0; i < stmt.body.length; i++) {
|
|
605
|
+
const s = stmt.body[i];
|
|
606
|
+
const isLast = i === stmt.body.length - 1;
|
|
607
|
+
if (alwaysReturnsOrThrows(s, isLast && isLastInBlock)) {
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return false;
|
|
612
|
+
|
|
613
|
+
case 'ForInStatement':
|
|
614
|
+
case 'WhileStatement':
|
|
615
|
+
// Loops don't guarantee execution (might iterate 0 times)
|
|
616
|
+
return false;
|
|
617
|
+
|
|
618
|
+
default:
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
513
623
|
function visitFunction(node: AST.FunctionDeclaration): void {
|
|
514
624
|
const wasInFunction = state.inFunction;
|
|
625
|
+
const prevReturnType = state.currentFunctionReturnType;
|
|
515
626
|
state.inFunction = true;
|
|
627
|
+
state.currentFunctionReturnType = node.returnType;
|
|
516
628
|
ctx.symbols.enterScope();
|
|
517
629
|
|
|
518
630
|
for (const param of node.params) {
|
|
@@ -526,8 +638,43 @@ export function createVisitors(
|
|
|
526
638
|
|
|
527
639
|
visitStatement(node.body);
|
|
528
640
|
|
|
641
|
+
// Check that typed functions always return or throw
|
|
642
|
+
if (node.returnType && !alwaysReturnsOrThrows(node.body, true)) {
|
|
643
|
+
ctx.error(
|
|
644
|
+
`Function '${node.name}' has return type '${node.returnType}' but not all code paths return or throw`,
|
|
645
|
+
node.location
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
ctx.symbols.exitScope();
|
|
650
|
+
state.inFunction = wasInFunction;
|
|
651
|
+
state.currentFunctionReturnType = prevReturnType;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function visitTool(node: AST.ToolDeclaration): void {
|
|
655
|
+
const wasInFunction = state.inFunction;
|
|
656
|
+
const prevReturnType = state.currentFunctionReturnType;
|
|
657
|
+
state.inFunction = true;
|
|
658
|
+
state.currentFunctionReturnType = node.returnType;
|
|
659
|
+
ctx.symbols.enterScope();
|
|
660
|
+
|
|
661
|
+
for (const param of node.params) {
|
|
662
|
+
ctx.declare(param.name, 'parameter', node.location, { typeAnnotation: param.typeAnnotation });
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
visitStatement(node.body);
|
|
666
|
+
|
|
667
|
+
// Tools always have a return type - check that all code paths return or throw
|
|
668
|
+
if (!alwaysReturnsOrThrows(node.body, true)) {
|
|
669
|
+
ctx.error(
|
|
670
|
+
`Tool '${node.name}' has return type '${node.returnType}' but not all code paths return or throw`,
|
|
671
|
+
node.location
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
529
675
|
ctx.symbols.exitScope();
|
|
530
676
|
state.inFunction = wasInFunction;
|
|
677
|
+
state.currentFunctionReturnType = prevReturnType;
|
|
531
678
|
}
|
|
532
679
|
|
|
533
680
|
return { visitStatement, visitExpression };
|
package/src/semantic/analyzer.ts
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Test fixture with exports for import validation tests
|
|
2
|
+
|
|
3
|
+
export function greet(name: text): text {
|
|
4
|
+
return "Hello " + name
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function add(a: number, b: number): number {
|
|
8
|
+
return a + b
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const VERSION = "1.0.0"
|
|
12
|
+
|
|
13
|
+
export model testModel = {
|
|
14
|
+
name: "gpt-4",
|
|
15
|
+
apiKey: "test-key",
|
|
16
|
+
url: "https://api.example.com"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// This function is NOT exported
|
|
20
|
+
function privateHelper(): text {
|
|
21
|
+
return "private"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// This constant is NOT exported
|
|
25
|
+
const INTERNAL_SECRET = "hidden"
|