@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.d.ts +82 -0
- package/dist/encoder.d.ts.map +1 -1
- package/dist/encoder.js +384 -71
- package/dist/encoder.js.map +1 -1
- package/package.json +1 -1
- package/src/encoder.ts +433 -75
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
|
-
|
|
394
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
604
|
-
|
|
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("
|
|
673
|
-
{ name: "
|
|
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
|
|
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
|
-
|
|
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: "
|
|
1086
|
-
toLowerCase: "
|
|
1087
|
-
trim: "string_trim",
|
|
1088
|
-
trimStart: "
|
|
1089
|
-
trimEnd: "
|
|
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: "
|
|
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
|
-
|
|
1115
|
-
|
|
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: "
|
|
1126
|
-
pop: { fn: "
|
|
1127
|
-
indexOf: { fn: "list_index_of",
|
|
1128
|
-
includes: { fn: "list_contains",
|
|
1129
|
-
join: { fn: "list_join",
|
|
1130
|
-
reverse: { fn: "
|
|
1131
|
-
slice: { fn: "
|
|
1132
|
-
splice: { fn: "list_remove_at" },
|
|
1133
|
-
sort: { fn: "list_sort",
|
|
1134
|
-
map: { fn: "list_map",
|
|
1135
|
-
filter: { fn: "
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
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:
|
|
1418
|
+
module: "std_collections",
|
|
1150
1419
|
selfName: "list",
|
|
1151
|
-
extraFields: (
|
|
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
|
-
|
|
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 {
|