@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/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: "shift_left" },
26
- [ts.SyntaxKind.GreaterThanGreaterThanToken]:{ module: "std", function: "shift_right" },
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: "less_than_or_equal" },
34
- [ts.SyntaxKind.GreaterThanEqualsToken]: { module: "std", function: "greater_than_or_equal" },
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) : { literal: { stringValue: "" } },
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 stdModule = this.buildStdModule();
128
- const modules: Module[] = [stdModule];
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) metadata["params"] = params;
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 { literal: { stringValue: "" } };
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
- ], "ts_std");
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
- ], "ts_std");
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
- ], "ts_std"),
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
- ], "ts_std"),
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: { lambda: { name: "", body: this.encodeExpr(node.whenTrue) } } },
418
- { name: "else", value: { lambda: { name: "", body: this.encodeExpr(node.whenFalse) } } },
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
- ], "ts_std");
473
+ ]);
449
474
  }
450
475
  if (ts.isVoidExpression(node)) {
451
- return { literal: { stringValue: "" } };
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.looksLikeStringConcat(node)) {
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
- private looksLikeStringConcat(node: ts.BinaryExpression): boolean {
506
- return ts.isStringLiteral(node.left) || ts.isStringLiteral(node.right) ||
507
- ts.isTemplateExpression(node.left) || ts.isNoSubstitutionTemplateLiteral(node.left);
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
- ], "ts_std");
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) metadata["params"] = params;
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: { lambda: { name: "", body: this.encodeBody(
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: { lambda: { name: "", body: this.encodeIf(node.elseStatement) } } });
774
+ fields.push({ name: "else", value: this.encodeIf(node.elseStatement) });
671
775
  } else {
672
- fields.push({ name: "else", value: { lambda: { name: "", body: this.encodeBody(
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 decls = node.initializer.declarations;
685
- if (decls.length > 0) {
686
- const d = decls[0];
687
- fields.push({ name: "variable", value: { literal: { stringValue: ts.isIdentifier(d.name) ? d.name.text : d.name.getText() } } });
688
- if (d.initializer) fields.push({ name: "start", value: this.encodeExpr(d.initializer) });
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: { lambda: { name: "", body: this.encodeExpr(node.initializer) } } });
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: { lambda: { name: "", body: this.encodeExpr(node.condition) } } });
810
+ fields.push({ name: "condition", value: this.encodeExpr(node.condition) });
696
811
  }
697
812
  if (node.incrementor) {
698
- fields.push({ name: "update", value: { lambda: { name: "", body: this.encodeExpr(node.incrementor) } } });
813
+ fields.push({ name: "update", value: this.encodeExpr(node.incrementor) });
699
814
  }
700
- fields.push({ name: "body", value: { lambda: { name: "", body: this.encodeBody(
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: { lambda: { name: "", body: this.encodeBody(
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: { lambda: { name: "", body: this.encodeExpr(node.expression) } } },
725
- { name: "body", value: { lambda: { name: "", body: this.encodeBody(
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: { lambda: { name: "", body: this.encodeExpr(node.expression) } } },
734
- { name: "body", value: { lambda: { name: "", body: this.encodeBody(
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: { lambda: { name: "", body: this.encodeBlock(node.tryBlock) } } },
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: { lambda: { name: "", body: this.encodeBlock(cc.block) } } });
869
+ catchFields.push({ name: "body", value: this.encodeBlock(cc.block) });
870
+ const catchEntry: Expression = { messageCreation: { typeName: "", fields: catchFields } };
751
871
  fields.push({
752
- name: "catch",
753
- value: { messageCreation: { typeName: "", fields: catchFields } },
872
+ name: "catches",
873
+ value: { literal: { listValue: { elements: [catchEntry] } } },
754
874
  });
755
875
  }
756
876
  if (node.finallyBlock) {
757
- fields.push({ name: "finally", value: { lambda: { name: "", body: this.encodeBlock(node.finallyBlock) } } });
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: "isDefault", value: { literal: { boolValue: true } } });
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: { lambda: { name: "", body: { block: { statements: stmts } } } } });
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: "value", value: this.encodeExpr(node.expression) },
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
- ], "ts_std");
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 buildStdModule(): Module {
949
- const functions: FunctionDef[] = [];
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
- functions.push({ name: fn, isBase: true });
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
- functions.sort((a, b) => a.name.localeCompare(b.name));
955
- return { name: "std", functions };
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
+ }