@ball-lang/encoder 1.59.10 → 1.59.11

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
@@ -1,4 +1,22 @@
1
1
  import ts from "typescript";
2
+ /**
3
+ * TS constructs whose only role is type-level or syntactic: dropping them
4
+ * cannot change what the program computes, so they never trip
5
+ * `strictBehaviorAffecting`. Everything else that reaches `warn()` does.
6
+ *
7
+ * Only kinds that actually REACH `warn()` belong here. A type alias or an
8
+ * interface at top level is encoded (`typeAliases[]` / `typeDefs[]`) and never
9
+ * warns; nested inside a function body it has no statement branch, so it hits
10
+ * the "Unhandled statement kind" warn — which is the case this set exempts.
11
+ * `SatisfiesExpression` is deliberately absent: `x satisfies T` is erased in
12
+ * `encodeExpr` exactly like an `as` cast, silently and without a warning, so an
13
+ * entry for it would be dead.
14
+ */
15
+ const ERASURE_ONLY_KINDS = new Set([
16
+ "TypeAliasDeclaration",
17
+ "InterfaceDeclaration",
18
+ "EmptyStatement",
19
+ ]);
2
20
  const BINARY_OPS = {
3
21
  [ts.SyntaxKind.PlusToken]: { module: "std", function: "add" },
4
22
  [ts.SyntaxKind.MinusToken]: { module: "std", function: "subtract" },
@@ -22,6 +40,10 @@ const BINARY_OPS = {
22
40
  [ts.SyntaxKind.BarBarToken]: { module: "std", function: "or" },
23
41
  [ts.SyntaxKind.QuestionQuestionToken]: { module: "std", function: "null_coalesce" },
24
42
  [ts.SyntaxKind.InstanceOfKeyword]: { module: "std", function: "is" },
43
+ // `**` used to fall through to the "Unhandled binary operator" placeholder
44
+ // (#490). math_pow is a declared, Dart-engine-implemented std base function
45
+ // that simply had no encoder emitting it.
46
+ [ts.SyntaxKind.AsteriskAsteriskToken]: { module: "std", function: "math_pow" },
25
47
  };
26
48
  const COMPOUND_OPS = {
27
49
  [ts.SyntaxKind.PlusEqualsToken]: "+=",
@@ -35,11 +57,15 @@ const COMPOUND_OPS = {
35
57
  [ts.SyntaxKind.LessThanLessThanEqualsToken]: "<<=",
36
58
  [ts.SyntaxKind.GreaterThanGreaterThanEqualsToken]: ">>=",
37
59
  [ts.SyntaxKind.QuestionQuestionEqualsToken]: "??=",
60
+ [ts.SyntaxKind.AsteriskAsteriskEqualsToken]: "**=",
38
61
  };
39
62
  export class TsEncoder {
40
63
  stdFunctions = new Set();
41
64
  warnings = [];
42
65
  strict = false;
66
+ strictBehaviorAffecting = false;
67
+ /** Monotonic counter for the synthetic loop variables `.forEach` desugars to. */
68
+ desugarCounter = 0;
43
69
  // Maps an operator lexeme to the canonical, language-agnostic Ball method
44
70
  // name every compiler already understands for operator overloads — mirrors
45
71
  // dart/encoder's `_canonicalOperatorName` exactly (same lexemes, same
@@ -72,6 +98,7 @@ export class TsEncoder {
72
98
  const modName = options.moduleName ?? "main";
73
99
  const entryFn = options.entryFunction ?? "main";
74
100
  this.strict = options.strict ?? false;
101
+ this.strictBehaviorAffecting = options.strictBehaviorAffecting ?? false;
75
102
  const sourceFile = ts.createSourceFile("input.ts", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
76
103
  const functions = [];
77
104
  const typeDefs = [];
@@ -335,7 +362,7 @@ export class TsEncoder {
335
362
  },
336
363
  }];
337
364
  }
338
- this.warn(`Unhandled statement kind: ${ts.SyntaxKind[node.kind]}`);
365
+ this.warn(`Unhandled statement kind: ${ts.SyntaxKind[node.kind]}`, ts.SyntaxKind[node.kind]);
339
366
  return [{ expression: { literal: { stringValue: `/* unhandled: ${ts.SyntaxKind[node.kind]} */` } } }];
340
367
  }
341
368
  encodeExpr(node) {
@@ -358,6 +385,14 @@ export class TsEncoder {
358
385
  if (node.kind === ts.SyntaxKind.NullKeyword || node.kind === ts.SyntaxKind.UndefinedKeyword) {
359
386
  return this.nullLiteral();
360
387
  }
388
+ if (node.kind === ts.SyntaxKind.SuperKeyword) {
389
+ // Mirrors dart/encoder's SuperExpression handling exactly: a bare
390
+ // reference named "super". ts/compiler already resolves that reference
391
+ // back to a real super-chain call, and conformance fixture
392
+ // 107_method_override_super proves the shape end to end — the TS encoder
393
+ // was simply the one place that never produced it (#490).
394
+ return { reference: { name: "super" } };
395
+ }
361
396
  if (node.kind === ts.SyntaxKind.ThisKeyword) {
362
397
  // Mirrors dart/encoder's ThisExpression handling exactly: a bare
363
398
  // reference named "self" (not a field-access base, not a special
@@ -390,8 +425,12 @@ export class TsEncoder {
390
425
  }
391
426
  if (ts.isPropertyAccessExpression(node)) {
392
427
  if (node.questionDotToken) {
393
- return this.stdCall("optional_access", [
394
- { name: "object", value: this.encodeExpr(node.expression) },
428
+ // Canonical name + field shape: compileStdCall's `case
429
+ // "null_aware_access"` reads `target` and a literal-string `field`
430
+ // (and so does dart/compiler). The old "optional_access" spelling with
431
+ // an `object` field existed in no compiler or engine (#489).
432
+ return this.stdCall("null_aware_access", [
433
+ { name: "target", value: this.encodeExpr(node.expression) },
395
434
  { name: "field", value: { literal: { stringValue: node.name.text } } },
396
435
  ]);
397
436
  }
@@ -399,9 +438,13 @@ export class TsEncoder {
399
438
  }
400
439
  if (ts.isElementAccessExpression(node)) {
401
440
  if (node.questionDotToken) {
402
- return this.stdCall("optional_access", [
403
- { name: "object", value: this.encodeExpr(node.expression) },
404
- { name: "field", value: this.encodeExpr(node.argumentExpression) },
441
+ // Optional ELEMENT access is a different shape: the index is an
442
+ // arbitrary expression, not a literal field name, so it routes through
443
+ // the compiler's dedicated `null_aware_index` case rather than being
444
+ // conflated with property access (#489).
445
+ return this.stdCall("null_aware_index", [
446
+ { name: "target", value: this.encodeExpr(node.expression) },
447
+ { name: "index", value: this.encodeExpr(node.argumentExpression) },
405
448
  ]);
406
449
  }
407
450
  return this.stdCall("index", [
@@ -502,9 +545,60 @@ export class TsEncoder {
502
545
  if (ts.isTaggedTemplateExpression(node)) {
503
546
  return this.encodeTaggedTemplate(node);
504
547
  }
505
- this.warn(`Unhandled expression kind: ${ts.SyntaxKind[node.kind]}`);
548
+ if (ts.isSatisfiesExpression(node)) {
549
+ // `x satisfies T` is a pure type-level assertion — erase it, exactly as
550
+ // `as`/`<T>` casts are erased above.
551
+ return this.encodeExpr(node.expression);
552
+ }
553
+ if (ts.isRegularExpressionLiteral(node)) {
554
+ return this.encodeRegexLiteral(node);
555
+ }
556
+ if (ts.isDeleteExpression(node)) {
557
+ return this.encodeDelete(node);
558
+ }
559
+ this.warn(`Unhandled expression kind: ${ts.SyntaxKind[node.kind]}`, ts.SyntaxKind[node.kind]);
506
560
  return { literal: { stringValue: `/* unhandled: ${ts.SyntaxKind[node.kind]} */` } };
507
561
  }
562
+ /**
563
+ * `delete obj.a` / `delete obj[k]` -> `std_collections.map_delete(map, key)`.
564
+ *
565
+ * `map_delete` has been declared and Dart-engine-implemented all along
566
+ * (dart/shared/lib/std_collections.dart) — no encoder in the repo had ever
567
+ * emitted it, so `delete` fell through to a placeholder literal (#490).
568
+ */
569
+ encodeDelete(node) {
570
+ const operand = node.expression;
571
+ if (ts.isPropertyAccessExpression(operand)) {
572
+ return this.stdCall("map_delete", [
573
+ { name: "map", value: this.encodeExpr(operand.expression) },
574
+ { name: "key", value: { literal: { stringValue: operand.name.text } } },
575
+ ], "std_collections");
576
+ }
577
+ if (ts.isElementAccessExpression(operand)) {
578
+ return this.stdCall("map_delete", [
579
+ { name: "map", value: this.encodeExpr(operand.expression) },
580
+ { name: "key", value: this.encodeExpr(operand.argumentExpression) },
581
+ ], "std_collections");
582
+ }
583
+ // `delete someIdentifier` is a no-op on a binding in sloppy mode and a
584
+ // SyntaxError in strict mode — there is nothing faithful to encode.
585
+ this.warn(`Unhandled delete target: ${ts.SyntaxKind[operand.kind]}`, ts.SyntaxKind[operand.kind]);
586
+ return { literal: { stringValue: `/* unhandled delete: ${ts.SyntaxKind[operand.kind]} */` } };
587
+ }
588
+ /**
589
+ * A regex literal encodes to its PATTERN SOURCE as a plain string.
590
+ *
591
+ * The universal std regex functions (`regex_match`/`regex_find`/
592
+ * `regex_find_all`/`regex_replace`/`regex_replace_all`) all take the pattern
593
+ * as a string and model no flags at all, so `/abc/i` cannot round-trip
594
+ * faithfully. Dropping `i`/`g`/`m` genuinely changes behaviour, so it is
595
+ * reported as a behaviour-affecting warning rather than silently accepted.
596
+ */
597
+ encodeRegexLiteral(node) {
598
+ const re = splitRegexLiteral(node.text);
599
+ this.warnDroppedRegexFlags(re, "");
600
+ return { literal: { stringValue: re.source } };
601
+ }
508
602
  encodeBinary(node) {
509
603
  const op = node.operatorToken.kind;
510
604
  if (op === ts.SyntaxKind.EqualsToken) {
@@ -535,12 +629,14 @@ export class TsEncoder {
535
629
  ], stdRef.module);
536
630
  }
537
631
  if (op === ts.SyntaxKind.InKeyword) {
538
- return this.stdCall("contains_key", [
632
+ // Canonical name (#489): `contains_key` matched no compiler case; the
633
+ // field shape (map/key) was already right.
634
+ return this.stdCall("map_contains_key", [
539
635
  { name: "map", value: this.encodeExpr(node.right) },
540
636
  { name: "key", value: this.encodeExpr(node.left) },
541
637
  ], "std_collections");
542
638
  }
543
- this.warn(`Unhandled binary operator: ${ts.SyntaxKind[op]}`);
639
+ this.warn(`Unhandled binary operator: ${ts.SyntaxKind[op]}`, ts.SyntaxKind[op]);
544
640
  return { literal: { stringValue: `/* binary: ${ts.SyntaxKind[op]} */` } };
545
641
  }
546
642
  /// Decide how to encode a `+` whose operands aren't both provable strings.
@@ -600,8 +696,18 @@ export class TsEncoder {
600
696
  { name: "value", value: this.encodeExpr(node.operand) },
601
697
  ]);
602
698
  }
603
- this.warn(`Unhandled prefix operator: ${ts.SyntaxKind[op]}`);
604
- return this.encodeExpr(node.operand);
699
+ // Fail loud (#490). Unary `+` is a numeric COERCION: `+"5"` is the number
700
+ // 5, not the string "5". Ball has no universal coerce-to-number base
701
+ // function (only string_to_int/string_to_double, which are wrong for a
702
+ // non-string operand), so there is nothing faithful to route to. This used
703
+ // to be the one unhandled path that emitted no placeholder at all — it
704
+ // returned the untouched operand, silently changing the value's type. Now
705
+ // it warns as behaviour-affecting and emits a placeholder like every other
706
+ // unhandled construct, so `strict`/`strictBehaviorAffecting` reject it and
707
+ // a lenient encode is at least visibly wrong instead of invisibly wrong.
708
+ // See ts/encoder/ENCODER_CARVEOUTS.md.
709
+ this.warn(`Unhandled prefix operator: ${ts.SyntaxKind[op]}`, ts.SyntaxKind[op]);
710
+ return { literal: { stringValue: `/* unary: ${ts.SyntaxKind[op]} */` } };
605
711
  }
606
712
  encodePostfixUnary(node) {
607
713
  if (node.operator === ts.SyntaxKind.PlusPlusToken) {
@@ -614,6 +720,12 @@ export class TsEncoder {
614
720
  ]);
615
721
  }
616
722
  encodeCall(node) {
723
+ // Regex routing runs BEFORE the arguments are encoded: a regex literal in
724
+ // argument position must become a `from`/`right` pattern string, not a
725
+ // standalone (and separately flag-warned) expression.
726
+ const asRegex = this.tryEncodeRegexCall(node);
727
+ if (asRegex)
728
+ return asRegex;
617
729
  const args = node.arguments.map((a, i) => ({
618
730
  name: `arg${i}`,
619
731
  value: this.encodeExpr(a),
@@ -658,6 +770,28 @@ export class TsEncoder {
658
770
  }
659
771
  }
660
772
  const obj = this.encodeExpr(node.expression.expression);
773
+ // `.forEach(cb)` has no runtime std equivalent — there is no
774
+ // list_for_each / list_foreach in dart/shared/lib/std_collections.dart at
775
+ // all, and the Dart encoder never routes iteration through a std call.
776
+ // Pointing at the TS compiler's (dead) `list_foreach` case would create a
777
+ // construct with no Dart-side meaning, so it is desugared to the native
778
+ // for_each control-flow node instead (#489).
779
+ if (method === "forEach" && node.arguments.length === 1) {
780
+ return this.desugarForEach(obj, node.arguments[0]);
781
+ }
782
+ // `.flat()` -> flatMap(identity). `list_flat_map` is the canonical
783
+ // std_collections function; `list_flatten` existed nowhere.
784
+ if (method === "flat" && node.arguments.length === 0) {
785
+ return this.stdCall("list_flat_map", [
786
+ { name: "list", value: obj },
787
+ { name: "function", value: identityLambda("__ball_flat_item") },
788
+ ], "std_collections");
789
+ }
790
+ // `.splice(i, n)` only maps cleanly to list_remove_at when n is exactly 1.
791
+ if (method === "splice" && !isSingleElementSplice(node)) {
792
+ this.warn(`Array.splice is only representable as std_collections.list_remove_at ` +
793
+ `for the splice(index, 1) form`, "ArraySplice");
794
+ }
661
795
  // Map common JS/TS method calls to their Ball std equivalents.
662
796
  // The engine's std module uses snake_case names with type prefixes.
663
797
  const stdMethod = this.mapMethodToStd(method, args);
@@ -667,10 +801,12 @@ export class TsEncoder {
667
801
  ...stdMethod.extraFields(args),
668
802
  ], stdMethod.module ?? "std");
669
803
  }
670
- // Optional chaining call: obj?.method()
804
+ // Optional chaining call: obj?.method(). Canonical name + field shape
805
+ // (target/method), matching compileStdCall's `case "null_aware_call"`;
806
+ // "optional_call" with an `object` field matched no case at all (#489).
671
807
  if (node.expression.questionDotToken || node.questionDotToken) {
672
- return this.stdCall("optional_call", [
673
- { name: "object", value: obj },
808
+ return this.stdCall("null_aware_call", [
809
+ { name: "target", value: obj },
674
810
  { name: "method", value: { literal: { stringValue: method } } },
675
811
  ...args,
676
812
  ]);
@@ -722,6 +858,131 @@ export class TsEncoder {
722
858
  },
723
859
  };
724
860
  }
861
+ /**
862
+ * Lower `xs.forEach(cb)` to the native `std.for_each` control-flow node.
863
+ *
864
+ * Ball has no `list_for_each` base function in any module — iteration is
865
+ * control flow, not a runtime call (Core Invariant #4), which is exactly how
866
+ * the Dart encoder treats it. When `cb` is an inline single-parameter
867
+ * function its parameter becomes the loop variable and its body becomes the
868
+ * loop body, so the emitted IR is indistinguishable from the equivalent
869
+ * `for (const x of xs)`. Otherwise the callback is invoked per element
870
+ * through `std.invoke`.
871
+ */
872
+ desugarForEach(iterable, callback) {
873
+ const isInline = ts.isArrowFunction(callback) || ts.isFunctionExpression(callback);
874
+ if (isInline) {
875
+ const fn = callback;
876
+ if (fn.parameters.length > 1) {
877
+ // JS hands the callback (value, index, array); for_each yields only the
878
+ // element, so the extra parameters would silently be undefined.
879
+ this.warn(`Array.forEach callback takes ${fn.parameters.length} parameters; ` +
880
+ `std.for_each yields only the element`, "ForEachExtraParams");
881
+ }
882
+ const param = fn.parameters[0];
883
+ if (fn.parameters.length <= 1 && (param === undefined || ts.isIdentifier(param.name))) {
884
+ const varName = param && ts.isIdentifier(param.name)
885
+ ? param.name.text
886
+ : `__ball_foreach_${this.desugarCounter++}`;
887
+ return this.stdCall("for_each", [
888
+ { name: "variable", value: { literal: { stringValue: varName } } },
889
+ { name: "iterable", value: iterable },
890
+ { name: "body", value: this.encodeBody(fn.body) },
891
+ ]);
892
+ }
893
+ }
894
+ // A callback reference (or a destructuring parameter): bind a synthetic
895
+ // loop variable and invoke the callback with it.
896
+ const varName = `__ball_foreach_${this.desugarCounter++}`;
897
+ return this.stdCall("for_each", [
898
+ { name: "variable", value: { literal: { stringValue: varName } } },
899
+ { name: "iterable", value: iterable },
900
+ { name: "body", value: this.stdCall("invoke", [
901
+ { name: "callee", value: this.encodeExpr(callback) },
902
+ { name: "arg0", value: { reference: { name: varName } } },
903
+ ]) },
904
+ ]);
905
+ }
906
+ /**
907
+ * Route the JS regex API onto the universal `std` regex base functions
908
+ * (#490). All five have been declared and Dart-engine-implemented since the
909
+ * std module was written; no encoder in the repo had ever emitted one, so
910
+ * `tests/conformance/STD_COVERAGE.md` marked them Encoder-emittable ❌.
911
+ *
912
+ * /re/.test(s) -> std.regex_match(left: s, right: "re")
913
+ * /re/.exec(s) -> std.regex_find(left: s, right: "re")
914
+ * s.match(/re/g) -> std.regex_find_all(left: s, right: "re")
915
+ * s.replace(/re/, x) -> std.regex_replace(value: s, from: "re", to: x)
916
+ * s.replace(/re/g, x) -> std.regex_replace_all(...) (`g` is honoured)
917
+ * s.replaceAll(/re/, x)-> std.regex_replace_all(...)
918
+ *
919
+ * Returns undefined when the call is not a regex shape, so the ordinary
920
+ * method-mapping path takes over.
921
+ */
922
+ tryEncodeRegexCall(node) {
923
+ if (!ts.isPropertyAccessExpression(node.expression))
924
+ return undefined;
925
+ const method = node.expression.name.text;
926
+ const receiver = node.expression.expression;
927
+ const args = node.arguments;
928
+ // Every std name below is spelled as a LITERAL argument to stdCall (never
929
+ // computed from a ternary) so ts/compiler/test/std_name_consistency.test.ts
930
+ // can see, by static scan, exactly which functions this encoder can emit.
931
+ const receiverRe = regexLiteralOf(receiver);
932
+ if (receiverRe && (method === "test" || method === "exec") && args.length === 1) {
933
+ this.warnDroppedRegexFlags(receiverRe, "");
934
+ const fields = [
935
+ { name: "left", value: this.encodeExpr(args[0]) },
936
+ { name: "right", value: { literal: { stringValue: receiverRe.source } } },
937
+ ];
938
+ return method === "test"
939
+ ? this.stdCall("regex_match", fields)
940
+ : this.stdCall("regex_find", fields);
941
+ }
942
+ const argRe = args.length > 0 ? regexLiteralOf(args[0]) : undefined;
943
+ if (!argRe)
944
+ return undefined;
945
+ if ((method === "replace" || method === "replaceAll") && args.length === 2) {
946
+ const all = method === "replaceAll" || argRe.flags.includes("g");
947
+ this.warnDroppedRegexFlags(argRe, "g");
948
+ const fields = [
949
+ { name: "value", value: this.encodeExpr(receiver) },
950
+ { name: "from", value: { literal: { stringValue: argRe.source } } },
951
+ { name: "to", value: this.encodeExpr(args[1]) },
952
+ ];
953
+ return all
954
+ ? this.stdCall("regex_replace_all", fields)
955
+ : this.stdCall("regex_replace", fields);
956
+ }
957
+ if ((method === "match" || method === "matchAll") && args.length === 1) {
958
+ // JS `.match(/re/)` (no `g`) returns a match ARRAY with capture groups
959
+ // and an index; `regex_find` returns just the matched substring. Only the
960
+ // global form maps cleanly, so the non-global one is reported as lossy
961
+ // rather than quietly re-shaped.
962
+ const global = method === "matchAll" || argRe.flags.includes("g");
963
+ this.warnDroppedRegexFlags(argRe, "g");
964
+ if (!global) {
965
+ this.warn(`String.match without the "g" flag returns a match array with capture ` +
966
+ `groups; std.regex_find returns only the matched substring`, "RegexMatchNonGlobal");
967
+ }
968
+ const fields = [
969
+ { name: "left", value: this.encodeExpr(receiver) },
970
+ { name: "right", value: { literal: { stringValue: argRe.source } } },
971
+ ];
972
+ return global
973
+ ? this.stdCall("regex_find_all", fields)
974
+ : this.stdCall("regex_find", fields);
975
+ }
976
+ return undefined;
977
+ }
978
+ /** Warn about every regex flag that the chosen std routing does not honour. */
979
+ warnDroppedRegexFlags(re, implied) {
980
+ const dropped = [...re.flags].filter((f) => !implied.includes(f)).join("");
981
+ if (dropped.length === 0)
982
+ return;
983
+ this.warn(`Dropping regular-expression flags "${dropped}" from /${re.source}/${re.flags} — ` +
984
+ `the std regex_* functions take a bare pattern string and model no flags`, "RegularExpressionLiteralFlags");
985
+ }
725
986
  encodeLambda(node) {
726
987
  const params = node.parameters.map(p => ts.isIdentifier(p.name) ? p.name.text : p.name.getText());
727
988
  const body = node.body ? this.encodeBody(ts.isBlock(node.body) ? node.body : node.body) : undefined;
@@ -823,12 +1084,17 @@ export class TsEncoder {
823
1084
  const d = node.initializer.declarations[0];
824
1085
  varName = ts.isIdentifier(d.name) ? d.name.text : d.name.getText();
825
1086
  }
826
- const fnName = ts.isForInStatement(node) ? "for_in" : "for_each";
827
- return this.stdCall(fnName, [
1087
+ const fields = [
828
1088
  { name: "variable", value: { literal: { stringValue: varName } } },
829
1089
  { name: "iterable", value: this.encodeExpr(node.expression) },
830
1090
  { name: "body", value: this.encodeBody(ts.isBlock(node.statement) ? node.statement : ts.factory.createBlock([node.statement])) },
831
- ]);
1091
+ ];
1092
+ // Both names are spelled as literals rather than computed from a ternary so
1093
+ // that a static scan of this file (ts/compiler/test/std_name_consistency.
1094
+ // test.ts) can see every std function the encoder is able to emit.
1095
+ return ts.isForInStatement(node)
1096
+ ? this.stdCall("for_in", fields)
1097
+ : this.stdCall("for_each", fields);
832
1098
  }
833
1099
  encodeWhile(node) {
834
1100
  return this.stdCall("while", [
@@ -984,7 +1250,7 @@ export class TsEncoder {
984
1250
  // operator) has no Ball representation yet — warn (and throw in
985
1251
  // strict mode) rather than silently dropping it from the encoded
986
1252
  // class (#242).
987
- this.warn(`Unhandled class member name: ${member.name.getText()}`);
1253
+ this.warn(`Unhandled class member name: ${member.name.getText()}`, ts.SyntaxKind[member.name.kind]);
988
1254
  }
989
1255
  if (fn) {
990
1256
  fn.name = `${className}.${fn.name}`;
@@ -1078,29 +1344,39 @@ export class TsEncoder {
1078
1344
  /**
1079
1345
  * Map a JS/TS method name to its Ball std function equivalent.
1080
1346
  * Returns null if no mapping exists (falls through to generic method call).
1347
+ *
1348
+ * Every entry names BOTH the canonical std function AND the field names that
1349
+ * function's arguments carry. Names alone are not enough: `compileStdCall`
1350
+ * looks arguments up by field name, so `string_replace` fed `other`/`arg1`
1351
+ * (the old positional scheme) silently compiled to `''`, and
1352
+ * `string_substring` fed the same way crashed. The field names below were
1353
+ * each read off the corresponding `case` body in ts/compiler/src/compiler.ts
1354
+ * (#489).
1081
1355
  */
1082
1356
  mapMethodToStd(method, _args) {
1083
- // String methods
1357
+ /** Name the i-th argument after `names[i]`, falling back to `argN`. */
1358
+ const named = (...names) => (a) => a.map((x, i) => ({ name: names[i] ?? `arg${i}`, value: x.value }));
1359
+ // String methods: `value` is the receiver.
1084
1360
  const STR_METHODS = {
1085
- toUpperCase: "string_to_upper_case",
1086
- toLowerCase: "string_to_lower_case",
1087
- trim: "string_trim",
1088
- trimStart: "string_trim_left",
1089
- trimEnd: "string_trim_right",
1090
- includes: "string_contains",
1091
- indexOf: "string_index_of",
1092
- startsWith: "string_starts_with",
1093
- endsWith: "string_ends_with",
1094
- split: "string_split",
1095
- substring: "string_substring",
1096
- slice: "string_substring",
1097
- replace: "string_replace_first",
1098
- replaceAll: "string_replace_all",
1099
- padStart: "string_pad_left",
1100
- padEnd: "string_pad_right",
1101
- repeat: "string_repeat",
1102
- charAt: "string_char_at",
1103
- charCodeAt: "string_code_unit_at",
1361
+ toUpperCase: { fn: "string_to_upper", args: [] },
1362
+ toLowerCase: { fn: "string_to_lower", args: [] },
1363
+ trim: { fn: "string_trim", args: [] },
1364
+ trimStart: { fn: "string_trim_start", args: [] },
1365
+ trimEnd: { fn: "string_trim_end", args: [] },
1366
+ includes: { fn: "string_contains", args: ["other"] },
1367
+ indexOf: { fn: "string_index_of", args: ["pattern", "start"] },
1368
+ startsWith: { fn: "string_starts_with", args: ["other"] },
1369
+ endsWith: { fn: "string_ends_with", args: ["other"] },
1370
+ split: { fn: "string_split", args: ["separator"] },
1371
+ substring: { fn: "string_substring", args: ["start", "end"] },
1372
+ slice: { fn: "string_substring", args: ["start", "end"] },
1373
+ replace: { fn: "string_replace", args: ["from", "to"] },
1374
+ replaceAll: { fn: "string_replace_all", args: ["from", "to"] },
1375
+ padStart: { fn: "string_pad_left", args: ["width", "padding"] },
1376
+ padEnd: { fn: "string_pad_right", args: ["width", "padding"] },
1377
+ repeat: { fn: "string_repeat", args: ["count"] },
1378
+ charAt: { fn: "string_char_at", args: ["index"] },
1379
+ charCodeAt: { fn: "string_code_unit_at", args: ["index"] },
1104
1380
  };
1105
1381
  // hasOwnProperty (not `in`) — `in` also matches names inherited from
1106
1382
  // Object.prototype (toString, valueOf, hasOwnProperty, ...). A bare `in`
@@ -1111,47 +1387,37 @@ export class TsEncoder {
1111
1387
  // corrupt call.function (a function object, not "to_string") in the Ball
1112
1388
  // IR. Found via coverage analysis of the always-dead toString branch.
1113
1389
  if (Object.prototype.hasOwnProperty.call(STR_METHODS, method)) {
1114
- return {
1115
- fn: STR_METHODS[method],
1116
- selfName: "value",
1117
- extraFields: (a) => a.map((x, i) => ({
1118
- name: i === 0 ? "other" : `arg${i}`,
1119
- value: x.value,
1120
- })),
1121
- };
1390
+ const m = STR_METHODS[method];
1391
+ return { fn: m.fn, selfName: "value", extraFields: named(...m.args) };
1122
1392
  }
1123
- // Array methods
1393
+ // Array methods: `list` is the receiver. All of them live in
1394
+ // std_collections — the module the Dart reference declares them in.
1124
1395
  const ARR_METHODS = {
1125
- push: { fn: "list_add" },
1126
- pop: { fn: "list_remove_last" },
1127
- indexOf: { fn: "list_index_of", mod: "std_collections" },
1128
- includes: { fn: "list_contains", mod: "std_collections" },
1129
- join: { fn: "list_join", mod: "std_collections" },
1130
- reverse: { fn: "list_reversed", mod: "std_collections" },
1131
- slice: { fn: "list_sublist", mod: "std_collections" },
1132
- splice: { fn: "list_remove_at" },
1133
- sort: { fn: "list_sort", mod: "std_collections" },
1134
- map: { fn: "list_map", mod: "std_collections" },
1135
- filter: { fn: "list_where", mod: "std_collections" },
1136
- forEach: { fn: "list_for_each", mod: "std_collections" },
1137
- reduce: { fn: "list_fold", mod: "std_collections" },
1138
- find: { fn: "list_first_where", mod: "std_collections" },
1139
- flat: { fn: "list_flatten", mod: "std_collections" },
1140
- concat: { fn: "list_concat", mod: "std_collections" },
1141
- every: { fn: "list_every", mod: "std_collections" },
1142
- some: { fn: "list_any", mod: "std_collections" },
1396
+ push: { fn: "list_push", args: ["value"] },
1397
+ pop: { fn: "list_pop", args: [] },
1398
+ indexOf: { fn: "list_index_of", args: ["value"] },
1399
+ includes: { fn: "list_contains", args: ["value"] },
1400
+ join: { fn: "list_join", args: ["separator"] },
1401
+ reverse: { fn: "list_reverse", args: [] },
1402
+ slice: { fn: "list_slice", args: ["start", "end"] },
1403
+ splice: { fn: "list_remove_at", args: ["index"] },
1404
+ sort: { fn: "list_sort", args: ["comparator"] },
1405
+ map: { fn: "list_map", args: ["function"] },
1406
+ filter: { fn: "list_filter", args: ["function"] },
1407
+ reduce: { fn: "list_reduce", args: ["function"] },
1408
+ find: { fn: "list_find", args: ["function"] },
1409
+ concat: { fn: "list_concat", args: ["other"] },
1410
+ every: { fn: "list_all", args: ["function"] },
1411
+ some: { fn: "list_any", args: ["function"] },
1143
1412
  };
1144
1413
  // Same hasOwnProperty rationale as STR_METHODS above.
1145
1414
  if (Object.prototype.hasOwnProperty.call(ARR_METHODS, method)) {
1146
1415
  const m = ARR_METHODS[method];
1147
1416
  return {
1148
1417
  fn: m.fn,
1149
- module: m.mod,
1418
+ module: "std_collections",
1150
1419
  selfName: "list",
1151
- extraFields: (a) => a.map((x, i) => ({
1152
- name: i === 0 ? "value" : `arg${i}`,
1153
- value: x.value,
1154
- })),
1420
+ extraFields: named(...m.args),
1155
1421
  };
1156
1422
  }
1157
1423
  // toString
@@ -1207,7 +1473,16 @@ export class TsEncoder {
1207
1473
  nullLiteral() {
1208
1474
  return { literal: {} };
1209
1475
  }
1210
- warn(msg) {
1476
+ /**
1477
+ * Record a warning, and turn it into a hard error when the caller asked for
1478
+ * one.
1479
+ *
1480
+ * `kind` is the TS SyntaxKind name (or another marker) the warning is about;
1481
+ * it is what `strictBehaviorAffecting` filters on, so the distinction between
1482
+ * "erased a type alias" and "silently changed what this program computes" is
1483
+ * mechanical rather than a substring match on the message.
1484
+ */
1485
+ warn(msg, kind) {
1211
1486
  this.warnings.push(msg);
1212
1487
  // In strict mode an unhandled node is a hard error: the encoder cannot
1213
1488
  // faithfully represent the construct and would otherwise emit a
@@ -1215,11 +1490,49 @@ export class TsEncoder {
1215
1490
  if (this.strict) {
1216
1491
  throw new EncodeError(msg, this.getWarnings());
1217
1492
  }
1493
+ if (this.strictBehaviorAffecting && !(kind !== undefined && ERASURE_ONLY_KINDS.has(kind))) {
1494
+ throw new EncodeError(msg, this.getWarnings());
1495
+ }
1218
1496
  }
1219
1497
  getWarnings() {
1220
1498
  return [...this.warnings];
1221
1499
  }
1222
1500
  }
1501
+ /**
1502
+ * Split a regex literal's raw text (`/source/flags`) into its two parts.
1503
+ * The closing delimiter is the LAST unescaped `/`, so a pattern containing an
1504
+ * escaped slash (`/a\/b/g`) splits correctly.
1505
+ */
1506
+ function splitRegexLiteral(text) {
1507
+ const lastSlash = text.lastIndexOf("/");
1508
+ return {
1509
+ source: text.slice(1, lastSlash),
1510
+ flags: text.slice(lastSlash + 1),
1511
+ };
1512
+ }
1513
+ /** The `{ source, flags }` of `node` when it is a regex literal, else undefined. */
1514
+ function regexLiteralOf(node) {
1515
+ if (ts.isParenthesizedExpression(node))
1516
+ return regexLiteralOf(node.expression);
1517
+ return ts.isRegularExpressionLiteral(node) ? splitRegexLiteral(node.text) : undefined;
1518
+ }
1519
+ /** A `(x) => x` lambda, in the encoder's own lambda shape. */
1520
+ function identityLambda(param) {
1521
+ return {
1522
+ lambda: {
1523
+ name: "",
1524
+ body: { reference: { name: param } },
1525
+ metadata: { params: [{ name: param }] },
1526
+ },
1527
+ };
1528
+ }
1529
+ /** True for the `xs.splice(i, 1)` form, the only one list_remove_at models. */
1530
+ function isSingleElementSplice(node) {
1531
+ if (node.arguments.length !== 2)
1532
+ return false;
1533
+ const count = node.arguments[1];
1534
+ return ts.isNumericLiteral(count) && count.text === "1";
1535
+ }
1223
1536
  /// Thrown by `encode(..., { strict: true })` when the encoder hits a TS
1224
1537
  /// construct it cannot represent. Carries the full accumulated warning list.
1225
1538
  export class EncodeError extends Error {