@ball-lang/encoder 0.1.0 → 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 +47 -0
- package/dist/encoder.d.ts +19 -2
- package/dist/encoder.d.ts.map +1 -1
- package/dist/encoder.js +340 -87
- package/dist/encoder.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/encoder.ts +357 -92
- package/src/index.ts +2 -2
package/src/encoder.ts
CHANGED
|
@@ -22,16 +22,16 @@ const BINARY_OPS: Record<number, StdRef> = {
|
|
|
22
22
|
[ts.SyntaxKind.AmpersandToken]: { module: "std", function: "bitwise_and" },
|
|
23
23
|
[ts.SyntaxKind.BarToken]: { module: "std", function: "bitwise_or" },
|
|
24
24
|
[ts.SyntaxKind.CaretToken]: { module: "std", function: "bitwise_xor" },
|
|
25
|
-
[ts.SyntaxKind.LessThanLessThanToken]: { module: "std", function: "
|
|
26
|
-
[ts.SyntaxKind.GreaterThanGreaterThanToken]:{ module: "std", function: "
|
|
25
|
+
[ts.SyntaxKind.LessThanLessThanToken]: { module: "std", function: "left_shift" },
|
|
26
|
+
[ts.SyntaxKind.GreaterThanGreaterThanToken]:{ module: "std", function: "right_shift" },
|
|
27
27
|
[ts.SyntaxKind.EqualsEqualsEqualsToken]: { module: "std", function: "equals" },
|
|
28
28
|
[ts.SyntaxKind.ExclamationEqualsEqualsToken]:{ module: "std", function: "not_equals" },
|
|
29
29
|
[ts.SyntaxKind.EqualsEqualsToken]: { module: "std", function: "equals" },
|
|
30
30
|
[ts.SyntaxKind.ExclamationEqualsToken]: { module: "std", function: "not_equals" },
|
|
31
31
|
[ts.SyntaxKind.LessThanToken]: { module: "std", function: "less_than" },
|
|
32
32
|
[ts.SyntaxKind.GreaterThanToken]: { module: "std", function: "greater_than" },
|
|
33
|
-
[ts.SyntaxKind.LessThanEqualsToken]: { module: "std", function: "
|
|
34
|
-
[ts.SyntaxKind.GreaterThanEqualsToken]: { module: "std", function: "
|
|
33
|
+
[ts.SyntaxKind.LessThanEqualsToken]: { module: "std", function: "lte" },
|
|
34
|
+
[ts.SyntaxKind.GreaterThanEqualsToken]: { module: "std", function: "gte" },
|
|
35
35
|
[ts.SyntaxKind.AmpersandAmpersandToken]: { module: "std", function: "and" },
|
|
36
36
|
[ts.SyntaxKind.BarBarToken]: { module: "std", function: "or" },
|
|
37
37
|
[ts.SyntaxKind.QuestionQuestionToken]: { module: "std", function: "null_coalesce" },
|
|
@@ -55,10 +55,12 @@ const COMPOUND_OPS: Record<number, string> = {
|
|
|
55
55
|
export class TsEncoder {
|
|
56
56
|
private stdFunctions = new Set<string>();
|
|
57
57
|
private warnings: string[] = [];
|
|
58
|
+
private strict = false;
|
|
58
59
|
|
|
59
60
|
encode(source: string, options: EncodeOptions = {}): Program {
|
|
60
61
|
const modName = options.moduleName ?? "main";
|
|
61
62
|
const entryFn = options.entryFunction ?? "main";
|
|
63
|
+
this.strict = options.strict ?? false;
|
|
62
64
|
|
|
63
65
|
const sourceFile = ts.createSourceFile(
|
|
64
66
|
"input.ts", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS
|
|
@@ -85,7 +87,7 @@ export class TsEncoder {
|
|
|
85
87
|
if (ts.isIdentifier(decl.name)) {
|
|
86
88
|
functions.push({
|
|
87
89
|
name: decl.name.text,
|
|
88
|
-
body: decl.initializer ? this.encodeExpr(decl.initializer) :
|
|
90
|
+
body: decl.initializer ? this.encodeExpr(decl.initializer) : this.nullLiteral(),
|
|
89
91
|
metadata: { kind: "top_level_variable" },
|
|
90
92
|
});
|
|
91
93
|
} else if (ts.isObjectBindingPattern(decl.name) && decl.initializer) {
|
|
@@ -124,8 +126,8 @@ export class TsEncoder {
|
|
|
124
126
|
}
|
|
125
127
|
}
|
|
126
128
|
|
|
127
|
-
const
|
|
128
|
-
const modules: Module[] = [
|
|
129
|
+
const baseModules = this.buildBaseModules();
|
|
130
|
+
const modules: Module[] = [...baseModules];
|
|
129
131
|
|
|
130
132
|
const userModule: Module = {
|
|
131
133
|
name: modName,
|
|
@@ -152,7 +154,22 @@ export class TsEncoder {
|
|
|
152
154
|
);
|
|
153
155
|
|
|
154
156
|
const metadata: Struct = {};
|
|
155
|
-
if (params.length > 0)
|
|
157
|
+
if (params.length > 0) {
|
|
158
|
+
// Emit params as an array of structs with a `name` field (and optional
|
|
159
|
+
// `type`, `default`, `is_rest` etc.), matching the Dart encoder format.
|
|
160
|
+
// The engine's _extractParams filters for structValue entries and reads
|
|
161
|
+
// each entry's `.structValue.fields.name.stringValue`.
|
|
162
|
+
const paramStructs: Record<string, unknown>[] = [];
|
|
163
|
+
for (const p of node.parameters) {
|
|
164
|
+
const pName = ts.isIdentifier(p.name) ? p.name.text : p.name.getText();
|
|
165
|
+
const pm: Record<string, unknown> = { name: pName };
|
|
166
|
+
if (p.type) pm["type"] = p.type.getText();
|
|
167
|
+
if (p.initializer) pm["default"] = p.initializer.getText();
|
|
168
|
+
if (p.dotDotDotToken) pm["is_rest"] = true;
|
|
169
|
+
paramStructs.push(pm);
|
|
170
|
+
}
|
|
171
|
+
metadata["params"] = paramStructs;
|
|
172
|
+
}
|
|
156
173
|
if (node.type) metadata["returnType"] = node.type.getText();
|
|
157
174
|
|
|
158
175
|
// Async functions
|
|
@@ -160,21 +177,6 @@ export class TsEncoder {
|
|
|
160
177
|
metadata["is_async"] = true;
|
|
161
178
|
}
|
|
162
179
|
|
|
163
|
-
// Rest parameters
|
|
164
|
-
const lastParam = node.parameters[node.parameters.length - 1];
|
|
165
|
-
if (lastParam?.dotDotDotToken) {
|
|
166
|
-
metadata["rest_param"] = ts.isIdentifier(lastParam.name) ? lastParam.name.text : lastParam.name.getText();
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Default parameter values
|
|
170
|
-
const defaults: Record<string, unknown> = {};
|
|
171
|
-
for (const p of node.parameters) {
|
|
172
|
-
if (p.initializer && ts.isIdentifier(p.name)) {
|
|
173
|
-
defaults[p.name.text] = p.initializer.getText();
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
if (Object.keys(defaults).length > 0) metadata["param_defaults"] = defaults;
|
|
177
|
-
|
|
178
180
|
const body = node.body ? this.encodeBody(node.body) : undefined;
|
|
179
181
|
|
|
180
182
|
const fn: FunctionDef = { name };
|
|
@@ -306,6 +308,22 @@ export class TsEncoder {
|
|
|
306
308
|
if (ts.isBlock(node)) {
|
|
307
309
|
return [{ expression: this.encodeBlock(node) }];
|
|
308
310
|
}
|
|
311
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
312
|
+
// Inner function declaration: encode as a let binding with a lambda.
|
|
313
|
+
const fnDef = this.encodeFunction(node);
|
|
314
|
+
return [{
|
|
315
|
+
let: {
|
|
316
|
+
name: fnDef.name,
|
|
317
|
+
value: {
|
|
318
|
+
lambda: {
|
|
319
|
+
name: fnDef.name,
|
|
320
|
+
body: fnDef.body,
|
|
321
|
+
metadata: fnDef.metadata,
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
}];
|
|
326
|
+
}
|
|
309
327
|
this.warn(`Unhandled statement kind: ${ts.SyntaxKind[node.kind]}`);
|
|
310
328
|
return [{ expression: { literal: { stringValue: `/* unhandled: ${ts.SyntaxKind[node.kind]} */` } } }];
|
|
311
329
|
}
|
|
@@ -328,9 +346,16 @@ export class TsEncoder {
|
|
|
328
346
|
return { literal: { boolValue: false } };
|
|
329
347
|
}
|
|
330
348
|
if (node.kind === ts.SyntaxKind.NullKeyword || node.kind === ts.SyntaxKind.UndefinedKeyword) {
|
|
331
|
-
return
|
|
349
|
+
return this.nullLiteral();
|
|
332
350
|
}
|
|
333
351
|
if (ts.isIdentifier(node)) {
|
|
352
|
+
// `undefined` is parsed as an identifier in expression position, not as
|
|
353
|
+
// a keyword. Encode it as the canonical null literal so it round-trips
|
|
354
|
+
// to `null` rather than an unbound reference. (NaN/Infinity stay as
|
|
355
|
+
// references — they resolve to real globals.)
|
|
356
|
+
if (node.text === "undefined") {
|
|
357
|
+
return this.nullLiteral();
|
|
358
|
+
}
|
|
334
359
|
return { reference: { name: node.text } };
|
|
335
360
|
}
|
|
336
361
|
if (ts.isBinaryExpression(node)) {
|
|
@@ -350,7 +375,7 @@ export class TsEncoder {
|
|
|
350
375
|
return this.stdCall("optional_access", [
|
|
351
376
|
{ name: "object", value: this.encodeExpr(node.expression) },
|
|
352
377
|
{ name: "field", value: { literal: { stringValue: node.name.text } } },
|
|
353
|
-
]
|
|
378
|
+
]);
|
|
354
379
|
}
|
|
355
380
|
return { fieldAccess: { object: this.encodeExpr(node.expression), field: node.name.text } };
|
|
356
381
|
}
|
|
@@ -359,7 +384,7 @@ export class TsEncoder {
|
|
|
359
384
|
return this.stdCall("optional_access", [
|
|
360
385
|
{ name: "object", value: this.encodeExpr(node.expression) },
|
|
361
386
|
{ name: "field", value: this.encodeExpr(node.argumentExpression) },
|
|
362
|
-
]
|
|
387
|
+
]);
|
|
363
388
|
}
|
|
364
389
|
return this.stdCall("index", [
|
|
365
390
|
{ name: "target", value: this.encodeExpr(node.expression) },
|
|
@@ -390,7 +415,7 @@ export class TsEncoder {
|
|
|
390
415
|
value: this.stdCall("computed_property", [
|
|
391
416
|
{ name: "key", value: this.encodeExpr(prop.name.expression) },
|
|
392
417
|
{ name: "value", value: this.encodeExpr(prop.initializer) },
|
|
393
|
-
]
|
|
418
|
+
]),
|
|
394
419
|
});
|
|
395
420
|
} else {
|
|
396
421
|
const propName = ts.isIdentifier(prop.name)
|
|
@@ -405,7 +430,7 @@ export class TsEncoder {
|
|
|
405
430
|
name: "__spread",
|
|
406
431
|
value: this.stdCall("spread", [
|
|
407
432
|
{ name: "value", value: this.encodeExpr(prop.expression) },
|
|
408
|
-
]
|
|
433
|
+
]),
|
|
409
434
|
});
|
|
410
435
|
}
|
|
411
436
|
}
|
|
@@ -414,8 +439,8 @@ export class TsEncoder {
|
|
|
414
439
|
if (ts.isConditionalExpression(node)) {
|
|
415
440
|
return this.stdCall("if", [
|
|
416
441
|
{ name: "condition", value: this.encodeExpr(node.condition) },
|
|
417
|
-
{ name: "then", value:
|
|
418
|
-
{ name: "else", value:
|
|
442
|
+
{ name: "then", value: this.encodeExpr(node.whenTrue) },
|
|
443
|
+
{ name: "else", value: this.encodeExpr(node.whenFalse) },
|
|
419
444
|
]);
|
|
420
445
|
}
|
|
421
446
|
if (ts.isTemplateExpression(node)) {
|
|
@@ -445,10 +470,13 @@ export class TsEncoder {
|
|
|
445
470
|
if (ts.isSpreadElement(node)) {
|
|
446
471
|
return this.stdCall("spread", [
|
|
447
472
|
{ name: "value", value: this.encodeExpr(node.expression) },
|
|
448
|
-
]
|
|
473
|
+
]);
|
|
449
474
|
}
|
|
450
475
|
if (ts.isVoidExpression(node)) {
|
|
451
|
-
|
|
476
|
+
// `void <expr>` always evaluates to `undefined`; encode as null.
|
|
477
|
+
// (The operand's side effects are dropped — matching how the engine
|
|
478
|
+
// treats a discarded value; this mirrors the prior behaviour.)
|
|
479
|
+
return this.nullLiteral();
|
|
452
480
|
}
|
|
453
481
|
if (ts.isTaggedTemplateExpression(node)) {
|
|
454
482
|
return this.encodeTaggedTemplate(node);
|
|
@@ -476,7 +504,7 @@ export class TsEncoder {
|
|
|
476
504
|
]);
|
|
477
505
|
}
|
|
478
506
|
|
|
479
|
-
if (op === ts.SyntaxKind.PlusToken && this.
|
|
507
|
+
if (op === ts.SyntaxKind.PlusToken && this.isProvablyString(node.left, node.right)) {
|
|
480
508
|
return this.stdCall("concat", [
|
|
481
509
|
{ name: "left", value: this.encodeExpr(node.left) },
|
|
482
510
|
{ name: "right", value: this.encodeExpr(node.right) },
|
|
@@ -502,9 +530,35 @@ export class TsEncoder {
|
|
|
502
530
|
return { literal: { stringValue: `/* binary: ${ts.SyntaxKind[op]} */` } };
|
|
503
531
|
}
|
|
504
532
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
533
|
+
/// Decide how to encode a `+` whose operands aren't both provable strings.
|
|
534
|
+
///
|
|
535
|
+
/// `std.add` is runtime-polymorphic — the engine concatenates when either
|
|
536
|
+
/// operand is a string and adds numerically otherwise (see
|
|
537
|
+
/// `compiled_engine.ts:_stdAdd`). `std.concat`, by contrast, *always*
|
|
538
|
+
/// stringifies both sides, so emitting it for `1 + 2` would yield `"12"`.
|
|
539
|
+
///
|
|
540
|
+
/// We therefore only emit the dedicated `concat` when at least one operand
|
|
541
|
+
/// is *provably* a string (a string literal/template, or a nested `+` that
|
|
542
|
+
/// is itself provably a concat). For every other shape — including
|
|
543
|
+
/// `strVar + strVar`, where the static type is unknown — we fall through to
|
|
544
|
+
/// the polymorphic `std.add`, letting the engine coerce at runtime instead
|
|
545
|
+
/// of guessing from literal shape. This avoids both the false-numeric and
|
|
546
|
+
/// false-concat hazards.
|
|
547
|
+
private isProvablyString(left: ts.Expression, right: ts.Expression): boolean {
|
|
548
|
+
return this.isStringExpr(left) || this.isStringExpr(right);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private isStringExpr(node: ts.Expression): boolean {
|
|
552
|
+
if (ts.isParenthesizedExpression(node)) return this.isStringExpr(node.expression);
|
|
553
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) ||
|
|
554
|
+
ts.isTemplateExpression(node)) {
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
// A nested `a + b` that is itself a provable concat produces a string.
|
|
558
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
559
|
+
return this.isProvablyString(node.left, node.right);
|
|
560
|
+
}
|
|
561
|
+
return false;
|
|
508
562
|
}
|
|
509
563
|
|
|
510
564
|
private encodePrefixUnary(node: ts.PrefixUnaryExpression): Expression {
|
|
@@ -556,16 +610,65 @@ export class TsEncoder {
|
|
|
556
610
|
}));
|
|
557
611
|
|
|
558
612
|
if (ts.isPropertyAccessExpression(node.expression)) {
|
|
559
|
-
const obj = this.encodeExpr(node.expression.expression);
|
|
560
613
|
const method = node.expression.name.text;
|
|
561
614
|
|
|
615
|
+
// console.log(x) / console.log(x, y, ...) → std.print per argument.
|
|
616
|
+
// console.error(x) → std.print_error(x).
|
|
617
|
+
// This ensures that TS programs using the idiomatic console API are
|
|
618
|
+
// faithfully represented in Ball IR using the universal std module, so
|
|
619
|
+
// the Ball engine can execute them without a "console" module.
|
|
620
|
+
if (ts.isIdentifier(node.expression.expression) &&
|
|
621
|
+
node.expression.expression.text === "console") {
|
|
622
|
+
if (method === "log" || method === "info" || method === "warn") {
|
|
623
|
+
if (args.length === 0) {
|
|
624
|
+
return this.stdCall("print", [
|
|
625
|
+
{ name: "message", value: { literal: { stringValue: "" } } },
|
|
626
|
+
]);
|
|
627
|
+
}
|
|
628
|
+
if (args.length === 1) {
|
|
629
|
+
return this.stdCall("print", [
|
|
630
|
+
{ name: "message", value: args[0].value },
|
|
631
|
+
]);
|
|
632
|
+
}
|
|
633
|
+
// Multiple arguments: emit one std.print per arg inside a block.
|
|
634
|
+
const stmts: Statement[] = args.map(a => ({
|
|
635
|
+
expression: this.stdCall("print", [
|
|
636
|
+
{ name: "message", value: a.value },
|
|
637
|
+
]),
|
|
638
|
+
}));
|
|
639
|
+
return { block: { statements: stmts } };
|
|
640
|
+
}
|
|
641
|
+
if (method === "error") {
|
|
642
|
+
if (args.length >= 1) {
|
|
643
|
+
return this.stdCall("print_error", [
|
|
644
|
+
{ name: "message", value: args[0].value },
|
|
645
|
+
]);
|
|
646
|
+
}
|
|
647
|
+
return this.stdCall("print_error", [
|
|
648
|
+
{ name: "message", value: { literal: { stringValue: "" } } },
|
|
649
|
+
]);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const obj = this.encodeExpr(node.expression.expression);
|
|
654
|
+
|
|
655
|
+
// Map common JS/TS method calls to their Ball std equivalents.
|
|
656
|
+
// The engine's std module uses snake_case names with type prefixes.
|
|
657
|
+
const stdMethod = this.mapMethodToStd(method, args);
|
|
658
|
+
if (stdMethod) {
|
|
659
|
+
return this.stdCall(stdMethod.fn, [
|
|
660
|
+
{ name: stdMethod.selfName ?? "value", value: obj },
|
|
661
|
+
...stdMethod.extraFields(args),
|
|
662
|
+
], stdMethod.module ?? "std");
|
|
663
|
+
}
|
|
664
|
+
|
|
562
665
|
// Optional chaining call: obj?.method()
|
|
563
666
|
if (node.expression.questionDotToken || node.questionDotToken) {
|
|
564
667
|
return this.stdCall("optional_call", [
|
|
565
668
|
{ name: "object", value: obj },
|
|
566
669
|
{ name: "method", value: { literal: { stringValue: method } } },
|
|
567
670
|
...args,
|
|
568
|
-
]
|
|
671
|
+
]);
|
|
569
672
|
}
|
|
570
673
|
|
|
571
674
|
return {
|
|
@@ -619,26 +722,24 @@ export class TsEncoder {
|
|
|
619
722
|
) : undefined;
|
|
620
723
|
|
|
621
724
|
const metadata: Struct = {};
|
|
622
|
-
if (params.length > 0)
|
|
725
|
+
if (params.length > 0) {
|
|
726
|
+
// Emit params as structs with a `name` field, matching the Dart encoder
|
|
727
|
+
// format and the engine's _extractParams expectations.
|
|
728
|
+
const paramStructs: Record<string, unknown>[] = [];
|
|
729
|
+
for (const p of node.parameters) {
|
|
730
|
+
const pName = ts.isIdentifier(p.name) ? p.name.text : p.name.getText();
|
|
731
|
+
const pm: Record<string, unknown> = { name: pName };
|
|
732
|
+
if (p.type) pm["type"] = p.type.getText();
|
|
733
|
+
if (p.initializer) pm["default"] = p.initializer.getText();
|
|
734
|
+
if (p.dotDotDotToken) pm["is_rest"] = true;
|
|
735
|
+
paramStructs.push(pm);
|
|
736
|
+
}
|
|
737
|
+
metadata["params"] = paramStructs;
|
|
738
|
+
}
|
|
623
739
|
if (node.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword)) {
|
|
624
740
|
metadata["is_async"] = true;
|
|
625
741
|
}
|
|
626
742
|
|
|
627
|
-
// Rest parameters
|
|
628
|
-
const lastParam = node.parameters[node.parameters.length - 1];
|
|
629
|
-
if (lastParam?.dotDotDotToken) {
|
|
630
|
-
metadata["rest_param"] = ts.isIdentifier(lastParam.name) ? lastParam.name.text : lastParam.name.getText();
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// Default parameter values
|
|
634
|
-
const defaults: Record<string, unknown> = {};
|
|
635
|
-
for (const p of node.parameters) {
|
|
636
|
-
if (p.initializer && ts.isIdentifier(p.name)) {
|
|
637
|
-
defaults[p.name.text] = p.initializer.getText();
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
if (Object.keys(defaults).length > 0) metadata["param_defaults"] = defaults;
|
|
641
|
-
|
|
642
743
|
// Destructured parameters
|
|
643
744
|
const destructured: Record<string, string> = {};
|
|
644
745
|
for (const p of node.parameters) {
|
|
@@ -659,47 +760,61 @@ export class TsEncoder {
|
|
|
659
760
|
}
|
|
660
761
|
|
|
661
762
|
private encodeIf(node: ts.IfStatement): Expression {
|
|
763
|
+
// Control flow fields (then, else) are direct expressions — NOT lambdas.
|
|
764
|
+
// The engine evaluates them lazily (Ball invariant #4). Wrapping in a
|
|
765
|
+
// lambda would eat flow signals like std.return/break/continue.
|
|
662
766
|
const fields: FieldValuePair[] = [
|
|
663
767
|
{ name: "condition", value: this.encodeExpr(node.expression) },
|
|
664
|
-
{ name: "then", value:
|
|
768
|
+
{ name: "then", value: this.encodeBody(
|
|
665
769
|
ts.isBlock(node.thenStatement) ? node.thenStatement : ts.factory.createBlock([node.thenStatement])
|
|
666
|
-
) }
|
|
770
|
+
) },
|
|
667
771
|
];
|
|
668
772
|
if (node.elseStatement) {
|
|
669
773
|
if (ts.isIfStatement(node.elseStatement)) {
|
|
670
|
-
fields.push({ name: "else", value:
|
|
774
|
+
fields.push({ name: "else", value: this.encodeIf(node.elseStatement) });
|
|
671
775
|
} else {
|
|
672
|
-
fields.push({ name: "else", value:
|
|
776
|
+
fields.push({ name: "else", value: this.encodeBody(
|
|
673
777
|
ts.isBlock(node.elseStatement) ? node.elseStatement : ts.factory.createBlock([node.elseStatement])
|
|
674
|
-
) }
|
|
778
|
+
) });
|
|
675
779
|
}
|
|
676
780
|
}
|
|
677
781
|
return this.stdCall("if", fields);
|
|
678
782
|
}
|
|
679
783
|
|
|
680
784
|
private encodeFor(node: ts.ForStatement): Expression {
|
|
785
|
+
// Control flow fields are direct expressions — the engine evaluates them
|
|
786
|
+
// lazily. No lambda wrappers (see encodeIf comment).
|
|
787
|
+
//
|
|
788
|
+
// The engine's _evalLazyFor reads `init` (not `variable`/`start`).
|
|
789
|
+
// For a variable declaration `let i = 0`, emit a block with a let binding.
|
|
790
|
+
// For a bare expression initializer, emit the expression directly.
|
|
681
791
|
const fields: FieldValuePair[] = [];
|
|
682
792
|
if (node.initializer) {
|
|
683
793
|
if (ts.isVariableDeclarationList(node.initializer)) {
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
|
|
794
|
+
const stmts: Statement[] = [];
|
|
795
|
+
for (const d of node.initializer.declarations) {
|
|
796
|
+
const varName = ts.isIdentifier(d.name) ? d.name.text : d.name.getText();
|
|
797
|
+
stmts.push({
|
|
798
|
+
let: {
|
|
799
|
+
name: varName,
|
|
800
|
+
value: d.initializer ? this.encodeExpr(d.initializer) : undefined,
|
|
801
|
+
},
|
|
802
|
+
});
|
|
689
803
|
}
|
|
804
|
+
fields.push({ name: "init", value: { block: { statements: stmts } } });
|
|
690
805
|
} else {
|
|
691
|
-
fields.push({ name: "init", value:
|
|
806
|
+
fields.push({ name: "init", value: this.encodeExpr(node.initializer) });
|
|
692
807
|
}
|
|
693
808
|
}
|
|
694
809
|
if (node.condition) {
|
|
695
|
-
fields.push({ name: "condition", value:
|
|
810
|
+
fields.push({ name: "condition", value: this.encodeExpr(node.condition) });
|
|
696
811
|
}
|
|
697
812
|
if (node.incrementor) {
|
|
698
|
-
fields.push({ name: "update", value:
|
|
813
|
+
fields.push({ name: "update", value: this.encodeExpr(node.incrementor) });
|
|
699
814
|
}
|
|
700
|
-
fields.push({ name: "body", value:
|
|
815
|
+
fields.push({ name: "body", value: this.encodeBody(
|
|
701
816
|
ts.isBlock(node.statement) ? node.statement : ts.factory.createBlock([node.statement])
|
|
702
|
-
) }
|
|
817
|
+
) });
|
|
703
818
|
return this.stdCall("for", fields);
|
|
704
819
|
}
|
|
705
820
|
|
|
@@ -713,48 +828,53 @@ export class TsEncoder {
|
|
|
713
828
|
return this.stdCall(fnName, [
|
|
714
829
|
{ name: "variable", value: { literal: { stringValue: varName } } },
|
|
715
830
|
{ name: "iterable", value: this.encodeExpr(node.expression) },
|
|
716
|
-
{ name: "body", value:
|
|
831
|
+
{ name: "body", value: this.encodeBody(
|
|
717
832
|
ts.isBlock(node.statement) ? node.statement : ts.factory.createBlock([node.statement])
|
|
718
|
-
) }
|
|
833
|
+
) },
|
|
719
834
|
]);
|
|
720
835
|
}
|
|
721
836
|
|
|
722
837
|
private encodeWhile(node: ts.WhileStatement): Expression {
|
|
723
838
|
return this.stdCall("while", [
|
|
724
|
-
{ name: "condition", value:
|
|
725
|
-
{ name: "body", value:
|
|
839
|
+
{ name: "condition", value: this.encodeExpr(node.expression) },
|
|
840
|
+
{ name: "body", value: this.encodeBody(
|
|
726
841
|
ts.isBlock(node.statement) ? node.statement : ts.factory.createBlock([node.statement])
|
|
727
|
-
) }
|
|
842
|
+
) },
|
|
728
843
|
]);
|
|
729
844
|
}
|
|
730
845
|
|
|
731
846
|
private encodeDoWhile(node: ts.DoStatement): Expression {
|
|
732
847
|
return this.stdCall("do_while", [
|
|
733
|
-
{ name: "condition", value:
|
|
734
|
-
{ name: "body", value:
|
|
848
|
+
{ name: "condition", value: this.encodeExpr(node.expression) },
|
|
849
|
+
{ name: "body", value: this.encodeBody(
|
|
735
850
|
ts.isBlock(node.statement) ? node.statement : ts.factory.createBlock([node.statement])
|
|
736
|
-
) }
|
|
851
|
+
) },
|
|
737
852
|
]);
|
|
738
853
|
}
|
|
739
854
|
|
|
740
855
|
private encodeTry(node: ts.TryStatement): Expression {
|
|
856
|
+
// try body is a direct block expression (not a lambda).
|
|
741
857
|
const fields: FieldValuePair[] = [
|
|
742
|
-
{ name: "body", value:
|
|
858
|
+
{ name: "body", value: this.encodeBlock(node.tryBlock) },
|
|
743
859
|
];
|
|
744
860
|
if (node.catchClause) {
|
|
745
861
|
const cc = node.catchClause;
|
|
862
|
+
// The Dart encoder emits catches as a listValue of catch entries, each
|
|
863
|
+
// with optional `type`, `variable`, and `body` fields. TS catch clauses
|
|
864
|
+
// are untyped, so we omit the `type` field.
|
|
746
865
|
const catchFields: FieldValuePair[] = [];
|
|
747
866
|
if (cc.variableDeclaration && ts.isIdentifier(cc.variableDeclaration.name)) {
|
|
748
867
|
catchFields.push({ name: "variable", value: { literal: { stringValue: cc.variableDeclaration.name.text } } });
|
|
749
868
|
}
|
|
750
|
-
catchFields.push({ name: "body", value:
|
|
869
|
+
catchFields.push({ name: "body", value: this.encodeBlock(cc.block) });
|
|
870
|
+
const catchEntry: Expression = { messageCreation: { typeName: "", fields: catchFields } };
|
|
751
871
|
fields.push({
|
|
752
|
-
name: "
|
|
753
|
-
value: {
|
|
872
|
+
name: "catches",
|
|
873
|
+
value: { literal: { listValue: { elements: [catchEntry] } } },
|
|
754
874
|
});
|
|
755
875
|
}
|
|
756
876
|
if (node.finallyBlock) {
|
|
757
|
-
fields.push({ name: "finally", value:
|
|
877
|
+
fields.push({ name: "finally", value: this.encodeBlock(node.finallyBlock) });
|
|
758
878
|
}
|
|
759
879
|
return this.stdCall("try", fields);
|
|
760
880
|
}
|
|
@@ -766,17 +886,17 @@ export class TsEncoder {
|
|
|
766
886
|
if (ts.isCaseClause(clause)) {
|
|
767
887
|
caseFields.push({ name: "value", value: this.encodeExpr(clause.expression) });
|
|
768
888
|
} else {
|
|
769
|
-
caseFields.push({ name: "
|
|
889
|
+
caseFields.push({ name: "is_default", value: { literal: { boolValue: true } } });
|
|
770
890
|
}
|
|
771
891
|
const stmts: Statement[] = [];
|
|
772
892
|
for (const s of clause.statements) {
|
|
773
893
|
stmts.push(...this.encodeStatement(s));
|
|
774
894
|
}
|
|
775
|
-
caseFields.push({ name: "body", value: {
|
|
895
|
+
caseFields.push({ name: "body", value: { block: { statements: stmts } } });
|
|
776
896
|
cases.push({ messageCreation: { typeName: "", fields: caseFields } });
|
|
777
897
|
}
|
|
778
898
|
return this.stdCall("switch", [
|
|
779
|
-
{ name: "
|
|
899
|
+
{ name: "subject", value: this.encodeExpr(node.expression) },
|
|
780
900
|
{ name: "cases", value: { literal: { listValue: { elements: cases } } } },
|
|
781
901
|
]);
|
|
782
902
|
}
|
|
@@ -929,7 +1049,94 @@ export class TsEncoder {
|
|
|
929
1049
|
{ name: "tag", value: tag },
|
|
930
1050
|
{ name: "strings", value: { literal: { listValue: { elements: parts } } } },
|
|
931
1051
|
{ name: "expressions", value: { literal: { listValue: { elements: exprs } } } },
|
|
932
|
-
]
|
|
1052
|
+
]);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* Map a JS/TS method name to its Ball std function equivalent.
|
|
1057
|
+
* Returns null if no mapping exists (falls through to generic method call).
|
|
1058
|
+
*/
|
|
1059
|
+
private mapMethodToStd(
|
|
1060
|
+
method: string,
|
|
1061
|
+
_args: { name: string; value: Expression }[],
|
|
1062
|
+
): { fn: string; module?: string; selfName?: string; extraFields: (a: typeof _args) => FieldValuePair[] } | null {
|
|
1063
|
+
// String methods
|
|
1064
|
+
const STR_METHODS: Record<string, string> = {
|
|
1065
|
+
toUpperCase: "string_to_upper_case",
|
|
1066
|
+
toLowerCase: "string_to_lower_case",
|
|
1067
|
+
trim: "string_trim",
|
|
1068
|
+
trimStart: "string_trim_left",
|
|
1069
|
+
trimEnd: "string_trim_right",
|
|
1070
|
+
includes: "string_contains",
|
|
1071
|
+
indexOf: "string_index_of",
|
|
1072
|
+
startsWith: "string_starts_with",
|
|
1073
|
+
endsWith: "string_ends_with",
|
|
1074
|
+
split: "string_split",
|
|
1075
|
+
substring: "string_substring",
|
|
1076
|
+
slice: "string_substring",
|
|
1077
|
+
replace: "string_replace_first",
|
|
1078
|
+
replaceAll: "string_replace_all",
|
|
1079
|
+
padStart: "string_pad_left",
|
|
1080
|
+
padEnd: "string_pad_right",
|
|
1081
|
+
repeat: "string_repeat",
|
|
1082
|
+
charAt: "string_char_at",
|
|
1083
|
+
charCodeAt: "string_code_unit_at",
|
|
1084
|
+
};
|
|
1085
|
+
if (method in STR_METHODS) {
|
|
1086
|
+
return {
|
|
1087
|
+
fn: STR_METHODS[method],
|
|
1088
|
+
selfName: "value",
|
|
1089
|
+
extraFields: (a) => a.map((x, i) => ({
|
|
1090
|
+
name: i === 0 ? "other" : `arg${i}`,
|
|
1091
|
+
value: x.value,
|
|
1092
|
+
})),
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// Array methods
|
|
1097
|
+
const ARR_METHODS: Record<string, { fn: string; mod?: string }> = {
|
|
1098
|
+
push: { fn: "list_add" },
|
|
1099
|
+
pop: { fn: "list_remove_last" },
|
|
1100
|
+
indexOf: { fn: "list_index_of", mod: "std_collections" },
|
|
1101
|
+
includes: { fn: "list_contains", mod: "std_collections" },
|
|
1102
|
+
join: { fn: "list_join", mod: "std_collections" },
|
|
1103
|
+
reverse: { fn: "list_reversed", mod: "std_collections" },
|
|
1104
|
+
slice: { fn: "list_sublist", mod: "std_collections" },
|
|
1105
|
+
splice: { fn: "list_remove_at" },
|
|
1106
|
+
sort: { fn: "list_sort", mod: "std_collections" },
|
|
1107
|
+
map: { fn: "list_map", mod: "std_collections" },
|
|
1108
|
+
filter: { fn: "list_where", mod: "std_collections" },
|
|
1109
|
+
forEach: { fn: "list_for_each", mod: "std_collections" },
|
|
1110
|
+
reduce: { fn: "list_fold", mod: "std_collections" },
|
|
1111
|
+
find: { fn: "list_first_where", mod: "std_collections" },
|
|
1112
|
+
flat: { fn: "list_flatten", mod: "std_collections" },
|
|
1113
|
+
concat: { fn: "list_concat", mod: "std_collections" },
|
|
1114
|
+
every: { fn: "list_every", mod: "std_collections" },
|
|
1115
|
+
some: { fn: "list_any", mod: "std_collections" },
|
|
1116
|
+
};
|
|
1117
|
+
if (method in ARR_METHODS) {
|
|
1118
|
+
const m = ARR_METHODS[method];
|
|
1119
|
+
return {
|
|
1120
|
+
fn: m.fn,
|
|
1121
|
+
module: m.mod,
|
|
1122
|
+
selfName: "list",
|
|
1123
|
+
extraFields: (a) => a.map((x, i) => ({
|
|
1124
|
+
name: i === 0 ? "value" : `arg${i}`,
|
|
1125
|
+
value: x.value,
|
|
1126
|
+
})),
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// toString
|
|
1131
|
+
if (method === "toString") {
|
|
1132
|
+
return {
|
|
1133
|
+
fn: "to_string",
|
|
1134
|
+
selfName: "value",
|
|
1135
|
+
extraFields: () => [],
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
return null;
|
|
933
1140
|
}
|
|
934
1141
|
|
|
935
1142
|
private stdCall(fn: string, fields: FieldValuePair[], module = "std"): Expression {
|
|
@@ -945,18 +1152,46 @@ export class TsEncoder {
|
|
|
945
1152
|
};
|
|
946
1153
|
}
|
|
947
1154
|
|
|
948
|
-
private
|
|
949
|
-
const
|
|
1155
|
+
private buildBaseModules(): Module[] {
|
|
1156
|
+
const byModule = new Map<string, string[]>();
|
|
950
1157
|
for (const ref of this.stdFunctions) {
|
|
951
|
-
const [, fn] = ref.split(":");
|
|
952
|
-
|
|
1158
|
+
const [mod, fn] = ref.split(":");
|
|
1159
|
+
if (!byModule.has(mod)) byModule.set(mod, []);
|
|
1160
|
+
byModule.get(mod)!.push(fn);
|
|
1161
|
+
}
|
|
1162
|
+
const modules: Module[] = [];
|
|
1163
|
+
for (const [mod, fns] of byModule) {
|
|
1164
|
+
fns.sort();
|
|
1165
|
+
modules.push({
|
|
1166
|
+
name: mod,
|
|
1167
|
+
functions: fns.map(fn => ({ name: fn, isBase: true })),
|
|
1168
|
+
});
|
|
953
1169
|
}
|
|
954
|
-
|
|
955
|
-
return
|
|
1170
|
+
modules.sort((a, b) => a.name === "std" ? -1 : b.name === "std" ? 1 : a.name.localeCompare(b.name));
|
|
1171
|
+
return modules;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/// The canonical encoding of `null`/`undefined`/`void`.
|
|
1175
|
+
///
|
|
1176
|
+
/// The Dart encoder represents a null literal as an *empty* `Literal`
|
|
1177
|
+
/// message with no `value` oneof field set (see
|
|
1178
|
+
/// `dart/encoder/lib/encoder.dart`: `Expression()..literal = Literal()`).
|
|
1179
|
+
/// We match that exactly: `{ literal: {} }`. Both engines treat an empty
|
|
1180
|
+
/// literal as `null` (the TS compiler's `compileLiteral` falls through to
|
|
1181
|
+
/// `return "null"` when no value field is present), so this round-trips
|
|
1182
|
+
/// correctly and is no longer conflated with the empty string `""`.
|
|
1183
|
+
private nullLiteral(): Expression {
|
|
1184
|
+
return { literal: {} };
|
|
956
1185
|
}
|
|
957
1186
|
|
|
958
1187
|
private warn(msg: string): void {
|
|
959
1188
|
this.warnings.push(msg);
|
|
1189
|
+
// In strict mode an unhandled node is a hard error: the encoder cannot
|
|
1190
|
+
// faithfully represent the construct and would otherwise emit a
|
|
1191
|
+
// `/* unhandled */` placeholder literal that silently changes semantics.
|
|
1192
|
+
if (this.strict) {
|
|
1193
|
+
throw new EncodeError(msg, this.getWarnings());
|
|
1194
|
+
}
|
|
960
1195
|
}
|
|
961
1196
|
|
|
962
1197
|
getWarnings(): string[] {
|
|
@@ -964,6 +1199,36 @@ export class TsEncoder {
|
|
|
964
1199
|
}
|
|
965
1200
|
}
|
|
966
1201
|
|
|
1202
|
+
/// Thrown by `encode(..., { strict: true })` when the encoder hits a TS
|
|
1203
|
+
/// construct it cannot represent. Carries the full accumulated warning list.
|
|
1204
|
+
export class EncodeError extends Error {
|
|
1205
|
+
readonly warnings: string[];
|
|
1206
|
+
constructor(message: string, warnings: string[]) {
|
|
1207
|
+
super(message);
|
|
1208
|
+
this.name = "EncodeError";
|
|
1209
|
+
this.warnings = warnings;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
export interface EncodeResult {
|
|
1214
|
+
program: Program;
|
|
1215
|
+
warnings: string[];
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/// Encode `source` to a Ball `Program`.
|
|
1219
|
+
///
|
|
1220
|
+
/// The simple overload returns just the `Program` for backwards
|
|
1221
|
+
/// compatibility. Pass `{ strict: true }` to throw an `EncodeError` on any
|
|
1222
|
+
/// unhandled construct. To inspect non-fatal warnings without strict mode,
|
|
1223
|
+
/// use `encodeWithWarnings`.
|
|
967
1224
|
export function encode(source: string, options: EncodeOptions = {}): Program {
|
|
968
1225
|
return new TsEncoder().encode(source, options);
|
|
969
1226
|
}
|
|
1227
|
+
|
|
1228
|
+
/// Like `encode`, but also surfaces the accumulated warnings (e.g. unhandled
|
|
1229
|
+
/// statement/expression kinds). Honors `options.strict` the same way.
|
|
1230
|
+
export function encodeWithWarnings(source: string, options: EncodeOptions = {}): EncodeResult {
|
|
1231
|
+
const encoder = new TsEncoder();
|
|
1232
|
+
const program = encoder.encode(source, options);
|
|
1233
|
+
return { program, warnings: encoder.getWarnings() };
|
|
1234
|
+
}
|