@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/src/encoder.ts
CHANGED
|
@@ -8,11 +8,43 @@ import type {
|
|
|
8
8
|
export interface EncodeOptions {
|
|
9
9
|
moduleName?: string;
|
|
10
10
|
entryFunction?: string;
|
|
11
|
+
/** Throw an `EncodeError` on ANY unhandled construct, lossy or not. */
|
|
11
12
|
strict?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Throw an `EncodeError` only for warnings that change what the program
|
|
15
|
+
* computes, while tolerating erasure-only ones (a type alias, an empty
|
|
16
|
+
* statement, a `satisfies` clause — none of which contribute to the
|
|
17
|
+
* program's value).
|
|
18
|
+
*
|
|
19
|
+
* `strict` is the blunt instrument: it rejects a file for a stray `;`.
|
|
20
|
+
* This mode is the one worth running over third-party code, because every
|
|
21
|
+
* warning it raises is a real semantic difference between the source and
|
|
22
|
+
* the encoded Ball program.
|
|
23
|
+
*/
|
|
24
|
+
strictBehaviorAffecting?: boolean;
|
|
12
25
|
}
|
|
13
26
|
|
|
14
27
|
type StdRef = { module: string; function: string };
|
|
15
28
|
|
|
29
|
+
/**
|
|
30
|
+
* TS constructs whose only role is type-level or syntactic: dropping them
|
|
31
|
+
* cannot change what the program computes, so they never trip
|
|
32
|
+
* `strictBehaviorAffecting`. Everything else that reaches `warn()` does.
|
|
33
|
+
*
|
|
34
|
+
* Only kinds that actually REACH `warn()` belong here. A type alias or an
|
|
35
|
+
* interface at top level is encoded (`typeAliases[]` / `typeDefs[]`) and never
|
|
36
|
+
* warns; nested inside a function body it has no statement branch, so it hits
|
|
37
|
+
* the "Unhandled statement kind" warn — which is the case this set exempts.
|
|
38
|
+
* `SatisfiesExpression` is deliberately absent: `x satisfies T` is erased in
|
|
39
|
+
* `encodeExpr` exactly like an `as` cast, silently and without a warning, so an
|
|
40
|
+
* entry for it would be dead.
|
|
41
|
+
*/
|
|
42
|
+
const ERASURE_ONLY_KINDS: ReadonlySet<string> = new Set([
|
|
43
|
+
"TypeAliasDeclaration",
|
|
44
|
+
"InterfaceDeclaration",
|
|
45
|
+
"EmptyStatement",
|
|
46
|
+
]);
|
|
47
|
+
|
|
16
48
|
const BINARY_OPS: Record<number, StdRef> = {
|
|
17
49
|
[ts.SyntaxKind.PlusToken]: { module: "std", function: "add" },
|
|
18
50
|
[ts.SyntaxKind.MinusToken]: { module: "std", function: "subtract" },
|
|
@@ -36,6 +68,10 @@ const BINARY_OPS: Record<number, StdRef> = {
|
|
|
36
68
|
[ts.SyntaxKind.BarBarToken]: { module: "std", function: "or" },
|
|
37
69
|
[ts.SyntaxKind.QuestionQuestionToken]: { module: "std", function: "null_coalesce" },
|
|
38
70
|
[ts.SyntaxKind.InstanceOfKeyword]: { module: "std", function: "is" },
|
|
71
|
+
// `**` used to fall through to the "Unhandled binary operator" placeholder
|
|
72
|
+
// (#490). math_pow is a declared, Dart-engine-implemented std base function
|
|
73
|
+
// that simply had no encoder emitting it.
|
|
74
|
+
[ts.SyntaxKind.AsteriskAsteriskToken]: { module: "std", function: "math_pow" },
|
|
39
75
|
};
|
|
40
76
|
|
|
41
77
|
const COMPOUND_OPS: Record<number, string> = {
|
|
@@ -50,12 +86,16 @@ const COMPOUND_OPS: Record<number, string> = {
|
|
|
50
86
|
[ts.SyntaxKind.LessThanLessThanEqualsToken]: "<<=",
|
|
51
87
|
[ts.SyntaxKind.GreaterThanGreaterThanEqualsToken]: ">>=",
|
|
52
88
|
[ts.SyntaxKind.QuestionQuestionEqualsToken]: "??=",
|
|
89
|
+
[ts.SyntaxKind.AsteriskAsteriskEqualsToken]: "**=",
|
|
53
90
|
};
|
|
54
91
|
|
|
55
92
|
export class TsEncoder {
|
|
56
93
|
private stdFunctions = new Set<string>();
|
|
57
94
|
private warnings: string[] = [];
|
|
58
95
|
private strict = false;
|
|
96
|
+
private strictBehaviorAffecting = false;
|
|
97
|
+
/** Monotonic counter for the synthetic loop variables `.forEach` desugars to. */
|
|
98
|
+
private desugarCounter = 0;
|
|
59
99
|
|
|
60
100
|
// Maps an operator lexeme to the canonical, language-agnostic Ball method
|
|
61
101
|
// name every compiler already understands for operator overloads — mirrors
|
|
@@ -90,6 +130,7 @@ export class TsEncoder {
|
|
|
90
130
|
const modName = options.moduleName ?? "main";
|
|
91
131
|
const entryFn = options.entryFunction ?? "main";
|
|
92
132
|
this.strict = options.strict ?? false;
|
|
133
|
+
this.strictBehaviorAffecting = options.strictBehaviorAffecting ?? false;
|
|
93
134
|
|
|
94
135
|
const sourceFile = ts.createSourceFile(
|
|
95
136
|
"input.ts", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS
|
|
@@ -353,7 +394,7 @@ export class TsEncoder {
|
|
|
353
394
|
},
|
|
354
395
|
}];
|
|
355
396
|
}
|
|
356
|
-
this.warn(`Unhandled statement kind: ${ts.SyntaxKind[node.kind]}
|
|
397
|
+
this.warn(`Unhandled statement kind: ${ts.SyntaxKind[node.kind]}`, ts.SyntaxKind[node.kind]);
|
|
357
398
|
return [{ expression: { literal: { stringValue: `/* unhandled: ${ts.SyntaxKind[node.kind]} */` } } }];
|
|
358
399
|
}
|
|
359
400
|
|
|
@@ -377,6 +418,14 @@ export class TsEncoder {
|
|
|
377
418
|
if (node.kind === ts.SyntaxKind.NullKeyword || node.kind === ts.SyntaxKind.UndefinedKeyword) {
|
|
378
419
|
return this.nullLiteral();
|
|
379
420
|
}
|
|
421
|
+
if (node.kind === ts.SyntaxKind.SuperKeyword) {
|
|
422
|
+
// Mirrors dart/encoder's SuperExpression handling exactly: a bare
|
|
423
|
+
// reference named "super". ts/compiler already resolves that reference
|
|
424
|
+
// back to a real super-chain call, and conformance fixture
|
|
425
|
+
// 107_method_override_super proves the shape end to end — the TS encoder
|
|
426
|
+
// was simply the one place that never produced it (#490).
|
|
427
|
+
return { reference: { name: "super" } };
|
|
428
|
+
}
|
|
380
429
|
if (node.kind === ts.SyntaxKind.ThisKeyword) {
|
|
381
430
|
// Mirrors dart/encoder's ThisExpression handling exactly: a bare
|
|
382
431
|
// reference named "self" (not a field-access base, not a special
|
|
@@ -409,8 +458,12 @@ export class TsEncoder {
|
|
|
409
458
|
}
|
|
410
459
|
if (ts.isPropertyAccessExpression(node)) {
|
|
411
460
|
if (node.questionDotToken) {
|
|
412
|
-
|
|
413
|
-
|
|
461
|
+
// Canonical name + field shape: compileStdCall's `case
|
|
462
|
+
// "null_aware_access"` reads `target` and a literal-string `field`
|
|
463
|
+
// (and so does dart/compiler). The old "optional_access" spelling with
|
|
464
|
+
// an `object` field existed in no compiler or engine (#489).
|
|
465
|
+
return this.stdCall("null_aware_access", [
|
|
466
|
+
{ name: "target", value: this.encodeExpr(node.expression) },
|
|
414
467
|
{ name: "field", value: { literal: { stringValue: node.name.text } } },
|
|
415
468
|
]);
|
|
416
469
|
}
|
|
@@ -418,9 +471,13 @@ export class TsEncoder {
|
|
|
418
471
|
}
|
|
419
472
|
if (ts.isElementAccessExpression(node)) {
|
|
420
473
|
if (node.questionDotToken) {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
474
|
+
// Optional ELEMENT access is a different shape: the index is an
|
|
475
|
+
// arbitrary expression, not a literal field name, so it routes through
|
|
476
|
+
// the compiler's dedicated `null_aware_index` case rather than being
|
|
477
|
+
// conflated with property access (#489).
|
|
478
|
+
return this.stdCall("null_aware_index", [
|
|
479
|
+
{ name: "target", value: this.encodeExpr(node.expression) },
|
|
480
|
+
{ name: "index", value: this.encodeExpr(node.argumentExpression) },
|
|
424
481
|
]);
|
|
425
482
|
}
|
|
426
483
|
return this.stdCall("index", [
|
|
@@ -518,10 +575,66 @@ export class TsEncoder {
|
|
|
518
575
|
if (ts.isTaggedTemplateExpression(node)) {
|
|
519
576
|
return this.encodeTaggedTemplate(node);
|
|
520
577
|
}
|
|
521
|
-
|
|
578
|
+
if (ts.isSatisfiesExpression(node)) {
|
|
579
|
+
// `x satisfies T` is a pure type-level assertion — erase it, exactly as
|
|
580
|
+
// `as`/`<T>` casts are erased above.
|
|
581
|
+
return this.encodeExpr(node.expression);
|
|
582
|
+
}
|
|
583
|
+
if (ts.isRegularExpressionLiteral(node)) {
|
|
584
|
+
return this.encodeRegexLiteral(node);
|
|
585
|
+
}
|
|
586
|
+
if (ts.isDeleteExpression(node)) {
|
|
587
|
+
return this.encodeDelete(node);
|
|
588
|
+
}
|
|
589
|
+
this.warn(`Unhandled expression kind: ${ts.SyntaxKind[node.kind]}`, ts.SyntaxKind[node.kind]);
|
|
522
590
|
return { literal: { stringValue: `/* unhandled: ${ts.SyntaxKind[node.kind]} */` } };
|
|
523
591
|
}
|
|
524
592
|
|
|
593
|
+
/**
|
|
594
|
+
* `delete obj.a` / `delete obj[k]` -> `std_collections.map_delete(map, key)`.
|
|
595
|
+
*
|
|
596
|
+
* `map_delete` has been declared and Dart-engine-implemented all along
|
|
597
|
+
* (dart/shared/lib/std_collections.dart) — no encoder in the repo had ever
|
|
598
|
+
* emitted it, so `delete` fell through to a placeholder literal (#490).
|
|
599
|
+
*/
|
|
600
|
+
private encodeDelete(node: ts.DeleteExpression): Expression {
|
|
601
|
+
const operand = node.expression;
|
|
602
|
+
if (ts.isPropertyAccessExpression(operand)) {
|
|
603
|
+
return this.stdCall("map_delete", [
|
|
604
|
+
{ name: "map", value: this.encodeExpr(operand.expression) },
|
|
605
|
+
{ name: "key", value: { literal: { stringValue: operand.name.text } } },
|
|
606
|
+
], "std_collections");
|
|
607
|
+
}
|
|
608
|
+
if (ts.isElementAccessExpression(operand)) {
|
|
609
|
+
return this.stdCall("map_delete", [
|
|
610
|
+
{ name: "map", value: this.encodeExpr(operand.expression) },
|
|
611
|
+
{ name: "key", value: this.encodeExpr(operand.argumentExpression) },
|
|
612
|
+
], "std_collections");
|
|
613
|
+
}
|
|
614
|
+
// `delete someIdentifier` is a no-op on a binding in sloppy mode and a
|
|
615
|
+
// SyntaxError in strict mode — there is nothing faithful to encode.
|
|
616
|
+
this.warn(
|
|
617
|
+
`Unhandled delete target: ${ts.SyntaxKind[operand.kind]}`,
|
|
618
|
+
ts.SyntaxKind[operand.kind],
|
|
619
|
+
);
|
|
620
|
+
return { literal: { stringValue: `/* unhandled delete: ${ts.SyntaxKind[operand.kind]} */` } };
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* A regex literal encodes to its PATTERN SOURCE as a plain string.
|
|
625
|
+
*
|
|
626
|
+
* The universal std regex functions (`regex_match`/`regex_find`/
|
|
627
|
+
* `regex_find_all`/`regex_replace`/`regex_replace_all`) all take the pattern
|
|
628
|
+
* as a string and model no flags at all, so `/abc/i` cannot round-trip
|
|
629
|
+
* faithfully. Dropping `i`/`g`/`m` genuinely changes behaviour, so it is
|
|
630
|
+
* reported as a behaviour-affecting warning rather than silently accepted.
|
|
631
|
+
*/
|
|
632
|
+
private encodeRegexLiteral(node: ts.RegularExpressionLiteral): Expression {
|
|
633
|
+
const re = splitRegexLiteral(node.text);
|
|
634
|
+
this.warnDroppedRegexFlags(re, "");
|
|
635
|
+
return { literal: { stringValue: re.source } };
|
|
636
|
+
}
|
|
637
|
+
|
|
525
638
|
private encodeBinary(node: ts.BinaryExpression): Expression {
|
|
526
639
|
const op = node.operatorToken.kind;
|
|
527
640
|
|
|
@@ -557,13 +670,15 @@ export class TsEncoder {
|
|
|
557
670
|
}
|
|
558
671
|
|
|
559
672
|
if (op === ts.SyntaxKind.InKeyword) {
|
|
560
|
-
|
|
673
|
+
// Canonical name (#489): `contains_key` matched no compiler case; the
|
|
674
|
+
// field shape (map/key) was already right.
|
|
675
|
+
return this.stdCall("map_contains_key", [
|
|
561
676
|
{ name: "map", value: this.encodeExpr(node.right) },
|
|
562
677
|
{ name: "key", value: this.encodeExpr(node.left) },
|
|
563
678
|
], "std_collections");
|
|
564
679
|
}
|
|
565
680
|
|
|
566
|
-
this.warn(`Unhandled binary operator: ${ts.SyntaxKind[op]}
|
|
681
|
+
this.warn(`Unhandled binary operator: ${ts.SyntaxKind[op]}`, ts.SyntaxKind[op]);
|
|
567
682
|
return { literal: { stringValue: `/* binary: ${ts.SyntaxKind[op]} */` } };
|
|
568
683
|
}
|
|
569
684
|
|
|
@@ -625,8 +740,18 @@ export class TsEncoder {
|
|
|
625
740
|
{ name: "value", value: this.encodeExpr(node.operand) },
|
|
626
741
|
]);
|
|
627
742
|
}
|
|
628
|
-
|
|
629
|
-
|
|
743
|
+
// Fail loud (#490). Unary `+` is a numeric COERCION: `+"5"` is the number
|
|
744
|
+
// 5, not the string "5". Ball has no universal coerce-to-number base
|
|
745
|
+
// function (only string_to_int/string_to_double, which are wrong for a
|
|
746
|
+
// non-string operand), so there is nothing faithful to route to. This used
|
|
747
|
+
// to be the one unhandled path that emitted no placeholder at all — it
|
|
748
|
+
// returned the untouched operand, silently changing the value's type. Now
|
|
749
|
+
// it warns as behaviour-affecting and emits a placeholder like every other
|
|
750
|
+
// unhandled construct, so `strict`/`strictBehaviorAffecting` reject it and
|
|
751
|
+
// a lenient encode is at least visibly wrong instead of invisibly wrong.
|
|
752
|
+
// See ts/encoder/ENCODER_CARVEOUTS.md.
|
|
753
|
+
this.warn(`Unhandled prefix operator: ${ts.SyntaxKind[op]}`, ts.SyntaxKind[op]);
|
|
754
|
+
return { literal: { stringValue: `/* unary: ${ts.SyntaxKind[op]} */` } };
|
|
630
755
|
}
|
|
631
756
|
|
|
632
757
|
private encodePostfixUnary(node: ts.PostfixUnaryExpression): Expression {
|
|
@@ -641,6 +766,12 @@ export class TsEncoder {
|
|
|
641
766
|
}
|
|
642
767
|
|
|
643
768
|
private encodeCall(node: ts.CallExpression): Expression {
|
|
769
|
+
// Regex routing runs BEFORE the arguments are encoded: a regex literal in
|
|
770
|
+
// argument position must become a `from`/`right` pattern string, not a
|
|
771
|
+
// standalone (and separately flag-warned) expression.
|
|
772
|
+
const asRegex = this.tryEncodeRegexCall(node);
|
|
773
|
+
if (asRegex) return asRegex;
|
|
774
|
+
|
|
644
775
|
const args = node.arguments.map((a, i) => ({
|
|
645
776
|
name: `arg${i}`,
|
|
646
777
|
value: this.encodeExpr(a),
|
|
@@ -689,6 +820,32 @@ export class TsEncoder {
|
|
|
689
820
|
|
|
690
821
|
const obj = this.encodeExpr(node.expression.expression);
|
|
691
822
|
|
|
823
|
+
// `.forEach(cb)` has no runtime std equivalent — there is no
|
|
824
|
+
// list_for_each / list_foreach in dart/shared/lib/std_collections.dart at
|
|
825
|
+
// all, and the Dart encoder never routes iteration through a std call.
|
|
826
|
+
// Pointing at the TS compiler's (dead) `list_foreach` case would create a
|
|
827
|
+
// construct with no Dart-side meaning, so it is desugared to the native
|
|
828
|
+
// for_each control-flow node instead (#489).
|
|
829
|
+
if (method === "forEach" && node.arguments.length === 1) {
|
|
830
|
+
return this.desugarForEach(obj, node.arguments[0]);
|
|
831
|
+
}
|
|
832
|
+
// `.flat()` -> flatMap(identity). `list_flat_map` is the canonical
|
|
833
|
+
// std_collections function; `list_flatten` existed nowhere.
|
|
834
|
+
if (method === "flat" && node.arguments.length === 0) {
|
|
835
|
+
return this.stdCall("list_flat_map", [
|
|
836
|
+
{ name: "list", value: obj },
|
|
837
|
+
{ name: "function", value: identityLambda("__ball_flat_item") },
|
|
838
|
+
], "std_collections");
|
|
839
|
+
}
|
|
840
|
+
// `.splice(i, n)` only maps cleanly to list_remove_at when n is exactly 1.
|
|
841
|
+
if (method === "splice" && !isSingleElementSplice(node)) {
|
|
842
|
+
this.warn(
|
|
843
|
+
`Array.splice is only representable as std_collections.list_remove_at ` +
|
|
844
|
+
`for the splice(index, 1) form`,
|
|
845
|
+
"ArraySplice",
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
|
|
692
849
|
// Map common JS/TS method calls to their Ball std equivalents.
|
|
693
850
|
// The engine's std module uses snake_case names with type prefixes.
|
|
694
851
|
const stdMethod = this.mapMethodToStd(method, args);
|
|
@@ -699,10 +856,12 @@ export class TsEncoder {
|
|
|
699
856
|
], stdMethod.module ?? "std");
|
|
700
857
|
}
|
|
701
858
|
|
|
702
|
-
// Optional chaining call: obj?.method()
|
|
859
|
+
// Optional chaining call: obj?.method(). Canonical name + field shape
|
|
860
|
+
// (target/method), matching compileStdCall's `case "null_aware_call"`;
|
|
861
|
+
// "optional_call" with an `object` field matched no case at all (#489).
|
|
703
862
|
if (node.expression.questionDotToken || node.questionDotToken) {
|
|
704
|
-
return this.stdCall("
|
|
705
|
-
{ name: "
|
|
863
|
+
return this.stdCall("null_aware_call", [
|
|
864
|
+
{ name: "target", value: obj },
|
|
706
865
|
{ name: "method", value: { literal: { stringValue: method } } },
|
|
707
866
|
...args,
|
|
708
867
|
]);
|
|
@@ -758,6 +917,145 @@ export class TsEncoder {
|
|
|
758
917
|
};
|
|
759
918
|
}
|
|
760
919
|
|
|
920
|
+
/**
|
|
921
|
+
* Lower `xs.forEach(cb)` to the native `std.for_each` control-flow node.
|
|
922
|
+
*
|
|
923
|
+
* Ball has no `list_for_each` base function in any module — iteration is
|
|
924
|
+
* control flow, not a runtime call (Core Invariant #4), which is exactly how
|
|
925
|
+
* the Dart encoder treats it. When `cb` is an inline single-parameter
|
|
926
|
+
* function its parameter becomes the loop variable and its body becomes the
|
|
927
|
+
* loop body, so the emitted IR is indistinguishable from the equivalent
|
|
928
|
+
* `for (const x of xs)`. Otherwise the callback is invoked per element
|
|
929
|
+
* through `std.invoke`.
|
|
930
|
+
*/
|
|
931
|
+
private desugarForEach(iterable: Expression, callback: ts.Expression): Expression {
|
|
932
|
+
const isInline = ts.isArrowFunction(callback) || ts.isFunctionExpression(callback);
|
|
933
|
+
if (isInline) {
|
|
934
|
+
const fn = callback as ts.ArrowFunction | ts.FunctionExpression;
|
|
935
|
+
if (fn.parameters.length > 1) {
|
|
936
|
+
// JS hands the callback (value, index, array); for_each yields only the
|
|
937
|
+
// element, so the extra parameters would silently be undefined.
|
|
938
|
+
this.warn(
|
|
939
|
+
`Array.forEach callback takes ${fn.parameters.length} parameters; ` +
|
|
940
|
+
`std.for_each yields only the element`,
|
|
941
|
+
"ForEachExtraParams",
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
const param = fn.parameters[0];
|
|
945
|
+
if (fn.parameters.length <= 1 && (param === undefined || ts.isIdentifier(param.name))) {
|
|
946
|
+
const varName = param && ts.isIdentifier(param.name)
|
|
947
|
+
? param.name.text
|
|
948
|
+
: `__ball_foreach_${this.desugarCounter++}`;
|
|
949
|
+
return this.stdCall("for_each", [
|
|
950
|
+
{ name: "variable", value: { literal: { stringValue: varName } } },
|
|
951
|
+
{ name: "iterable", value: iterable },
|
|
952
|
+
{ name: "body", value: this.encodeBody(fn.body) },
|
|
953
|
+
]);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
// A callback reference (or a destructuring parameter): bind a synthetic
|
|
957
|
+
// loop variable and invoke the callback with it.
|
|
958
|
+
const varName = `__ball_foreach_${this.desugarCounter++}`;
|
|
959
|
+
return this.stdCall("for_each", [
|
|
960
|
+
{ name: "variable", value: { literal: { stringValue: varName } } },
|
|
961
|
+
{ name: "iterable", value: iterable },
|
|
962
|
+
{ name: "body", value: this.stdCall("invoke", [
|
|
963
|
+
{ name: "callee", value: this.encodeExpr(callback) },
|
|
964
|
+
{ name: "arg0", value: { reference: { name: varName } } },
|
|
965
|
+
]) },
|
|
966
|
+
]);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Route the JS regex API onto the universal `std` regex base functions
|
|
971
|
+
* (#490). All five have been declared and Dart-engine-implemented since the
|
|
972
|
+
* std module was written; no encoder in the repo had ever emitted one, so
|
|
973
|
+
* `tests/conformance/STD_COVERAGE.md` marked them Encoder-emittable ❌.
|
|
974
|
+
*
|
|
975
|
+
* /re/.test(s) -> std.regex_match(left: s, right: "re")
|
|
976
|
+
* /re/.exec(s) -> std.regex_find(left: s, right: "re")
|
|
977
|
+
* s.match(/re/g) -> std.regex_find_all(left: s, right: "re")
|
|
978
|
+
* s.replace(/re/, x) -> std.regex_replace(value: s, from: "re", to: x)
|
|
979
|
+
* s.replace(/re/g, x) -> std.regex_replace_all(...) (`g` is honoured)
|
|
980
|
+
* s.replaceAll(/re/, x)-> std.regex_replace_all(...)
|
|
981
|
+
*
|
|
982
|
+
* Returns undefined when the call is not a regex shape, so the ordinary
|
|
983
|
+
* method-mapping path takes over.
|
|
984
|
+
*/
|
|
985
|
+
private tryEncodeRegexCall(node: ts.CallExpression): Expression | undefined {
|
|
986
|
+
if (!ts.isPropertyAccessExpression(node.expression)) return undefined;
|
|
987
|
+
const method = node.expression.name.text;
|
|
988
|
+
const receiver = node.expression.expression;
|
|
989
|
+
const args = node.arguments;
|
|
990
|
+
|
|
991
|
+
// Every std name below is spelled as a LITERAL argument to stdCall (never
|
|
992
|
+
// computed from a ternary) so ts/compiler/test/std_name_consistency.test.ts
|
|
993
|
+
// can see, by static scan, exactly which functions this encoder can emit.
|
|
994
|
+
const receiverRe = regexLiteralOf(receiver);
|
|
995
|
+
if (receiverRe && (method === "test" || method === "exec") && args.length === 1) {
|
|
996
|
+
this.warnDroppedRegexFlags(receiverRe, "");
|
|
997
|
+
const fields: FieldValuePair[] = [
|
|
998
|
+
{ name: "left", value: this.encodeExpr(args[0]) },
|
|
999
|
+
{ name: "right", value: { literal: { stringValue: receiverRe.source } } },
|
|
1000
|
+
];
|
|
1001
|
+
return method === "test"
|
|
1002
|
+
? this.stdCall("regex_match", fields)
|
|
1003
|
+
: this.stdCall("regex_find", fields);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const argRe = args.length > 0 ? regexLiteralOf(args[0]) : undefined;
|
|
1007
|
+
if (!argRe) return undefined;
|
|
1008
|
+
|
|
1009
|
+
if ((method === "replace" || method === "replaceAll") && args.length === 2) {
|
|
1010
|
+
const all = method === "replaceAll" || argRe.flags.includes("g");
|
|
1011
|
+
this.warnDroppedRegexFlags(argRe, "g");
|
|
1012
|
+
const fields: FieldValuePair[] = [
|
|
1013
|
+
{ name: "value", value: this.encodeExpr(receiver) },
|
|
1014
|
+
{ name: "from", value: { literal: { stringValue: argRe.source } } },
|
|
1015
|
+
{ name: "to", value: this.encodeExpr(args[1]) },
|
|
1016
|
+
];
|
|
1017
|
+
return all
|
|
1018
|
+
? this.stdCall("regex_replace_all", fields)
|
|
1019
|
+
: this.stdCall("regex_replace", fields);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
if ((method === "match" || method === "matchAll") && args.length === 1) {
|
|
1023
|
+
// JS `.match(/re/)` (no `g`) returns a match ARRAY with capture groups
|
|
1024
|
+
// and an index; `regex_find` returns just the matched substring. Only the
|
|
1025
|
+
// global form maps cleanly, so the non-global one is reported as lossy
|
|
1026
|
+
// rather than quietly re-shaped.
|
|
1027
|
+
const global = method === "matchAll" || argRe.flags.includes("g");
|
|
1028
|
+
this.warnDroppedRegexFlags(argRe, "g");
|
|
1029
|
+
if (!global) {
|
|
1030
|
+
this.warn(
|
|
1031
|
+
`String.match without the "g" flag returns a match array with capture ` +
|
|
1032
|
+
`groups; std.regex_find returns only the matched substring`,
|
|
1033
|
+
"RegexMatchNonGlobal",
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
const fields: FieldValuePair[] = [
|
|
1037
|
+
{ name: "left", value: this.encodeExpr(receiver) },
|
|
1038
|
+
{ name: "right", value: { literal: { stringValue: argRe.source } } },
|
|
1039
|
+
];
|
|
1040
|
+
return global
|
|
1041
|
+
? this.stdCall("regex_find_all", fields)
|
|
1042
|
+
: this.stdCall("regex_find", fields);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
return undefined;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/** Warn about every regex flag that the chosen std routing does not honour. */
|
|
1049
|
+
private warnDroppedRegexFlags(re: { source: string; flags: string }, implied: string): void {
|
|
1050
|
+
const dropped = [...re.flags].filter((f) => !implied.includes(f)).join("");
|
|
1051
|
+
if (dropped.length === 0) return;
|
|
1052
|
+
this.warn(
|
|
1053
|
+
`Dropping regular-expression flags "${dropped}" from /${re.source}/${re.flags} — ` +
|
|
1054
|
+
`the std regex_* functions take a bare pattern string and model no flags`,
|
|
1055
|
+
"RegularExpressionLiteralFlags",
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
761
1059
|
private encodeLambda(node: ts.ArrowFunction | ts.FunctionExpression): Expression {
|
|
762
1060
|
const params = node.parameters.map(p =>
|
|
763
1061
|
ts.isIdentifier(p.name) ? p.name.text : p.name.getText()
|
|
@@ -869,14 +1167,19 @@ export class TsEncoder {
|
|
|
869
1167
|
const d = node.initializer.declarations[0];
|
|
870
1168
|
varName = ts.isIdentifier(d.name) ? d.name.text : d.name.getText();
|
|
871
1169
|
}
|
|
872
|
-
const
|
|
873
|
-
return this.stdCall(fnName, [
|
|
1170
|
+
const fields: FieldValuePair[] = [
|
|
874
1171
|
{ name: "variable", value: { literal: { stringValue: varName } } },
|
|
875
1172
|
{ name: "iterable", value: this.encodeExpr(node.expression) },
|
|
876
1173
|
{ name: "body", value: this.encodeBody(
|
|
877
1174
|
ts.isBlock(node.statement) ? node.statement : ts.factory.createBlock([node.statement])
|
|
878
1175
|
) },
|
|
879
|
-
]
|
|
1176
|
+
];
|
|
1177
|
+
// Both names are spelled as literals rather than computed from a ternary so
|
|
1178
|
+
// that a static scan of this file (ts/compiler/test/std_name_consistency.
|
|
1179
|
+
// test.ts) can see every std function the encoder is able to emit.
|
|
1180
|
+
return ts.isForInStatement(node)
|
|
1181
|
+
? this.stdCall("for_in", fields)
|
|
1182
|
+
: this.stdCall("for_each", fields);
|
|
880
1183
|
}
|
|
881
1184
|
|
|
882
1185
|
private encodeWhile(node: ts.WhileStatement): Expression {
|
|
@@ -1047,7 +1350,10 @@ export class TsEncoder {
|
|
|
1047
1350
|
// operator) has no Ball representation yet — warn (and throw in
|
|
1048
1351
|
// strict mode) rather than silently dropping it from the encoded
|
|
1049
1352
|
// class (#242).
|
|
1050
|
-
this.warn(
|
|
1353
|
+
this.warn(
|
|
1354
|
+
`Unhandled class member name: ${member.name.getText()}`,
|
|
1355
|
+
ts.SyntaxKind[member.name.kind],
|
|
1356
|
+
);
|
|
1051
1357
|
}
|
|
1052
1358
|
if (fn) {
|
|
1053
1359
|
fn.name = `${className}.${fn.name}`;
|
|
@@ -1146,32 +1452,45 @@ export class TsEncoder {
|
|
|
1146
1452
|
/**
|
|
1147
1453
|
* Map a JS/TS method name to its Ball std function equivalent.
|
|
1148
1454
|
* Returns null if no mapping exists (falls through to generic method call).
|
|
1455
|
+
*
|
|
1456
|
+
* Every entry names BOTH the canonical std function AND the field names that
|
|
1457
|
+
* function's arguments carry. Names alone are not enough: `compileStdCall`
|
|
1458
|
+
* looks arguments up by field name, so `string_replace` fed `other`/`arg1`
|
|
1459
|
+
* (the old positional scheme) silently compiled to `''`, and
|
|
1460
|
+
* `string_substring` fed the same way crashed. The field names below were
|
|
1461
|
+
* each read off the corresponding `case` body in ts/compiler/src/compiler.ts
|
|
1462
|
+
* (#489).
|
|
1149
1463
|
*/
|
|
1150
1464
|
private mapMethodToStd(
|
|
1151
1465
|
method: string,
|
|
1152
1466
|
_args: { name: string; value: Expression }[],
|
|
1153
1467
|
): { fn: string; module?: string; selfName?: string; extraFields: (a: typeof _args) => FieldValuePair[] } | null {
|
|
1154
|
-
|
|
1155
|
-
const
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1468
|
+
/** Name the i-th argument after `names[i]`, falling back to `argN`. */
|
|
1469
|
+
const named = (...names: string[]) =>
|
|
1470
|
+
(a: typeof _args): FieldValuePair[] =>
|
|
1471
|
+
a.map((x, i) => ({ name: names[i] ?? `arg${i}`, value: x.value }));
|
|
1472
|
+
|
|
1473
|
+
// String methods: `value` is the receiver.
|
|
1474
|
+
const STR_METHODS: Record<string, { fn: string; args: string[] }> = {
|
|
1475
|
+
toUpperCase: { fn: "string_to_upper", args: [] },
|
|
1476
|
+
toLowerCase: { fn: "string_to_lower", args: [] },
|
|
1477
|
+
trim: { fn: "string_trim", args: [] },
|
|
1478
|
+
trimStart: { fn: "string_trim_start", args: [] },
|
|
1479
|
+
trimEnd: { fn: "string_trim_end", args: [] },
|
|
1480
|
+
includes: { fn: "string_contains", args: ["other"] },
|
|
1481
|
+
indexOf: { fn: "string_index_of", args: ["pattern", "start"] },
|
|
1482
|
+
startsWith: { fn: "string_starts_with", args: ["other"] },
|
|
1483
|
+
endsWith: { fn: "string_ends_with", args: ["other"] },
|
|
1484
|
+
split: { fn: "string_split", args: ["separator"] },
|
|
1485
|
+
substring: { fn: "string_substring", args: ["start", "end"] },
|
|
1486
|
+
slice: { fn: "string_substring", args: ["start", "end"] },
|
|
1487
|
+
replace: { fn: "string_replace", args: ["from", "to"] },
|
|
1488
|
+
replaceAll: { fn: "string_replace_all", args: ["from", "to"] },
|
|
1489
|
+
padStart: { fn: "string_pad_left", args: ["width", "padding"] },
|
|
1490
|
+
padEnd: { fn: "string_pad_right", args: ["width", "padding"] },
|
|
1491
|
+
repeat: { fn: "string_repeat", args: ["count"] },
|
|
1492
|
+
charAt: { fn: "string_char_at", args: ["index"] },
|
|
1493
|
+
charCodeAt: { fn: "string_code_unit_at", args: ["index"] },
|
|
1175
1494
|
};
|
|
1176
1495
|
// hasOwnProperty (not `in`) — `in` also matches names inherited from
|
|
1177
1496
|
// Object.prototype (toString, valueOf, hasOwnProperty, ...). A bare `in`
|
|
@@ -1182,48 +1501,38 @@ export class TsEncoder {
|
|
|
1182
1501
|
// corrupt call.function (a function object, not "to_string") in the Ball
|
|
1183
1502
|
// IR. Found via coverage analysis of the always-dead toString branch.
|
|
1184
1503
|
if (Object.prototype.hasOwnProperty.call(STR_METHODS, method)) {
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
forEach: { fn: "list_for_each", mod: "std_collections" },
|
|
1209
|
-
reduce: { fn: "list_fold", mod: "std_collections" },
|
|
1210
|
-
find: { fn: "list_first_where", mod: "std_collections" },
|
|
1211
|
-
flat: { fn: "list_flatten", mod: "std_collections" },
|
|
1212
|
-
concat: { fn: "list_concat", mod: "std_collections" },
|
|
1213
|
-
every: { fn: "list_every", mod: "std_collections" },
|
|
1214
|
-
some: { fn: "list_any", mod: "std_collections" },
|
|
1504
|
+
const m = STR_METHODS[method];
|
|
1505
|
+
return { fn: m.fn, selfName: "value", extraFields: named(...m.args) };
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// Array methods: `list` is the receiver. All of them live in
|
|
1509
|
+
// std_collections — the module the Dart reference declares them in.
|
|
1510
|
+
const ARR_METHODS: Record<string, { fn: string; args: string[] }> = {
|
|
1511
|
+
push: { fn: "list_push", args: ["value"] },
|
|
1512
|
+
pop: { fn: "list_pop", args: [] },
|
|
1513
|
+
indexOf: { fn: "list_index_of", args: ["value"] },
|
|
1514
|
+
includes: { fn: "list_contains", args: ["value"] },
|
|
1515
|
+
join: { fn: "list_join", args: ["separator"] },
|
|
1516
|
+
reverse: { fn: "list_reverse", args: [] },
|
|
1517
|
+
slice: { fn: "list_slice", args: ["start", "end"] },
|
|
1518
|
+
splice: { fn: "list_remove_at", args: ["index"] },
|
|
1519
|
+
sort: { fn: "list_sort", args: ["comparator"] },
|
|
1520
|
+
map: { fn: "list_map", args: ["function"] },
|
|
1521
|
+
filter: { fn: "list_filter", args: ["function"] },
|
|
1522
|
+
reduce: { fn: "list_reduce", args: ["function"] },
|
|
1523
|
+
find: { fn: "list_find", args: ["function"] },
|
|
1524
|
+
concat: { fn: "list_concat", args: ["other"] },
|
|
1525
|
+
every: { fn: "list_all", args: ["function"] },
|
|
1526
|
+
some: { fn: "list_any", args: ["function"] },
|
|
1215
1527
|
};
|
|
1216
1528
|
// Same hasOwnProperty rationale as STR_METHODS above.
|
|
1217
1529
|
if (Object.prototype.hasOwnProperty.call(ARR_METHODS, method)) {
|
|
1218
1530
|
const m = ARR_METHODS[method];
|
|
1219
1531
|
return {
|
|
1220
1532
|
fn: m.fn,
|
|
1221
|
-
module:
|
|
1533
|
+
module: "std_collections",
|
|
1222
1534
|
selfName: "list",
|
|
1223
|
-
extraFields: (
|
|
1224
|
-
name: i === 0 ? "value" : `arg${i}`,
|
|
1225
|
-
value: x.value,
|
|
1226
|
-
})),
|
|
1535
|
+
extraFields: named(...m.args),
|
|
1227
1536
|
};
|
|
1228
1537
|
}
|
|
1229
1538
|
|
|
@@ -1284,7 +1593,16 @@ export class TsEncoder {
|
|
|
1284
1593
|
return { literal: {} };
|
|
1285
1594
|
}
|
|
1286
1595
|
|
|
1287
|
-
|
|
1596
|
+
/**
|
|
1597
|
+
* Record a warning, and turn it into a hard error when the caller asked for
|
|
1598
|
+
* one.
|
|
1599
|
+
*
|
|
1600
|
+
* `kind` is the TS SyntaxKind name (or another marker) the warning is about;
|
|
1601
|
+
* it is what `strictBehaviorAffecting` filters on, so the distinction between
|
|
1602
|
+
* "erased a type alias" and "silently changed what this program computes" is
|
|
1603
|
+
* mechanical rather than a substring match on the message.
|
|
1604
|
+
*/
|
|
1605
|
+
private warn(msg: string, kind?: string): void {
|
|
1288
1606
|
this.warnings.push(msg);
|
|
1289
1607
|
// In strict mode an unhandled node is a hard error: the encoder cannot
|
|
1290
1608
|
// faithfully represent the construct and would otherwise emit a
|
|
@@ -1292,6 +1610,9 @@ export class TsEncoder {
|
|
|
1292
1610
|
if (this.strict) {
|
|
1293
1611
|
throw new EncodeError(msg, this.getWarnings());
|
|
1294
1612
|
}
|
|
1613
|
+
if (this.strictBehaviorAffecting && !(kind !== undefined && ERASURE_ONLY_KINDS.has(kind))) {
|
|
1614
|
+
throw new EncodeError(msg, this.getWarnings());
|
|
1615
|
+
}
|
|
1295
1616
|
}
|
|
1296
1617
|
|
|
1297
1618
|
getWarnings(): string[] {
|
|
@@ -1299,6 +1620,43 @@ export class TsEncoder {
|
|
|
1299
1620
|
}
|
|
1300
1621
|
}
|
|
1301
1622
|
|
|
1623
|
+
/**
|
|
1624
|
+
* Split a regex literal's raw text (`/source/flags`) into its two parts.
|
|
1625
|
+
* The closing delimiter is the LAST unescaped `/`, so a pattern containing an
|
|
1626
|
+
* escaped slash (`/a\/b/g`) splits correctly.
|
|
1627
|
+
*/
|
|
1628
|
+
function splitRegexLiteral(text: string): { source: string; flags: string } {
|
|
1629
|
+
const lastSlash = text.lastIndexOf("/");
|
|
1630
|
+
return {
|
|
1631
|
+
source: text.slice(1, lastSlash),
|
|
1632
|
+
flags: text.slice(lastSlash + 1),
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
/** The `{ source, flags }` of `node` when it is a regex literal, else undefined. */
|
|
1637
|
+
function regexLiteralOf(node: ts.Node): { source: string; flags: string } | undefined {
|
|
1638
|
+
if (ts.isParenthesizedExpression(node)) return regexLiteralOf(node.expression);
|
|
1639
|
+
return ts.isRegularExpressionLiteral(node) ? splitRegexLiteral(node.text) : undefined;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
/** A `(x) => x` lambda, in the encoder's own lambda shape. */
|
|
1643
|
+
function identityLambda(param: string): Expression {
|
|
1644
|
+
return {
|
|
1645
|
+
lambda: {
|
|
1646
|
+
name: "",
|
|
1647
|
+
body: { reference: { name: param } },
|
|
1648
|
+
metadata: { params: [{ name: param }] },
|
|
1649
|
+
},
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
/** True for the `xs.splice(i, 1)` form, the only one list_remove_at models. */
|
|
1654
|
+
function isSingleElementSplice(node: ts.CallExpression): boolean {
|
|
1655
|
+
if (node.arguments.length !== 2) return false;
|
|
1656
|
+
const count = node.arguments[1];
|
|
1657
|
+
return ts.isNumericLiteral(count) && count.text === "1";
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1302
1660
|
/// Thrown by `encode(..., { strict: true })` when the encoder hits a TS
|
|
1303
1661
|
/// construct it cannot represent. Carries the full accumulated warning list.
|
|
1304
1662
|
export class EncodeError extends Error {
|