@ball-lang/compiler 1.6.0 → 1.6.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"preamble.d.ts","sourceRoot":"","sources":["../src/preamble.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mBAAmB,QAk1C/B,CAAC"}
1
+ {"version":3,"file":"preamble.d.ts","sourceRoot":"","sources":["../src/preamble.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mBAAmB,QAm3C/B,CAAC"}
package/dist/preamble.js CHANGED
@@ -116,6 +116,10 @@ class BallDouble {
116
116
  get isFinite(): boolean { return Number.isFinite(this.value); }
117
117
  get isInfinite(): boolean { return !Number.isFinite(this.value) && !Number.isNaN(this.value); }
118
118
  get isNegative(): boolean { return this.value < 0 || (this.value === 0 && 1/this.value === -Infinity); }
119
+ // Mirrors the Number.prototype.remainder polyfill below (truncating
120
+ // remainder, matching JS % and Dart's num.remainder) — BallDouble wraps
121
+ // a JS number so it never inherits Number.prototype and needs its own.
122
+ remainder(other: any): number { return this.value % Number(other); }
119
123
  toString(): string {
120
124
  const v = this.value;
121
125
  if (!isFinite(v)) return v.toString();
@@ -364,6 +368,13 @@ function __ball_bitxor(a: any, b: any): any { return __i64_wrap(__to_bigint(a) ^
364
368
  function __ball_bitnot(a: any): any { return __i64_wrap(~__to_bigint(a)); }
365
369
  function __ball_shl(a: any, b: any): any { return __i64_wrap(__to_bigint(a) << __to_bigint(b)); }
366
370
  function __ball_shr(a: any, b: any): any { return __i64_wrap(__to_bigint(a) >> __to_bigint(b)); }
371
+ // Unsigned/logical shift: reinterpret a as an unsigned 64-bit value (add
372
+ // 2^64 if negative) before shifting, so zeros fill from the left instead of
373
+ // the sign bit — unlike >>> on raw JS numbers, which is only 32-bit.
374
+ function __ball_ushr(a: any, b: any): any {
375
+ const unsigned = ((__to_bigint(a) % __I64_MOD) + __I64_MOD) % __I64_MOD;
376
+ return __i64_wrap(unsigned >> __to_bigint(b));
377
+ }
367
378
  function __ball_negate(a: any): any {
368
379
  if (typeof a === 'bigint') return __i64_wrap(-a);
369
380
  if (a instanceof BallDouble) return new BallDouble(-a.value);
@@ -782,6 +793,9 @@ function __ball_cascade(target: any, ops: any[]): any {
782
793
  Object.defineProperty(_ballNp, 'isInfinite', {
783
794
  configurable: true, get() { const n = Number(this); return n === Infinity || n === -Infinity; },
784
795
  });
796
+ Object.defineProperty(_ballNp, 'isNegative', {
797
+ configurable: true, get() { const n = Number(this); return n < 0 || (n === 0 && 1 / n === -Infinity); },
798
+ });
785
799
  if (!_ballNp.abs) _ballNp.abs = function () { return Math.abs(Number(this)); };
786
800
  if (!_ballNp.ceil) _ballNp.ceil = function () { return Math.ceil(Number(this)); };
787
801
  if (!_ballNp.floor) _ballNp.floor = function () { return Math.floor(Number(this)); };
@@ -919,14 +933,33 @@ function __ball_cascade(target: any, ops: any[]): any {
919
933
  if (this == null || typeof this !== 'object') return [];
920
934
  return Object.entries(this).map(([k, v]: any) => ({ key: k, value: v }));
921
935
  });
936
+ // .keys/.values on a non-Map must FAIL LOUD (throw a catchable error), not
937
+ // silently return [] — the silent-degradation class of bug that hid issue
938
+ // #55 (mirrors the fix already applied to the Dart/C++ compilers).
939
+ //
940
+ // A getter installed on Object.prototype is invoked in "sloppy" (non-strict)
941
+ // script contexts with this auto-boxed to a Number/String/Boolean WRAPPER
942
+ // object for a primitive receiver (e.g. (42).keys boxes this to a Number
943
+ // instance) — typeof this is then 'object', not 'number', so a bare
944
+ // __ball_is_type(this, 'Map') (which only excludes Array/BallDouble/Set)
945
+ // would wrongly treat a boxed int/string as Map-like. Exclude the wrapper
946
+ // types explicitly instead of widening the shared type-check.
947
+ const __isGenuineMap = (v: any) =>
948
+ typeof v === 'object' && v !== null && !Array.isArray(v) &&
949
+ !(v instanceof BallDouble) && !(v instanceof Set) &&
950
+ !(v instanceof Number) && !(v instanceof String) && !(v instanceof Boolean);
922
951
  defDartGetter('keys', function (this: any) {
923
952
  if (this instanceof Map) return [..._nativeMapKeys.call(this)];
924
- if (this == null || typeof this !== 'object') return [];
953
+ if (!__isGenuineMap(this)) {
954
+ throw new Error('type \'' + __ball_to_string(this) + '\' has no .keys getter (not a Map)');
955
+ }
925
956
  return Object.keys(this);
926
957
  });
927
958
  defDartGetter('values', function (this: any) {
928
959
  if (this instanceof Map) return [..._nativeMapValues.call(this)];
929
- if (this == null || typeof this !== 'object') return [];
960
+ if (!__isGenuineMap(this)) {
961
+ throw new Error('type \'' + __ball_to_string(this) + '\' has no .values getter (not a Map)');
962
+ }
930
963
  return Object.values(this);
931
964
  });
932
965
  defDartGetter('length', function (this: any) {
@@ -1 +1 @@
1
- {"version":3,"file":"preamble.js","sourceRoot":"","sources":["../src/preamble.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAk1C5C,CAAC"}
1
+ {"version":3,"file":"preamble.js","sourceRoot":"","sources":["../src/preamble.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAm3C5C,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ball-lang/compiler",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
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
@@ -99,6 +99,19 @@ export class BallCompiler {
99
99
  */
100
100
  private renameStack: Map<string, string>[] = [];
101
101
 
102
+ /**
103
+ * Stack of `std.label` names currently enclosing the statement being
104
+ * compiled (see `emitLabelStmt`/`emitGotoStmt`). A `std.label` lowers to a
105
+ * labelled `while (true) { ... break name; }`, and a nested `std.goto`
106
+ * targeting one of these names lowers to `continue name;` — TS (unlike
107
+ * Dart's `continue` targeting a switch case, or C++'s real `goto`) only
108
+ * allows `continue`/`break` to target an *enclosing* labelled loop, so this
109
+ * only supports backward jumps to a label the goto is lexically inside of.
110
+ * A `goto` to any other name fails loud at compile time (see `emitGotoStmt`)
111
+ * rather than silently emitting invalid or wrong-behaving TS.
112
+ */
113
+ private activeGotoLabels: string[] = [];
114
+
102
115
  constructor(program: Program) {
103
116
  this.program = program;
104
117
  }
@@ -3138,7 +3151,7 @@ function __isUnknownFnError(e: any): boolean {
3138
3151
  const kinds = new Set([
3139
3152
  "if", "for", "for_in", "for_each", "while", "do_while", "try",
3140
3153
  "return", "break", "continue", "labeled", "throw", "rethrow",
3141
- "assign", "switch", "switch_expr",
3154
+ "assign", "switch", "switch_expr", "label", "goto",
3142
3155
  ]);
3143
3156
  if (!kinds.has(call.function)) return false;
3144
3157
  // Accept both explicit std module AND empty module (the encoder
@@ -3177,6 +3190,8 @@ function __isUnknownFnError(e: any): boolean {
3177
3190
  if (body) this.emitStatementOrExpression(body, false);
3178
3191
  break;
3179
3192
  }
3193
+ case "label": this.emitLabelStmt(call); break;
3194
+ case "goto": this.emitGotoStmt(call); break;
3180
3195
  case "throw": {
3181
3196
  const v = field(call, "value");
3182
3197
  if (v) {
@@ -3417,6 +3432,47 @@ function __isUnknownFnError(e: any): boolean {
3417
3432
  this.writeln(`}`);
3418
3433
  }
3419
3434
 
3435
+ /**
3436
+ * `std.label(name, body)` — a named jump target that `std.goto(label)`
3437
+ * re-enters. TS has no real `goto`, so this lowers to a labelled
3438
+ * `while (true)` loop: running the body once and falling off the end
3439
+ * (the common case, and the only shape a `goto` targeting it can
3440
+ * restart) matches Ball's "run once, or re-run from the top on goto"
3441
+ * semantics, with `break name;` after the body covering the fall-off
3442
+ * case exactly as the Dart compiler's switch-based simulation does.
3443
+ * See `activeGotoLabels` for why only backward jumps are supported.
3444
+ */
3445
+ private emitLabelStmt(call: FunctionCall): void {
3446
+ const name = stringField(call, "name");
3447
+ const body = field(call, "body");
3448
+ if (!name || !body) {
3449
+ throw new Error("std.label requires a string \"name\" and a \"body\"");
3450
+ }
3451
+ this.activeGotoLabels.push(name);
3452
+ this.writeln(`${name}: while (true) {`);
3453
+ this.depth++;
3454
+ this.emitStatementOrExpression(body, false);
3455
+ this.writeln(`break ${name};`);
3456
+ this.depth--;
3457
+ this.writeln(`}`);
3458
+ this.activeGotoLabels.pop();
3459
+ }
3460
+
3461
+ /** `std.goto(label)` — see `emitLabelStmt` and `activeGotoLabels`. */
3462
+ private emitGotoStmt(call: FunctionCall): void {
3463
+ const label = stringField(call, "label");
3464
+ if (!label) throw new Error('std.goto requires a string "label"');
3465
+ if (!this.activeGotoLabels.includes(label)) {
3466
+ throw new Error(
3467
+ `std.goto("${label}") is not inside its own std.label("${label}", ...) body — ` +
3468
+ "the TS compiler only supports goto as a backward jump that restarts an " +
3469
+ "enclosing label (mirrors a labelled loop); forward jumps and jumps to a " +
3470
+ "sibling label are not supported (#226).",
3471
+ );
3472
+ }
3473
+ this.writeln(`continue ${label};`);
3474
+ }
3475
+
3420
3476
  private emitDoWhileStmt(call: FunctionCall): void {
3421
3477
  const cond = field(call, "condition");
3422
3478
  const body = field(call, "body");
@@ -3917,7 +3973,15 @@ function __isUnknownFnError(e: any): boolean {
3917
3973
  this.currentClassName !== undefined &&
3918
3974
  this.currentClassFields.has(fa.field) &&
3919
3975
  !this.currentClassMethodNames.has(fa.field);
3920
- const obj = isSelfRef || isSuperFieldRef ? "this" : this.expr(fa.object);
3976
+ const rawObj = isSelfRef || isSuperFieldRef ? "this" : this.expr(fa.object);
3977
+ // A bare numeric-literal receiver (e.g. `0`, `-4`, `123n`) parses as the
3978
+ // start of a float literal when immediately followed by `.field` —
3979
+ // `0.isNegative` is `SyntaxError: Identifier cannot follow number`.
3980
+ // Parenthesize whenever the compiled object text IS (not merely
3981
+ // contains) such a token; a double literal already compiles through
3982
+ // `new BallDouble(...)` and a negated int through `__ball_negate(...)`,
3983
+ // so both are already safe — only the bare-token case needs this.
3984
+ const obj = /^-?\d+n?$/.test(rawObj) ? `(${rawObj})` : rawObj;
3921
3985
  const f = fa.field;
3922
3986
  if (f === "length") return `${obj}.length`;
3923
3987
  // Positional record field: `.$1` / `.$2` → [0] / [1].
@@ -4289,7 +4353,7 @@ function __isUnknownFnError(e: any): boolean {
4289
4353
  case "bitwise_not": return `__ball_bitnot(${this.expr(fg("value", "arg0")!)})`;
4290
4354
  case "left_shift": return `__ball_shl(${this.expr(fg("left", "value", "arg0")!)}, ${this.expr(fg("right", "other", "arg1")!)})`;
4291
4355
  case "right_shift": return `__ball_shr(${this.expr(fg("left", "value", "arg0")!)}, ${this.expr(fg("right", "other", "arg1")!)})`;
4292
- case "unsigned_right_shift": return bin(">>>");
4356
+ case "unsigned_right_shift": return `__ball_ushr(${this.expr(fg("left", "value", "arg0")!)}, ${this.expr(fg("right", "other", "arg1")!)})`;
4293
4357
  case "integer_divide":
4294
4358
  return `__ball_divide(${this.expr(f.get("left")!)}, ${this.expr(f.get("right")!)})`;
4295
4359
  case "concat": return bin("+");
@@ -4998,6 +5062,10 @@ function __isUnknownFnError(e: any): boolean {
4998
5062
  case "list_length": return `${this.expr(f.get("list")!)}.length`;
4999
5063
  case "list_filter": return `${this.expr(f.get("list")!)}.filter(${this.expr(f.get("function") ?? f.get("callback") ?? f.get("value")!)})`;
5000
5064
  case "list_map": return `${this.expr(f.get("list")!)}.map(${this.expr(f.get("function") ?? f.get("callback") ?? f.get("value")!)})`;
5065
+ // No-seed combine (Dart's Iterable.reduce): starts at the first
5066
+ // element, folds from the second; empty list throws (matches JS
5067
+ // Array.prototype.reduce with no initialValue).
5068
+ case "list_reduce": return `${this.expr(f.get("list")!)}.reduce(${this.expr(f.get("function") ?? f.get("callback") ?? f.get("value")!)})`;
5001
5069
  case "list_sort": {
5002
5070
  const l = this.expr(f.get("list")!);
5003
5071
  const cmp = f.get("comparator") ?? f.get("function") ?? f.get("value");
package/src/preamble.ts CHANGED
@@ -116,6 +116,10 @@ class BallDouble {
116
116
  get isFinite(): boolean { return Number.isFinite(this.value); }
117
117
  get isInfinite(): boolean { return !Number.isFinite(this.value) && !Number.isNaN(this.value); }
118
118
  get isNegative(): boolean { return this.value < 0 || (this.value === 0 && 1/this.value === -Infinity); }
119
+ // Mirrors the Number.prototype.remainder polyfill below (truncating
120
+ // remainder, matching JS % and Dart's num.remainder) — BallDouble wraps
121
+ // a JS number so it never inherits Number.prototype and needs its own.
122
+ remainder(other: any): number { return this.value % Number(other); }
119
123
  toString(): string {
120
124
  const v = this.value;
121
125
  if (!isFinite(v)) return v.toString();
@@ -364,6 +368,13 @@ function __ball_bitxor(a: any, b: any): any { return __i64_wrap(__to_bigint(a) ^
364
368
  function __ball_bitnot(a: any): any { return __i64_wrap(~__to_bigint(a)); }
365
369
  function __ball_shl(a: any, b: any): any { return __i64_wrap(__to_bigint(a) << __to_bigint(b)); }
366
370
  function __ball_shr(a: any, b: any): any { return __i64_wrap(__to_bigint(a) >> __to_bigint(b)); }
371
+ // Unsigned/logical shift: reinterpret a as an unsigned 64-bit value (add
372
+ // 2^64 if negative) before shifting, so zeros fill from the left instead of
373
+ // the sign bit — unlike >>> on raw JS numbers, which is only 32-bit.
374
+ function __ball_ushr(a: any, b: any): any {
375
+ const unsigned = ((__to_bigint(a) % __I64_MOD) + __I64_MOD) % __I64_MOD;
376
+ return __i64_wrap(unsigned >> __to_bigint(b));
377
+ }
367
378
  function __ball_negate(a: any): any {
368
379
  if (typeof a === 'bigint') return __i64_wrap(-a);
369
380
  if (a instanceof BallDouble) return new BallDouble(-a.value);
@@ -782,6 +793,9 @@ function __ball_cascade(target: any, ops: any[]): any {
782
793
  Object.defineProperty(_ballNp, 'isInfinite', {
783
794
  configurable: true, get() { const n = Number(this); return n === Infinity || n === -Infinity; },
784
795
  });
796
+ Object.defineProperty(_ballNp, 'isNegative', {
797
+ configurable: true, get() { const n = Number(this); return n < 0 || (n === 0 && 1 / n === -Infinity); },
798
+ });
785
799
  if (!_ballNp.abs) _ballNp.abs = function () { return Math.abs(Number(this)); };
786
800
  if (!_ballNp.ceil) _ballNp.ceil = function () { return Math.ceil(Number(this)); };
787
801
  if (!_ballNp.floor) _ballNp.floor = function () { return Math.floor(Number(this)); };
@@ -919,14 +933,33 @@ function __ball_cascade(target: any, ops: any[]): any {
919
933
  if (this == null || typeof this !== 'object') return [];
920
934
  return Object.entries(this).map(([k, v]: any) => ({ key: k, value: v }));
921
935
  });
936
+ // .keys/.values on a non-Map must FAIL LOUD (throw a catchable error), not
937
+ // silently return [] — the silent-degradation class of bug that hid issue
938
+ // #55 (mirrors the fix already applied to the Dart/C++ compilers).
939
+ //
940
+ // A getter installed on Object.prototype is invoked in "sloppy" (non-strict)
941
+ // script contexts with this auto-boxed to a Number/String/Boolean WRAPPER
942
+ // object for a primitive receiver (e.g. (42).keys boxes this to a Number
943
+ // instance) — typeof this is then 'object', not 'number', so a bare
944
+ // __ball_is_type(this, 'Map') (which only excludes Array/BallDouble/Set)
945
+ // would wrongly treat a boxed int/string as Map-like. Exclude the wrapper
946
+ // types explicitly instead of widening the shared type-check.
947
+ const __isGenuineMap = (v: any) =>
948
+ typeof v === 'object' && v !== null && !Array.isArray(v) &&
949
+ !(v instanceof BallDouble) && !(v instanceof Set) &&
950
+ !(v instanceof Number) && !(v instanceof String) && !(v instanceof Boolean);
922
951
  defDartGetter('keys', function (this: any) {
923
952
  if (this instanceof Map) return [..._nativeMapKeys.call(this)];
924
- if (this == null || typeof this !== 'object') return [];
953
+ if (!__isGenuineMap(this)) {
954
+ throw new Error('type \'' + __ball_to_string(this) + '\' has no .keys getter (not a Map)');
955
+ }
925
956
  return Object.keys(this);
926
957
  });
927
958
  defDartGetter('values', function (this: any) {
928
959
  if (this instanceof Map) return [..._nativeMapValues.call(this)];
929
- if (this == null || typeof this !== 'object') return [];
960
+ if (!__isGenuineMap(this)) {
961
+ throw new Error('type \'' + __ball_to_string(this) + '\' has no .values getter (not a Map)');
962
+ }
930
963
  return Object.values(this);
931
964
  });
932
965
  defDartGetter('length', function (this: any) {