@ball-lang/engine 0.1.0
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/LICENSE +21 -0
- package/README.md +113 -0
- package/dist/index.d.ts +132 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +817 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
- package/src/index.ts +853 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ball TypeScript Engine — interprets Ball programs directly from JSON.
|
|
3
|
+
*
|
|
4
|
+
* Runs in Node.js and browsers. No protobuf dependency — works with
|
|
5
|
+
* proto3 JSON representation of Ball programs.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { BallEngine } from '@ball-lang/engine';
|
|
9
|
+
* const engine = new BallEngine(programJson, { stdout: console.log });
|
|
10
|
+
* engine.run();
|
|
11
|
+
*/
|
|
12
|
+
// ── Flow signals ────────────────────────────────────────────────────────────
|
|
13
|
+
class FlowSignal {
|
|
14
|
+
kind;
|
|
15
|
+
value;
|
|
16
|
+
label;
|
|
17
|
+
constructor(kind, value, label) {
|
|
18
|
+
this.kind = kind;
|
|
19
|
+
this.value = value;
|
|
20
|
+
this.label = label;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
class BallException {
|
|
24
|
+
typeName;
|
|
25
|
+
value;
|
|
26
|
+
constructor(typeName, value) {
|
|
27
|
+
this.typeName = typeName;
|
|
28
|
+
this.value = value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
class BallRuntimeError extends Error {
|
|
32
|
+
constructor(message) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = 'BallRuntimeError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// ── Scope ───────────────────────────────────────────────────────────────────
|
|
38
|
+
class Scope {
|
|
39
|
+
bindings = new Map();
|
|
40
|
+
parent;
|
|
41
|
+
constructor(parent) {
|
|
42
|
+
this.parent = parent;
|
|
43
|
+
}
|
|
44
|
+
bind(name, value) {
|
|
45
|
+
this.bindings.set(name, value);
|
|
46
|
+
}
|
|
47
|
+
has(name) {
|
|
48
|
+
return this.bindings.has(name) || (this.parent?.has(name) ?? false);
|
|
49
|
+
}
|
|
50
|
+
lookup(name) {
|
|
51
|
+
if (this.bindings.has(name))
|
|
52
|
+
return this.bindings.get(name);
|
|
53
|
+
if (this.parent)
|
|
54
|
+
return this.parent.lookup(name);
|
|
55
|
+
throw new BallRuntimeError(`Undefined variable: "${name}"`);
|
|
56
|
+
}
|
|
57
|
+
assign(name, value) {
|
|
58
|
+
if (this.bindings.has(name)) {
|
|
59
|
+
this.bindings.set(name, value);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (this.parent) {
|
|
63
|
+
this.parent.assign(name, value);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
throw new BallRuntimeError(`Cannot assign to undefined variable: "${name}"`);
|
|
67
|
+
}
|
|
68
|
+
child() {
|
|
69
|
+
return new Scope(this);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export class BallEngine {
|
|
73
|
+
program;
|
|
74
|
+
stdout;
|
|
75
|
+
stderr;
|
|
76
|
+
functions = new Map();
|
|
77
|
+
currentModule = '';
|
|
78
|
+
activeException = null;
|
|
79
|
+
output = [];
|
|
80
|
+
constructor(program, options = {}) {
|
|
81
|
+
this.program = typeof program === 'string' ? JSON.parse(program) : program;
|
|
82
|
+
this.stdout = options.stdout ?? ((msg) => this.output.push(msg));
|
|
83
|
+
this.stderr = options.stderr ?? (() => { });
|
|
84
|
+
this.buildLookupTables();
|
|
85
|
+
}
|
|
86
|
+
buildLookupTables() {
|
|
87
|
+
for (const mod of this.program.modules) {
|
|
88
|
+
for (const fn of mod.functions) {
|
|
89
|
+
this.functions.set(`${mod.name}.${fn.name}`, fn);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
run() {
|
|
94
|
+
const key = `${this.program.entryModule}.${this.program.entryFunction}`;
|
|
95
|
+
const fn = this.functions.get(key);
|
|
96
|
+
if (!fn)
|
|
97
|
+
throw new BallRuntimeError(`Entry function "${key}" not found`);
|
|
98
|
+
const scope = new Scope();
|
|
99
|
+
this.currentModule = this.program.entryModule;
|
|
100
|
+
const result = this.callFunction(this.program.entryModule, fn, null, scope);
|
|
101
|
+
if (result instanceof FlowSignal && result.kind === 'return') {
|
|
102
|
+
return this.output;
|
|
103
|
+
}
|
|
104
|
+
return this.output;
|
|
105
|
+
}
|
|
106
|
+
getOutput() {
|
|
107
|
+
return this.output;
|
|
108
|
+
}
|
|
109
|
+
// ── Expression evaluation ─────────────────────────────────────────────
|
|
110
|
+
evalExpr(expr, scope) {
|
|
111
|
+
if (expr.call)
|
|
112
|
+
return this.evalCall(expr.call, scope);
|
|
113
|
+
if (expr.literal)
|
|
114
|
+
return this.evalLiteral(expr.literal, scope);
|
|
115
|
+
if (expr.reference)
|
|
116
|
+
return this.evalReference(expr.reference, scope);
|
|
117
|
+
if (expr.fieldAccess)
|
|
118
|
+
return this.evalFieldAccess(expr.fieldAccess, scope);
|
|
119
|
+
if (expr.messageCreation)
|
|
120
|
+
return this.evalMessageCreation(expr.messageCreation, scope);
|
|
121
|
+
if (expr.block)
|
|
122
|
+
return this.evalBlock(expr.block, scope);
|
|
123
|
+
if (expr.lambda)
|
|
124
|
+
return this.evalLambda(expr.lambda, scope);
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
evalCall(call, scope) {
|
|
128
|
+
const moduleName = call.module || this.currentModule;
|
|
129
|
+
// Lazy control flow
|
|
130
|
+
if (moduleName === 'std' || moduleName === 'dart_std') {
|
|
131
|
+
switch (call.function) {
|
|
132
|
+
case 'if': return this.evalLazyIf(call, scope);
|
|
133
|
+
case 'for': return this.evalLazyFor(call, scope);
|
|
134
|
+
case 'for_in': return this.evalLazyForIn(call, scope);
|
|
135
|
+
case 'while': return this.evalLazyWhile(call, scope);
|
|
136
|
+
case 'do_while': return this.evalLazyDoWhile(call, scope);
|
|
137
|
+
case 'switch': return this.evalLazySwitch(call, scope);
|
|
138
|
+
case 'try': return this.evalLazyTry(call, scope);
|
|
139
|
+
case 'and': return this.evalShortCircuitAnd(call, scope);
|
|
140
|
+
case 'or': return this.evalShortCircuitOr(call, scope);
|
|
141
|
+
case 'return': return this.evalReturn(call, scope);
|
|
142
|
+
case 'break': return new FlowSignal('break', undefined, this.lazyStringField(call, 'label'));
|
|
143
|
+
case 'continue': return new FlowSignal('continue', undefined, this.lazyStringField(call, 'label'));
|
|
144
|
+
case 'assign': return this.evalAssign(call, scope);
|
|
145
|
+
case 'labeled': return this.evalLabeled(call, scope);
|
|
146
|
+
case 'pre_increment':
|
|
147
|
+
case 'post_increment':
|
|
148
|
+
case 'pre_decrement':
|
|
149
|
+
case 'post_decrement':
|
|
150
|
+
return this.evalIncDec(call, scope);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Eager evaluation
|
|
154
|
+
const input = call.input ? this.evalExpr(call.input, scope) : null;
|
|
155
|
+
// Fast path for explicit std calls
|
|
156
|
+
if (call.module === 'std' || call.module === 'dart_std') {
|
|
157
|
+
return this.callBaseFunction(call.module, call.function, input);
|
|
158
|
+
}
|
|
159
|
+
const key = `${moduleName}.${call.function}`;
|
|
160
|
+
const fn = this.functions.get(key);
|
|
161
|
+
if (fn?.isBase)
|
|
162
|
+
return this.callBaseFunction(moduleName, call.function, input);
|
|
163
|
+
// Scope closure lookup
|
|
164
|
+
if (!call.module && scope.has(call.function)) {
|
|
165
|
+
const bound = scope.lookup(call.function);
|
|
166
|
+
if (typeof bound === 'function')
|
|
167
|
+
return bound(input);
|
|
168
|
+
}
|
|
169
|
+
// Module function lookup
|
|
170
|
+
if (fn)
|
|
171
|
+
return this.callFunction(moduleName, fn, input, scope);
|
|
172
|
+
// Fallback: scan all modules
|
|
173
|
+
for (const mod of this.program.modules) {
|
|
174
|
+
for (const f of mod.functions) {
|
|
175
|
+
if (f.name === call.function) {
|
|
176
|
+
return this.callFunction(mod.name, f, input, scope);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
throw new BallRuntimeError(`Function "${key}" not found`);
|
|
181
|
+
}
|
|
182
|
+
callFunction(moduleName, fn, input, parentScope) {
|
|
183
|
+
if (fn.isBase)
|
|
184
|
+
return this.callBaseFunction(moduleName, fn.name, input);
|
|
185
|
+
if (!fn.body)
|
|
186
|
+
return null;
|
|
187
|
+
const prevModule = this.currentModule;
|
|
188
|
+
this.currentModule = moduleName;
|
|
189
|
+
const fnScope = parentScope.child();
|
|
190
|
+
fnScope.bind('input', input);
|
|
191
|
+
// Destructure input fields as named parameters
|
|
192
|
+
const params = fn.metadata?.params;
|
|
193
|
+
if (params && Array.isArray(params)) {
|
|
194
|
+
if (params.length === 1 && (input === null || input === undefined || typeof input !== 'object' || Array.isArray(input))) {
|
|
195
|
+
// Single-param function with non-object input: bind directly
|
|
196
|
+
const name = typeof params[0] === 'string' ? params[0] : params[0].name;
|
|
197
|
+
if (name)
|
|
198
|
+
fnScope.bind(name, input);
|
|
199
|
+
}
|
|
200
|
+
else if (input && typeof input === 'object' && !Array.isArray(input)) {
|
|
201
|
+
for (let i = 0; i < params.length; i++) {
|
|
202
|
+
const p = params[i];
|
|
203
|
+
const name = typeof p === 'string' ? p : p.name;
|
|
204
|
+
if (!name)
|
|
205
|
+
continue;
|
|
206
|
+
if (name in input) {
|
|
207
|
+
fnScope.bind(name, input[name]);
|
|
208
|
+
}
|
|
209
|
+
else if (`arg${i}` in input) {
|
|
210
|
+
fnScope.bind(name, input[`arg${i}`]);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
let result = this.evalExpr(fn.body, fnScope);
|
|
216
|
+
this.currentModule = prevModule;
|
|
217
|
+
if (result instanceof FlowSignal && result.kind === 'return') {
|
|
218
|
+
return result.value;
|
|
219
|
+
}
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
// ── Literals ──────────────────────────────────────────────────────────
|
|
223
|
+
evalLiteral(lit, scope) {
|
|
224
|
+
if (lit.intValue !== undefined)
|
|
225
|
+
return typeof lit.intValue === 'string' ? parseInt(lit.intValue) : lit.intValue;
|
|
226
|
+
if (lit.doubleValue !== undefined)
|
|
227
|
+
return lit.doubleValue;
|
|
228
|
+
if (lit.stringValue !== undefined)
|
|
229
|
+
return lit.stringValue;
|
|
230
|
+
if (lit.boolValue !== undefined)
|
|
231
|
+
return lit.boolValue;
|
|
232
|
+
if (lit.listValue)
|
|
233
|
+
return lit.listValue.elements.map(e => this.evalExpr(e, scope));
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
evalReference(ref, scope) {
|
|
237
|
+
return scope.lookup(ref.name);
|
|
238
|
+
}
|
|
239
|
+
evalFieldAccess(fa, scope) {
|
|
240
|
+
const obj = this.evalExpr(fa.object, scope);
|
|
241
|
+
// Handle string properties
|
|
242
|
+
if (typeof obj === 'string') {
|
|
243
|
+
if (fa.field === 'length')
|
|
244
|
+
return obj.length;
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
// Handle array properties
|
|
248
|
+
if (Array.isArray(obj)) {
|
|
249
|
+
if (fa.field === 'length')
|
|
250
|
+
return obj.length;
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
if (obj && typeof obj === 'object' && fa.field in obj)
|
|
254
|
+
return obj[fa.field];
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
evalMessageCreation(mc, scope) {
|
|
258
|
+
const result = {};
|
|
259
|
+
for (const f of mc.fields) {
|
|
260
|
+
result[f.name] = this.evalExpr(f.value, scope);
|
|
261
|
+
}
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
evalBlock(block, scope) {
|
|
265
|
+
const blockScope = scope.child();
|
|
266
|
+
let lastResult = null;
|
|
267
|
+
for (const stmt of block.statements) {
|
|
268
|
+
if (stmt.let) {
|
|
269
|
+
const val = this.evalExpr(stmt.let.value, blockScope);
|
|
270
|
+
if (val instanceof FlowSignal)
|
|
271
|
+
return val;
|
|
272
|
+
blockScope.bind(stmt.let.name, val);
|
|
273
|
+
}
|
|
274
|
+
if (stmt.expression) {
|
|
275
|
+
lastResult = this.evalExpr(stmt.expression, blockScope);
|
|
276
|
+
if (lastResult instanceof FlowSignal)
|
|
277
|
+
return lastResult;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (block.result) {
|
|
281
|
+
lastResult = this.evalExpr(block.result, blockScope);
|
|
282
|
+
}
|
|
283
|
+
return lastResult;
|
|
284
|
+
}
|
|
285
|
+
evalLambda(lambda, scope) {
|
|
286
|
+
return (input) => {
|
|
287
|
+
const lambdaScope = scope.child();
|
|
288
|
+
lambdaScope.bind('input', input);
|
|
289
|
+
const params = lambda.metadata?.params;
|
|
290
|
+
if (params && Array.isArray(params)) {
|
|
291
|
+
if (params.length === 1 && (input === null || input === undefined || typeof input !== 'object' || Array.isArray(input))) {
|
|
292
|
+
const name = typeof params[0] === 'string' ? params[0] : params[0].name;
|
|
293
|
+
if (name)
|
|
294
|
+
lambdaScope.bind(name, input);
|
|
295
|
+
}
|
|
296
|
+
else if (input && typeof input === 'object' && !Array.isArray(input)) {
|
|
297
|
+
for (let i = 0; i < params.length; i++) {
|
|
298
|
+
const p = params[i];
|
|
299
|
+
const name = typeof p === 'string' ? p : p.name;
|
|
300
|
+
if (!name)
|
|
301
|
+
continue;
|
|
302
|
+
if (name in input) {
|
|
303
|
+
lambdaScope.bind(name, input[name]);
|
|
304
|
+
}
|
|
305
|
+
else if (`arg${i}` in input) {
|
|
306
|
+
lambdaScope.bind(name, input[`arg${i}`]);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const result = this.evalExpr(lambda.body, lambdaScope);
|
|
312
|
+
if (result instanceof FlowSignal && result.kind === 'return')
|
|
313
|
+
return result.value;
|
|
314
|
+
return result;
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
// ── Lazy control flow ─────────────────────────────────────────────────
|
|
318
|
+
lazyFields(call) {
|
|
319
|
+
if (!call.input?.messageCreation)
|
|
320
|
+
return {};
|
|
321
|
+
const result = {};
|
|
322
|
+
for (const f of call.input.messageCreation.fields) {
|
|
323
|
+
result[f.name] = f.value;
|
|
324
|
+
}
|
|
325
|
+
return result;
|
|
326
|
+
}
|
|
327
|
+
lazyStringField(call, name) {
|
|
328
|
+
const fields = this.lazyFields(call);
|
|
329
|
+
const expr = fields[name];
|
|
330
|
+
if (!expr?.literal?.stringValue)
|
|
331
|
+
return undefined;
|
|
332
|
+
return expr.literal.stringValue;
|
|
333
|
+
}
|
|
334
|
+
evalLazyIf(call, scope) {
|
|
335
|
+
const fields = this.lazyFields(call);
|
|
336
|
+
if (!fields.condition)
|
|
337
|
+
return null;
|
|
338
|
+
const cond = this.evalExpr(fields.condition, scope);
|
|
339
|
+
if (this.toBool(cond)) {
|
|
340
|
+
return fields.then ? this.evalExpr(fields.then, scope) : null;
|
|
341
|
+
}
|
|
342
|
+
return fields.else ? this.evalExpr(fields.else, scope) : null;
|
|
343
|
+
}
|
|
344
|
+
evalLazyWhile(call, scope) {
|
|
345
|
+
const fields = this.lazyFields(call);
|
|
346
|
+
while (true) {
|
|
347
|
+
if (fields.condition) {
|
|
348
|
+
if (!this.toBool(this.evalExpr(fields.condition, scope)))
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
if (fields.body) {
|
|
352
|
+
const result = this.evalExpr(fields.body, scope);
|
|
353
|
+
if (result instanceof FlowSignal) {
|
|
354
|
+
if (result.kind === 'return')
|
|
355
|
+
return result;
|
|
356
|
+
if (result.label)
|
|
357
|
+
return result;
|
|
358
|
+
if (result.kind === 'break')
|
|
359
|
+
break;
|
|
360
|
+
if (result.kind === 'continue')
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
evalLazyDoWhile(call, scope) {
|
|
368
|
+
const fields = this.lazyFields(call);
|
|
369
|
+
do {
|
|
370
|
+
if (fields.body) {
|
|
371
|
+
const result = this.evalExpr(fields.body, scope);
|
|
372
|
+
if (result instanceof FlowSignal) {
|
|
373
|
+
if (result.kind === 'return')
|
|
374
|
+
return result;
|
|
375
|
+
if (result.kind === 'break')
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (fields.condition) {
|
|
380
|
+
if (!this.toBool(this.evalExpr(fields.condition, scope)))
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
else
|
|
384
|
+
break;
|
|
385
|
+
} while (true);
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
evalLazyFor(call, scope) {
|
|
389
|
+
const fields = this.lazyFields(call);
|
|
390
|
+
const forScope = scope.child();
|
|
391
|
+
if (fields.init) {
|
|
392
|
+
// Handle init as string literal "var i = 0" (encoder format)
|
|
393
|
+
if (fields.init.literal?.stringValue) {
|
|
394
|
+
const match = fields.init.literal.stringValue.match(/^(?:var|final|int|double|String)\s+(\w+)\s*=\s*(.+)$/);
|
|
395
|
+
if (match) {
|
|
396
|
+
const varName = match[1];
|
|
397
|
+
const rawVal = match[2].trim();
|
|
398
|
+
const parsed = rawVal === 'true' ? true : rawVal === 'false' ? false : (isNaN(Number(rawVal)) ? rawVal : Number(rawVal));
|
|
399
|
+
forScope.bind(varName, parsed);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
else if (fields.init.block) {
|
|
403
|
+
this.evalBlock(fields.init.block, forScope);
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
this.evalExpr(fields.init, forScope);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
while (true) {
|
|
410
|
+
if (fields.condition) {
|
|
411
|
+
if (!this.toBool(this.evalExpr(fields.condition, forScope)))
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
if (fields.body) {
|
|
415
|
+
const result = this.evalExpr(fields.body, forScope);
|
|
416
|
+
if (result instanceof FlowSignal) {
|
|
417
|
+
if (result.kind === 'return')
|
|
418
|
+
return result;
|
|
419
|
+
if (result.label)
|
|
420
|
+
return result;
|
|
421
|
+
if (result.kind === 'break')
|
|
422
|
+
break;
|
|
423
|
+
if (result.kind === 'continue') {
|
|
424
|
+
if (fields.update)
|
|
425
|
+
this.evalExpr(fields.update, forScope);
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (fields.update)
|
|
431
|
+
this.evalExpr(fields.update, forScope);
|
|
432
|
+
}
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
evalLazyForIn(call, scope) {
|
|
436
|
+
const fields = this.lazyFields(call);
|
|
437
|
+
const varName = fields.variable?.literal?.stringValue ?? 'item';
|
|
438
|
+
if (!fields.iterable || !fields.body)
|
|
439
|
+
return null;
|
|
440
|
+
const iterable = this.evalExpr(fields.iterable, scope);
|
|
441
|
+
if (!Array.isArray(iterable))
|
|
442
|
+
throw new BallRuntimeError('for_in: iterable is not a List');
|
|
443
|
+
for (const item of iterable) {
|
|
444
|
+
const loopScope = scope.child();
|
|
445
|
+
loopScope.bind(varName, item);
|
|
446
|
+
const result = this.evalExpr(fields.body, loopScope);
|
|
447
|
+
if (result instanceof FlowSignal) {
|
|
448
|
+
if (result.kind === 'return')
|
|
449
|
+
return result;
|
|
450
|
+
if (result.label)
|
|
451
|
+
return result;
|
|
452
|
+
if (result.kind === 'break')
|
|
453
|
+
break;
|
|
454
|
+
if (result.kind === 'continue')
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
evalLazySwitch(call, scope) {
|
|
461
|
+
const fields = this.lazyFields(call);
|
|
462
|
+
if (!fields.subject || !fields.cases)
|
|
463
|
+
return null;
|
|
464
|
+
const subject = this.evalExpr(fields.subject, scope);
|
|
465
|
+
const cases = fields.cases.literal?.listValue?.elements ?? [];
|
|
466
|
+
let defaultBody;
|
|
467
|
+
for (const c of cases) {
|
|
468
|
+
if (!c.messageCreation)
|
|
469
|
+
continue;
|
|
470
|
+
const cf = {};
|
|
471
|
+
for (const f of c.messageCreation.fields)
|
|
472
|
+
cf[f.name] = f.value;
|
|
473
|
+
if (cf.is_default?.literal?.boolValue) {
|
|
474
|
+
defaultBody = cf.body;
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
if (cf.value) {
|
|
478
|
+
const caseVal = this.evalExpr(cf.value, scope);
|
|
479
|
+
if (caseVal === subject && cf.body)
|
|
480
|
+
return this.evalExpr(cf.body, scope);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (defaultBody)
|
|
484
|
+
return this.evalExpr(defaultBody, scope);
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
evalLazyTry(call, scope) {
|
|
488
|
+
const fields = this.lazyFields(call);
|
|
489
|
+
let result = null;
|
|
490
|
+
try {
|
|
491
|
+
result = fields.body ? this.evalExpr(fields.body, scope) : null;
|
|
492
|
+
}
|
|
493
|
+
catch (e) {
|
|
494
|
+
result = null;
|
|
495
|
+
const catches = fields.catches?.literal?.listValue?.elements ?? [];
|
|
496
|
+
let caught = false;
|
|
497
|
+
for (const c of catches) {
|
|
498
|
+
if (!c.messageCreation)
|
|
499
|
+
continue;
|
|
500
|
+
const cf = {};
|
|
501
|
+
for (const f of c.messageCreation.fields)
|
|
502
|
+
cf[f.name] = f.value;
|
|
503
|
+
const catchType = cf.type?.literal?.stringValue;
|
|
504
|
+
if (catchType) {
|
|
505
|
+
const matches = e instanceof BallException ? e.typeName === catchType : e.constructor?.name === catchType;
|
|
506
|
+
if (!matches)
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
const variable = cf.variable?.literal?.stringValue ?? 'e';
|
|
510
|
+
if (cf.body) {
|
|
511
|
+
const catchScope = scope.child();
|
|
512
|
+
catchScope.bind(variable, e instanceof BallException ? e.value : (e instanceof Error ? e.message : String(e)));
|
|
513
|
+
const prev = this.activeException;
|
|
514
|
+
this.activeException = e;
|
|
515
|
+
try {
|
|
516
|
+
result = this.evalExpr(cf.body, catchScope);
|
|
517
|
+
}
|
|
518
|
+
finally {
|
|
519
|
+
this.activeException = prev;
|
|
520
|
+
}
|
|
521
|
+
caught = true;
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (!caught)
|
|
526
|
+
throw e;
|
|
527
|
+
}
|
|
528
|
+
finally {
|
|
529
|
+
if (fields.finally)
|
|
530
|
+
this.evalExpr(fields.finally, scope);
|
|
531
|
+
}
|
|
532
|
+
return result;
|
|
533
|
+
}
|
|
534
|
+
evalShortCircuitAnd(call, scope) {
|
|
535
|
+
const fields = this.lazyFields(call);
|
|
536
|
+
if (!fields.left || !fields.right)
|
|
537
|
+
return false;
|
|
538
|
+
if (!this.toBool(this.evalExpr(fields.left, scope)))
|
|
539
|
+
return false;
|
|
540
|
+
return this.toBool(this.evalExpr(fields.right, scope));
|
|
541
|
+
}
|
|
542
|
+
evalShortCircuitOr(call, scope) {
|
|
543
|
+
const fields = this.lazyFields(call);
|
|
544
|
+
if (!fields.left || !fields.right)
|
|
545
|
+
return false;
|
|
546
|
+
if (this.toBool(this.evalExpr(fields.left, scope)))
|
|
547
|
+
return true;
|
|
548
|
+
return this.toBool(this.evalExpr(fields.right, scope));
|
|
549
|
+
}
|
|
550
|
+
evalReturn(call, scope) {
|
|
551
|
+
const fields = this.lazyFields(call);
|
|
552
|
+
const val = fields.value ? this.evalExpr(fields.value, scope) : null;
|
|
553
|
+
return new FlowSignal('return', val);
|
|
554
|
+
}
|
|
555
|
+
evalAssign(call, scope) {
|
|
556
|
+
const fields = this.lazyFields(call);
|
|
557
|
+
if (!fields.value)
|
|
558
|
+
return null;
|
|
559
|
+
const val = this.evalExpr(fields.value, scope);
|
|
560
|
+
// Target can be a reference expression or a string literal name.
|
|
561
|
+
const target = fields.target ?? fields.name ?? fields.variable;
|
|
562
|
+
if (!target)
|
|
563
|
+
return null;
|
|
564
|
+
let name;
|
|
565
|
+
if (target.reference) {
|
|
566
|
+
name = target.reference.name;
|
|
567
|
+
}
|
|
568
|
+
else if (target.literal?.stringValue) {
|
|
569
|
+
name = target.literal.stringValue;
|
|
570
|
+
}
|
|
571
|
+
if (!name)
|
|
572
|
+
return null;
|
|
573
|
+
const op = fields.op?.literal?.stringValue;
|
|
574
|
+
if (op && op !== '=') {
|
|
575
|
+
const current = scope.lookup(name);
|
|
576
|
+
const computed = this.applyCompoundOp(op, current, val);
|
|
577
|
+
try {
|
|
578
|
+
scope.assign(name, computed);
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
scope.bind(name, computed);
|
|
582
|
+
}
|
|
583
|
+
return computed;
|
|
584
|
+
}
|
|
585
|
+
try {
|
|
586
|
+
scope.assign(name, val);
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
scope.bind(name, val);
|
|
590
|
+
}
|
|
591
|
+
return val;
|
|
592
|
+
}
|
|
593
|
+
applyCompoundOp(op, current, val) {
|
|
594
|
+
const a = this.toNum(current);
|
|
595
|
+
const b = this.toNum(val);
|
|
596
|
+
switch (op) {
|
|
597
|
+
case '+=': return a + b;
|
|
598
|
+
case '-=': return a - b;
|
|
599
|
+
case '*=': return a * b;
|
|
600
|
+
case '/=': return Math.trunc(a / b);
|
|
601
|
+
case '%=': return a % b;
|
|
602
|
+
case '&=': return (a | 0) & (b | 0);
|
|
603
|
+
case '|=': return (a | 0) | (b | 0);
|
|
604
|
+
case '^=': return (a | 0) ^ (b | 0);
|
|
605
|
+
case '<<=': return (a | 0) << (b | 0);
|
|
606
|
+
case '>>=': return (a | 0) >> (b | 0);
|
|
607
|
+
default: return val;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
evalIncDec(call, scope) {
|
|
611
|
+
const fields = this.lazyFields(call);
|
|
612
|
+
const valueExpr = fields.value;
|
|
613
|
+
if (!valueExpr)
|
|
614
|
+
return null;
|
|
615
|
+
if (valueExpr.reference) {
|
|
616
|
+
const name = valueExpr.reference.name;
|
|
617
|
+
const current = this.toNum(scope.lookup(name));
|
|
618
|
+
const isInc = call.function.includes('increment');
|
|
619
|
+
const isPre = call.function.startsWith('pre');
|
|
620
|
+
const updated = isInc ? current + 1 : current - 1;
|
|
621
|
+
scope.assign(name, updated);
|
|
622
|
+
return isPre ? updated : current;
|
|
623
|
+
}
|
|
624
|
+
// Fallback: just compute
|
|
625
|
+
const val = this.toNum(this.evalExpr(valueExpr, scope));
|
|
626
|
+
const isInc = call.function.includes('increment');
|
|
627
|
+
return isInc ? val + 1 : val - 1;
|
|
628
|
+
}
|
|
629
|
+
evalLabeled(call, scope) {
|
|
630
|
+
const fields = this.lazyFields(call);
|
|
631
|
+
const label = fields.label?.literal?.stringValue;
|
|
632
|
+
if (!fields.body)
|
|
633
|
+
return null;
|
|
634
|
+
const result = this.evalExpr(fields.body, scope);
|
|
635
|
+
if (result instanceof FlowSignal && result.label === label) {
|
|
636
|
+
if (result.kind === 'break')
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
return result;
|
|
640
|
+
}
|
|
641
|
+
// ── Base function dispatch ────────────────────────────────────────────
|
|
642
|
+
callBaseFunction(module, fn, input) {
|
|
643
|
+
const left = input?.left;
|
|
644
|
+
const right = input?.right;
|
|
645
|
+
const value = input?.value;
|
|
646
|
+
switch (fn) {
|
|
647
|
+
// I/O
|
|
648
|
+
case 'print':
|
|
649
|
+
this.stdout(this.ballToString(value ?? input?.message ?? input));
|
|
650
|
+
return null;
|
|
651
|
+
// Arithmetic
|
|
652
|
+
case 'add': {
|
|
653
|
+
if (typeof left === 'string' || typeof right === 'string')
|
|
654
|
+
return String(left ?? '') + String(right ?? '');
|
|
655
|
+
return this.numOp(left, right, (a, b) => a + b);
|
|
656
|
+
}
|
|
657
|
+
case 'subtract': return this.numOp(left, right, (a, b) => a - b);
|
|
658
|
+
case 'multiply': return this.numOp(left, right, (a, b) => a * b);
|
|
659
|
+
case 'divide': return Math.trunc(this.toNum(left) / this.toNum(right));
|
|
660
|
+
case 'divide_double': return this.toNum(left) / this.toNum(right);
|
|
661
|
+
case 'modulo': return this.numOp(left, right, (a, b) => a % b);
|
|
662
|
+
case 'negate': return -this.toNum(value ?? input);
|
|
663
|
+
// Comparison
|
|
664
|
+
case 'equals': return left === right;
|
|
665
|
+
case 'not_equals': return left !== right;
|
|
666
|
+
case 'less_than': return this.toNum(left) < this.toNum(right);
|
|
667
|
+
case 'greater_than': return this.toNum(left) > this.toNum(right);
|
|
668
|
+
case 'lte': return this.toNum(left) <= this.toNum(right);
|
|
669
|
+
case 'gte': return this.toNum(left) >= this.toNum(right);
|
|
670
|
+
// Logical
|
|
671
|
+
case 'not': return !this.toBool(value ?? input);
|
|
672
|
+
// String
|
|
673
|
+
case 'concat': return String(left ?? '') + String(right ?? '');
|
|
674
|
+
case 'to_string': return this.ballToString(value ?? input);
|
|
675
|
+
case 'string_length': return String(value ?? input).length;
|
|
676
|
+
case 'string_contains': return String(input?.string ?? '').includes(String(input?.substring ?? ''));
|
|
677
|
+
case 'string_substring': return String(input?.string ?? '').substring(input?.start ?? 0, input?.end);
|
|
678
|
+
case 'string_to_upper': return String(value ?? input).toUpperCase();
|
|
679
|
+
case 'string_to_lower': return String(value ?? input).toLowerCase();
|
|
680
|
+
case 'string_trim': return String(value ?? input).trim();
|
|
681
|
+
case 'string_split': return String(input?.string ?? '').split(String(input?.delimiter ?? ''));
|
|
682
|
+
case 'string_replace': return String(input?.string ?? '').replace(String(input?.from ?? ''), String(input?.to ?? ''));
|
|
683
|
+
case 'string_replace_all': return String(input?.string ?? '').replaceAll(String(input?.from ?? ''), String(input?.to ?? ''));
|
|
684
|
+
case 'string_starts_with': return String(input?.string ?? '').startsWith(String(input?.prefix ?? ''));
|
|
685
|
+
case 'string_ends_with': return String(input?.string ?? '').endsWith(String(input?.suffix ?? ''));
|
|
686
|
+
case 'string_index_of': return String(input?.string ?? '').indexOf(String(input?.substring ?? ''));
|
|
687
|
+
// Type ops
|
|
688
|
+
case 'is': return typeof input?.value === input?.type;
|
|
689
|
+
case 'as': return input?.value;
|
|
690
|
+
// Math
|
|
691
|
+
case 'math_abs': return Math.abs(this.toNum(value ?? input));
|
|
692
|
+
case 'math_floor': return Math.floor(this.toNum(value ?? input));
|
|
693
|
+
case 'math_ceil': return Math.ceil(this.toNum(value ?? input));
|
|
694
|
+
case 'math_round': return Math.round(this.toNum(value ?? input));
|
|
695
|
+
case 'math_sqrt': return Math.sqrt(this.toNum(value ?? input));
|
|
696
|
+
case 'math_pow': return Math.pow(this.toNum(input?.base ?? left), this.toNum(input?.exponent ?? right));
|
|
697
|
+
case 'math_min': return Math.min(this.toNum(left), this.toNum(right));
|
|
698
|
+
case 'math_max': return Math.max(this.toNum(left), this.toNum(right));
|
|
699
|
+
case 'math_pi': return Math.PI;
|
|
700
|
+
// Error handling
|
|
701
|
+
case 'throw': {
|
|
702
|
+
const rawVal = input?.value ?? input?.message ?? input;
|
|
703
|
+
const typeName = input?.type ?? rawVal?.__type ?? 'Exception';
|
|
704
|
+
throw new BallException(typeName, rawVal);
|
|
705
|
+
}
|
|
706
|
+
case 'rethrow': {
|
|
707
|
+
if (this.activeException)
|
|
708
|
+
throw this.activeException;
|
|
709
|
+
throw new BallRuntimeError('rethrow outside of catch');
|
|
710
|
+
}
|
|
711
|
+
case 'assert': {
|
|
712
|
+
if (!this.toBool(input?.condition ?? input)) {
|
|
713
|
+
throw new BallRuntimeError(`Assertion failed: ${input?.message ?? ''}`);
|
|
714
|
+
}
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
// Collections
|
|
718
|
+
case 'list_push': {
|
|
719
|
+
const l = [...(input?.list ?? [])];
|
|
720
|
+
l.push(input?.value);
|
|
721
|
+
return l;
|
|
722
|
+
}
|
|
723
|
+
case 'list_length': return (input?.list ?? input ?? []).length;
|
|
724
|
+
case 'list_get': return (input?.list ?? [])[input?.index ?? 0];
|
|
725
|
+
case 'list_map': {
|
|
726
|
+
const list = input?.list ?? [];
|
|
727
|
+
const fn = input?.function;
|
|
728
|
+
if (typeof fn === 'function')
|
|
729
|
+
return list.map((item) => fn(item));
|
|
730
|
+
return list;
|
|
731
|
+
}
|
|
732
|
+
case 'list_filter': {
|
|
733
|
+
const list = input?.list ?? [];
|
|
734
|
+
const fn = input?.function;
|
|
735
|
+
if (typeof fn === 'function')
|
|
736
|
+
return list.filter((item) => fn(item));
|
|
737
|
+
return list;
|
|
738
|
+
}
|
|
739
|
+
case 'map_get': return (input?.map ?? {})[input?.key];
|
|
740
|
+
case 'map_set': {
|
|
741
|
+
const m = { ...(input?.map ?? {}) };
|
|
742
|
+
m[input?.key] = input?.value;
|
|
743
|
+
return m;
|
|
744
|
+
}
|
|
745
|
+
case 'map_keys': return Object.keys(input?.map ?? input ?? {});
|
|
746
|
+
// Increment/decrement (pure value ops)
|
|
747
|
+
case 'pre_increment':
|
|
748
|
+
case 'post_increment': return this.toNum(value ?? input) + 1;
|
|
749
|
+
case 'pre_decrement':
|
|
750
|
+
case 'post_decrement': return this.toNum(value ?? input) - 1;
|
|
751
|
+
// Index
|
|
752
|
+
case 'index': {
|
|
753
|
+
const target = input?.target ?? input?.list;
|
|
754
|
+
const idx = input?.index ?? input?.key;
|
|
755
|
+
if (Array.isArray(target))
|
|
756
|
+
return target[idx];
|
|
757
|
+
if (target && typeof target === 'object')
|
|
758
|
+
return target[idx];
|
|
759
|
+
if (typeof target === 'string')
|
|
760
|
+
return target[idx];
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
// Bitwise
|
|
764
|
+
case 'bitwise_and': return (this.toNum(left) | 0) & (this.toNum(right) | 0);
|
|
765
|
+
case 'bitwise_or': return (this.toNum(left) | 0) | (this.toNum(right) | 0);
|
|
766
|
+
case 'bitwise_xor': return (this.toNum(left) | 0) ^ (this.toNum(right) | 0);
|
|
767
|
+
case 'bitwise_not': return ~(this.toNum(value ?? input) | 0);
|
|
768
|
+
case 'left_shift': return (this.toNum(left) | 0) << (this.toNum(right) | 0);
|
|
769
|
+
case 'right_shift': return (this.toNum(left) | 0) >> (this.toNum(right) | 0);
|
|
770
|
+
// Null
|
|
771
|
+
case 'null_coalesce': return left ?? right;
|
|
772
|
+
// Misc
|
|
773
|
+
case 'paren': return value ?? input;
|
|
774
|
+
case 'string_interpolation': return String(value ?? input);
|
|
775
|
+
case 'int_to_string': return String(Math.trunc(this.toNum(value ?? input)));
|
|
776
|
+
case 'double_to_string': return String(this.toNum(value ?? input));
|
|
777
|
+
case 'string_to_int': return parseInt(String(value ?? input));
|
|
778
|
+
case 'string_to_double': return parseFloat(String(value ?? input));
|
|
779
|
+
case 'length': return (value ?? input)?.length ?? 0;
|
|
780
|
+
default:
|
|
781
|
+
throw new BallRuntimeError(`Unknown base function: ${module}.${fn}`);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
// ── Helpers ───────────────────────────────────────────────────────────
|
|
785
|
+
toBool(v) {
|
|
786
|
+
if (v === null || v === undefined || v === false || v === 0 || v === '')
|
|
787
|
+
return false;
|
|
788
|
+
return true;
|
|
789
|
+
}
|
|
790
|
+
toNum(v) {
|
|
791
|
+
if (typeof v === 'number')
|
|
792
|
+
return v;
|
|
793
|
+
if (typeof v === 'string')
|
|
794
|
+
return Number(v) || 0;
|
|
795
|
+
if (typeof v === 'boolean')
|
|
796
|
+
return v ? 1 : 0;
|
|
797
|
+
return 0;
|
|
798
|
+
}
|
|
799
|
+
numOp(left, right, op) {
|
|
800
|
+
return op(this.toNum(left), this.toNum(right));
|
|
801
|
+
}
|
|
802
|
+
ballToString(v) {
|
|
803
|
+
if (v === null || v === undefined)
|
|
804
|
+
return 'null';
|
|
805
|
+
if (typeof v === 'number') {
|
|
806
|
+
return Number.isInteger(v) ? v.toString() : v.toString();
|
|
807
|
+
}
|
|
808
|
+
if (typeof v === 'boolean')
|
|
809
|
+
return v.toString();
|
|
810
|
+
if (Array.isArray(v))
|
|
811
|
+
return `[${v.map(x => this.ballToString(x)).join(', ')}]`;
|
|
812
|
+
if (typeof v === 'object')
|
|
813
|
+
return JSON.stringify(v);
|
|
814
|
+
return String(v);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
//# sourceMappingURL=index.js.map
|