@ball-lang/compiler 1.5.9 → 1.6.1
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/compiler.d.ts +25 -0
- package/dist/compiler.d.ts.map +1 -1
- package/dist/compiler.js +71 -3
- package/dist/compiler.js.map +1 -1
- package/dist/preamble.d.ts.map +1 -1
- package/dist/preamble.js +35 -2
- package/dist/preamble.js.map +1 -1
- package/package.json +1 -1
- package/src/compiler.ts +71 -3
- package/src/preamble.ts +35 -2
package/dist/preamble.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"preamble.d.ts","sourceRoot":"","sources":["../src/preamble.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mBAAmB,
|
|
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
|
|
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
|
|
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) {
|
package/dist/preamble.js.map
CHANGED
|
@@ -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
|
|
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.
|
|
3
|
+
"version": "1.6.1",
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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) {
|