@vibe-lang/runtime 0.2.5
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 +46 -0
- package/src/ast/index.ts +375 -0
- package/src/ast.ts +2 -0
- package/src/debug/advanced-features.ts +482 -0
- package/src/debug/bun-inspector.ts +424 -0
- package/src/debug/handoff-manager.ts +283 -0
- package/src/debug/index.ts +150 -0
- package/src/debug/runner.ts +365 -0
- package/src/debug/server.ts +565 -0
- package/src/debug/stack-merger.ts +267 -0
- package/src/debug/state.ts +581 -0
- package/src/debug/test/advanced-features.test.ts +300 -0
- package/src/debug/test/e2e.test.ts +218 -0
- package/src/debug/test/handoff-manager.test.ts +256 -0
- package/src/debug/test/runner.test.ts +256 -0
- package/src/debug/test/stack-merger.test.ts +163 -0
- package/src/debug/test/state.test.ts +400 -0
- package/src/debug/test/ts-debug-integration.test.ts +374 -0
- package/src/debug/test/ts-import-tracker.test.ts +125 -0
- package/src/debug/test/ts-source-map.test.ts +169 -0
- package/src/debug/ts-import-tracker.ts +151 -0
- package/src/debug/ts-source-map.ts +171 -0
- package/src/errors/index.ts +124 -0
- package/src/index.ts +358 -0
- package/src/lexer/index.ts +348 -0
- package/src/lexer.ts +2 -0
- package/src/parser/index.ts +792 -0
- package/src/parser/parse.ts +45 -0
- package/src/parser/test/async.test.ts +248 -0
- package/src/parser/test/destructuring.test.ts +167 -0
- package/src/parser/test/do-expression.test.ts +486 -0
- package/src/parser/test/errors/do-expression.test.ts +95 -0
- package/src/parser/test/errors/error-locations.test.ts +230 -0
- package/src/parser/test/errors/invalid-expressions.test.ts +144 -0
- package/src/parser/test/errors/missing-tokens.test.ts +126 -0
- package/src/parser/test/errors/model-declaration.test.ts +185 -0
- package/src/parser/test/errors/nested-blocks.test.ts +226 -0
- package/src/parser/test/errors/unclosed-delimiters.test.ts +122 -0
- package/src/parser/test/errors/unexpected-tokens.test.ts +120 -0
- package/src/parser/test/import-export.test.ts +143 -0
- package/src/parser/test/literals.test.ts +404 -0
- package/src/parser/test/model-declaration.test.ts +161 -0
- package/src/parser/test/nested-blocks.test.ts +402 -0
- package/src/parser/test/parser.test.ts +743 -0
- package/src/parser/test/private.test.ts +136 -0
- package/src/parser/test/template-literal.test.ts +127 -0
- package/src/parser/test/tool-declaration.test.ts +302 -0
- package/src/parser/test/ts-block.test.ts +252 -0
- package/src/parser/test/type-annotations.test.ts +254 -0
- package/src/parser/visitor/helpers.ts +330 -0
- package/src/parser/visitor.ts +794 -0
- package/src/parser.ts +2 -0
- package/src/runtime/ai/cache-chunking.test.ts +69 -0
- package/src/runtime/ai/cache-chunking.ts +73 -0
- package/src/runtime/ai/client.ts +109 -0
- package/src/runtime/ai/context.ts +168 -0
- package/src/runtime/ai/formatters.ts +316 -0
- package/src/runtime/ai/index.ts +38 -0
- package/src/runtime/ai/language-ref.ts +38 -0
- package/src/runtime/ai/providers/anthropic.ts +253 -0
- package/src/runtime/ai/providers/google.ts +201 -0
- package/src/runtime/ai/providers/openai.ts +156 -0
- package/src/runtime/ai/retry.ts +100 -0
- package/src/runtime/ai/return-tools.ts +301 -0
- package/src/runtime/ai/test/client.test.ts +83 -0
- package/src/runtime/ai/test/formatters.test.ts +485 -0
- package/src/runtime/ai/test/retry.test.ts +137 -0
- package/src/runtime/ai/test/return-tools.test.ts +450 -0
- package/src/runtime/ai/test/tool-loop.test.ts +319 -0
- package/src/runtime/ai/test/tool-schema.test.ts +241 -0
- package/src/runtime/ai/tool-loop.ts +203 -0
- package/src/runtime/ai/tool-schema.ts +151 -0
- package/src/runtime/ai/types.ts +113 -0
- package/src/runtime/ai-logger.ts +255 -0
- package/src/runtime/ai-provider.ts +347 -0
- package/src/runtime/async/dependencies.ts +276 -0
- package/src/runtime/async/executor.ts +293 -0
- package/src/runtime/async/index.ts +43 -0
- package/src/runtime/async/scheduling.ts +163 -0
- package/src/runtime/async/test/dependencies.test.ts +284 -0
- package/src/runtime/async/test/executor.test.ts +388 -0
- package/src/runtime/context.ts +357 -0
- package/src/runtime/exec/ai.ts +139 -0
- package/src/runtime/exec/expressions.ts +475 -0
- package/src/runtime/exec/frames.ts +26 -0
- package/src/runtime/exec/functions.ts +305 -0
- package/src/runtime/exec/interpolation.ts +312 -0
- package/src/runtime/exec/statements.ts +604 -0
- package/src/runtime/exec/tools.ts +129 -0
- package/src/runtime/exec/typescript.ts +215 -0
- package/src/runtime/exec/variables.ts +279 -0
- package/src/runtime/index.ts +975 -0
- package/src/runtime/modules.ts +452 -0
- package/src/runtime/serialize.ts +103 -0
- package/src/runtime/state.ts +489 -0
- package/src/runtime/stdlib/core.ts +45 -0
- package/src/runtime/stdlib/directory.test.ts +156 -0
- package/src/runtime/stdlib/edit.test.ts +154 -0
- package/src/runtime/stdlib/fastEdit.test.ts +201 -0
- package/src/runtime/stdlib/glob.test.ts +106 -0
- package/src/runtime/stdlib/grep.test.ts +144 -0
- package/src/runtime/stdlib/index.ts +16 -0
- package/src/runtime/stdlib/readFile.test.ts +123 -0
- package/src/runtime/stdlib/tools/index.ts +707 -0
- package/src/runtime/stdlib/writeFile.test.ts +157 -0
- package/src/runtime/step.ts +969 -0
- package/src/runtime/test/ai-context.test.ts +1086 -0
- package/src/runtime/test/ai-result-object.test.ts +419 -0
- package/src/runtime/test/ai-tool-flow.test.ts +859 -0
- package/src/runtime/test/async-execution-order.test.ts +618 -0
- package/src/runtime/test/async-execution.test.ts +344 -0
- package/src/runtime/test/async-nested.test.ts +660 -0
- package/src/runtime/test/async-parallel-timing.test.ts +546 -0
- package/src/runtime/test/basic1.test.ts +154 -0
- package/src/runtime/test/binary-operators.test.ts +431 -0
- package/src/runtime/test/break-statement.test.ts +257 -0
- package/src/runtime/test/context-modes.test.ts +650 -0
- package/src/runtime/test/context.test.ts +466 -0
- package/src/runtime/test/core-functions.test.ts +228 -0
- package/src/runtime/test/e2e.test.ts +88 -0
- package/src/runtime/test/error-locations/error-locations.test.ts +80 -0
- package/src/runtime/test/error-locations/main-error.vibe +4 -0
- package/src/runtime/test/error-locations/main-import-error.vibe +3 -0
- package/src/runtime/test/error-locations/utils/helper.vibe +5 -0
- package/src/runtime/test/for-in.test.ts +312 -0
- package/src/runtime/test/helpers.ts +69 -0
- package/src/runtime/test/imports.test.ts +334 -0
- package/src/runtime/test/json-expressions.test.ts +232 -0
- package/src/runtime/test/literals.test.ts +372 -0
- package/src/runtime/test/logical-indexing.test.ts +478 -0
- package/src/runtime/test/member-methods.test.ts +324 -0
- package/src/runtime/test/model-config.test.ts +338 -0
- package/src/runtime/test/null-handling.test.ts +342 -0
- package/src/runtime/test/private-visibility.test.ts +332 -0
- package/src/runtime/test/runtime-state.test.ts +514 -0
- package/src/runtime/test/scoping.test.ts +370 -0
- package/src/runtime/test/string-interpolation.test.ts +354 -0
- package/src/runtime/test/template-literal.test.ts +181 -0
- package/src/runtime/test/tool-execution.test.ts +467 -0
- package/src/runtime/test/tool-schema-generation.test.ts +477 -0
- package/src/runtime/test/tostring.test.ts +210 -0
- package/src/runtime/test/ts-block.test.ts +594 -0
- package/src/runtime/test/ts-error-location.test.ts +231 -0
- package/src/runtime/test/types.test.ts +732 -0
- package/src/runtime/test/verbose-logger.test.ts +710 -0
- package/src/runtime/test/vibe-expression.test.ts +54 -0
- package/src/runtime/test/vibe-value-errors.test.ts +541 -0
- package/src/runtime/test/while.test.ts +232 -0
- package/src/runtime/tools/builtin.ts +30 -0
- package/src/runtime/tools/directory-tools.ts +70 -0
- package/src/runtime/tools/file-tools.ts +228 -0
- package/src/runtime/tools/index.ts +5 -0
- package/src/runtime/tools/registry.ts +48 -0
- package/src/runtime/tools/search-tools.ts +134 -0
- package/src/runtime/tools/security.ts +36 -0
- package/src/runtime/tools/system-tools.ts +312 -0
- package/src/runtime/tools/test/fixtures/base-types.ts +40 -0
- package/src/runtime/tools/test/fixtures/test-types.ts +132 -0
- package/src/runtime/tools/test/registry.test.ts +713 -0
- package/src/runtime/tools/test/security.test.ts +86 -0
- package/src/runtime/tools/test/system-tools.test.ts +679 -0
- package/src/runtime/tools/test/ts-schema.test.ts +357 -0
- package/src/runtime/tools/ts-schema.ts +341 -0
- package/src/runtime/tools/types.ts +89 -0
- package/src/runtime/tools/utility-tools.ts +198 -0
- package/src/runtime/ts-eval.ts +126 -0
- package/src/runtime/types.ts +797 -0
- package/src/runtime/validation.ts +160 -0
- package/src/runtime/verbose-logger.ts +459 -0
- package/src/runtime.ts +2 -0
- package/src/semantic/analyzer-context.ts +62 -0
- package/src/semantic/analyzer-validators.ts +575 -0
- package/src/semantic/analyzer-visitors.ts +534 -0
- package/src/semantic/analyzer.ts +83 -0
- package/src/semantic/index.ts +11 -0
- package/src/semantic/symbol-table.ts +58 -0
- package/src/semantic/test/async-validation.test.ts +301 -0
- package/src/semantic/test/compress-validation.test.ts +179 -0
- package/src/semantic/test/const-reassignment.test.ts +111 -0
- package/src/semantic/test/control-flow.test.ts +346 -0
- package/src/semantic/test/destructuring.test.ts +185 -0
- package/src/semantic/test/duplicate-declarations.test.ts +168 -0
- package/src/semantic/test/export-validation.test.ts +111 -0
- package/src/semantic/test/fixtures/math.ts +31 -0
- package/src/semantic/test/imports.test.ts +148 -0
- package/src/semantic/test/json-type.test.ts +68 -0
- package/src/semantic/test/literals.test.ts +127 -0
- package/src/semantic/test/model-validation.test.ts +179 -0
- package/src/semantic/test/prompt-validation.test.ts +343 -0
- package/src/semantic/test/scoping.test.ts +312 -0
- package/src/semantic/test/tool-validation.test.ts +306 -0
- package/src/semantic/test/ts-type-checking.test.ts +563 -0
- package/src/semantic/test/type-constraints.test.ts +111 -0
- package/src/semantic/test/type-inference.test.ts +87 -0
- package/src/semantic/test/type-validation.test.ts +552 -0
- package/src/semantic/test/undefined-variables.test.ts +163 -0
- package/src/semantic/ts-block-checker.ts +204 -0
- package/src/semantic/ts-signatures.ts +194 -0
- package/src/semantic/ts-types.ts +170 -0
- package/src/semantic/types.ts +58 -0
- package/tests/fixtures/conditional-logic.vibe +14 -0
- package/tests/fixtures/function-call.vibe +16 -0
- package/tests/fixtures/imports/cycle-detection/a.vibe +6 -0
- package/tests/fixtures/imports/cycle-detection/b.vibe +5 -0
- package/tests/fixtures/imports/cycle-detection/main.vibe +3 -0
- package/tests/fixtures/imports/module-isolation/main-b.vibe +8 -0
- package/tests/fixtures/imports/module-isolation/main.vibe +9 -0
- package/tests/fixtures/imports/module-isolation/moduleA.vibe +6 -0
- package/tests/fixtures/imports/module-isolation/moduleB.vibe +6 -0
- package/tests/fixtures/imports/nested-import/helper.vibe +6 -0
- package/tests/fixtures/imports/nested-import/main.vibe +3 -0
- package/tests/fixtures/imports/nested-import/utils.ts +3 -0
- package/tests/fixtures/imports/nested-isolation/file2.vibe +15 -0
- package/tests/fixtures/imports/nested-isolation/file3.vibe +10 -0
- package/tests/fixtures/imports/nested-isolation/main.vibe +21 -0
- package/tests/fixtures/imports/pure-cycle/a.vibe +5 -0
- package/tests/fixtures/imports/pure-cycle/b.vibe +5 -0
- package/tests/fixtures/imports/pure-cycle/main.vibe +3 -0
- package/tests/fixtures/imports/ts-boolean/checks.ts +14 -0
- package/tests/fixtures/imports/ts-boolean/main.vibe +10 -0
- package/tests/fixtures/imports/ts-boolean/type-mismatch.vibe +5 -0
- package/tests/fixtures/imports/ts-boolean/use-constant.vibe +18 -0
- package/tests/fixtures/imports/ts-error-handling/helpers.ts +42 -0
- package/tests/fixtures/imports/ts-error-handling/main.vibe +5 -0
- package/tests/fixtures/imports/ts-import/main.vibe +4 -0
- package/tests/fixtures/imports/ts-import/math.ts +9 -0
- package/tests/fixtures/imports/ts-variables/call-non-function.vibe +5 -0
- package/tests/fixtures/imports/ts-variables/data.ts +10 -0
- package/tests/fixtures/imports/ts-variables/import-json.vibe +5 -0
- package/tests/fixtures/imports/ts-variables/import-type-mismatch.vibe +5 -0
- package/tests/fixtures/imports/ts-variables/import-variable.vibe +5 -0
- package/tests/fixtures/imports/vibe-import/greet.vibe +5 -0
- package/tests/fixtures/imports/vibe-import/main.vibe +3 -0
- package/tests/fixtures/multiple-ai-calls.vibe +10 -0
- package/tests/fixtures/simple-greeting.vibe +6 -0
- package/tests/fixtures/template-literals.vibe +11 -0
- package/tests/integration/basic-ai/basic-ai.integration.test.ts +166 -0
- package/tests/integration/basic-ai/basic-ai.vibe +12 -0
- package/tests/integration/bug-fix/bug-fix.integration.test.ts +201 -0
- package/tests/integration/bug-fix/buggy-code.ts +22 -0
- package/tests/integration/bug-fix/fix-bug.vibe +21 -0
- package/tests/integration/compress/compress.integration.test.ts +206 -0
- package/tests/integration/destructuring/destructuring.integration.test.ts +92 -0
- package/tests/integration/hello-world-translator/hello-world-translator.integration.test.ts +61 -0
- package/tests/integration/line-annotator/context-modes.integration.test.ts +261 -0
- package/tests/integration/line-annotator/line-annotator.integration.test.ts +148 -0
- package/tests/integration/multi-feature/cumulative-sum.integration.test.ts +75 -0
- package/tests/integration/multi-feature/number-analyzer.integration.test.ts +191 -0
- package/tests/integration/multi-feature/number-analyzer.vibe +59 -0
- package/tests/integration/tool-calls/tool-calls.integration.test.ts +93 -0
|
@@ -0,0 +1,969 @@
|
|
|
1
|
+
// Core stepping and instruction execution
|
|
2
|
+
|
|
3
|
+
import type { RuntimeState, Instruction, StackFrame, FrameEntry } from './types';
|
|
4
|
+
import { isVibeValue, resolveValue, createVibeError } from './types';
|
|
5
|
+
import type { ContextMode } from '../ast';
|
|
6
|
+
import { buildLocalContext, buildGlobalContext } from './context';
|
|
7
|
+
import { execDeclareVar, execAssignVar } from './exec/variables';
|
|
8
|
+
import { execAIVibe } from './exec/ai';
|
|
9
|
+
import {
|
|
10
|
+
execStatement,
|
|
11
|
+
execStatements,
|
|
12
|
+
execReturnValue,
|
|
13
|
+
execIfBranch,
|
|
14
|
+
execEnterBlock,
|
|
15
|
+
execExitBlock,
|
|
16
|
+
finalizeModelDeclaration,
|
|
17
|
+
} from './exec/statements';
|
|
18
|
+
import { currentFrame } from './state';
|
|
19
|
+
import { RuntimeError } from '../errors';
|
|
20
|
+
import { requireBoolean } from './validation';
|
|
21
|
+
import {
|
|
22
|
+
execExpression,
|
|
23
|
+
execPushValue,
|
|
24
|
+
execBuildObject,
|
|
25
|
+
execBuildArray,
|
|
26
|
+
execBuildRange,
|
|
27
|
+
execCollectArgs,
|
|
28
|
+
} from './exec/expressions';
|
|
29
|
+
import {
|
|
30
|
+
execInterpolateString,
|
|
31
|
+
execInterpolateTemplate,
|
|
32
|
+
execTsEval,
|
|
33
|
+
} from './exec/typescript';
|
|
34
|
+
import {
|
|
35
|
+
execInterpolatePromptString,
|
|
36
|
+
execInterpolateRegularString,
|
|
37
|
+
execClearPromptContext,
|
|
38
|
+
} from './exec/interpolation';
|
|
39
|
+
import { execCallFunction } from './exec/functions';
|
|
40
|
+
import { execPushFrame, execPopFrame } from './exec/frames';
|
|
41
|
+
import { execToolDeclaration } from './exec/tools';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Apply context mode on scope exit.
|
|
45
|
+
* - verbose: keep all entries (add scope-exit marker)
|
|
46
|
+
* - forget: remove all entries added during scope (back to entryIndex)
|
|
47
|
+
* - compress: pause for AI to summarize and replace entries with summary
|
|
48
|
+
* Note: Only loops support context modes. Functions always "forget".
|
|
49
|
+
*/
|
|
50
|
+
function applyContextMode(
|
|
51
|
+
state: RuntimeState,
|
|
52
|
+
frame: StackFrame,
|
|
53
|
+
contextMode: ContextMode,
|
|
54
|
+
entryIndex: number,
|
|
55
|
+
scopeType: 'for' | 'while',
|
|
56
|
+
label?: string
|
|
57
|
+
): RuntimeState {
|
|
58
|
+
if (contextMode === 'forget') {
|
|
59
|
+
// Forget: remove all entries from scope (back to before scope-enter)
|
|
60
|
+
const newOrderedEntries = frame.orderedEntries.slice(0, entryIndex);
|
|
61
|
+
return {
|
|
62
|
+
...state,
|
|
63
|
+
callStack: [
|
|
64
|
+
...state.callStack.slice(0, -1),
|
|
65
|
+
{ ...frame, orderedEntries: newOrderedEntries },
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (contextMode === 'verbose') {
|
|
71
|
+
// Verbose: add scope-exit marker, keep all entries
|
|
72
|
+
const newOrderedEntries = [
|
|
73
|
+
...frame.orderedEntries,
|
|
74
|
+
{ kind: 'scope-exit' as const, scopeType, label },
|
|
75
|
+
];
|
|
76
|
+
return {
|
|
77
|
+
...state,
|
|
78
|
+
callStack: [
|
|
79
|
+
...state.callStack.slice(0, -1),
|
|
80
|
+
{ ...frame, orderedEntries: newOrderedEntries },
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Compress mode: pause for AI summarization
|
|
86
|
+
if (typeof contextMode === 'object' && 'compress' in contextMode) {
|
|
87
|
+
const { arg1, arg2 } = contextMode.compress;
|
|
88
|
+
|
|
89
|
+
// Resolve prompt and model from args
|
|
90
|
+
let prompt: string | null = null;
|
|
91
|
+
let modelName: string | null = null;
|
|
92
|
+
|
|
93
|
+
if (arg1) {
|
|
94
|
+
if (arg1.kind === 'literal') {
|
|
95
|
+
// String literal is always a prompt
|
|
96
|
+
prompt = arg1.value;
|
|
97
|
+
} else {
|
|
98
|
+
// Identifier - check if it's a model or prompt variable
|
|
99
|
+
const varValue = lookupVariable(state, arg1.name);
|
|
100
|
+
if (varValue && typeof varValue === 'object' && '__vibeModel' in varValue) {
|
|
101
|
+
// It's a model
|
|
102
|
+
modelName = arg1.name;
|
|
103
|
+
} else {
|
|
104
|
+
// It's a prompt (text value)
|
|
105
|
+
prompt = String(varValue ?? '');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (arg2 && arg2.kind === 'identifier') {
|
|
111
|
+
// Second arg is always model
|
|
112
|
+
modelName = arg2.name;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Fall back to lastUsedModel if no explicit model
|
|
116
|
+
const resolvedModel = modelName ?? state.lastUsedModel;
|
|
117
|
+
if (!resolvedModel) {
|
|
118
|
+
throw new RuntimeError('compress requires a model but none declared', { line: 0, column: 0 }, '');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Extract entries to summarize (from scope-enter to now)
|
|
122
|
+
const entriesToSummarize = frame.orderedEntries.slice(entryIndex);
|
|
123
|
+
|
|
124
|
+
// If empty scope, skip compression
|
|
125
|
+
if (entriesToSummarize.length <= 1) {
|
|
126
|
+
// Only scope-enter, nothing to summarize
|
|
127
|
+
const newOrderedEntries = [
|
|
128
|
+
...frame.orderedEntries,
|
|
129
|
+
{ kind: 'scope-exit' as const, scopeType, label },
|
|
130
|
+
];
|
|
131
|
+
return {
|
|
132
|
+
...state,
|
|
133
|
+
callStack: [
|
|
134
|
+
...state.callStack.slice(0, -1),
|
|
135
|
+
{ ...frame, orderedEntries: newOrderedEntries },
|
|
136
|
+
],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Pause for AI summarization
|
|
141
|
+
return {
|
|
142
|
+
...state,
|
|
143
|
+
status: 'awaiting_compress',
|
|
144
|
+
pendingCompress: {
|
|
145
|
+
prompt,
|
|
146
|
+
model: resolvedModel,
|
|
147
|
+
entriesToSummarize,
|
|
148
|
+
entryIndex,
|
|
149
|
+
scopeType,
|
|
150
|
+
label,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Default: just return unchanged
|
|
156
|
+
return state;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Look up a variable's value in the current scope chain.
|
|
161
|
+
*/
|
|
162
|
+
function lookupVariable(state: RuntimeState, name: string): unknown {
|
|
163
|
+
// Search from current frame up through scope chain
|
|
164
|
+
for (let i = state.callStack.length - 1; i >= 0; i--) {
|
|
165
|
+
const frame = state.callStack[i];
|
|
166
|
+
if (name in frame.locals) {
|
|
167
|
+
return frame.locals[name].value;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Get the next instruction that will be executed (or null if done/paused)
|
|
174
|
+
export function getNextInstruction(state: RuntimeState): Instruction | null {
|
|
175
|
+
if (state.status !== 'running' || state.instructionStack.length === 0) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
return state.instructionStack[0];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Step N instructions (or until pause/complete)
|
|
182
|
+
export function stepN(state: RuntimeState, n: number): RuntimeState {
|
|
183
|
+
let current = state;
|
|
184
|
+
for (let i = 0; i < n && current.status === 'running'; i++) {
|
|
185
|
+
current = step(current);
|
|
186
|
+
}
|
|
187
|
+
return current;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Step until a condition is met (returns state where condition is true BEFORE executing)
|
|
191
|
+
export function stepUntilCondition(
|
|
192
|
+
state: RuntimeState,
|
|
193
|
+
predicate: (state: RuntimeState, nextInstruction: Instruction | null) => boolean
|
|
194
|
+
): RuntimeState {
|
|
195
|
+
let current = state;
|
|
196
|
+
|
|
197
|
+
while (current.status === 'running') {
|
|
198
|
+
const next = getNextInstruction(current);
|
|
199
|
+
|
|
200
|
+
if (predicate(current, next)) {
|
|
201
|
+
return current;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!next) {
|
|
205
|
+
return current;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
current = step(current);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return current;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Step until we're about to execute a specific statement type
|
|
215
|
+
export function stepUntilStatement(
|
|
216
|
+
state: RuntimeState,
|
|
217
|
+
statementType: string
|
|
218
|
+
): RuntimeState {
|
|
219
|
+
return stepUntilCondition(state, (_state, next) => {
|
|
220
|
+
if (next?.op === 'exec_statement') {
|
|
221
|
+
return next.stmt.type === statementType;
|
|
222
|
+
}
|
|
223
|
+
return false;
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Step until we're about to execute a specific instruction operation
|
|
228
|
+
export function stepUntilOp(
|
|
229
|
+
state: RuntimeState,
|
|
230
|
+
op: Instruction['op']
|
|
231
|
+
): RuntimeState {
|
|
232
|
+
return stepUntilCondition(state, (_state, next) => next?.op === op);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Execute a single instruction and return new state
|
|
236
|
+
export function step(state: RuntimeState): RuntimeState {
|
|
237
|
+
if (state.status !== 'running') {
|
|
238
|
+
return state;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (state.instructionStack.length === 0) {
|
|
242
|
+
return {
|
|
243
|
+
...state,
|
|
244
|
+
status: 'completed',
|
|
245
|
+
localContext: buildLocalContext(state),
|
|
246
|
+
globalContext: buildGlobalContext(state),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const stateWithContext: RuntimeState = {
|
|
251
|
+
...state,
|
|
252
|
+
localContext: buildLocalContext(state),
|
|
253
|
+
globalContext: buildGlobalContext(state),
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const [instruction, ...restInstructions] = stateWithContext.instructionStack;
|
|
257
|
+
const newState: RuntimeState = { ...stateWithContext, instructionStack: restInstructions };
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
return executeInstruction(newState, instruction);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
263
|
+
return {
|
|
264
|
+
...newState,
|
|
265
|
+
status: 'error',
|
|
266
|
+
error: errorObj.message,
|
|
267
|
+
errorObject: errorObj,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Run until we hit a pause point or complete
|
|
273
|
+
export function runUntilPause(state: RuntimeState): RuntimeState {
|
|
274
|
+
let current = state;
|
|
275
|
+
while (current.status === 'running' && current.instructionStack.length > 0) {
|
|
276
|
+
current = step(current);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (current.status === 'running' && current.instructionStack.length === 0) {
|
|
280
|
+
return {
|
|
281
|
+
...current,
|
|
282
|
+
status: 'completed',
|
|
283
|
+
localContext: buildLocalContext(current),
|
|
284
|
+
globalContext: buildGlobalContext(current),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
return current;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Execute a single instruction
|
|
291
|
+
function executeInstruction(state: RuntimeState, instruction: Instruction): RuntimeState {
|
|
292
|
+
switch (instruction.op) {
|
|
293
|
+
case 'exec_statement':
|
|
294
|
+
return execStatement(state, instruction.stmt);
|
|
295
|
+
|
|
296
|
+
case 'exec_expression':
|
|
297
|
+
return execExpression(state, instruction.expr);
|
|
298
|
+
|
|
299
|
+
case 'exec_statements':
|
|
300
|
+
return execStatements(state, instruction.stmts, instruction.index, instruction.location);
|
|
301
|
+
|
|
302
|
+
case 'declare_var':
|
|
303
|
+
return execDeclareVar(state, instruction.name, instruction.isConst, instruction.type, undefined, instruction.isPrivate, instruction.location);
|
|
304
|
+
|
|
305
|
+
case 'assign_var':
|
|
306
|
+
return execAssignVar(state, instruction.name, instruction.location);
|
|
307
|
+
|
|
308
|
+
case 'call_function':
|
|
309
|
+
return execCallFunction(state, instruction.funcName, instruction.argCount, instruction.location);
|
|
310
|
+
|
|
311
|
+
case 'push_frame':
|
|
312
|
+
return execPushFrame(state, instruction.name);
|
|
313
|
+
|
|
314
|
+
case 'pop_frame':
|
|
315
|
+
return execPopFrame(state);
|
|
316
|
+
|
|
317
|
+
case 'return_value':
|
|
318
|
+
return execReturnValue(state);
|
|
319
|
+
|
|
320
|
+
case 'enter_block':
|
|
321
|
+
return execEnterBlock(state, instruction.savedKeys);
|
|
322
|
+
|
|
323
|
+
case 'exit_block':
|
|
324
|
+
return execExitBlock(state, instruction.savedKeys, instruction.location);
|
|
325
|
+
|
|
326
|
+
case 'clear_async_context':
|
|
327
|
+
// Clear all async context flags (used after fire-and-forget async statements)
|
|
328
|
+
return {
|
|
329
|
+
...state,
|
|
330
|
+
currentAsyncVarName: null,
|
|
331
|
+
currentAsyncIsConst: false,
|
|
332
|
+
currentAsyncType: null,
|
|
333
|
+
currentAsyncIsPrivate: false,
|
|
334
|
+
currentAsyncIsDestructure: false,
|
|
335
|
+
currentAsyncIsFireAndForget: false,
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
case 'ai_vibe':
|
|
339
|
+
return execAIVibe(state, instruction.model, instruction.context, instruction.operationType);
|
|
340
|
+
|
|
341
|
+
case 'ts_eval':
|
|
342
|
+
return execTsEval(state, instruction.params, instruction.body, instruction.location);
|
|
343
|
+
|
|
344
|
+
case 'call_imported_ts':
|
|
345
|
+
throw new Error('call_imported_ts should be handled in execCallFunction');
|
|
346
|
+
|
|
347
|
+
case 'if_branch':
|
|
348
|
+
return execIfBranch(state, instruction.consequent, instruction.alternate);
|
|
349
|
+
|
|
350
|
+
case 'for_in_init': {
|
|
351
|
+
const { stmt } = instruction;
|
|
352
|
+
let items = state.lastResult;
|
|
353
|
+
|
|
354
|
+
// Handle VibeValue with error - throw the error
|
|
355
|
+
if (isVibeValue(items) && items.err && items.errDetails) {
|
|
356
|
+
throw new RuntimeError(
|
|
357
|
+
`${items.errDetails.type}: ${items.errDetails.message}`,
|
|
358
|
+
instruction.location,
|
|
359
|
+
''
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Auto-unwrap VibeValue - check if value is iterable
|
|
364
|
+
if (isVibeValue(items)) {
|
|
365
|
+
const innerValue = items.value;
|
|
366
|
+
if (!Array.isArray(innerValue) && typeof innerValue !== 'number') {
|
|
367
|
+
const valueType = innerValue === null ? 'null' : typeof innerValue;
|
|
368
|
+
throw new RuntimeError(
|
|
369
|
+
`Cannot iterate over VibeValue: value is ${valueType}, not an array. Use .toolCalls to iterate tool calls.`,
|
|
370
|
+
instruction.location,
|
|
371
|
+
''
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
items = innerValue;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Handle range: single number N → [1, 2, ..., N] (inclusive)
|
|
378
|
+
if (typeof items === 'number') {
|
|
379
|
+
if (!Number.isInteger(items)) {
|
|
380
|
+
throw new RuntimeError(`for-in range must be an integer, got ${items}`, instruction.location, '');
|
|
381
|
+
}
|
|
382
|
+
if (items < 0) {
|
|
383
|
+
throw new RuntimeError(`for-in range must be non-negative, got ${items}`, instruction.location, '');
|
|
384
|
+
}
|
|
385
|
+
items = Array.from({ length: items }, (_, i) => i + 1);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Note: Explicit ranges now use the `..` operator (e.g., 2..5)
|
|
389
|
+
// which produces an array before reaching for_in_init
|
|
390
|
+
|
|
391
|
+
if (!Array.isArray(items)) {
|
|
392
|
+
throw new RuntimeError('for-in requires array or range', instruction.location, '');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const frame = currentFrame(state);
|
|
396
|
+
const savedKeys = Object.keys(frame.locals);
|
|
397
|
+
|
|
398
|
+
// Add scope-enter marker
|
|
399
|
+
const label = stmt.variable;
|
|
400
|
+
const entryIndex = frame.orderedEntries.length;
|
|
401
|
+
const newOrderedEntries = [
|
|
402
|
+
...frame.orderedEntries,
|
|
403
|
+
{ kind: 'scope-enter' as const, scopeType: 'for' as const, label },
|
|
404
|
+
];
|
|
405
|
+
const updatedState = {
|
|
406
|
+
...state,
|
|
407
|
+
callStack: [
|
|
408
|
+
...state.callStack.slice(0, -1),
|
|
409
|
+
{ ...frame, orderedEntries: newOrderedEntries },
|
|
410
|
+
],
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
...updatedState,
|
|
415
|
+
instructionStack: [
|
|
416
|
+
{ op: 'for_in_iterate', variable: stmt.variable, items, index: 0, body: stmt.body, savedKeys, contextMode: stmt.contextMode, label, entryIndex, location: instruction.location },
|
|
417
|
+
...state.instructionStack,
|
|
418
|
+
],
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
case 'for_in_iterate': {
|
|
423
|
+
const { variable, items, index, body, savedKeys, contextMode, label, entryIndex, location } = instruction;
|
|
424
|
+
|
|
425
|
+
if (index >= items.length) {
|
|
426
|
+
// Loop complete - add scope-exit marker and apply context mode
|
|
427
|
+
const frame = currentFrame(state);
|
|
428
|
+
const exitState = applyContextMode(state, frame, contextMode!, entryIndex, 'for', label);
|
|
429
|
+
|
|
430
|
+
// Cleanup scope variables (will await pending async first)
|
|
431
|
+
return execExitBlock(exitState, savedKeys, location);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// First iteration: declare the loop variable
|
|
435
|
+
// Subsequent iterations: assign the new value
|
|
436
|
+
const frame = currentFrame(state);
|
|
437
|
+
let newState: RuntimeState;
|
|
438
|
+
if (frame.locals[variable]) {
|
|
439
|
+
// Variable exists - assign new value
|
|
440
|
+
newState = execAssignVar({ ...state, lastResult: items[index] }, variable);
|
|
441
|
+
} else {
|
|
442
|
+
// First iteration - declare the variable
|
|
443
|
+
newState = execDeclareVar(state, variable, false, null, items[index]);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Get current local variable names to know what to clean up after body execution
|
|
447
|
+
const bodyFrame = currentFrame(newState);
|
|
448
|
+
const bodyKeys = Object.keys(bodyFrame.locals);
|
|
449
|
+
|
|
450
|
+
// Push: enter block, body execution, exit block, then next iteration
|
|
451
|
+
return {
|
|
452
|
+
...newState,
|
|
453
|
+
instructionStack: [
|
|
454
|
+
{ op: 'enter_block', savedKeys: bodyKeys, location: instruction.location },
|
|
455
|
+
...body.body.map(s => ({ op: 'exec_statement' as const, stmt: s, location: s.location })),
|
|
456
|
+
{ op: 'exit_block', savedKeys: bodyKeys, location: instruction.location },
|
|
457
|
+
{ op: 'for_in_iterate', variable, items, index: index + 1, body, savedKeys, contextMode, label, entryIndex, location: instruction.location },
|
|
458
|
+
...state.instructionStack,
|
|
459
|
+
],
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
case 'while_init': {
|
|
464
|
+
const { stmt, savedKeys } = instruction;
|
|
465
|
+
const condition = requireBoolean(state.lastResult, 'while condition');
|
|
466
|
+
|
|
467
|
+
if (!condition) {
|
|
468
|
+
// Condition false - exit loop (first check, no scope entered yet)
|
|
469
|
+
return state;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Add scope-enter marker on first true condition
|
|
473
|
+
const frame = currentFrame(state);
|
|
474
|
+
const label = undefined;
|
|
475
|
+
const entryIndex = frame.orderedEntries.length;
|
|
476
|
+
const newOrderedEntries = [
|
|
477
|
+
...frame.orderedEntries,
|
|
478
|
+
{ kind: 'scope-enter' as const, scopeType: 'while' as const },
|
|
479
|
+
];
|
|
480
|
+
const updatedState = {
|
|
481
|
+
...state,
|
|
482
|
+
callStack: [
|
|
483
|
+
...state.callStack.slice(0, -1),
|
|
484
|
+
{ ...frame, orderedEntries: newOrderedEntries },
|
|
485
|
+
],
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
// Condition true - execute body then re-check condition
|
|
489
|
+
return {
|
|
490
|
+
...updatedState,
|
|
491
|
+
instructionStack: [
|
|
492
|
+
{ op: 'while_iterate', stmt, savedKeys, contextMode: stmt.contextMode, label, entryIndex, location: instruction.location },
|
|
493
|
+
...state.instructionStack,
|
|
494
|
+
],
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
case 'while_iterate': {
|
|
499
|
+
const { stmt, savedKeys, contextMode, label, entryIndex } = instruction;
|
|
500
|
+
const bodyFrame = currentFrame(state);
|
|
501
|
+
const bodyKeys = Object.keys(bodyFrame.locals);
|
|
502
|
+
|
|
503
|
+
// Execute body, cleanup, re-evaluate condition, then check if loop continues
|
|
504
|
+
return {
|
|
505
|
+
...state,
|
|
506
|
+
instructionStack: [
|
|
507
|
+
{ op: 'enter_block', savedKeys: bodyKeys, location: instruction.location },
|
|
508
|
+
...stmt.body.body.map(s => ({ op: 'exec_statement' as const, stmt: s, location: s.location })),
|
|
509
|
+
{ op: 'exit_block', savedKeys: bodyKeys, location: instruction.location },
|
|
510
|
+
{ op: 'exec_expression', expr: stmt.condition, location: stmt.condition.location },
|
|
511
|
+
{ op: 'while_check', stmt, savedKeys, contextMode, label, entryIndex, location: instruction.location },
|
|
512
|
+
...state.instructionStack,
|
|
513
|
+
],
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
case 'while_check': {
|
|
518
|
+
const { stmt, savedKeys, contextMode, label, entryIndex, location } = instruction;
|
|
519
|
+
const condition = requireBoolean(state.lastResult, 'while condition');
|
|
520
|
+
|
|
521
|
+
if (!condition) {
|
|
522
|
+
// Loop complete - add scope-exit marker and apply context mode
|
|
523
|
+
const frame = currentFrame(state);
|
|
524
|
+
const exitState = applyContextMode(state, frame, contextMode!, entryIndex, 'while', label);
|
|
525
|
+
|
|
526
|
+
// Cleanup scope variables (will await pending async first)
|
|
527
|
+
return execExitBlock(exitState, savedKeys, location);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Condition still true - continue loop
|
|
531
|
+
return {
|
|
532
|
+
...state,
|
|
533
|
+
instructionStack: [
|
|
534
|
+
{ op: 'while_iterate', stmt, savedKeys, contextMode, label, entryIndex, location: instruction.location },
|
|
535
|
+
...state.instructionStack,
|
|
536
|
+
],
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
case 'break_loop': {
|
|
541
|
+
const { savedKeys, contextMode, label, entryIndex, scopeType, location } = instruction;
|
|
542
|
+
|
|
543
|
+
// First, await any pending async operations in the current scope
|
|
544
|
+
const pendingAsyncIds: string[] = [];
|
|
545
|
+
for (const opId of state.pendingAsyncIds) {
|
|
546
|
+
const operation = state.asyncOperations.get(opId);
|
|
547
|
+
if (operation && (operation.status === 'pending' || operation.status === 'running')) {
|
|
548
|
+
pendingAsyncIds.push(opId);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (pendingAsyncIds.length > 0) {
|
|
553
|
+
// Need to await async operations before breaking
|
|
554
|
+
return {
|
|
555
|
+
...state,
|
|
556
|
+
status: 'awaiting_async',
|
|
557
|
+
awaitingAsyncIds: pendingAsyncIds,
|
|
558
|
+
// Re-queue break_loop to continue after async completes
|
|
559
|
+
instructionStack: [instruction, ...state.instructionStack],
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Apply context mode (may trigger compress for summarization)
|
|
564
|
+
const frame = currentFrame(state);
|
|
565
|
+
const exitState = applyContextMode(state, frame, contextMode ?? 'forget', entryIndex, scopeType, label);
|
|
566
|
+
|
|
567
|
+
// If compress triggered awaiting_compress, don't proceed with exit_block yet
|
|
568
|
+
if (exitState.status === 'awaiting_compress') {
|
|
569
|
+
// Re-queue a simplified break cleanup after compress completes
|
|
570
|
+
return {
|
|
571
|
+
...exitState,
|
|
572
|
+
instructionStack: [
|
|
573
|
+
{ op: 'exit_block', savedKeys, location },
|
|
574
|
+
...state.instructionStack,
|
|
575
|
+
],
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Cleanup scope variables
|
|
580
|
+
return execExitBlock(exitState, savedKeys, location);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
case 'push_value':
|
|
584
|
+
return execPushValue(state);
|
|
585
|
+
|
|
586
|
+
case 'build_object':
|
|
587
|
+
return execBuildObject(state, instruction.keys);
|
|
588
|
+
|
|
589
|
+
case 'build_array':
|
|
590
|
+
return execBuildArray(state, instruction.count);
|
|
591
|
+
|
|
592
|
+
case 'build_range':
|
|
593
|
+
return execBuildRange(state);
|
|
594
|
+
|
|
595
|
+
case 'collect_args':
|
|
596
|
+
return execCollectArgs(state, instruction.count);
|
|
597
|
+
|
|
598
|
+
case 'literal':
|
|
599
|
+
return { ...state, lastResult: instruction.value };
|
|
600
|
+
|
|
601
|
+
case 'interpolate_string':
|
|
602
|
+
// Regular string interpolation - {var} expands to value
|
|
603
|
+
return execInterpolateRegularString(state, instruction.template, instruction.location);
|
|
604
|
+
|
|
605
|
+
case 'interpolate_prompt_string':
|
|
606
|
+
// Prompt string interpolation - {var} = reference, !{var} = expand
|
|
607
|
+
return execInterpolatePromptString(state, instruction.template, instruction.location);
|
|
608
|
+
|
|
609
|
+
case 'clear_prompt_context':
|
|
610
|
+
// Clear the inPromptContext flag after evaluating prompt
|
|
611
|
+
return execClearPromptContext(state);
|
|
612
|
+
|
|
613
|
+
case 'interpolate_template':
|
|
614
|
+
// Legacy template literal handling - redirect to regular string (unified to {var} pattern)
|
|
615
|
+
return execInterpolateRegularString(state, instruction.template, instruction.location);
|
|
616
|
+
|
|
617
|
+
case 'binary_op': {
|
|
618
|
+
const rawRight = state.lastResult;
|
|
619
|
+
const rawLeft = state.valueStack[state.valueStack.length - 1];
|
|
620
|
+
const newStack = state.valueStack.slice(0, -1);
|
|
621
|
+
|
|
622
|
+
// Error propagation: if either operand is a VibeValue with error, propagate it
|
|
623
|
+
if (isVibeValue(rawLeft) && rawLeft.err) {
|
|
624
|
+
return { ...state, valueStack: newStack, lastResult: rawLeft };
|
|
625
|
+
}
|
|
626
|
+
if (isVibeValue(rawRight) && rawRight.err) {
|
|
627
|
+
return { ...state, valueStack: newStack, lastResult: rawRight };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Auto-unwrap VibeValue for operations
|
|
631
|
+
const left = resolveValue(rawLeft);
|
|
632
|
+
const right = resolveValue(rawRight);
|
|
633
|
+
|
|
634
|
+
// Handle null in operations
|
|
635
|
+
const op = instruction.operator;
|
|
636
|
+
|
|
637
|
+
// String concatenation with + - coerce null to empty string
|
|
638
|
+
if (op === '+' && (typeof left === 'string' || typeof right === 'string')) {
|
|
639
|
+
const leftStr = left === null ? '' : String(left);
|
|
640
|
+
const rightStr = right === null ? '' : String(right);
|
|
641
|
+
return { ...state, valueStack: newStack, lastResult: leftStr + rightStr };
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Arithmetic operations with null - create error
|
|
645
|
+
if (left === null || right === null) {
|
|
646
|
+
if (op === '-' || op === '*' || op === '/' || op === '%' || (op === '+' && typeof left !== 'string' && typeof right !== 'string')) {
|
|
647
|
+
const errorValue = createVibeError(
|
|
648
|
+
`Cannot perform arithmetic operation '${op}' with null`,
|
|
649
|
+
instruction.location
|
|
650
|
+
);
|
|
651
|
+
return { ...state, valueStack: newStack, lastResult: errorValue };
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const result = evaluateBinaryOp(op, left, right);
|
|
656
|
+
return { ...state, valueStack: newStack, lastResult: result };
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
case 'unary_op': {
|
|
660
|
+
const rawOperand = state.lastResult;
|
|
661
|
+
|
|
662
|
+
// Error propagation: if operand is VibeValue with error, propagate it
|
|
663
|
+
if (isVibeValue(rawOperand) && rawOperand.err) {
|
|
664
|
+
return { ...state, lastResult: rawOperand };
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Auto-unwrap VibeValue for operations
|
|
668
|
+
const operand = resolveValue(rawOperand);
|
|
669
|
+
const op = instruction.operator;
|
|
670
|
+
|
|
671
|
+
// Unary minus with null - create error
|
|
672
|
+
if (operand === null && op === '-') {
|
|
673
|
+
const errorValue = createVibeError(
|
|
674
|
+
`Cannot perform unary '${op}' on null`,
|
|
675
|
+
instruction.location
|
|
676
|
+
);
|
|
677
|
+
return { ...state, lastResult: errorValue };
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const result = evaluateUnaryOp(op, operand);
|
|
681
|
+
return { ...state, lastResult: result };
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
case 'index_access': {
|
|
685
|
+
const rawIndex = state.lastResult;
|
|
686
|
+
const rawArr = state.valueStack[state.valueStack.length - 1];
|
|
687
|
+
const newStack = state.valueStack.slice(0, -1);
|
|
688
|
+
|
|
689
|
+
// Error propagation: if array or index is a VibeValue with error, propagate it
|
|
690
|
+
if (isVibeValue(rawArr) && rawArr.err) {
|
|
691
|
+
return { ...state, valueStack: newStack, lastResult: rawArr };
|
|
692
|
+
}
|
|
693
|
+
if (isVibeValue(rawIndex) && rawIndex.err) {
|
|
694
|
+
return { ...state, valueStack: newStack, lastResult: rawIndex };
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Auto-unwrap VibeValue
|
|
698
|
+
const arr = resolveValue(rawArr) as unknown[];
|
|
699
|
+
const index = resolveValue(rawIndex) as number;
|
|
700
|
+
|
|
701
|
+
if (!Array.isArray(arr)) {
|
|
702
|
+
throw new Error(`Cannot index non-array: ${typeof arr}`);
|
|
703
|
+
}
|
|
704
|
+
if (typeof index !== 'number' || !Number.isInteger(index)) {
|
|
705
|
+
throw new Error(`Array index must be an integer, got ${typeof index}`);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Support negative indices (Python-style: -1 = last, -2 = second to last, etc.)
|
|
709
|
+
const normalizedIndex = index < 0 ? arr.length + index : index;
|
|
710
|
+
if (normalizedIndex < 0 || normalizedIndex >= arr.length) {
|
|
711
|
+
throw new Error(`Array index out of bounds: ${index} (length: ${arr.length})`);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return { ...state, valueStack: newStack, lastResult: arr[normalizedIndex] };
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
case 'slice_access': {
|
|
718
|
+
const { hasStart, hasEnd } = instruction;
|
|
719
|
+
|
|
720
|
+
// Pop values in reverse order they were pushed
|
|
721
|
+
let rawEnd: unknown;
|
|
722
|
+
let rawStart: unknown;
|
|
723
|
+
let newStack = state.valueStack;
|
|
724
|
+
|
|
725
|
+
if (hasEnd) {
|
|
726
|
+
rawEnd = newStack[newStack.length - 1];
|
|
727
|
+
newStack = newStack.slice(0, -1);
|
|
728
|
+
}
|
|
729
|
+
if (hasStart) {
|
|
730
|
+
rawStart = newStack[newStack.length - 1];
|
|
731
|
+
newStack = newStack.slice(0, -1);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const rawArr = newStack[newStack.length - 1];
|
|
735
|
+
newStack = newStack.slice(0, -1);
|
|
736
|
+
|
|
737
|
+
// Error propagation: if array or indices are VibeValues with errors, propagate
|
|
738
|
+
if (isVibeValue(rawArr) && rawArr.err) {
|
|
739
|
+
return { ...state, valueStack: newStack, lastResult: rawArr };
|
|
740
|
+
}
|
|
741
|
+
if (hasStart && isVibeValue(rawStart) && rawStart.err) {
|
|
742
|
+
return { ...state, valueStack: newStack, lastResult: rawStart };
|
|
743
|
+
}
|
|
744
|
+
if (hasEnd && isVibeValue(rawEnd) && rawEnd.err) {
|
|
745
|
+
return { ...state, valueStack: newStack, lastResult: rawEnd };
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Auto-unwrap VibeValue
|
|
749
|
+
const arr = resolveValue(rawArr) as unknown[];
|
|
750
|
+
const start = hasStart ? resolveValue(rawStart) as number : undefined;
|
|
751
|
+
const end = hasEnd ? resolveValue(rawEnd) as number : undefined;
|
|
752
|
+
|
|
753
|
+
if (!Array.isArray(arr)) {
|
|
754
|
+
throw new Error(`Cannot slice non-array: ${typeof arr}`);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Default values: start=0, end=arr.length (Python-style)
|
|
758
|
+
let startIdx = start ?? 0;
|
|
759
|
+
let endIdx = end ?? arr.length;
|
|
760
|
+
|
|
761
|
+
if (typeof startIdx !== 'number' || !Number.isInteger(startIdx)) {
|
|
762
|
+
throw new Error(`Slice start must be an integer, got ${typeof startIdx}`);
|
|
763
|
+
}
|
|
764
|
+
if (typeof endIdx !== 'number' || !Number.isInteger(endIdx)) {
|
|
765
|
+
throw new Error(`Slice end must be an integer, got ${typeof endIdx}`);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Support negative indices (Python-style: -1 = last, -2 = second to last, etc.)
|
|
769
|
+
if (startIdx < 0) startIdx = arr.length + startIdx;
|
|
770
|
+
if (endIdx < 0) endIdx = arr.length + endIdx;
|
|
771
|
+
|
|
772
|
+
// Exclusive end slice (Python-style)
|
|
773
|
+
const sliced = arr.slice(startIdx, endIdx);
|
|
774
|
+
return { ...state, valueStack: newStack, lastResult: sliced };
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Tool operations
|
|
778
|
+
case 'exec_tool_declaration':
|
|
779
|
+
return execToolDeclaration(state, instruction.decl);
|
|
780
|
+
|
|
781
|
+
// Model declaration - config values are on the valueStack
|
|
782
|
+
case 'declare_model': {
|
|
783
|
+
return finalizeModelDeclaration(state, instruction.stmt);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Destructuring assignment - assign AI result fields to variables
|
|
787
|
+
case 'destructure_assign': {
|
|
788
|
+
const { fields, isConst } = instruction;
|
|
789
|
+
|
|
790
|
+
// Get the result value (should be Record<string, unknown> from AI return)
|
|
791
|
+
let fieldValues: Record<string, unknown>;
|
|
792
|
+
let rawResult = state.lastResult;
|
|
793
|
+
|
|
794
|
+
// Check if this is a pending async operation - if so, await it
|
|
795
|
+
if (isVibeValue(rawResult) && rawResult.asyncOperationId) {
|
|
796
|
+
const opId = rawResult.asyncOperationId;
|
|
797
|
+
const operation = state.asyncOperations.get(opId);
|
|
798
|
+
|
|
799
|
+
// If operation is still pending or running, trigger await
|
|
800
|
+
if (operation && (operation.status === 'pending' || operation.status === 'running')) {
|
|
801
|
+
return {
|
|
802
|
+
...state,
|
|
803
|
+
status: 'awaiting_async',
|
|
804
|
+
awaitingAsyncIds: [opId],
|
|
805
|
+
// Re-add current instruction to retry after await
|
|
806
|
+
instructionStack: [instruction, ...state.instructionStack],
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// If operation completed, use the result
|
|
811
|
+
if (operation && operation.status === 'completed' && operation.result) {
|
|
812
|
+
rawResult = operation.result;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Unwrap VibeValue if present
|
|
817
|
+
if (isVibeValue(rawResult)) {
|
|
818
|
+
rawResult = rawResult.value;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (typeof rawResult === 'object' && rawResult !== null) {
|
|
822
|
+
fieldValues = rawResult as Record<string, unknown>;
|
|
823
|
+
} else {
|
|
824
|
+
throw new RuntimeError(
|
|
825
|
+
`Destructuring requires an object, got ${typeof rawResult}`,
|
|
826
|
+
instruction.location,
|
|
827
|
+
''
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Declare each field as a variable
|
|
832
|
+
let newState: RuntimeState = { ...state, pendingDestructuring: null };
|
|
833
|
+
for (const field of fields) {
|
|
834
|
+
const value = fieldValues[field.name];
|
|
835
|
+
if (value === undefined) {
|
|
836
|
+
throw new RuntimeError(
|
|
837
|
+
`Missing field '${field.name}' in destructuring result`,
|
|
838
|
+
instruction.location,
|
|
839
|
+
''
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
newState = execDeclareVar(newState, field.name, isConst, field.type, value, field.isPrivate);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
return newState;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Member/property access
|
|
849
|
+
case 'member_access': {
|
|
850
|
+
const rawObject = state.lastResult;
|
|
851
|
+
const property = instruction.property;
|
|
852
|
+
|
|
853
|
+
// Handle VibeValue reserved properties first
|
|
854
|
+
if (isVibeValue(rawObject)) {
|
|
855
|
+
// Reserved property: .err - return boolean (true if error)
|
|
856
|
+
if (property === 'err') {
|
|
857
|
+
return { ...state, lastResult: rawObject.err };
|
|
858
|
+
}
|
|
859
|
+
// Reserved property: .errDetails - return error details object
|
|
860
|
+
if (property === 'errDetails') {
|
|
861
|
+
return { ...state, lastResult: rawObject.errDetails };
|
|
862
|
+
}
|
|
863
|
+
// Reserved property: .toolCalls - return tool calls array
|
|
864
|
+
if (property === 'toolCalls') {
|
|
865
|
+
return { ...state, lastResult: rawObject.toolCalls };
|
|
866
|
+
}
|
|
867
|
+
// For all other properties, unwrap and continue with normal handling below
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Unwrap VibeValue and AIResultObject for normal property access
|
|
871
|
+
const object = resolveValue(rawObject);
|
|
872
|
+
|
|
873
|
+
// Handle toString() method on any type
|
|
874
|
+
if (property === 'toString') {
|
|
875
|
+
return { ...state, lastResult: { __boundMethod: true, object, method: 'toString' } };
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Handle built-in methods on arrays
|
|
879
|
+
if (Array.isArray(object)) {
|
|
880
|
+
if (property === 'len' || property === 'push' || property === 'pop') {
|
|
881
|
+
// Return bound method for calling
|
|
882
|
+
return { ...state, lastResult: { __boundMethod: true, object, method: property } };
|
|
883
|
+
}
|
|
884
|
+
// For numeric properties, do index access
|
|
885
|
+
const index = Number(property);
|
|
886
|
+
if (!isNaN(index)) {
|
|
887
|
+
return { ...state, lastResult: object[index] };
|
|
888
|
+
}
|
|
889
|
+
throw new Error(`Unknown array property: ${property}`);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Handle built-in methods on strings
|
|
893
|
+
if (typeof object === 'string') {
|
|
894
|
+
if (property === 'len') {
|
|
895
|
+
return { ...state, lastResult: { __boundMethod: true, object, method: property } };
|
|
896
|
+
}
|
|
897
|
+
throw new Error(`Unknown string property: ${property}`);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// Handle regular object property access
|
|
901
|
+
if (typeof object === 'object' && object !== null) {
|
|
902
|
+
const val = (object as Record<string, unknown>)[property];
|
|
903
|
+
return { ...state, lastResult: val };
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
throw new Error(`Cannot access property '${property}' on ${typeof object}`);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
default:
|
|
910
|
+
throw new Error(`Unknown instruction: ${(instruction as Instruction).op}`);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Evaluate binary operators
|
|
915
|
+
function evaluateBinaryOp(op: string, left: unknown, right: unknown): unknown {
|
|
916
|
+
switch (op) {
|
|
917
|
+
// Addition / concatenation
|
|
918
|
+
case '+':
|
|
919
|
+
// Array concatenation: [1,2] + [3,4] = [1,2,3,4]
|
|
920
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
921
|
+
return [...left, ...right];
|
|
922
|
+
}
|
|
923
|
+
// String/number addition (JS handles coercion)
|
|
924
|
+
return (left as number) + (right as number);
|
|
925
|
+
case '-':
|
|
926
|
+
return (left as number) - (right as number);
|
|
927
|
+
case '*':
|
|
928
|
+
return (left as number) * (right as number);
|
|
929
|
+
case '/':
|
|
930
|
+
return (left as number) / (right as number);
|
|
931
|
+
case '%':
|
|
932
|
+
return (left as number) % (right as number);
|
|
933
|
+
|
|
934
|
+
// Comparison operators
|
|
935
|
+
case '==':
|
|
936
|
+
return left === right;
|
|
937
|
+
case '!=':
|
|
938
|
+
return left !== right;
|
|
939
|
+
case '<':
|
|
940
|
+
return (left as number) < (right as number);
|
|
941
|
+
case '>':
|
|
942
|
+
return (left as number) > (right as number);
|
|
943
|
+
case '<=':
|
|
944
|
+
return (left as number) <= (right as number);
|
|
945
|
+
case '>=':
|
|
946
|
+
return (left as number) >= (right as number);
|
|
947
|
+
|
|
948
|
+
// Logical operators
|
|
949
|
+
case 'and':
|
|
950
|
+
return Boolean(left) && Boolean(right);
|
|
951
|
+
case 'or':
|
|
952
|
+
return Boolean(left) || Boolean(right);
|
|
953
|
+
|
|
954
|
+
default:
|
|
955
|
+
throw new Error(`Unknown binary operator: ${op}`);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Evaluate unary operators
|
|
960
|
+
function evaluateUnaryOp(op: string, operand: unknown): unknown {
|
|
961
|
+
switch (op) {
|
|
962
|
+
case 'not':
|
|
963
|
+
return !Boolean(operand);
|
|
964
|
+
case '-':
|
|
965
|
+
return -(operand as number);
|
|
966
|
+
default:
|
|
967
|
+
throw new Error(`Unknown unary operator: ${op}`);
|
|
968
|
+
}
|
|
969
|
+
}
|