@ball-lang/engine 0.2.1 → 1.3.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/README.md +31 -9
- package/dist/ball_file.d.ts +25 -0
- package/dist/ball_file.d.ts.map +1 -0
- package/dist/ball_file.js +50 -0
- package/dist/ball_file.js.map +1 -0
- package/dist/compiled_engine.d.ts +319 -0
- package/dist/compiled_engine.d.ts.map +1 -0
- package/dist/compiled_engine.js +10112 -0
- package/dist/compiled_engine.js.map +1 -0
- package/dist/engine_setup.d.ts +45 -0
- package/dist/engine_setup.d.ts.map +1 -0
- package/dist/engine_setup.js +2027 -0
- package/dist/engine_setup.js.map +1 -0
- package/dist/index.d.ts +33 -121
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +86 -949
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/ball_file.ts +50 -0
- package/src/compiled_engine.ts +9948 -0
- package/src/engine_setup.ts +1733 -0
- package/src/index.ts +121 -975
package/src/index.ts
CHANGED
|
@@ -1,993 +1,139 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Ball TypeScript Engine — interprets Ball programs directly from JSON.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* This is a compatibility wrapper around the compiled (self-hosted)
|
|
5
|
+
* Ball engine. The compiled engine is generated by compiling
|
|
6
|
+
* `dart/self_host/engine.ball.json` through `@ball-lang/compiler`.
|
|
6
7
|
*
|
|
7
8
|
* Usage:
|
|
8
9
|
* import { BallEngine } from '@ball-lang/engine';
|
|
9
|
-
* const engine = new BallEngine(programJson
|
|
10
|
-
* engine.run();
|
|
10
|
+
* const engine = new BallEngine(programJson);
|
|
11
|
+
* await engine.run();
|
|
12
|
+
* console.log(engine.getOutput());
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
interface FunctionDef {
|
|
37
|
-
name: string;
|
|
38
|
-
isBase?: boolean;
|
|
39
|
-
body?: Expression;
|
|
40
|
-
outputType?: string;
|
|
41
|
-
metadata?: Record<string, any>;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface Expression {
|
|
45
|
-
call?: FunctionCall;
|
|
46
|
-
literal?: Literal;
|
|
47
|
-
reference?: { name: string };
|
|
48
|
-
fieldAccess?: { object: Expression; field: string };
|
|
49
|
-
messageCreation?: { fields: FieldValuePair[] };
|
|
50
|
-
block?: Block;
|
|
51
|
-
lambda?: Lambda;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
interface FunctionCall {
|
|
55
|
-
module?: string;
|
|
56
|
-
function: string;
|
|
57
|
-
input?: Expression;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
interface Literal {
|
|
61
|
-
intValue?: string | number;
|
|
62
|
-
doubleValue?: number;
|
|
63
|
-
stringValue?: string;
|
|
64
|
-
boolValue?: boolean;
|
|
65
|
-
listValue?: { elements: Expression[] };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
interface FieldValuePair {
|
|
69
|
-
name: string;
|
|
70
|
-
value: Expression;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
interface Block {
|
|
74
|
-
statements: Statement[];
|
|
75
|
-
result?: Expression;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
interface Statement {
|
|
79
|
-
let?: { name: string; value: Expression; metadata?: Record<string, any> };
|
|
80
|
-
expression?: Expression;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
interface Lambda {
|
|
84
|
-
body: Expression;
|
|
85
|
-
metadata?: Record<string, any>;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// ── Flow signals ────────────────────────────────────────────────────────────
|
|
89
|
-
|
|
90
|
-
class FlowSignal {
|
|
91
|
-
kind: string;
|
|
92
|
-
value?: BallValue;
|
|
93
|
-
label?: string;
|
|
94
|
-
constructor(kind: string, value?: BallValue, label?: string) {
|
|
95
|
-
this.kind = kind;
|
|
96
|
-
this.value = value;
|
|
97
|
-
this.label = label;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
class BallException {
|
|
102
|
-
typeName: string;
|
|
103
|
-
value: BallValue;
|
|
104
|
-
constructor(typeName: string, value: BallValue) {
|
|
105
|
-
this.typeName = typeName;
|
|
106
|
-
this.value = value;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
class BallRuntimeError extends Error {
|
|
111
|
-
constructor(message: string) {
|
|
112
|
-
super(message);
|
|
113
|
-
this.name = 'BallRuntimeError';
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// ── Scope ───────────────────────────────────────────────────────────────────
|
|
118
|
-
|
|
119
|
-
class Scope {
|
|
120
|
-
private bindings = new Map<string, BallValue>();
|
|
121
|
-
private parent?: Scope;
|
|
122
|
-
constructor(parent?: Scope) {
|
|
123
|
-
this.parent = parent;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
bind(name: string, value: BallValue): void {
|
|
127
|
-
this.bindings.set(name, value);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
has(name: string): boolean {
|
|
131
|
-
return this.bindings.has(name) || (this.parent?.has(name) ?? false);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
lookup(name: string): BallValue {
|
|
135
|
-
if (this.bindings.has(name)) return this.bindings.get(name);
|
|
136
|
-
if (this.parent) return this.parent.lookup(name);
|
|
137
|
-
throw new BallRuntimeError(`Undefined variable: "${name}"`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
assign(name: string, value: BallValue): void {
|
|
141
|
-
if (this.bindings.has(name)) {
|
|
142
|
-
this.bindings.set(name, value);
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
if (this.parent) {
|
|
146
|
-
this.parent.assign(name, value);
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
throw new BallRuntimeError(`Cannot assign to undefined variable: "${name}"`);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
child(): Scope {
|
|
153
|
-
return new Scope(this);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// ── Engine ──────────────────────────────────────────────────────────────────
|
|
15
|
+
import {
|
|
16
|
+
BallEngine as CompiledEngine,
|
|
17
|
+
} from './compiled_engine.ts';
|
|
18
|
+
import * as _compiled from './compiled_engine.ts';
|
|
19
|
+
import { createEngineSetup } from './engine_setup.ts';
|
|
20
|
+
import { unwrapBallFile } from './ball_file.ts';
|
|
21
|
+
|
|
22
|
+
// All proto3-JSON normalization, the method-dispatch handler, the extra
|
|
23
|
+
// std-function registrations, and the compiled-engine patches live in the
|
|
24
|
+
// shared `engine_setup` module so the Phase 2.7b conformance harness (which
|
|
25
|
+
// runs against a freshly compiled engine) can reuse the exact same setup.
|
|
26
|
+
const _setup = createEngineSetup(_compiled as any);
|
|
27
|
+
const protoWrap = _setup.protoWrap;
|
|
28
|
+
const MethodDispatchHandler = _setup.MethodDispatchHandler;
|
|
29
|
+
const StdModuleHandler = _compiled.StdModuleHandler;
|
|
30
|
+
const registerExtraStdFunctions = _setup.registerExtraStdFunctions;
|
|
31
|
+
const seedGlobalScope = _setup.seedGlobalScope;
|
|
32
|
+
const patchCompiledEngine = _setup.patchCompiledEngine;
|
|
33
|
+
const patchScopeBindings = _setup.patchScopeBindings;
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
// ── Compatibility wrapper ──────────────────────────────────────────────────
|
|
158
37
|
|
|
159
38
|
export interface BallEngineOptions {
|
|
160
39
|
stdout?: (msg: string) => void;
|
|
161
40
|
stderr?: (msg: string) => void;
|
|
41
|
+
/** Maximum execution time in milliseconds (null = unbounded). */
|
|
42
|
+
timeoutMs?: number | null;
|
|
43
|
+
/** Maximum memory usage in bytes (null = unbounded). */
|
|
44
|
+
maxMemoryBytes?: number | null;
|
|
45
|
+
/** Maximum number of modules allowed in the program (default: 1000000). */
|
|
46
|
+
maxModules?: number;
|
|
47
|
+
/** Maximum expression nesting depth (default: 1000000). */
|
|
48
|
+
maxExpressionDepth?: number;
|
|
49
|
+
/** Maximum program JSON size in bytes (null = skip check). */
|
|
50
|
+
maxProgramSizeBytes?: number | null;
|
|
51
|
+
/** Whether to run in sandbox mode (blocks file I/O, env access, etc.). */
|
|
52
|
+
sandbox?: boolean;
|
|
53
|
+
/** Maximum recursion depth (default: 100000). */
|
|
54
|
+
maxRecursionDepth?: number;
|
|
162
55
|
}
|
|
163
56
|
|
|
164
57
|
export class BallEngine {
|
|
165
|
-
private
|
|
166
|
-
private
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
58
|
+
private _compiledEngine: CompiledEngine;
|
|
59
|
+
private _output: string[] = [];
|
|
60
|
+
|
|
61
|
+
constructor(program: any, options: BallEngineOptions = {}) {
|
|
62
|
+
// Ball files are self-describing `google.protobuf.Any` envelopes. Unwrap
|
|
63
|
+
// the `@type` envelope (if present) before normalizing; callers passing an
|
|
64
|
+
// already-unwrapped Program object are still supported.
|
|
65
|
+
const parsed = typeof program === 'string' ? JSON.parse(program) : program;
|
|
66
|
+
const normalized = protoWrap(unwrapBallFile(parsed));
|
|
67
|
+
|
|
68
|
+
const stdHandler = new StdModuleHandler();
|
|
69
|
+
const methodHandler = new MethodDispatchHandler();
|
|
70
|
+
const outputCapture = this._output;
|
|
71
|
+
|
|
72
|
+
const stdoutFn = options.stdout ?? ((msg: string) => {
|
|
73
|
+
outputCapture.push(msg);
|
|
74
|
+
});
|
|
75
|
+
const stderrFn = options.stderr ?? (() => {});
|
|
76
|
+
|
|
77
|
+
// The self-hosted engine constructor takes 16 positional parameters:
|
|
78
|
+
// program, stdout, stderr, stdinReader, envGet, args, enableProfiling,
|
|
79
|
+
// maxRecursionDepth, timeoutMs, maxMemoryBytes, maxModules,
|
|
80
|
+
// maxExpressionDepth, maxProgramSizeBytes, sandbox, moduleHandlers,
|
|
81
|
+
// resolver
|
|
82
|
+
// (older IR revisions only had 9). Options default to permissive values
|
|
83
|
+
// unless the caller explicitly sets them.
|
|
84
|
+
this._compiledEngine = new CompiledEngine(
|
|
85
|
+
normalized,
|
|
86
|
+
stdoutFn,
|
|
87
|
+
stderrFn,
|
|
88
|
+
null, // stdinReader
|
|
89
|
+
null, // envGet
|
|
90
|
+
[], // args
|
|
91
|
+
false, // enableProfiling
|
|
92
|
+
options.maxRecursionDepth ?? 100000, // maxRecursionDepth
|
|
93
|
+
options.timeoutMs ?? null, // timeoutMs (null = unbounded)
|
|
94
|
+
options.maxMemoryBytes ?? null, // maxMemoryBytes (null = unbounded)
|
|
95
|
+
options.maxModules ?? 1000000, // maxModules
|
|
96
|
+
options.maxExpressionDepth ?? 1000000, // maxExpressionDepth
|
|
97
|
+
options.maxProgramSizeBytes ?? null, // maxProgramSizeBytes (null = skip)
|
|
98
|
+
options.sandbox ?? false, // sandbox
|
|
99
|
+
[methodHandler as any, stdHandler], // moduleHandlers
|
|
100
|
+
null, // resolver
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// Patch scope bindings to use null-prototype objects (avoids
|
|
104
|
+
// Object.prototype.values/entries/keys getters polluting `in` checks).
|
|
105
|
+
if (typeof (globalThis as any)._patchScopeBindings === 'function') {
|
|
106
|
+
(globalThis as any)._patchScopeBindings(this._compiledEngine._globalScope);
|
|
107
|
+
} else {
|
|
108
|
+
patchScopeBindings(this._compiledEngine._globalScope);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
registerExtraStdFunctions(stdHandler);
|
|
112
|
+
seedGlobalScope(this._compiledEngine);
|
|
113
|
+
patchCompiledEngine(this._compiledEngine);
|
|
114
|
+
|
|
115
|
+
// Note: double formatting (12 vs 12.0) handled by BallDouble in preamble.
|
|
116
|
+
// The compiled engine returns raw numbers for literals; BallDouble wrapping
|
|
117
|
+
// happens in arithmetic operations (_stdAdd, _stdBinary, etc.).
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Run the program. Returns a promise that resolves to the captured
|
|
122
|
+
* output lines (same content as `getOutput()`).
|
|
123
|
+
*
|
|
124
|
+
* NOTE: The compiled engine is async internally. If you were relying
|
|
125
|
+
* on synchronous `run()`, wrap your call in `await`.
|
|
126
|
+
*/
|
|
127
|
+
async run(): Promise<string[]> {
|
|
128
|
+
await this._compiledEngine.run();
|
|
129
|
+
return this._output;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Retrieve lines printed via `std.print` (when no custom stdout was given). */
|
|
237
133
|
getOutput(): string[] {
|
|
238
|
-
return this.
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// ── Expression evaluation ─────────────────────────────────────────────
|
|
242
|
-
|
|
243
|
-
private evalExpr(expr: Expression, scope: Scope): BallValue {
|
|
244
|
-
if (expr.call) return this.evalCall(expr.call, scope);
|
|
245
|
-
if (expr.literal) return this.evalLiteral(expr.literal, scope);
|
|
246
|
-
if (expr.reference) return this.evalReference(expr.reference, scope);
|
|
247
|
-
if (expr.fieldAccess) return this.evalFieldAccess(expr.fieldAccess, scope);
|
|
248
|
-
if (expr.messageCreation) return this.evalMessageCreation(expr.messageCreation, scope);
|
|
249
|
-
if (expr.block) return this.evalBlock(expr.block, scope);
|
|
250
|
-
if (expr.lambda) return this.evalLambda(expr.lambda, scope);
|
|
251
|
-
return null;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
private evalCall(call: FunctionCall, scope: Scope): BallValue {
|
|
255
|
-
const moduleName = call.module || this.currentModule;
|
|
256
|
-
|
|
257
|
-
// Lazy control flow
|
|
258
|
-
if (moduleName === 'std' || moduleName === 'dart_std') {
|
|
259
|
-
switch (call.function) {
|
|
260
|
-
case 'if': return this.evalLazyIf(call, scope);
|
|
261
|
-
case 'for': return this.evalLazyFor(call, scope);
|
|
262
|
-
case 'for_in': return this.evalLazyForIn(call, scope);
|
|
263
|
-
case 'while': return this.evalLazyWhile(call, scope);
|
|
264
|
-
case 'do_while': return this.evalLazyDoWhile(call, scope);
|
|
265
|
-
case 'switch': return this.evalLazySwitch(call, scope);
|
|
266
|
-
case 'try': return this.evalLazyTry(call, scope);
|
|
267
|
-
case 'and': return this.evalShortCircuitAnd(call, scope);
|
|
268
|
-
case 'or': return this.evalShortCircuitOr(call, scope);
|
|
269
|
-
case 'return': return this.evalReturn(call, scope);
|
|
270
|
-
case 'break': return new FlowSignal('break', undefined, this.lazyStringField(call, 'label'));
|
|
271
|
-
case 'continue': return new FlowSignal('continue', undefined, this.lazyStringField(call, 'label'));
|
|
272
|
-
case 'assign': return this.evalAssign(call, scope);
|
|
273
|
-
case 'labeled': return this.evalLabeled(call, scope);
|
|
274
|
-
case 'pre_increment': case 'post_increment':
|
|
275
|
-
case 'pre_decrement': case 'post_decrement':
|
|
276
|
-
return this.evalIncDec(call, scope);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Eager evaluation
|
|
281
|
-
const input = call.input ? this.evalExpr(call.input, scope) : null;
|
|
282
|
-
|
|
283
|
-
// Fast path for explicit std calls
|
|
284
|
-
if (call.module === 'std' || call.module === 'dart_std') {
|
|
285
|
-
return this.callBaseFunction(call.module, call.function, input);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Method call on object (has 'self' field) — instance method dispatch.
|
|
289
|
-
if (input && typeof input === 'object' && !Array.isArray(input) && 'self' in input) {
|
|
290
|
-
const self = input.self;
|
|
291
|
-
if (self && typeof self === 'object' && !Array.isArray(self)) {
|
|
292
|
-
const typeName: string | undefined = self.__type__;
|
|
293
|
-
if (typeName) {
|
|
294
|
-
const colonIdx = typeName.indexOf(':');
|
|
295
|
-
const modPart = colonIdx >= 0 ? typeName.substring(0, colonIdx) : this.currentModule;
|
|
296
|
-
// Try ClassName.methodName
|
|
297
|
-
const methodKey = `${modPart}.${typeName}.${call.function}`;
|
|
298
|
-
const method = this.functions.get(methodKey);
|
|
299
|
-
if (method) return this.callFunction(modPart, method, input, scope);
|
|
300
|
-
// Walk __super__ chain for inherited methods.
|
|
301
|
-
let superObj = self.__super__;
|
|
302
|
-
while (superObj && typeof superObj === 'object' && !Array.isArray(superObj)) {
|
|
303
|
-
const superType: string | undefined = superObj.__type__;
|
|
304
|
-
if (superType) {
|
|
305
|
-
const sColonIdx = superType.indexOf(':');
|
|
306
|
-
const sModPart = sColonIdx >= 0 ? superType.substring(0, sColonIdx) : modPart;
|
|
307
|
-
const sTypeName = sColonIdx >= 0 ? superType : `${sModPart}:${superType}`;
|
|
308
|
-
const superMethodKey = `${sModPart}.${sTypeName}.${call.function}`;
|
|
309
|
-
const superMethod = this.functions.get(superMethodKey);
|
|
310
|
-
if (superMethod) return this.callFunction(sModPart, superMethod, input, scope);
|
|
311
|
-
}
|
|
312
|
-
superObj = superObj.__super__;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
// Fall through to normal resolution if no method found on the type.
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const key = `${moduleName}.${call.function}`;
|
|
320
|
-
const fn = this.functions.get(key);
|
|
321
|
-
if (fn?.isBase) return this.callBaseFunction(moduleName, call.function, input);
|
|
322
|
-
|
|
323
|
-
// Scope closure lookup
|
|
324
|
-
if (!call.module && scope.has(call.function)) {
|
|
325
|
-
const bound = scope.lookup(call.function);
|
|
326
|
-
if (typeof bound === 'function') return bound(input);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Module function lookup
|
|
330
|
-
if (fn) return this.callFunction(moduleName, fn, input, scope);
|
|
331
|
-
|
|
332
|
-
// Fallback: scan all modules
|
|
333
|
-
for (const mod of this.program.modules) {
|
|
334
|
-
for (const f of mod.functions) {
|
|
335
|
-
if (f.name === call.function) {
|
|
336
|
-
return this.callFunction(mod.name, f, input, scope);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
throw new BallRuntimeError(`Function "${key}" not found`);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
private callFunction(moduleName: string, fn: FunctionDef, input: BallValue, parentScope: Scope): BallValue {
|
|
345
|
-
if (fn.isBase) return this.callBaseFunction(moduleName, fn.name, input);
|
|
346
|
-
if (!fn.body) return null;
|
|
347
|
-
|
|
348
|
-
const prevModule = this.currentModule;
|
|
349
|
-
this.currentModule = moduleName;
|
|
350
|
-
|
|
351
|
-
const fnScope = parentScope.child();
|
|
352
|
-
fnScope.bind('input', input);
|
|
353
|
-
|
|
354
|
-
// Bind 'self' for instance method calls so `this` references resolve.
|
|
355
|
-
if (input && typeof input === 'object' && !Array.isArray(input) && 'self' in input) {
|
|
356
|
-
fnScope.bind('self', input.self);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Destructure input fields as named parameters
|
|
360
|
-
const params = fn.metadata?.params;
|
|
361
|
-
if (params && Array.isArray(params)) {
|
|
362
|
-
if (params.length === 1 && (input === null || input === undefined || typeof input !== 'object' || Array.isArray(input))) {
|
|
363
|
-
// Single-param function with non-object input: bind directly
|
|
364
|
-
const name = typeof params[0] === 'string' ? params[0] : params[0].name;
|
|
365
|
-
if (name) fnScope.bind(name, input);
|
|
366
|
-
} else if (input && typeof input === 'object' && !Array.isArray(input)) {
|
|
367
|
-
for (let i = 0; i < params.length; i++) {
|
|
368
|
-
const p = params[i];
|
|
369
|
-
const name = typeof p === 'string' ? p : p.name;
|
|
370
|
-
if (!name) continue;
|
|
371
|
-
if (name in input) {
|
|
372
|
-
fnScope.bind(name, input[name]);
|
|
373
|
-
} else if (`arg${i}` in input) {
|
|
374
|
-
fnScope.bind(name, input[`arg${i}`]);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
let result = this.evalExpr(fn.body, fnScope);
|
|
381
|
-
this.currentModule = prevModule;
|
|
382
|
-
|
|
383
|
-
if (result instanceof FlowSignal && result.kind === 'return') {
|
|
384
|
-
return result.value;
|
|
385
|
-
}
|
|
386
|
-
return result;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// ── Literals ──────────────────────────────────────────────────────────
|
|
390
|
-
|
|
391
|
-
private evalLiteral(lit: Literal, scope: Scope): BallValue {
|
|
392
|
-
if (lit.intValue !== undefined) return typeof lit.intValue === 'string' ? parseInt(lit.intValue) : lit.intValue;
|
|
393
|
-
if (lit.doubleValue !== undefined) return lit.doubleValue;
|
|
394
|
-
if (lit.stringValue !== undefined) return lit.stringValue;
|
|
395
|
-
if (lit.boolValue !== undefined) return lit.boolValue;
|
|
396
|
-
if (lit.listValue) return lit.listValue.elements.map(e => this.evalExpr(e, scope));
|
|
397
|
-
return null;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
private evalReference(ref: { name: string }, scope: Scope): BallValue {
|
|
401
|
-
const name = ref.name;
|
|
402
|
-
if (scope.has(name)) return scope.lookup(name);
|
|
403
|
-
|
|
404
|
-
// Constructor tear-off: resolve class names to callable closures.
|
|
405
|
-
const ctorEntry = this.constructors.get(name);
|
|
406
|
-
if (ctorEntry) {
|
|
407
|
-
return (input: BallValue) => this.callFunction(ctorEntry.module, ctorEntry.fn, input, scope);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// Try stripping module prefix (e.g. "main:Foo" -> "Foo").
|
|
411
|
-
const colonIdx = name.indexOf(':');
|
|
412
|
-
if (colonIdx >= 0) {
|
|
413
|
-
const bare = name.substring(colonIdx + 1);
|
|
414
|
-
const bareEntry = this.constructors.get(bare);
|
|
415
|
-
if (bareEntry) {
|
|
416
|
-
return (input: BallValue) => this.callFunction(bareEntry.module, bareEntry.fn, input, scope);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// Enum type reference: resolve to a map of enum values.
|
|
421
|
-
const enumVals = this.enumValues.get(name);
|
|
422
|
-
if (enumVals) return enumVals;
|
|
423
|
-
|
|
424
|
-
return scope.lookup(name);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
private evalFieldAccess(fa: { object: Expression; field: string }, scope: Scope): BallValue {
|
|
428
|
-
const obj = this.evalExpr(fa.object, scope);
|
|
429
|
-
// Handle string properties
|
|
430
|
-
if (typeof obj === 'string') {
|
|
431
|
-
if (fa.field === 'length') return obj.length;
|
|
432
|
-
return null;
|
|
433
|
-
}
|
|
434
|
-
// Handle array properties
|
|
435
|
-
if (Array.isArray(obj)) {
|
|
436
|
-
if (fa.field === 'length') return obj.length;
|
|
437
|
-
return null;
|
|
438
|
-
}
|
|
439
|
-
if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
440
|
-
if (fa.field in obj) return obj[fa.field];
|
|
441
|
-
// Walk __super__ chain for inherited fields.
|
|
442
|
-
let superObj = obj.__super__;
|
|
443
|
-
while (superObj && typeof superObj === 'object' && !Array.isArray(superObj)) {
|
|
444
|
-
if (fa.field in superObj) return superObj[fa.field];
|
|
445
|
-
superObj = superObj.__super__;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
return null;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
private evalMessageCreation(mc: { fields: FieldValuePair[] }, scope: Scope): BallValue {
|
|
452
|
-
const result: Record<string, BallValue> = {};
|
|
453
|
-
for (const f of mc.fields) {
|
|
454
|
-
result[f.name] = this.evalExpr(f.value, scope);
|
|
455
|
-
}
|
|
456
|
-
return result;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
private evalBlock(block: Block, scope: Scope): BallValue {
|
|
460
|
-
const blockScope = scope.child();
|
|
461
|
-
let lastResult: BallValue = null;
|
|
462
|
-
|
|
463
|
-
for (const stmt of block.statements) {
|
|
464
|
-
if (stmt.let) {
|
|
465
|
-
const val = this.evalExpr(stmt.let.value, blockScope);
|
|
466
|
-
if (val instanceof FlowSignal) return val;
|
|
467
|
-
blockScope.bind(stmt.let.name, val);
|
|
468
|
-
}
|
|
469
|
-
if (stmt.expression) {
|
|
470
|
-
lastResult = this.evalExpr(stmt.expression, blockScope);
|
|
471
|
-
if (lastResult instanceof FlowSignal) return lastResult;
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
if (block.result) {
|
|
476
|
-
lastResult = this.evalExpr(block.result, blockScope);
|
|
477
|
-
}
|
|
478
|
-
return lastResult;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
private evalLambda(lambda: Lambda, scope: Scope): BallValue {
|
|
482
|
-
return (input: BallValue) => {
|
|
483
|
-
const lambdaScope = scope.child();
|
|
484
|
-
lambdaScope.bind('input', input);
|
|
485
|
-
const params = lambda.metadata?.params;
|
|
486
|
-
if (params && Array.isArray(params)) {
|
|
487
|
-
if (params.length === 1 && (input === null || input === undefined || typeof input !== 'object' || Array.isArray(input))) {
|
|
488
|
-
const name = typeof params[0] === 'string' ? params[0] : params[0].name;
|
|
489
|
-
if (name) lambdaScope.bind(name, input);
|
|
490
|
-
} else if (input && typeof input === 'object' && !Array.isArray(input)) {
|
|
491
|
-
for (let i = 0; i < params.length; i++) {
|
|
492
|
-
const p = params[i];
|
|
493
|
-
const name = typeof p === 'string' ? p : p.name;
|
|
494
|
-
if (!name) continue;
|
|
495
|
-
if (name in input) {
|
|
496
|
-
lambdaScope.bind(name, input[name]);
|
|
497
|
-
} else if (`arg${i}` in input) {
|
|
498
|
-
lambdaScope.bind(name, input[`arg${i}`]);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
const result = this.evalExpr(lambda.body, lambdaScope);
|
|
504
|
-
if (result instanceof FlowSignal && result.kind === 'return') return result.value;
|
|
505
|
-
return result;
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// ── Lazy control flow ─────────────────────────────────────────────────
|
|
510
|
-
|
|
511
|
-
private lazyFields(call: FunctionCall): Record<string, Expression> {
|
|
512
|
-
if (!call.input?.messageCreation) return {};
|
|
513
|
-
const result: Record<string, Expression> = {};
|
|
514
|
-
for (const f of call.input.messageCreation.fields) {
|
|
515
|
-
result[f.name] = f.value;
|
|
516
|
-
}
|
|
517
|
-
return result;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
private lazyStringField(call: FunctionCall, name: string): string | undefined {
|
|
521
|
-
const fields = this.lazyFields(call);
|
|
522
|
-
const expr = fields[name];
|
|
523
|
-
if (!expr?.literal?.stringValue) return undefined;
|
|
524
|
-
return expr.literal.stringValue;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
private evalLazyIf(call: FunctionCall, scope: Scope): BallValue {
|
|
528
|
-
const fields = this.lazyFields(call);
|
|
529
|
-
if (!fields.condition) return null;
|
|
530
|
-
const cond = this.evalExpr(fields.condition, scope);
|
|
531
|
-
if (this.toBool(cond)) {
|
|
532
|
-
return fields.then ? this.evalExpr(fields.then, scope) : null;
|
|
533
|
-
}
|
|
534
|
-
return fields.else ? this.evalExpr(fields.else, scope) : null;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
private evalLazyWhile(call: FunctionCall, scope: Scope): BallValue {
|
|
538
|
-
const fields = this.lazyFields(call);
|
|
539
|
-
while (true) {
|
|
540
|
-
if (fields.condition) {
|
|
541
|
-
if (!this.toBool(this.evalExpr(fields.condition, scope))) break;
|
|
542
|
-
}
|
|
543
|
-
if (fields.body) {
|
|
544
|
-
const result = this.evalExpr(fields.body, scope);
|
|
545
|
-
if (result instanceof FlowSignal) {
|
|
546
|
-
if (result.kind === 'return') return result;
|
|
547
|
-
if (result.label) return result;
|
|
548
|
-
if (result.kind === 'break') break;
|
|
549
|
-
if (result.kind === 'continue') continue;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
return null;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
private evalLazyDoWhile(call: FunctionCall, scope: Scope): BallValue {
|
|
557
|
-
const fields = this.lazyFields(call);
|
|
558
|
-
do {
|
|
559
|
-
if (fields.body) {
|
|
560
|
-
const result = this.evalExpr(fields.body, scope);
|
|
561
|
-
if (result instanceof FlowSignal) {
|
|
562
|
-
if (result.kind === 'return') return result;
|
|
563
|
-
if (result.kind === 'break') break;
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
if (fields.condition) {
|
|
567
|
-
if (!this.toBool(this.evalExpr(fields.condition, scope))) break;
|
|
568
|
-
} else break;
|
|
569
|
-
} while (true);
|
|
570
|
-
return null;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
private evalLazyFor(call: FunctionCall, scope: Scope): BallValue {
|
|
574
|
-
const fields = this.lazyFields(call);
|
|
575
|
-
const forScope = scope.child();
|
|
576
|
-
if (fields.init) {
|
|
577
|
-
// Handle init as string literal "var i = 0" (encoder format)
|
|
578
|
-
if (fields.init.literal?.stringValue) {
|
|
579
|
-
const match = fields.init.literal.stringValue.match(/^(?:var|final|int|double|String)\s+(\w+)\s*=\s*(.+)$/);
|
|
580
|
-
if (match) {
|
|
581
|
-
const varName = match[1];
|
|
582
|
-
const rawVal = match[2].trim();
|
|
583
|
-
const parsed = rawVal === 'true' ? true : rawVal === 'false' ? false : (isNaN(Number(rawVal)) ? rawVal : Number(rawVal));
|
|
584
|
-
forScope.bind(varName, parsed);
|
|
585
|
-
}
|
|
586
|
-
} else if (fields.init.block) {
|
|
587
|
-
this.evalBlock(fields.init.block, forScope);
|
|
588
|
-
} else {
|
|
589
|
-
this.evalExpr(fields.init, forScope);
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
while (true) {
|
|
593
|
-
if (fields.condition) {
|
|
594
|
-
if (!this.toBool(this.evalExpr(fields.condition, forScope))) break;
|
|
595
|
-
}
|
|
596
|
-
if (fields.body) {
|
|
597
|
-
const result = this.evalExpr(fields.body, forScope);
|
|
598
|
-
if (result instanceof FlowSignal) {
|
|
599
|
-
if (result.kind === 'return') return result;
|
|
600
|
-
if (result.label) return result;
|
|
601
|
-
if (result.kind === 'break') break;
|
|
602
|
-
if (result.kind === 'continue') { if (fields.update) this.evalExpr(fields.update, forScope); continue; }
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
if (fields.update) this.evalExpr(fields.update, forScope);
|
|
606
|
-
}
|
|
607
|
-
return null;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
private evalLazyForIn(call: FunctionCall, scope: Scope): BallValue {
|
|
611
|
-
const fields = this.lazyFields(call);
|
|
612
|
-
const varName = fields.variable?.literal?.stringValue ?? 'item';
|
|
613
|
-
if (!fields.iterable || !fields.body) return null;
|
|
614
|
-
const iterable = this.evalExpr(fields.iterable, scope);
|
|
615
|
-
if (!Array.isArray(iterable)) throw new BallRuntimeError('for_in: iterable is not a List');
|
|
616
|
-
for (const item of iterable) {
|
|
617
|
-
const loopScope = scope.child();
|
|
618
|
-
loopScope.bind(varName, item);
|
|
619
|
-
const result = this.evalExpr(fields.body, loopScope);
|
|
620
|
-
if (result instanceof FlowSignal) {
|
|
621
|
-
if (result.kind === 'return') return result;
|
|
622
|
-
if (result.label) return result;
|
|
623
|
-
if (result.kind === 'break') break;
|
|
624
|
-
if (result.kind === 'continue') continue;
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
return null;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
private evalLazySwitch(call: FunctionCall, scope: Scope): BallValue {
|
|
631
|
-
const fields = this.lazyFields(call);
|
|
632
|
-
if (!fields.subject || !fields.cases) return null;
|
|
633
|
-
const subject = this.evalExpr(fields.subject, scope);
|
|
634
|
-
const cases = fields.cases.literal?.listValue?.elements ?? [];
|
|
635
|
-
let defaultBody: Expression | undefined;
|
|
636
|
-
for (const c of cases) {
|
|
637
|
-
if (!c.messageCreation) continue;
|
|
638
|
-
const cf: Record<string, Expression> = {};
|
|
639
|
-
for (const f of c.messageCreation.fields) cf[f.name] = f.value;
|
|
640
|
-
if (cf.is_default?.literal?.boolValue) { defaultBody = cf.body; continue; }
|
|
641
|
-
if (cf.value) {
|
|
642
|
-
const caseVal = this.evalExpr(cf.value, scope);
|
|
643
|
-
if (caseVal === subject && cf.body) return this.evalExpr(cf.body, scope);
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
if (defaultBody) return this.evalExpr(defaultBody, scope);
|
|
647
|
-
return null;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
private evalLazyTry(call: FunctionCall, scope: Scope): BallValue {
|
|
651
|
-
const fields = this.lazyFields(call);
|
|
652
|
-
let result: BallValue = null;
|
|
653
|
-
try {
|
|
654
|
-
result = fields.body ? this.evalExpr(fields.body, scope) : null;
|
|
655
|
-
} catch (e: any) {
|
|
656
|
-
result = null;
|
|
657
|
-
const catches = fields.catches?.literal?.listValue?.elements ?? [];
|
|
658
|
-
let caught = false;
|
|
659
|
-
for (const c of catches) {
|
|
660
|
-
if (!c.messageCreation) continue;
|
|
661
|
-
const cf: Record<string, Expression> = {};
|
|
662
|
-
for (const f of c.messageCreation.fields) cf[f.name] = f.value;
|
|
663
|
-
const catchType = cf.type?.literal?.stringValue;
|
|
664
|
-
if (catchType) {
|
|
665
|
-
const matches = e instanceof BallException ? e.typeName === catchType : e.constructor?.name === catchType;
|
|
666
|
-
if (!matches) continue;
|
|
667
|
-
}
|
|
668
|
-
const variable = cf.variable?.literal?.stringValue ?? 'e';
|
|
669
|
-
if (cf.body) {
|
|
670
|
-
const catchScope = scope.child();
|
|
671
|
-
catchScope.bind(variable, e instanceof BallException ? e.value : (e instanceof Error ? e.message : String(e)));
|
|
672
|
-
const prev = this.activeException;
|
|
673
|
-
this.activeException = e;
|
|
674
|
-
try { result = this.evalExpr(cf.body, catchScope); }
|
|
675
|
-
finally { this.activeException = prev; }
|
|
676
|
-
caught = true;
|
|
677
|
-
break;
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
if (!caught) throw e;
|
|
681
|
-
} finally {
|
|
682
|
-
if (fields.finally) this.evalExpr(fields.finally, scope);
|
|
683
|
-
}
|
|
684
|
-
return result;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
private evalShortCircuitAnd(call: FunctionCall, scope: Scope): BallValue {
|
|
688
|
-
const fields = this.lazyFields(call);
|
|
689
|
-
if (!fields.left || !fields.right) return false;
|
|
690
|
-
if (!this.toBool(this.evalExpr(fields.left, scope))) return false;
|
|
691
|
-
return this.toBool(this.evalExpr(fields.right, scope));
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
private evalShortCircuitOr(call: FunctionCall, scope: Scope): BallValue {
|
|
695
|
-
const fields = this.lazyFields(call);
|
|
696
|
-
if (!fields.left || !fields.right) return false;
|
|
697
|
-
if (this.toBool(this.evalExpr(fields.left, scope))) return true;
|
|
698
|
-
return this.toBool(this.evalExpr(fields.right, scope));
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
private evalReturn(call: FunctionCall, scope: Scope): BallValue {
|
|
702
|
-
const fields = this.lazyFields(call);
|
|
703
|
-
const val = fields.value ? this.evalExpr(fields.value, scope) : null;
|
|
704
|
-
return new FlowSignal('return', val);
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
private evalAssign(call: FunctionCall, scope: Scope): BallValue {
|
|
708
|
-
const fields = this.lazyFields(call);
|
|
709
|
-
if (!fields.value) return null;
|
|
710
|
-
const val = this.evalExpr(fields.value, scope);
|
|
711
|
-
|
|
712
|
-
// Target can be a reference expression or a string literal name.
|
|
713
|
-
const target = fields.target ?? fields.name ?? fields.variable;
|
|
714
|
-
if (!target) return null;
|
|
715
|
-
|
|
716
|
-
let name: string | undefined;
|
|
717
|
-
if (target.reference) {
|
|
718
|
-
name = target.reference.name;
|
|
719
|
-
} else if (target.literal?.stringValue) {
|
|
720
|
-
name = target.literal.stringValue;
|
|
721
|
-
}
|
|
722
|
-
if (!name) return null;
|
|
723
|
-
|
|
724
|
-
const op = fields.op?.literal?.stringValue;
|
|
725
|
-
if (op && op !== '=') {
|
|
726
|
-
const current = scope.lookup(name);
|
|
727
|
-
const computed = this.applyCompoundOp(op, current, val);
|
|
728
|
-
try { scope.assign(name, computed); } catch { scope.bind(name, computed); }
|
|
729
|
-
return computed;
|
|
730
|
-
}
|
|
731
|
-
try { scope.assign(name, val); } catch { scope.bind(name, val); }
|
|
732
|
-
return val;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
private applyCompoundOp(op: string, current: BallValue, val: BallValue): BallValue {
|
|
736
|
-
const a = this.toNum(current);
|
|
737
|
-
const b = this.toNum(val);
|
|
738
|
-
switch (op) {
|
|
739
|
-
case '+=': return a + b;
|
|
740
|
-
case '-=': return a - b;
|
|
741
|
-
case '*=': return a * b;
|
|
742
|
-
case '/=': return Math.trunc(a / b);
|
|
743
|
-
case '%=': return a % b;
|
|
744
|
-
case '&=': return (a | 0) & (b | 0);
|
|
745
|
-
case '|=': return (a | 0) | (b | 0);
|
|
746
|
-
case '^=': return (a | 0) ^ (b | 0);
|
|
747
|
-
case '<<=': return (a | 0) << (b | 0);
|
|
748
|
-
case '>>=': return (a | 0) >> (b | 0);
|
|
749
|
-
default: return val;
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
private evalIncDec(call: FunctionCall, scope: Scope): BallValue {
|
|
754
|
-
const fields = this.lazyFields(call);
|
|
755
|
-
const valueExpr = fields.value;
|
|
756
|
-
if (!valueExpr) return null;
|
|
757
|
-
|
|
758
|
-
if (valueExpr.reference) {
|
|
759
|
-
const name = valueExpr.reference.name;
|
|
760
|
-
const current = this.toNum(scope.lookup(name));
|
|
761
|
-
const isInc = call.function.includes('increment');
|
|
762
|
-
const isPre = call.function.startsWith('pre');
|
|
763
|
-
const updated = isInc ? current + 1 : current - 1;
|
|
764
|
-
scope.assign(name, updated);
|
|
765
|
-
return isPre ? updated : current;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
// Fallback: just compute
|
|
769
|
-
const val = this.toNum(this.evalExpr(valueExpr, scope));
|
|
770
|
-
const isInc = call.function.includes('increment');
|
|
771
|
-
return isInc ? val + 1 : val - 1;
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
private evalLabeled(call: FunctionCall, scope: Scope): BallValue {
|
|
775
|
-
const fields = this.lazyFields(call);
|
|
776
|
-
const label = fields.label?.literal?.stringValue;
|
|
777
|
-
if (!fields.body) return null;
|
|
778
|
-
const result = this.evalExpr(fields.body, scope);
|
|
779
|
-
if (result instanceof FlowSignal && result.label === label) {
|
|
780
|
-
if (result.kind === 'break') return null;
|
|
781
|
-
}
|
|
782
|
-
return result;
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
// ── Base function dispatch ────────────────────────────────────────────
|
|
786
|
-
|
|
787
|
-
private callBaseFunction(module: string, fn: string, input: BallValue): BallValue {
|
|
788
|
-
const left = input?.left;
|
|
789
|
-
const right = input?.right;
|
|
790
|
-
const value = input?.value;
|
|
791
|
-
|
|
792
|
-
switch (fn) {
|
|
793
|
-
// I/O
|
|
794
|
-
case 'print': this.stdout(this.ballToString(value ?? input?.message ?? input)); return null;
|
|
795
|
-
|
|
796
|
-
// Arithmetic
|
|
797
|
-
case 'add': {
|
|
798
|
-
if (typeof left === 'string' || typeof right === 'string') return String(left ?? '') + String(right ?? '');
|
|
799
|
-
return this.numOp(left, right, (a, b) => a + b);
|
|
800
|
-
}
|
|
801
|
-
case 'subtract': return this.numOp(left, right, (a, b) => a - b);
|
|
802
|
-
case 'multiply': return this.numOp(left, right, (a, b) => a * b);
|
|
803
|
-
case 'divide': return Math.trunc(this.toNum(left) / this.toNum(right));
|
|
804
|
-
case 'divide_double': return this.toNum(left) / this.toNum(right);
|
|
805
|
-
case 'modulo': return this.numOp(left, right, (a, b) => a % b);
|
|
806
|
-
case 'negate': return -this.toNum(value ?? input);
|
|
807
|
-
|
|
808
|
-
// Comparison
|
|
809
|
-
case 'equals': return left === right;
|
|
810
|
-
case 'not_equals': return left !== right;
|
|
811
|
-
case 'less_than': return this.toNum(left) < this.toNum(right);
|
|
812
|
-
case 'greater_than': return this.toNum(left) > this.toNum(right);
|
|
813
|
-
case 'lte': return this.toNum(left) <= this.toNum(right);
|
|
814
|
-
case 'gte': return this.toNum(left) >= this.toNum(right);
|
|
815
|
-
|
|
816
|
-
// Logical
|
|
817
|
-
case 'not': return !this.toBool(value ?? input);
|
|
818
|
-
|
|
819
|
-
// String
|
|
820
|
-
case 'concat': return String(left ?? '') + String(right ?? '');
|
|
821
|
-
case 'to_string': return this.ballToString(value ?? input);
|
|
822
|
-
case 'string_length': return String(value ?? input).length;
|
|
823
|
-
case 'string_contains': return String(input?.string ?? '').includes(String(input?.substring ?? ''));
|
|
824
|
-
case 'string_substring': return String(input?.string ?? '').substring(input?.start ?? 0, input?.end);
|
|
825
|
-
case 'string_to_upper': return String(value ?? input).toUpperCase();
|
|
826
|
-
case 'string_to_lower': return String(value ?? input).toLowerCase();
|
|
827
|
-
case 'string_trim': return String(value ?? input).trim();
|
|
828
|
-
case 'string_split': return String(input?.string ?? '').split(String(input?.delimiter ?? ''));
|
|
829
|
-
case 'string_replace': return String(input?.string ?? '').replace(String(input?.from ?? ''), String(input?.to ?? ''));
|
|
830
|
-
case 'string_replace_all': return String(input?.string ?? '').replaceAll(String(input?.from ?? ''), String(input?.to ?? ''));
|
|
831
|
-
case 'string_starts_with': return String(input?.string ?? '').startsWith(String(input?.prefix ?? ''));
|
|
832
|
-
case 'string_ends_with': return String(input?.string ?? '').endsWith(String(input?.suffix ?? ''));
|
|
833
|
-
case 'string_index_of': return String(input?.string ?? '').indexOf(String(input?.substring ?? ''));
|
|
834
|
-
|
|
835
|
-
// Type ops
|
|
836
|
-
case 'is': return this.typeMatches(input?.value, input?.type);
|
|
837
|
-
case 'is_not': return !this.typeMatches(input?.value, input?.type);
|
|
838
|
-
case 'as': return input?.value;
|
|
839
|
-
|
|
840
|
-
// Math
|
|
841
|
-
case 'math_abs': return Math.abs(this.toNum(value ?? input));
|
|
842
|
-
case 'math_floor': return Math.floor(this.toNum(value ?? input));
|
|
843
|
-
case 'math_ceil': return Math.ceil(this.toNum(value ?? input));
|
|
844
|
-
case 'math_round': return Math.round(this.toNum(value ?? input));
|
|
845
|
-
case 'math_sqrt': return Math.sqrt(this.toNum(value ?? input));
|
|
846
|
-
case 'math_pow': return Math.pow(this.toNum(input?.base ?? left), this.toNum(input?.exponent ?? right));
|
|
847
|
-
case 'math_min': return Math.min(this.toNum(left), this.toNum(right));
|
|
848
|
-
case 'math_max': return Math.max(this.toNum(left), this.toNum(right));
|
|
849
|
-
case 'math_pi': return Math.PI;
|
|
850
|
-
|
|
851
|
-
// Error handling
|
|
852
|
-
case 'throw': {
|
|
853
|
-
const rawVal = input?.value ?? input?.message ?? input;
|
|
854
|
-
const typeName = input?.type ?? rawVal?.__type ?? 'Exception';
|
|
855
|
-
throw new BallException(typeName, rawVal);
|
|
856
|
-
}
|
|
857
|
-
case 'rethrow': {
|
|
858
|
-
if (this.activeException) throw this.activeException;
|
|
859
|
-
throw new BallRuntimeError('rethrow outside of catch');
|
|
860
|
-
}
|
|
861
|
-
case 'assert': {
|
|
862
|
-
if (!this.toBool(input?.condition ?? input)) {
|
|
863
|
-
throw new BallRuntimeError(`Assertion failed: ${input?.message ?? ''}`);
|
|
864
|
-
}
|
|
865
|
-
return null;
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
// Collections
|
|
869
|
-
case 'list_push': { const l = [...(input?.list ?? [])]; l.push(input?.value); return l; }
|
|
870
|
-
case 'list_length': return (input?.list ?? input ?? []).length;
|
|
871
|
-
case 'list_get': return (input?.list ?? [])[input?.index ?? 0];
|
|
872
|
-
case 'list_map': {
|
|
873
|
-
const list = input?.list ?? [];
|
|
874
|
-
const fn = input?.function;
|
|
875
|
-
if (typeof fn === 'function') return list.map((item: any) => fn(item));
|
|
876
|
-
return list;
|
|
877
|
-
}
|
|
878
|
-
case 'list_filter': {
|
|
879
|
-
const list = input?.list ?? [];
|
|
880
|
-
const fn = input?.function;
|
|
881
|
-
if (typeof fn === 'function') return list.filter((item: any) => fn(item));
|
|
882
|
-
return list;
|
|
883
|
-
}
|
|
884
|
-
case 'map_get': return (input?.map ?? {})[input?.key];
|
|
885
|
-
case 'map_set': { const m = { ...(input?.map ?? {}) }; m[input?.key] = input?.value; return m; }
|
|
886
|
-
case 'map_keys': return Object.keys(input?.map ?? input ?? {});
|
|
887
|
-
|
|
888
|
-
// Increment/decrement (pure value ops)
|
|
889
|
-
case 'pre_increment': case 'post_increment': return this.toNum(value ?? input) + 1;
|
|
890
|
-
case 'pre_decrement': case 'post_decrement': return this.toNum(value ?? input) - 1;
|
|
891
|
-
|
|
892
|
-
// Index
|
|
893
|
-
case 'index': {
|
|
894
|
-
const target = input?.target ?? input?.list;
|
|
895
|
-
const idx = input?.index ?? input?.key;
|
|
896
|
-
if (Array.isArray(target)) return target[idx];
|
|
897
|
-
if (target && typeof target === 'object') return target[idx];
|
|
898
|
-
if (typeof target === 'string') return target[idx];
|
|
899
|
-
return null;
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
// Bitwise
|
|
903
|
-
case 'bitwise_and': return (this.toNum(left) | 0) & (this.toNum(right) | 0);
|
|
904
|
-
case 'bitwise_or': return (this.toNum(left) | 0) | (this.toNum(right) | 0);
|
|
905
|
-
case 'bitwise_xor': return (this.toNum(left) | 0) ^ (this.toNum(right) | 0);
|
|
906
|
-
case 'bitwise_not': return ~(this.toNum(value ?? input) | 0);
|
|
907
|
-
case 'left_shift': return (this.toNum(left) | 0) << (this.toNum(right) | 0);
|
|
908
|
-
case 'right_shift': return (this.toNum(left) | 0) >> (this.toNum(right) | 0);
|
|
909
|
-
|
|
910
|
-
// Null
|
|
911
|
-
case 'null_coalesce': return left ?? right;
|
|
912
|
-
|
|
913
|
-
// Misc
|
|
914
|
-
case 'paren': return value ?? input;
|
|
915
|
-
case 'string_interpolation': return String(value ?? input);
|
|
916
|
-
case 'int_to_string': return String(Math.trunc(this.toNum(value ?? input)));
|
|
917
|
-
case 'double_to_string': return String(this.toNum(value ?? input));
|
|
918
|
-
case 'string_to_int': return parseInt(String(value ?? input));
|
|
919
|
-
case 'string_to_double': return parseFloat(String(value ?? input));
|
|
920
|
-
case 'length': return (value ?? input)?.length ?? 0;
|
|
921
|
-
|
|
922
|
-
default:
|
|
923
|
-
throw new BallRuntimeError(`Unknown base function: ${module}.${fn}`);
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
// ── Helpers ───────────────────────────────────────────────────────────
|
|
928
|
-
|
|
929
|
-
private typeMatches(value: BallValue, type: string | undefined): boolean {
|
|
930
|
-
if (type === undefined || type === null) return false;
|
|
931
|
-
// Primitive type checks
|
|
932
|
-
switch (type) {
|
|
933
|
-
case 'int': return typeof value === 'number' && Number.isInteger(value);
|
|
934
|
-
case 'double': return typeof value === 'number';
|
|
935
|
-
case 'num': return typeof value === 'number';
|
|
936
|
-
case 'String': return typeof value === 'string';
|
|
937
|
-
case 'bool': return typeof value === 'boolean';
|
|
938
|
-
case 'List': return Array.isArray(value);
|
|
939
|
-
case 'Map': return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
940
|
-
case 'Null': case 'void': return value === null || value === undefined;
|
|
941
|
-
case 'Object': case 'dynamic': return true;
|
|
942
|
-
case 'Function': return typeof value === 'function';
|
|
943
|
-
}
|
|
944
|
-
// Check BallObject __type__ and walk __super__ chain
|
|
945
|
-
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
946
|
-
if (this.typeNameMatches(value.__type__, type)) return true;
|
|
947
|
-
let superObj = value.__super__;
|
|
948
|
-
while (superObj && typeof superObj === 'object' && !Array.isArray(superObj)) {
|
|
949
|
-
if (this.typeNameMatches(superObj.__type__, type)) return true;
|
|
950
|
-
superObj = superObj.__super__;
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
// Fallback: JS typeof check
|
|
954
|
-
return typeof value === type;
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
private typeNameMatches(objType: string | undefined, checkType: string): boolean {
|
|
958
|
-
if (!objType) return false;
|
|
959
|
-
if (objType === checkType) return true;
|
|
960
|
-
// objType is "module:Foo", checkType is "Foo"
|
|
961
|
-
if (objType.endsWith(':' + checkType)) return true;
|
|
962
|
-
// objType is "Foo", checkType is "module:Foo"
|
|
963
|
-
if (checkType.endsWith(':' + objType)) return true;
|
|
964
|
-
return false;
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
private toBool(v: BallValue): boolean {
|
|
968
|
-
if (v === null || v === undefined || v === false || v === 0 || v === '') return false;
|
|
969
|
-
return true;
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
private toNum(v: BallValue): number {
|
|
973
|
-
if (typeof v === 'number') return v;
|
|
974
|
-
if (typeof v === 'string') return Number(v) || 0;
|
|
975
|
-
if (typeof v === 'boolean') return v ? 1 : 0;
|
|
976
|
-
return 0;
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
private numOp(left: BallValue, right: BallValue, op: (a: number, b: number) => number): number {
|
|
980
|
-
return op(this.toNum(left), this.toNum(right));
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
private ballToString(v: BallValue): string {
|
|
984
|
-
if (v === null || v === undefined) return 'null';
|
|
985
|
-
if (typeof v === 'number') {
|
|
986
|
-
return Number.isInteger(v) ? v.toString() : v.toString();
|
|
987
|
-
}
|
|
988
|
-
if (typeof v === 'boolean') return v.toString();
|
|
989
|
-
if (Array.isArray(v)) return `[${v.map(x => this.ballToString(x)).join(', ')}]`;
|
|
990
|
-
if (typeof v === 'object') return JSON.stringify(v);
|
|
991
|
-
return String(v);
|
|
134
|
+
return this._output;
|
|
992
135
|
}
|
|
993
136
|
}
|
|
137
|
+
|
|
138
|
+
// Re-export useful compiled-engine types for advanced consumers.
|
|
139
|
+
export { StdModuleHandler } from './compiled_engine.ts';
|