@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/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: "shift_left" },
12
- [ts.SyntaxKind.GreaterThanGreaterThanToken]: { module: "std", function: "shift_right" },
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: "less_than_or_equal" },
20
- [ts.SyntaxKind.GreaterThanEqualsToken]: { module: "std", function: "greater_than_or_equal" },
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) : { literal: { stringValue: "" } },
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 stdModule = this.buildStdModule();
114
- const modules = [stdModule];
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
- metadata["params"] = params;
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 { literal: { stringValue: "" } };
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
- ], "ts_std");
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
- ], "ts_std");
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
- ], "ts_std"),
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
- ], "ts_std"),
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: { lambda: { name: "", body: this.encodeExpr(node.whenTrue) } } },
400
- { name: "else", value: { lambda: { name: "", body: this.encodeExpr(node.whenFalse) } } },
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
- ], "ts_std");
458
+ ]);
431
459
  }
432
460
  if (ts.isVoidExpression(node)) {
433
- return { literal: { stringValue: "" } };
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.looksLikeStringConcat(node)) {
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
- looksLikeStringConcat(node) {
480
- return ts.isStringLiteral(node.left) || ts.isStringLiteral(node.right) ||
481
- ts.isTemplateExpression(node.left) || ts.isNoSubstitutionTemplateLiteral(node.left);
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
- ], "ts_std");
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
- metadata["params"] = params;
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: { lambda: { name: "", body: this.encodeBody(ts.isBlock(node.thenStatement) ? node.thenStatement : ts.factory.createBlock([node.thenStatement])) } } },
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: { lambda: { name: "", body: this.encodeIf(node.elseStatement) } } });
733
+ fields.push({ name: "else", value: this.encodeIf(node.elseStatement) });
627
734
  }
628
735
  else {
629
- fields.push({ name: "else", value: { lambda: { name: "", body: this.encodeBody(ts.isBlock(node.elseStatement) ? node.elseStatement : ts.factory.createBlock([node.elseStatement])) } } });
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 decls = node.initializer.declarations;
639
- if (decls.length > 0) {
640
- const d = decls[0];
641
- fields.push({ name: "variable", value: { literal: { stringValue: ts.isIdentifier(d.name) ? d.name.text : d.name.getText() } } });
642
- if (d.initializer)
643
- fields.push({ name: "start", value: this.encodeExpr(d.initializer) });
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: { lambda: { name: "", body: this.encodeExpr(node.initializer) } } });
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: { lambda: { name: "", body: this.encodeExpr(node.condition) } } });
768
+ fields.push({ name: "condition", value: this.encodeExpr(node.condition) });
652
769
  }
653
770
  if (node.incrementor) {
654
- fields.push({ name: "update", value: { lambda: { name: "", body: this.encodeExpr(node.incrementor) } } });
771
+ fields.push({ name: "update", value: this.encodeExpr(node.incrementor) });
655
772
  }
656
- fields.push({ name: "body", value: { lambda: { name: "", body: this.encodeBody(ts.isBlock(node.statement) ? node.statement : ts.factory.createBlock([node.statement])) } } });
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: { lambda: { name: "", body: this.encodeBody(ts.isBlock(node.statement) ? node.statement : ts.factory.createBlock([node.statement])) } } },
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: { lambda: { name: "", body: this.encodeExpr(node.expression) } } },
675
- { name: "body", value: { lambda: { name: "", body: this.encodeBody(ts.isBlock(node.statement) ? node.statement : ts.factory.createBlock([node.statement])) } } },
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: { lambda: { name: "", body: this.encodeExpr(node.expression) } } },
681
- { name: "body", value: { lambda: { name: "", body: this.encodeBody(ts.isBlock(node.statement) ? node.statement : ts.factory.createBlock([node.statement])) } } },
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: { lambda: { name: "", body: this.encodeBlock(node.tryBlock) } } },
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: { lambda: { name: "", body: this.encodeBlock(cc.block) } } });
815
+ catchFields.push({ name: "body", value: this.encodeBlock(cc.block) });
816
+ const catchEntry = { messageCreation: { typeName: "", fields: catchFields } };
695
817
  fields.push({
696
- name: "catch",
697
- value: { messageCreation: { typeName: "", fields: catchFields } },
818
+ name: "catches",
819
+ value: { literal: { listValue: { elements: [catchEntry] } } },
698
820
  });
699
821
  }
700
822
  if (node.finallyBlock) {
701
- fields.push({ name: "finally", value: { lambda: { name: "", body: this.encodeBlock(node.finallyBlock) } } });
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: "isDefault", value: { literal: { boolValue: true } } });
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: { lambda: { name: "", body: { block: { statements: stmts } } } } });
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: "value", value: this.encodeExpr(node.expression) },
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
- ], "ts_std");
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
- buildStdModule() {
877
- const functions = [];
1078
+ buildBaseModules() {
1079
+ const byModule = new Map();
878
1080
  for (const ref of this.stdFunctions) {
879
- const [, fn] = ref.split(":");
880
- functions.push({ name: fn, isBase: true });
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
- functions.sort((a, b) => a.name.localeCompare(b.name));
883
- return { name: "std", functions };
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