@ball-lang/compiler 1.59.10 → 1.59.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ball-lang/compiler",
3
- "version": "1.59.10",
3
+ "version": "1.59.12",
4
4
  "description": "Ball → TypeScript compiler. Consumes a Ball protobuf Program and emits idiomatic TypeScript via ts-morph. The canonical TS compiler for Ball — lives in TS land so TS syntax knowledge doesn't leak into other languages.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/compiler.ts CHANGED
@@ -3900,6 +3900,18 @@ function __isUnknownFnError(e: any): boolean {
3900
3900
  const idx = field(c, "index");
3901
3901
  if (t && idx) return `${this.expr(t)}[${this.expr(idx)}]`;
3902
3902
  }
3903
+ // `null_aware_index` in ASSIGNMENT-TARGET position must be a plain `[…]`:
3904
+ // `a?.[i] = v` is a SyntaxError ("the left-hand side of an assignment
3905
+ // expression must be a variable or a property access"), which is why the
3906
+ // read path emitting `?.[` alone broke the whole self-hosted engine. Dart's
3907
+ // `a?[i] = v` short-circuits when `a` is null; JS has no such form, so the
3908
+ // plain index is the closest faithful lowering for the write path.
3909
+ if (c && (c.function === "null_aware_index") &&
3910
+ (c.module === "" || c.module === "std")) {
3911
+ const t = field(c, "self") ?? field(c, "target");
3912
+ const idx = field(c, "index") ?? field(c, "key");
3913
+ if (t && idx) return `${this.expr(t)}[${this.expr(idx)}]`;
3914
+ }
3903
3915
  return this.expr(e);
3904
3916
  }
3905
3917
 
@@ -4835,7 +4847,13 @@ function __isUnknownFnError(e: any): boolean {
4835
4847
  if (!self_ || !idx) {
4836
4848
  throw new Error("TS compiler: null_aware_index requires a \"self\"/\"target\" and an \"index\"/\"key\" field");
4837
4849
  }
4838
- return `${this.expr(self_)}[${this.expr(idx)}]`;
4850
+ // `?.[` — NOT a plain `[`. Emitting `target[index]` threw
4851
+ // "Cannot read properties of null" for exactly the null receiver the
4852
+ // construct exists to guard against, which is the opposite of what
4853
+ // `a?.[i]` means. Caught by the optional-element-access round-trip
4854
+ // fixture added with #489 (no encoder emitted this shape before, so
4855
+ // nothing had ever executed it).
4856
+ return `${this.expr(self_)}?.[${this.expr(idx)}]`;
4839
4857
  }
4840
4858
  case "null_aware_access": {
4841
4859
  const target = f.get("target");
@@ -5076,6 +5094,23 @@ function __isUnknownFnError(e: any): boolean {
5076
5094
  if (v && from && to) return `${this.expr(v)}.replace(${this.expr(from)}, ${this.expr(to)})`;
5077
5095
  return "''";
5078
5096
  }
5097
+ // ── Regex (std) ───────────────────────────────────────────────────
5098
+ // Field shapes mirror the Dart reference engine exactly
5099
+ // (dart/engine/lib/engine_std.dart): match/find/find_all take
5100
+ // left = subject, right = pattern; replace/replace_all take
5101
+ // value/from/to. The pattern is always a plain string — the std regex
5102
+ // functions model no flags, so the TS encoder reports a dropped flag as
5103
+ // a behaviour-affecting warning rather than encoding it (#490).
5104
+ case "regex_match":
5105
+ return `new RegExp(${this.expr(fg("right", "pattern", "arg1")!)}).test(${this.expr(fg("left", "value", "arg0")!)})`;
5106
+ case "regex_find":
5107
+ return `(${this.expr(fg("left", "value", "arg0")!)}.match(new RegExp(${this.expr(fg("right", "pattern", "arg1")!)})) ?? [null])[0]`;
5108
+ case "regex_find_all":
5109
+ return `(${this.expr(fg("left", "value", "arg0")!)}.match(new RegExp(${this.expr(fg("right", "pattern", "arg1")!)}, 'g')) ?? [])`;
5110
+ case "regex_replace":
5111
+ return `${this.expr(f.get("value")!)}.replace(new RegExp(${this.expr(f.get("from")!)}), ${this.expr(f.get("to")!)})`;
5112
+ case "regex_replace_all":
5113
+ return `${this.expr(f.get("value")!)}.replace(new RegExp(${this.expr(f.get("from")!)}, 'g'), ${this.expr(f.get("to")!)})`;
5079
5114
  case "string_replace_all": {
5080
5115
  const v = f.get("value") ?? f.get("string");
5081
5116
  const from = f.get("from") ?? f.get("pattern");
@@ -5427,6 +5462,11 @@ function __isUnknownFnError(e: any): boolean {
5427
5462
  }
5428
5463
  case "list_any": return `${this.expr(f.get("list")!)}.some(${this.expr(f.get("function") ?? f.get("callback") ?? f.get("value")!)})`;
5429
5464
  case "list_all": return `${this.expr(f.get("list")!)}.every(${this.expr(f.get("function") ?? f.get("callback") ?? f.get("value")!)})`;
5465
+ // Dart's Iterable.firstWhere / JS Array.find. Declared in
5466
+ // dart/shared/lib/std_collections.dart and implemented by the Dart
5467
+ // engine, but never reachable from TS before (#489).
5468
+ case "list_find": return `${this.expr(f.get("list")!)}.find(${this.expr(f.get("function") ?? f.get("callback") ?? f.get("value")!)})`;
5469
+ case "list_flat_map": return `${this.expr(f.get("list")!)}.flatMap(${this.expr(f.get("function") ?? f.get("callback") ?? f.get("value")!)})`;
5430
5470
  case "list_to_list": return `[...${this.expr(f.get("list")!)}]`;
5431
5471
  case "list_foreach": return `${this.expr(f.get("list")!)}.forEach(${this.expr(f.get("function") ?? f.get("callback") ?? f.get("value")!)})`;
5432
5472
  case "map_foreach": {
@@ -5758,6 +5798,10 @@ function __isUnknownFnError(e: any): boolean {
5758
5798
  const subjectStr = this.expr(subjectExpr);
5759
5799
  const caseExprs = casesField.literal?.listValue?.elements ?? [];
5760
5800
  let defaultBody: Expression | undefined;
5801
+ // Tracked separately from `defaultBody`: a `default:` arm that carries no
5802
+ // body still CATCHES (it just evaluates to nothing), so it is not a
5803
+ // non-exhaustive switch. Only "no default arm at all" is.
5804
+ let hasDefault = false;
5761
5805
  // Each branch carries its match condition, the pattern's bindings, the
5762
5806
  // body expression and an optional `when` guard. The guard references the
5763
5807
  // bound variables, so it must be evaluated where those bindings are in
@@ -5788,6 +5832,7 @@ function __isUnknownFnError(e: any): boolean {
5788
5832
  // Check for is_default flag
5789
5833
  if (isDefaultFlag) {
5790
5834
  defaultBody = body;
5835
+ hasDefault = true;
5791
5836
  continue;
5792
5837
  }
5793
5838
  if (!body) continue;
@@ -5799,6 +5844,7 @@ function __isUnknownFnError(e: any): boolean {
5799
5844
  // refutable branch (the guard can still fall through).
5800
5845
  if (result.condition === "true" && result.bindings.length === 0 && !guardField) {
5801
5846
  defaultBody = body;
5847
+ hasDefault = true;
5802
5848
  break;
5803
5849
  }
5804
5850
  branches.push({ cond: result.condition, bindings: result.bindings, body, guard: guardField });
@@ -5811,6 +5857,7 @@ function __isUnknownFnError(e: any): boolean {
5811
5857
  const caseMatch = pattern ?? valueField;
5812
5858
  if (!caseMatch) {
5813
5859
  defaultBody = body;
5860
+ hasDefault = true;
5814
5861
  continue;
5815
5862
  }
5816
5863
  const patText = patternLiteralText(caseMatch);
@@ -5826,13 +5873,41 @@ function __isUnknownFnError(e: any): boolean {
5826
5873
  const cond = patternToTsCondition(patText, subjectStr);
5827
5874
  if (cond === "true" && !guardField) {
5828
5875
  defaultBody = body;
5876
+ hasDefault = true;
5829
5877
  break;
5830
5878
  }
5831
5879
  // Check if the pattern introduces variable bindings.
5832
5880
  const bindings = patternBindings(patText, subjectStr);
5833
5881
  branches.push({ cond, bindings, body, guard: guardField });
5834
5882
  }
5835
- const tail = defaultBody ? this.expr(defaultBody) : "undefined";
5883
+ // What the switch evaluates to when nothing matched.
5884
+ //
5885
+ // A defaultless switch EXPRESSION that matched nothing is non-exhaustive.
5886
+ // Dart's engine throws BallRuntimeError('Non-exhaustive switch expression')
5887
+ // (dart/engine/lib/engine_control_flow.dart), C# throws
5888
+ // BallRuntimeException with the same text (csharp/compiler/src/BaseCall.cs),
5889
+ // and Go/Rust throw it too (issue #467). TypeScript answered `undefined`
5890
+ // instead — a silent wrong value where every other target fails loud, and
5891
+ // exactly the silent-degradation shape CLAUDE.md's fail-loud rule forbids.
5892
+ //
5893
+ // Two guards keep this faithful, both mirroring the Rust port:
5894
+ // - only a switch EXPRESSION throws. A statement `switch` legally does
5895
+ // nothing when no case matches, so it still yields `undefined`.
5896
+ // - only "no `default` arm AT ALL" is non-exhaustive; a `default:` that
5897
+ // carries no body still catches.
5898
+ //
5899
+ // Engine-safety: the self-hosted engine's own oneof dispatchers
5900
+ // (`_evalExpression` on whichExpr, `_evalLiteral` on whichValue) are
5901
+ // defaultless switch expressions, but each carries an explicit `notSet`
5902
+ // arm, so this tail is unreachable there. Measured, not assumed —
5903
+ // engine_runtime.test.ts runs the whole conformance corpus through the
5904
+ // engine compiled by THIS compiler.
5905
+ const nonExhaustive = call.function === "switch_expr" && !hasDefault;
5906
+ const tail = defaultBody
5907
+ ? this.expr(defaultBody)
5908
+ : nonExhaustive
5909
+ ? "(() => { throw 'Non-exhaustive switch expression'; })()"
5910
+ : "undefined";
5836
5911
  if (branches.length === 0) return tail;
5837
5912
  let result = tail;
5838
5913
  for (const { cond, bindings, body, guard } of [...branches].reverse()) {