@vibe-lang/runtime 0.2.6 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-lang/runtime",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "Vibe language runtime and CLI",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -4,12 +4,34 @@ import { vibeAstVisitor } from './visitor';
4
4
  import { setCurrentFile } from './visitor/helpers';
5
5
  import { ParserError } from '../errors';
6
6
  import type { Program } from '../ast';
7
+ import type { IRecognitionException } from 'chevrotain';
7
8
 
8
9
  export interface ParseOptions {
9
10
  /** File path to include in source locations (for error reporting) */
10
11
  file?: string;
11
12
  }
12
13
 
14
+ /**
15
+ * Transform Chevrotain errors into user-friendly messages
16
+ */
17
+ function improveErrorMessage(error: IRecognitionException): string {
18
+ const ruleStack = error.context?.ruleStack ?? [];
19
+ const previousToken = error.previousToken;
20
+ const message = error.message;
21
+
22
+ // Missing type annotation for function/tool parameter
23
+ // Detected: in "parameter" or "toolParameter" rule, expected Colon, previous token is Identifier
24
+ if (
25
+ (ruleStack.includes('parameter') || ruleStack.includes('toolParameter')) &&
26
+ message.includes('Colon') &&
27
+ previousToken?.tokenType?.name === 'Identifier'
28
+ ) {
29
+ return `Missing type annotation for parameter '${previousToken.image}'`;
30
+ }
31
+
32
+ return message;
33
+ }
34
+
13
35
  /**
14
36
  * Parse a Vibe source code string into an AST
15
37
  */
@@ -28,7 +50,7 @@ export function parse(source: string, options?: ParseOptions): Program {
28
50
  if (vibeParser.errors.length > 0) {
29
51
  const error = vibeParser.errors[0];
30
52
  throw new ParserError(
31
- error.message,
53
+ improveErrorMessage(error),
32
54
  error.token.image,
33
55
  { line: error.token.startLine ?? 1, column: error.token.startColumn ?? 1, file: options?.file },
34
56
  source
@@ -58,6 +58,40 @@ function foo {
58
58
  `)).toThrow();
59
59
  });
60
60
 
61
+ test('function parameter missing type annotation', () => {
62
+ expect(() => parse(`function foo(x) { }`)).toThrow(
63
+ "Missing type annotation for parameter 'x'"
64
+ );
65
+ });
66
+
67
+ test('function first parameter missing type annotation', () => {
68
+ expect(() => parse(`function foo(x, y: text) { }`)).toThrow(
69
+ "Missing type annotation for parameter 'x'"
70
+ );
71
+ });
72
+
73
+ test('function second parameter missing type annotation', () => {
74
+ expect(() => parse(`function foo(x: text, y) { }`)).toThrow(
75
+ "Missing type annotation for parameter 'y'"
76
+ );
77
+ });
78
+
79
+ test('export function parameter missing type annotation', () => {
80
+ expect(() => parse(`export function foo(guesser, answerer) { }`)).toThrow(
81
+ "Missing type annotation for parameter 'guesser'"
82
+ );
83
+ });
84
+
85
+ // ============================================================================
86
+ // tool declaration
87
+ // ============================================================================
88
+
89
+ test('tool parameter missing type annotation', () => {
90
+ expect(() => parse(`tool myTool(x) { ts() { return 1; } }`)).toThrow(
91
+ "Missing type annotation for parameter 'x'"
92
+ );
93
+ });
94
+
61
95
  // ============================================================================
62
96
  // if statement
63
97
  // ============================================================================
@@ -1,6 +1,8 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
2
  import { parse } from '../../parser/parse';
3
3
  import { Runtime, type AIProvider } from '../index';
4
+ import { createInitialState, resumeWithAIResponse } from '../state';
5
+ import { runUntilPause } from '../step';
4
6
 
5
7
  describe('Runtime - Model Config Value Resolution', () => {
6
8
  const mockProvider: AIProvider = {
@@ -335,4 +337,144 @@ let key = ts(testModel) {
335
337
  await runtime.run();
336
338
  expect(runtime.getValue('key')).toBe('fallback-key');
337
339
  });
340
+
341
+ // ============================================================================
342
+ // Model as Function Parameter
343
+ // ============================================================================
344
+
345
+ test('function with model parameter - access model properties', async () => {
346
+ const runtime = createRuntime(`
347
+ model myModel = {
348
+ name: "gpt-4",
349
+ apiKey: "test-key",
350
+ provider: "openai"
351
+ }
352
+
353
+ function getModelName(m: model): text {
354
+ return ts(m) { return m.name; }
355
+ }
356
+
357
+ let name = getModelName(myModel)
358
+ `);
359
+ await runtime.run();
360
+ expect(runtime.getValue('name')).toBe('gpt-4');
361
+ });
362
+
363
+ test('function with multiple model parameters', async () => {
364
+ const runtime = createRuntime(`
365
+ model guesserModel = {
366
+ name: "claude-sonnet-4-20250514",
367
+ apiKey: "key1",
368
+ provider: "anthropic"
369
+ }
370
+
371
+ model answererModel = {
372
+ name: "gpt-4",
373
+ apiKey: "key2",
374
+ provider: "openai"
375
+ }
376
+
377
+ function getModelNames(guesser: model, answerer: model): text {
378
+ let g = ts(guesser) { return guesser.name; }
379
+ let a = ts(answerer) { return answerer.name; }
380
+ return g + " vs " + a
381
+ }
382
+
383
+ let result = getModelNames(guesserModel, answererModel)
384
+ `);
385
+ await runtime.run();
386
+ expect(runtime.getValue('result')).toBe('claude-sonnet-4-20250514 vs gpt-4');
387
+ });
388
+
389
+ test('function with model and other parameters', async () => {
390
+ const runtime = createRuntime(`
391
+ model testModel = {
392
+ name: "test-model",
393
+ apiKey: "key",
394
+ provider: "test"
395
+ }
396
+
397
+ function processWithModel(m: model, data: text): text {
398
+ let modelName = ts(m) { return m.name; }
399
+ return modelName + ": " + data
400
+ }
401
+
402
+ let output = processWithModel(testModel, "hello")
403
+ `);
404
+ await runtime.run();
405
+ expect(runtime.getValue('output')).toBe('test-model: hello');
406
+ });
407
+
408
+ test('function with model parameter used in do expression', () => {
409
+ const ast = parse(`
410
+ model myModel = {
411
+ name: "gpt-4",
412
+ apiKey: "key",
413
+ provider: "openai"
414
+ }
415
+
416
+ function askQuestion(m: model, question: text): text {
417
+ return do question m default
418
+ }
419
+
420
+ let answer = askQuestion(myModel, "What is 2+2?")
421
+ `);
422
+ let state = createInitialState(ast);
423
+ state = runUntilPause(state);
424
+
425
+ // Should be waiting for AI response
426
+ expect(state.status).toBe('awaiting_ai');
427
+ expect(state.pendingAI?.prompt).toBe('What is 2+2?');
428
+ expect(state.pendingAI?.model).toBe('m'); // Model param name inside function
429
+
430
+ // Resume with mock response
431
+ state = resumeWithAIResponse(state, 'The answer is 4');
432
+ state = runUntilPause(state);
433
+
434
+ expect(state.status).toBe('completed');
435
+ expect(state.callStack[0].locals['answer']?.value).toBe('The answer is 4');
436
+ });
437
+
438
+ test('function with two model parameters used in different do expressions', () => {
439
+ const ast = parse(`
440
+ model guesser = {
441
+ name: "claude-sonnet-4-20250514",
442
+ apiKey: "key1",
443
+ provider: "anthropic"
444
+ }
445
+
446
+ model answerer = {
447
+ name: "gpt-4",
448
+ apiKey: "key2",
449
+ provider: "openai"
450
+ }
451
+
452
+ function playRound(g: model, a: model, category: text): text {
453
+ let question = do "Ask a question about {category}" g default
454
+ let answer = do "Answer: {question}" a default
455
+ return answer
456
+ }
457
+
458
+ let result = playRound(guesser, answerer, "animals")
459
+ `);
460
+ let state = createInitialState(ast);
461
+ state = runUntilPause(state);
462
+
463
+ // First AI call uses guesser model (param name 'g')
464
+ expect(state.status).toBe('awaiting_ai');
465
+ expect(state.pendingAI?.model).toBe('g');
466
+
467
+ state = resumeWithAIResponse(state, 'Is a penguin a bird?');
468
+ state = runUntilPause(state);
469
+
470
+ // Second AI call uses answerer model (param name 'a')
471
+ expect(state.status).toBe('awaiting_ai');
472
+ expect(state.pendingAI?.model).toBe('a');
473
+
474
+ state = resumeWithAIResponse(state, 'Yes, a penguin is a bird.');
475
+ state = runUntilPause(state);
476
+
477
+ expect(state.status).toBe('completed');
478
+ expect(state.callStack[0].locals['result']?.value).toBe('Yes, a penguin is a bird.');
479
+ });
338
480
  });