@ball-lang/encoder 0.1.0 → 1.3.4
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/dist/encoder.js
CHANGED
|
@@ -8,16 +8,16 @@ const BINARY_OPS = {
|
|
|
8
8
|
[ts.SyntaxKind.AmpersandToken]: { module: "std", function: "bitwise_and" },
|
|
9
9
|
[ts.SyntaxKind.BarToken]: { module: "std", function: "bitwise_or" },
|
|
10
10
|
[ts.SyntaxKind.CaretToken]: { module: "std", function: "bitwise_xor" },
|
|
11
|
-
[ts.SyntaxKind.LessThanLessThanToken]: { module: "std", function: "
|
|
12
|
-
[ts.SyntaxKind.GreaterThanGreaterThanToken]: { module: "std", function: "
|
|
11
|
+
[ts.SyntaxKind.LessThanLessThanToken]: { module: "std", function: "left_shift" },
|
|
12
|
+
[ts.SyntaxKind.GreaterThanGreaterThanToken]: { module: "std", function: "right_shift" },
|
|
13
13
|
[ts.SyntaxKind.EqualsEqualsEqualsToken]: { module: "std", function: "equals" },
|
|
14
14
|
[ts.SyntaxKind.ExclamationEqualsEqualsToken]: { module: "std", function: "not_equals" },
|
|
15
15
|
[ts.SyntaxKind.EqualsEqualsToken]: { module: "std", function: "equals" },
|
|
16
16
|
[ts.SyntaxKind.ExclamationEqualsToken]: { module: "std", function: "not_equals" },
|
|
17
17
|
[ts.SyntaxKind.LessThanToken]: { module: "std", function: "less_than" },
|
|
18
18
|
[ts.SyntaxKind.GreaterThanToken]: { module: "std", function: "greater_than" },
|
|
19
|
-
[ts.SyntaxKind.LessThanEqualsToken]: { module: "std", function: "
|
|
20
|
-
[ts.SyntaxKind.GreaterThanEqualsToken]: { module: "std", function: "
|
|
19
|
+
[ts.SyntaxKind.LessThanEqualsToken]: { module: "std", function: "lte" },
|
|
20
|
+
[ts.SyntaxKind.GreaterThanEqualsToken]: { module: "std", function: "gte" },
|
|
21
21
|
[ts.SyntaxKind.AmpersandAmpersandToken]: { module: "std", function: "and" },
|
|
22
22
|
[ts.SyntaxKind.BarBarToken]: { module: "std", function: "or" },
|
|
23
23
|
[ts.SyntaxKind.QuestionQuestionToken]: { module: "std", function: "null_coalesce" },
|
|
@@ -39,9 +39,11 @@ const COMPOUND_OPS = {
|
|
|
39
39
|
export class TsEncoder {
|
|
40
40
|
stdFunctions = new Set();
|
|
41
41
|
warnings = [];
|
|
42
|
+
strict = false;
|
|
42
43
|
encode(source, options = {}) {
|
|
43
44
|
const modName = options.moduleName ?? "main";
|
|
44
45
|
const entryFn = options.entryFunction ?? "main";
|
|
46
|
+
this.strict = options.strict ?? false;
|
|
45
47
|
const sourceFile = ts.createSourceFile("input.ts", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
46
48
|
const functions = [];
|
|
47
49
|
const typeDefs = [];
|
|
@@ -68,7 +70,7 @@ export class TsEncoder {
|
|
|
68
70
|
if (ts.isIdentifier(decl.name)) {
|
|
69
71
|
functions.push({
|
|
70
72
|
name: decl.name.text,
|
|
71
|
-
body: decl.initializer ? this.encodeExpr(decl.initializer) :
|
|
73
|
+
body: decl.initializer ? this.encodeExpr(decl.initializer) : this.nullLiteral(),
|
|
72
74
|
metadata: { kind: "top_level_variable" },
|
|
73
75
|
});
|
|
74
76
|
}
|
|
@@ -110,8 +112,8 @@ export class TsEncoder {
|
|
|
110
112
|
});
|
|
111
113
|
}
|
|
112
114
|
}
|
|
113
|
-
const
|
|
114
|
-
const modules = [
|
|
115
|
+
const baseModules = this.buildBaseModules();
|
|
116
|
+
const modules = [...baseModules];
|
|
115
117
|
const userModule = {
|
|
116
118
|
name: modName,
|
|
117
119
|
functions,
|
|
@@ -132,28 +134,31 @@ export class TsEncoder {
|
|
|
132
134
|
const name = node.name ? (ts.isIdentifier(node.name) ? node.name.text : node.name.getText()) : "";
|
|
133
135
|
const params = node.parameters.map(p => ts.isIdentifier(p.name) ? p.name.text : p.name.getText());
|
|
134
136
|
const metadata = {};
|
|
135
|
-
if (params.length > 0)
|
|
136
|
-
|
|
137
|
+
if (params.length > 0) {
|
|
138
|
+
// Emit params as an array of structs with a `name` field (and optional
|
|
139
|
+
// `type`, `default`, `is_rest` etc.), matching the Dart encoder format.
|
|
140
|
+
// The engine's _extractParams filters for structValue entries and reads
|
|
141
|
+
// each entry's `.structValue.fields.name.stringValue`.
|
|
142
|
+
const paramStructs = [];
|
|
143
|
+
for (const p of node.parameters) {
|
|
144
|
+
const pName = ts.isIdentifier(p.name) ? p.name.text : p.name.getText();
|
|
145
|
+
const pm = { name: pName };
|
|
146
|
+
if (p.type)
|
|
147
|
+
pm["type"] = p.type.getText();
|
|
148
|
+
if (p.initializer)
|
|
149
|
+
pm["default"] = p.initializer.getText();
|
|
150
|
+
if (p.dotDotDotToken)
|
|
151
|
+
pm["is_rest"] = true;
|
|
152
|
+
paramStructs.push(pm);
|
|
153
|
+
}
|
|
154
|
+
metadata["params"] = paramStructs;
|
|
155
|
+
}
|
|
137
156
|
if (node.type)
|
|
138
157
|
metadata["returnType"] = node.type.getText();
|
|
139
158
|
// Async functions
|
|
140
159
|
if (node.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword)) {
|
|
141
160
|
metadata["is_async"] = true;
|
|
142
161
|
}
|
|
143
|
-
// Rest parameters
|
|
144
|
-
const lastParam = node.parameters[node.parameters.length - 1];
|
|
145
|
-
if (lastParam?.dotDotDotToken) {
|
|
146
|
-
metadata["rest_param"] = ts.isIdentifier(lastParam.name) ? lastParam.name.text : lastParam.name.getText();
|
|
147
|
-
}
|
|
148
|
-
// Default parameter values
|
|
149
|
-
const defaults = {};
|
|
150
|
-
for (const p of node.parameters) {
|
|
151
|
-
if (p.initializer && ts.isIdentifier(p.name)) {
|
|
152
|
-
defaults[p.name.text] = p.initializer.getText();
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
if (Object.keys(defaults).length > 0)
|
|
156
|
-
metadata["param_defaults"] = defaults;
|
|
157
162
|
const body = node.body ? this.encodeBody(node.body) : undefined;
|
|
158
163
|
const fn = { name };
|
|
159
164
|
if (body)
|
|
@@ -286,6 +291,22 @@ export class TsEncoder {
|
|
|
286
291
|
if (ts.isBlock(node)) {
|
|
287
292
|
return [{ expression: this.encodeBlock(node) }];
|
|
288
293
|
}
|
|
294
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
295
|
+
// Inner function declaration: encode as a let binding with a lambda.
|
|
296
|
+
const fnDef = this.encodeFunction(node);
|
|
297
|
+
return [{
|
|
298
|
+
let: {
|
|
299
|
+
name: fnDef.name,
|
|
300
|
+
value: {
|
|
301
|
+
lambda: {
|
|
302
|
+
name: fnDef.name,
|
|
303
|
+
body: fnDef.body,
|
|
304
|
+
metadata: fnDef.metadata,
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
}];
|
|
309
|
+
}
|
|
289
310
|
this.warn(`Unhandled statement kind: ${ts.SyntaxKind[node.kind]}`);
|
|
290
311
|
return [{ expression: { literal: { stringValue: `/* unhandled: ${ts.SyntaxKind[node.kind]} */` } } }];
|
|
291
312
|
}
|
|
@@ -307,9 +328,16 @@ export class TsEncoder {
|
|
|
307
328
|
return { literal: { boolValue: false } };
|
|
308
329
|
}
|
|
309
330
|
if (node.kind === ts.SyntaxKind.NullKeyword || node.kind === ts.SyntaxKind.UndefinedKeyword) {
|
|
310
|
-
return
|
|
331
|
+
return this.nullLiteral();
|
|
311
332
|
}
|
|
312
333
|
if (ts.isIdentifier(node)) {
|
|
334
|
+
// `undefined` is parsed as an identifier in expression position, not as
|
|
335
|
+
// a keyword. Encode it as the canonical null literal so it round-trips
|
|
336
|
+
// to `null` rather than an unbound reference. (NaN/Infinity stay as
|
|
337
|
+
// references — they resolve to real globals.)
|
|
338
|
+
if (node.text === "undefined") {
|
|
339
|
+
return this.nullLiteral();
|
|
340
|
+
}
|
|
313
341
|
return { reference: { name: node.text } };
|
|
314
342
|
}
|
|
315
343
|
if (ts.isBinaryExpression(node)) {
|
|
@@ -329,7 +357,7 @@ export class TsEncoder {
|
|
|
329
357
|
return this.stdCall("optional_access", [
|
|
330
358
|
{ name: "object", value: this.encodeExpr(node.expression) },
|
|
331
359
|
{ name: "field", value: { literal: { stringValue: node.name.text } } },
|
|
332
|
-
]
|
|
360
|
+
]);
|
|
333
361
|
}
|
|
334
362
|
return { fieldAccess: { object: this.encodeExpr(node.expression), field: node.name.text } };
|
|
335
363
|
}
|
|
@@ -338,7 +366,7 @@ export class TsEncoder {
|
|
|
338
366
|
return this.stdCall("optional_access", [
|
|
339
367
|
{ name: "object", value: this.encodeExpr(node.expression) },
|
|
340
368
|
{ name: "field", value: this.encodeExpr(node.argumentExpression) },
|
|
341
|
-
]
|
|
369
|
+
]);
|
|
342
370
|
}
|
|
343
371
|
return this.stdCall("index", [
|
|
344
372
|
{ name: "target", value: this.encodeExpr(node.expression) },
|
|
@@ -369,7 +397,7 @@ export class TsEncoder {
|
|
|
369
397
|
value: this.stdCall("computed_property", [
|
|
370
398
|
{ name: "key", value: this.encodeExpr(prop.name.expression) },
|
|
371
399
|
{ name: "value", value: this.encodeExpr(prop.initializer) },
|
|
372
|
-
]
|
|
400
|
+
]),
|
|
373
401
|
});
|
|
374
402
|
}
|
|
375
403
|
else {
|
|
@@ -387,7 +415,7 @@ export class TsEncoder {
|
|
|
387
415
|
name: "__spread",
|
|
388
416
|
value: this.stdCall("spread", [
|
|
389
417
|
{ name: "value", value: this.encodeExpr(prop.expression) },
|
|
390
|
-
]
|
|
418
|
+
]),
|
|
391
419
|
});
|
|
392
420
|
}
|
|
393
421
|
}
|
|
@@ -396,8 +424,8 @@ export class TsEncoder {
|
|
|
396
424
|
if (ts.isConditionalExpression(node)) {
|
|
397
425
|
return this.stdCall("if", [
|
|
398
426
|
{ name: "condition", value: this.encodeExpr(node.condition) },
|
|
399
|
-
{ name: "then", value:
|
|
400
|
-
{ name: "else", value:
|
|
427
|
+
{ name: "then", value: this.encodeExpr(node.whenTrue) },
|
|
428
|
+
{ name: "else", value: this.encodeExpr(node.whenFalse) },
|
|
401
429
|
]);
|
|
402
430
|
}
|
|
403
431
|
if (ts.isTemplateExpression(node)) {
|
|
@@ -427,10 +455,13 @@ export class TsEncoder {
|
|
|
427
455
|
if (ts.isSpreadElement(node)) {
|
|
428
456
|
return this.stdCall("spread", [
|
|
429
457
|
{ name: "value", value: this.encodeExpr(node.expression) },
|
|
430
|
-
]
|
|
458
|
+
]);
|
|
431
459
|
}
|
|
432
460
|
if (ts.isVoidExpression(node)) {
|
|
433
|
-
|
|
461
|
+
// `void <expr>` always evaluates to `undefined`; encode as null.
|
|
462
|
+
// (The operand's side effects are dropped — matching how the engine
|
|
463
|
+
// treats a discarded value; this mirrors the prior behaviour.)
|
|
464
|
+
return this.nullLiteral();
|
|
434
465
|
}
|
|
435
466
|
if (ts.isTaggedTemplateExpression(node)) {
|
|
436
467
|
return this.encodeTaggedTemplate(node);
|
|
@@ -454,7 +485,7 @@ export class TsEncoder {
|
|
|
454
485
|
{ name: "op", value: { literal: { stringValue: compound } } },
|
|
455
486
|
]);
|
|
456
487
|
}
|
|
457
|
-
if (op === ts.SyntaxKind.PlusToken && this.
|
|
488
|
+
if (op === ts.SyntaxKind.PlusToken && this.isProvablyString(node.left, node.right)) {
|
|
458
489
|
return this.stdCall("concat", [
|
|
459
490
|
{ name: "left", value: this.encodeExpr(node.left) },
|
|
460
491
|
{ name: "right", value: this.encodeExpr(node.right) },
|
|
@@ -476,9 +507,35 @@ export class TsEncoder {
|
|
|
476
507
|
this.warn(`Unhandled binary operator: ${ts.SyntaxKind[op]}`);
|
|
477
508
|
return { literal: { stringValue: `/* binary: ${ts.SyntaxKind[op]} */` } };
|
|
478
509
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
510
|
+
/// Decide how to encode a `+` whose operands aren't both provable strings.
|
|
511
|
+
///
|
|
512
|
+
/// `std.add` is runtime-polymorphic — the engine concatenates when either
|
|
513
|
+
/// operand is a string and adds numerically otherwise (see
|
|
514
|
+
/// `compiled_engine.ts:_stdAdd`). `std.concat`, by contrast, *always*
|
|
515
|
+
/// stringifies both sides, so emitting it for `1 + 2` would yield `"12"`.
|
|
516
|
+
///
|
|
517
|
+
/// We therefore only emit the dedicated `concat` when at least one operand
|
|
518
|
+
/// is *provably* a string (a string literal/template, or a nested `+` that
|
|
519
|
+
/// is itself provably a concat). For every other shape — including
|
|
520
|
+
/// `strVar + strVar`, where the static type is unknown — we fall through to
|
|
521
|
+
/// the polymorphic `std.add`, letting the engine coerce at runtime instead
|
|
522
|
+
/// of guessing from literal shape. This avoids both the false-numeric and
|
|
523
|
+
/// false-concat hazards.
|
|
524
|
+
isProvablyString(left, right) {
|
|
525
|
+
return this.isStringExpr(left) || this.isStringExpr(right);
|
|
526
|
+
}
|
|
527
|
+
isStringExpr(node) {
|
|
528
|
+
if (ts.isParenthesizedExpression(node))
|
|
529
|
+
return this.isStringExpr(node.expression);
|
|
530
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) ||
|
|
531
|
+
ts.isTemplateExpression(node)) {
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
534
|
+
// A nested `a + b` that is itself a provable concat produces a string.
|
|
535
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
536
|
+
return this.isProvablyString(node.left, node.right);
|
|
537
|
+
}
|
|
538
|
+
return false;
|
|
482
539
|
}
|
|
483
540
|
encodePrefixUnary(node) {
|
|
484
541
|
const op = node.operator;
|
|
@@ -526,15 +583,61 @@ export class TsEncoder {
|
|
|
526
583
|
value: this.encodeExpr(a),
|
|
527
584
|
}));
|
|
528
585
|
if (ts.isPropertyAccessExpression(node.expression)) {
|
|
529
|
-
const obj = this.encodeExpr(node.expression.expression);
|
|
530
586
|
const method = node.expression.name.text;
|
|
587
|
+
// console.log(x) / console.log(x, y, ...) → std.print per argument.
|
|
588
|
+
// console.error(x) → std.print_error(x).
|
|
589
|
+
// This ensures that TS programs using the idiomatic console API are
|
|
590
|
+
// faithfully represented in Ball IR using the universal std module, so
|
|
591
|
+
// the Ball engine can execute them without a "console" module.
|
|
592
|
+
if (ts.isIdentifier(node.expression.expression) &&
|
|
593
|
+
node.expression.expression.text === "console") {
|
|
594
|
+
if (method === "log" || method === "info" || method === "warn") {
|
|
595
|
+
if (args.length === 0) {
|
|
596
|
+
return this.stdCall("print", [
|
|
597
|
+
{ name: "message", value: { literal: { stringValue: "" } } },
|
|
598
|
+
]);
|
|
599
|
+
}
|
|
600
|
+
if (args.length === 1) {
|
|
601
|
+
return this.stdCall("print", [
|
|
602
|
+
{ name: "message", value: args[0].value },
|
|
603
|
+
]);
|
|
604
|
+
}
|
|
605
|
+
// Multiple arguments: emit one std.print per arg inside a block.
|
|
606
|
+
const stmts = args.map(a => ({
|
|
607
|
+
expression: this.stdCall("print", [
|
|
608
|
+
{ name: "message", value: a.value },
|
|
609
|
+
]),
|
|
610
|
+
}));
|
|
611
|
+
return { block: { statements: stmts } };
|
|
612
|
+
}
|
|
613
|
+
if (method === "error") {
|
|
614
|
+
if (args.length >= 1) {
|
|
615
|
+
return this.stdCall("print_error", [
|
|
616
|
+
{ name: "message", value: args[0].value },
|
|
617
|
+
]);
|
|
618
|
+
}
|
|
619
|
+
return this.stdCall("print_error", [
|
|
620
|
+
{ name: "message", value: { literal: { stringValue: "" } } },
|
|
621
|
+
]);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
const obj = this.encodeExpr(node.expression.expression);
|
|
625
|
+
// Map common JS/TS method calls to their Ball std equivalents.
|
|
626
|
+
// The engine's std module uses snake_case names with type prefixes.
|
|
627
|
+
const stdMethod = this.mapMethodToStd(method, args);
|
|
628
|
+
if (stdMethod) {
|
|
629
|
+
return this.stdCall(stdMethod.fn, [
|
|
630
|
+
{ name: stdMethod.selfName ?? "value", value: obj },
|
|
631
|
+
...stdMethod.extraFields(args),
|
|
632
|
+
], stdMethod.module ?? "std");
|
|
633
|
+
}
|
|
531
634
|
// Optional chaining call: obj?.method()
|
|
532
635
|
if (node.expression.questionDotToken || node.questionDotToken) {
|
|
533
636
|
return this.stdCall("optional_call", [
|
|
534
637
|
{ name: "object", value: obj },
|
|
535
638
|
{ name: "method", value: { literal: { stringValue: method } } },
|
|
536
639
|
...args,
|
|
537
|
-
]
|
|
640
|
+
]);
|
|
538
641
|
}
|
|
539
642
|
return {
|
|
540
643
|
call: {
|
|
@@ -579,25 +682,26 @@ export class TsEncoder {
|
|
|
579
682
|
const params = node.parameters.map(p => ts.isIdentifier(p.name) ? p.name.text : p.name.getText());
|
|
580
683
|
const body = node.body ? this.encodeBody(ts.isBlock(node.body) ? node.body : node.body) : undefined;
|
|
581
684
|
const metadata = {};
|
|
582
|
-
if (params.length > 0)
|
|
583
|
-
|
|
685
|
+
if (params.length > 0) {
|
|
686
|
+
// Emit params as structs with a `name` field, matching the Dart encoder
|
|
687
|
+
// format and the engine's _extractParams expectations.
|
|
688
|
+
const paramStructs = [];
|
|
689
|
+
for (const p of node.parameters) {
|
|
690
|
+
const pName = ts.isIdentifier(p.name) ? p.name.text : p.name.getText();
|
|
691
|
+
const pm = { name: pName };
|
|
692
|
+
if (p.type)
|
|
693
|
+
pm["type"] = p.type.getText();
|
|
694
|
+
if (p.initializer)
|
|
695
|
+
pm["default"] = p.initializer.getText();
|
|
696
|
+
if (p.dotDotDotToken)
|
|
697
|
+
pm["is_rest"] = true;
|
|
698
|
+
paramStructs.push(pm);
|
|
699
|
+
}
|
|
700
|
+
metadata["params"] = paramStructs;
|
|
701
|
+
}
|
|
584
702
|
if (node.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword)) {
|
|
585
703
|
metadata["is_async"] = true;
|
|
586
704
|
}
|
|
587
|
-
// Rest parameters
|
|
588
|
-
const lastParam = node.parameters[node.parameters.length - 1];
|
|
589
|
-
if (lastParam?.dotDotDotToken) {
|
|
590
|
-
metadata["rest_param"] = ts.isIdentifier(lastParam.name) ? lastParam.name.text : lastParam.name.getText();
|
|
591
|
-
}
|
|
592
|
-
// Default parameter values
|
|
593
|
-
const defaults = {};
|
|
594
|
-
for (const p of node.parameters) {
|
|
595
|
-
if (p.initializer && ts.isIdentifier(p.name)) {
|
|
596
|
-
defaults[p.name.text] = p.initializer.getText();
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
if (Object.keys(defaults).length > 0)
|
|
600
|
-
metadata["param_defaults"] = defaults;
|
|
601
705
|
// Destructured parameters
|
|
602
706
|
const destructured = {};
|
|
603
707
|
for (const p of node.parameters) {
|
|
@@ -617,43 +721,56 @@ export class TsEncoder {
|
|
|
617
721
|
};
|
|
618
722
|
}
|
|
619
723
|
encodeIf(node) {
|
|
724
|
+
// Control flow fields (then, else) are direct expressions — NOT lambdas.
|
|
725
|
+
// The engine evaluates them lazily (Ball invariant #4). Wrapping in a
|
|
726
|
+
// lambda would eat flow signals like std.return/break/continue.
|
|
620
727
|
const fields = [
|
|
621
728
|
{ name: "condition", value: this.encodeExpr(node.expression) },
|
|
622
|
-
{ name: "then", value:
|
|
729
|
+
{ name: "then", value: this.encodeBody(ts.isBlock(node.thenStatement) ? node.thenStatement : ts.factory.createBlock([node.thenStatement])) },
|
|
623
730
|
];
|
|
624
731
|
if (node.elseStatement) {
|
|
625
732
|
if (ts.isIfStatement(node.elseStatement)) {
|
|
626
|
-
fields.push({ name: "else", value:
|
|
733
|
+
fields.push({ name: "else", value: this.encodeIf(node.elseStatement) });
|
|
627
734
|
}
|
|
628
735
|
else {
|
|
629
|
-
fields.push({ name: "else", value:
|
|
736
|
+
fields.push({ name: "else", value: this.encodeBody(ts.isBlock(node.elseStatement) ? node.elseStatement : ts.factory.createBlock([node.elseStatement])) });
|
|
630
737
|
}
|
|
631
738
|
}
|
|
632
739
|
return this.stdCall("if", fields);
|
|
633
740
|
}
|
|
634
741
|
encodeFor(node) {
|
|
742
|
+
// Control flow fields are direct expressions — the engine evaluates them
|
|
743
|
+
// lazily. No lambda wrappers (see encodeIf comment).
|
|
744
|
+
//
|
|
745
|
+
// The engine's _evalLazyFor reads `init` (not `variable`/`start`).
|
|
746
|
+
// For a variable declaration `let i = 0`, emit a block with a let binding.
|
|
747
|
+
// For a bare expression initializer, emit the expression directly.
|
|
635
748
|
const fields = [];
|
|
636
749
|
if (node.initializer) {
|
|
637
750
|
if (ts.isVariableDeclarationList(node.initializer)) {
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
751
|
+
const stmts = [];
|
|
752
|
+
for (const d of node.initializer.declarations) {
|
|
753
|
+
const varName = ts.isIdentifier(d.name) ? d.name.text : d.name.getText();
|
|
754
|
+
stmts.push({
|
|
755
|
+
let: {
|
|
756
|
+
name: varName,
|
|
757
|
+
value: d.initializer ? this.encodeExpr(d.initializer) : undefined,
|
|
758
|
+
},
|
|
759
|
+
});
|
|
644
760
|
}
|
|
761
|
+
fields.push({ name: "init", value: { block: { statements: stmts } } });
|
|
645
762
|
}
|
|
646
763
|
else {
|
|
647
|
-
fields.push({ name: "init", value:
|
|
764
|
+
fields.push({ name: "init", value: this.encodeExpr(node.initializer) });
|
|
648
765
|
}
|
|
649
766
|
}
|
|
650
767
|
if (node.condition) {
|
|
651
|
-
fields.push({ name: "condition", value:
|
|
768
|
+
fields.push({ name: "condition", value: this.encodeExpr(node.condition) });
|
|
652
769
|
}
|
|
653
770
|
if (node.incrementor) {
|
|
654
|
-
fields.push({ name: "update", value:
|
|
771
|
+
fields.push({ name: "update", value: this.encodeExpr(node.incrementor) });
|
|
655
772
|
}
|
|
656
|
-
fields.push({ name: "body", value:
|
|
773
|
+
fields.push({ name: "body", value: this.encodeBody(ts.isBlock(node.statement) ? node.statement : ts.factory.createBlock([node.statement])) });
|
|
657
774
|
return this.stdCall("for", fields);
|
|
658
775
|
}
|
|
659
776
|
encodeForOf(node) {
|
|
@@ -666,39 +783,44 @@ export class TsEncoder {
|
|
|
666
783
|
return this.stdCall(fnName, [
|
|
667
784
|
{ name: "variable", value: { literal: { stringValue: varName } } },
|
|
668
785
|
{ name: "iterable", value: this.encodeExpr(node.expression) },
|
|
669
|
-
{ name: "body", value:
|
|
786
|
+
{ name: "body", value: this.encodeBody(ts.isBlock(node.statement) ? node.statement : ts.factory.createBlock([node.statement])) },
|
|
670
787
|
]);
|
|
671
788
|
}
|
|
672
789
|
encodeWhile(node) {
|
|
673
790
|
return this.stdCall("while", [
|
|
674
|
-
{ name: "condition", value:
|
|
675
|
-
{ name: "body", value:
|
|
791
|
+
{ name: "condition", value: this.encodeExpr(node.expression) },
|
|
792
|
+
{ name: "body", value: this.encodeBody(ts.isBlock(node.statement) ? node.statement : ts.factory.createBlock([node.statement])) },
|
|
676
793
|
]);
|
|
677
794
|
}
|
|
678
795
|
encodeDoWhile(node) {
|
|
679
796
|
return this.stdCall("do_while", [
|
|
680
|
-
{ name: "condition", value:
|
|
681
|
-
{ name: "body", value:
|
|
797
|
+
{ name: "condition", value: this.encodeExpr(node.expression) },
|
|
798
|
+
{ name: "body", value: this.encodeBody(ts.isBlock(node.statement) ? node.statement : ts.factory.createBlock([node.statement])) },
|
|
682
799
|
]);
|
|
683
800
|
}
|
|
684
801
|
encodeTry(node) {
|
|
802
|
+
// try body is a direct block expression (not a lambda).
|
|
685
803
|
const fields = [
|
|
686
|
-
{ name: "body", value:
|
|
804
|
+
{ name: "body", value: this.encodeBlock(node.tryBlock) },
|
|
687
805
|
];
|
|
688
806
|
if (node.catchClause) {
|
|
689
807
|
const cc = node.catchClause;
|
|
808
|
+
// The Dart encoder emits catches as a listValue of catch entries, each
|
|
809
|
+
// with optional `type`, `variable`, and `body` fields. TS catch clauses
|
|
810
|
+
// are untyped, so we omit the `type` field.
|
|
690
811
|
const catchFields = [];
|
|
691
812
|
if (cc.variableDeclaration && ts.isIdentifier(cc.variableDeclaration.name)) {
|
|
692
813
|
catchFields.push({ name: "variable", value: { literal: { stringValue: cc.variableDeclaration.name.text } } });
|
|
693
814
|
}
|
|
694
|
-
catchFields.push({ name: "body", value:
|
|
815
|
+
catchFields.push({ name: "body", value: this.encodeBlock(cc.block) });
|
|
816
|
+
const catchEntry = { messageCreation: { typeName: "", fields: catchFields } };
|
|
695
817
|
fields.push({
|
|
696
|
-
name: "
|
|
697
|
-
value: {
|
|
818
|
+
name: "catches",
|
|
819
|
+
value: { literal: { listValue: { elements: [catchEntry] } } },
|
|
698
820
|
});
|
|
699
821
|
}
|
|
700
822
|
if (node.finallyBlock) {
|
|
701
|
-
fields.push({ name: "finally", value:
|
|
823
|
+
fields.push({ name: "finally", value: this.encodeBlock(node.finallyBlock) });
|
|
702
824
|
}
|
|
703
825
|
return this.stdCall("try", fields);
|
|
704
826
|
}
|
|
@@ -710,17 +832,17 @@ export class TsEncoder {
|
|
|
710
832
|
caseFields.push({ name: "value", value: this.encodeExpr(clause.expression) });
|
|
711
833
|
}
|
|
712
834
|
else {
|
|
713
|
-
caseFields.push({ name: "
|
|
835
|
+
caseFields.push({ name: "is_default", value: { literal: { boolValue: true } } });
|
|
714
836
|
}
|
|
715
837
|
const stmts = [];
|
|
716
838
|
for (const s of clause.statements) {
|
|
717
839
|
stmts.push(...this.encodeStatement(s));
|
|
718
840
|
}
|
|
719
|
-
caseFields.push({ name: "body", value: {
|
|
841
|
+
caseFields.push({ name: "body", value: { block: { statements: stmts } } });
|
|
720
842
|
cases.push({ messageCreation: { typeName: "", fields: caseFields } });
|
|
721
843
|
}
|
|
722
844
|
return this.stdCall("switch", [
|
|
723
|
-
{ name: "
|
|
845
|
+
{ name: "subject", value: this.encodeExpr(node.expression) },
|
|
724
846
|
{ name: "cases", value: { literal: { listValue: { elements: cases } } } },
|
|
725
847
|
]);
|
|
726
848
|
}
|
|
@@ -859,7 +981,87 @@ export class TsEncoder {
|
|
|
859
981
|
{ name: "tag", value: tag },
|
|
860
982
|
{ name: "strings", value: { literal: { listValue: { elements: parts } } } },
|
|
861
983
|
{ name: "expressions", value: { literal: { listValue: { elements: exprs } } } },
|
|
862
|
-
]
|
|
984
|
+
]);
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Map a JS/TS method name to its Ball std function equivalent.
|
|
988
|
+
* Returns null if no mapping exists (falls through to generic method call).
|
|
989
|
+
*/
|
|
990
|
+
mapMethodToStd(method, _args) {
|
|
991
|
+
// String methods
|
|
992
|
+
const STR_METHODS = {
|
|
993
|
+
toUpperCase: "string_to_upper_case",
|
|
994
|
+
toLowerCase: "string_to_lower_case",
|
|
995
|
+
trim: "string_trim",
|
|
996
|
+
trimStart: "string_trim_left",
|
|
997
|
+
trimEnd: "string_trim_right",
|
|
998
|
+
includes: "string_contains",
|
|
999
|
+
indexOf: "string_index_of",
|
|
1000
|
+
startsWith: "string_starts_with",
|
|
1001
|
+
endsWith: "string_ends_with",
|
|
1002
|
+
split: "string_split",
|
|
1003
|
+
substring: "string_substring",
|
|
1004
|
+
slice: "string_substring",
|
|
1005
|
+
replace: "string_replace_first",
|
|
1006
|
+
replaceAll: "string_replace_all",
|
|
1007
|
+
padStart: "string_pad_left",
|
|
1008
|
+
padEnd: "string_pad_right",
|
|
1009
|
+
repeat: "string_repeat",
|
|
1010
|
+
charAt: "string_char_at",
|
|
1011
|
+
charCodeAt: "string_code_unit_at",
|
|
1012
|
+
};
|
|
1013
|
+
if (method in STR_METHODS) {
|
|
1014
|
+
return {
|
|
1015
|
+
fn: STR_METHODS[method],
|
|
1016
|
+
selfName: "value",
|
|
1017
|
+
extraFields: (a) => a.map((x, i) => ({
|
|
1018
|
+
name: i === 0 ? "other" : `arg${i}`,
|
|
1019
|
+
value: x.value,
|
|
1020
|
+
})),
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
// Array methods
|
|
1024
|
+
const ARR_METHODS = {
|
|
1025
|
+
push: { fn: "list_add" },
|
|
1026
|
+
pop: { fn: "list_remove_last" },
|
|
1027
|
+
indexOf: { fn: "list_index_of", mod: "std_collections" },
|
|
1028
|
+
includes: { fn: "list_contains", mod: "std_collections" },
|
|
1029
|
+
join: { fn: "list_join", mod: "std_collections" },
|
|
1030
|
+
reverse: { fn: "list_reversed", mod: "std_collections" },
|
|
1031
|
+
slice: { fn: "list_sublist", mod: "std_collections" },
|
|
1032
|
+
splice: { fn: "list_remove_at" },
|
|
1033
|
+
sort: { fn: "list_sort", mod: "std_collections" },
|
|
1034
|
+
map: { fn: "list_map", mod: "std_collections" },
|
|
1035
|
+
filter: { fn: "list_where", mod: "std_collections" },
|
|
1036
|
+
forEach: { fn: "list_for_each", mod: "std_collections" },
|
|
1037
|
+
reduce: { fn: "list_fold", mod: "std_collections" },
|
|
1038
|
+
find: { fn: "list_first_where", mod: "std_collections" },
|
|
1039
|
+
flat: { fn: "list_flatten", mod: "std_collections" },
|
|
1040
|
+
concat: { fn: "list_concat", mod: "std_collections" },
|
|
1041
|
+
every: { fn: "list_every", mod: "std_collections" },
|
|
1042
|
+
some: { fn: "list_any", mod: "std_collections" },
|
|
1043
|
+
};
|
|
1044
|
+
if (method in ARR_METHODS) {
|
|
1045
|
+
const m = ARR_METHODS[method];
|
|
1046
|
+
return {
|
|
1047
|
+
fn: m.fn,
|
|
1048
|
+
module: m.mod,
|
|
1049
|
+
selfName: "list",
|
|
1050
|
+
extraFields: (a) => a.map((x, i) => ({
|
|
1051
|
+
name: i === 0 ? "value" : `arg${i}`,
|
|
1052
|
+
value: x.value,
|
|
1053
|
+
})),
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
// toString
|
|
1057
|
+
if (method === "toString") {
|
|
1058
|
+
return {
|
|
1059
|
+
fn: "to_string",
|
|
1060
|
+
selfName: "value",
|
|
1061
|
+
extraFields: () => [],
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
return null;
|
|
863
1065
|
}
|
|
864
1066
|
stdCall(fn, fields, module = "std") {
|
|
865
1067
|
this.stdFunctions.add(`${module}:${fn}`);
|
|
@@ -873,23 +1075,74 @@ export class TsEncoder {
|
|
|
873
1075
|
},
|
|
874
1076
|
};
|
|
875
1077
|
}
|
|
876
|
-
|
|
877
|
-
const
|
|
1078
|
+
buildBaseModules() {
|
|
1079
|
+
const byModule = new Map();
|
|
878
1080
|
for (const ref of this.stdFunctions) {
|
|
879
|
-
const [, fn] = ref.split(":");
|
|
880
|
-
|
|
1081
|
+
const [mod, fn] = ref.split(":");
|
|
1082
|
+
if (!byModule.has(mod))
|
|
1083
|
+
byModule.set(mod, []);
|
|
1084
|
+
byModule.get(mod).push(fn);
|
|
1085
|
+
}
|
|
1086
|
+
const modules = [];
|
|
1087
|
+
for (const [mod, fns] of byModule) {
|
|
1088
|
+
fns.sort();
|
|
1089
|
+
modules.push({
|
|
1090
|
+
name: mod,
|
|
1091
|
+
functions: fns.map(fn => ({ name: fn, isBase: true })),
|
|
1092
|
+
});
|
|
881
1093
|
}
|
|
882
|
-
|
|
883
|
-
return
|
|
1094
|
+
modules.sort((a, b) => a.name === "std" ? -1 : b.name === "std" ? 1 : a.name.localeCompare(b.name));
|
|
1095
|
+
return modules;
|
|
1096
|
+
}
|
|
1097
|
+
/// The canonical encoding of `null`/`undefined`/`void`.
|
|
1098
|
+
///
|
|
1099
|
+
/// The Dart encoder represents a null literal as an *empty* `Literal`
|
|
1100
|
+
/// message with no `value` oneof field set (see
|
|
1101
|
+
/// `dart/encoder/lib/encoder.dart`: `Expression()..literal = Literal()`).
|
|
1102
|
+
/// We match that exactly: `{ literal: {} }`. Both engines treat an empty
|
|
1103
|
+
/// literal as `null` (the TS compiler's `compileLiteral` falls through to
|
|
1104
|
+
/// `return "null"` when no value field is present), so this round-trips
|
|
1105
|
+
/// correctly and is no longer conflated with the empty string `""`.
|
|
1106
|
+
nullLiteral() {
|
|
1107
|
+
return { literal: {} };
|
|
884
1108
|
}
|
|
885
1109
|
warn(msg) {
|
|
886
1110
|
this.warnings.push(msg);
|
|
1111
|
+
// In strict mode an unhandled node is a hard error: the encoder cannot
|
|
1112
|
+
// faithfully represent the construct and would otherwise emit a
|
|
1113
|
+
// `/* unhandled */` placeholder literal that silently changes semantics.
|
|
1114
|
+
if (this.strict) {
|
|
1115
|
+
throw new EncodeError(msg, this.getWarnings());
|
|
1116
|
+
}
|
|
887
1117
|
}
|
|
888
1118
|
getWarnings() {
|
|
889
1119
|
return [...this.warnings];
|
|
890
1120
|
}
|
|
891
1121
|
}
|
|
1122
|
+
/// Thrown by `encode(..., { strict: true })` when the encoder hits a TS
|
|
1123
|
+
/// construct it cannot represent. Carries the full accumulated warning list.
|
|
1124
|
+
export class EncodeError extends Error {
|
|
1125
|
+
warnings;
|
|
1126
|
+
constructor(message, warnings) {
|
|
1127
|
+
super(message);
|
|
1128
|
+
this.name = "EncodeError";
|
|
1129
|
+
this.warnings = warnings;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
/// Encode `source` to a Ball `Program`.
|
|
1133
|
+
///
|
|
1134
|
+
/// The simple overload returns just the `Program` for backwards
|
|
1135
|
+
/// compatibility. Pass `{ strict: true }` to throw an `EncodeError` on any
|
|
1136
|
+
/// unhandled construct. To inspect non-fatal warnings without strict mode,
|
|
1137
|
+
/// use `encodeWithWarnings`.
|
|
892
1138
|
export function encode(source, options = {}) {
|
|
893
1139
|
return new TsEncoder().encode(source, options);
|
|
894
1140
|
}
|
|
1141
|
+
/// Like `encode`, but also surfaces the accumulated warnings (e.g. unhandled
|
|
1142
|
+
/// statement/expression kinds). Honors `options.strict` the same way.
|
|
1143
|
+
export function encodeWithWarnings(source, options = {}) {
|
|
1144
|
+
const encoder = new TsEncoder();
|
|
1145
|
+
const program = encoder.encode(source, options);
|
|
1146
|
+
return { program, warnings: encoder.getWarnings() };
|
|
1147
|
+
}
|
|
895
1148
|
//# sourceMappingURL=encoder.js.map
|