Expand static native object and array semantics (#569)

* Support primitive and function property descriptors

* Dispatch literal bracket calls to native class methods

* Handle numeric class method keys in bracket calls

* Call function values returned by class accessors

* Expand native object and array semantics

* Keep new corpus fixtures aligned with compiler gates

* Lower Object.isFrozen arguments once
This commit is contained in:
Chris Tate
2026-09-29 08:30:54 -05:00
committed by GitHub
parent cbb8fc380a
commit f8906d0b75
38 changed files with 1644 additions and 183 deletions
@@ -0,0 +1,14 @@
let reads = 0;
class Provider {
get callable() {
reads++;
return function(value) { return value + 1; };
}
}
const provider = new Provider();
console.log(provider.callable(4), reads);
class PrivateProvider {
#method(value = 2) { return value + 3; }
get method() { return this.#method; }
}
console.log(new PrivateProvider().method());
@@ -0,0 +1,19 @@
class Base {
value() { return 'base'; }
}
class Derived extends Base {
value() { return 'derived'; }
}
const base = new Base();
const derived = new Derived();
console.log(base['value'](), derived['value']());
let asBase = derived;
console.log(asBase['value']());
class Keyword {
w\u0069th() { return 42; }
}
console.log(new Keyword()['with']());
class Numeric {
[7]() { return 'seven'; }
}
console.log(new Numeric()[7]());
@@ -0,0 +1,13 @@
const record = { 0: 2, 1: 4, 2: 6, length: 3 };
console.log(Array.prototype.findLast.call(record, value => value < 6));
console.log(Array.prototype.findLastIndex.call(record, value => value < 6));
console.log(Array.prototype.findLast.call(record, value => value > 9));
console.log(Array.prototype.findLastIndex.call(record, value => value > 9));
const sparse = { 0: 1, 2: 3, length: 3 };
const seen = [];
console.log(Array.prototype.findLastIndex.call(sparse, (value, index) => {
seen.push(`${index}:${value}`);
return value === undefined;
}));
console.log(seen.join(','));
@@ -0,0 +1,13 @@
const arrayLike = { 0: 'a', 1: 'b', 2: 'c', 3: 'd', length: 4 };
console.log(Array.prototype.slice.call(arrayLike).join(','));
console.log(Array.prototype.slice.call(arrayLike, 1, 3).join(','));
console.log(Array.prototype.slice.call(arrayLike, -2).join(','));
console.log(Array.prototype.slice.call(arrayLike, 0, -1).join(','));
console.log(Array.prototype.slice.call(arrayLike, 8).length);
console.log(Array.prototype.slice.call(arrayLike, -9, 2).join(','));
console.log(Array.prototype.slice.call(arrayLike, 3, 1).length);
console.log(Array.prototype.slice.call(arrayLike, 1, undefined).join(','));
const values = JSON.parse('[1,2,3,4]');
console.log(Array.prototype.slice.call(values, -3, -1).join(','));
console.log(values.join(','));
@@ -0,0 +1,15 @@
const values = JSON.parse('[4,1,3,2]');
console.log(values.toReversed().join(','), values.join(','));
console.log(values.toSorted().join(','), values.join(','));
console.log(values.toSorted((a, b) => b - a).join(','));
console.log(values.toSpliced(1, 2, 8, 9).join(','), values.join(','));
console.log(values.toSpliced(-2, 1).join(','));
console.log(values.with(-1, 7).join(','), values.join(','));
try { values.with(9, 0); } catch (error) { console.log(error.name); }
Object.freeze(values);
console.log(values.toReversed().join(','));
console.log(values.toSorted().join(','));
console.log(values.toSpliced(0, 1, 6).join(','));
console.log(values.with(0, 5).join(','));
console.log(values.join(','));
@@ -0,0 +1,17 @@
const values = JSON.parse('[1,2,3,4,5]');
console.log(values.fill(8, 1, -1) === values, values.join(','));
console.log(values.copyWithin(2, 0, 3) === values, values.join(','));
console.log(values.copyWithin(0, 2, 5).join(','));
console.log(values.copyWithin(-2, 0, 2).join(','));
const frozen = JSON.parse('[1,2,3]');
Object.freeze(frozen);
try { frozen.fill(9); } catch (error) { console.log(error.name); }
try { frozen.copyWithin(1, 0); } catch (error) { console.log(error.name); }
console.log(frozen.join(','));
const sealed = JSON.parse('[1,2,3]');
Object.seal(sealed);
sealed.fill(7, 1);
sealed.copyWithin(0, 1);
console.log(sealed.join(','));
+21
View File
@@ -0,0 +1,21 @@
const empty = JSON.parse('[]');
Object.freeze(empty);
console.log(empty.sort().length, empty.reverse().length);
console.log(empty.fill(4).length, empty.copyWithin(0, 0).length);
for (const operation of [() => empty.push(), () => empty.pop(), () => empty.shift(), () => empty.unshift(), () => empty.splice(0, 0)]) {
try { operation(); } catch (error) { console.log(error.name); }
}
const one = JSON.parse('[1]');
Object.freeze(one);
console.log(one.sort().join(','), one.reverse().join(','));
console.log(one.fill(9, 0, 0).join(','), one.copyWithin(0, 1).join(','));
try { one.fill(9); } catch (error) { console.log(error.name); }
try { one.copyWithin(0, 0); } catch (error) { console.log(error.name); }
console.log(one.join(','));
const two = JSON.parse('[2,1]');
Object.freeze(two);
try { two.sort(); } catch (error) { console.log(error.name); }
try { two.reverse(); } catch (error) { console.log(error.name); }
console.log(two.join(','));
@@ -0,0 +1,28 @@
const values = JSON.parse('[4,2,4,1]');
console.log(values.indexOf(4), values.indexOf(4, 1), values.indexOf(4, -2));
console.log(values.lastIndexOf(4), values.lastIndexOf(4, 1), values.lastIndexOf(4, -3));
console.log(values.includes(4, 3), values.includes(4, -2), values.includes(4, Infinity));
console.log(values.findLast((value) => value > 1), values.findLastIndex((value) => value > 1));
console.log(values.findLast((value) => value > 9), values.findLastIndex((value) => value > 9));
console.log(values.slice(-3, -1).join(','), values.slice(99).join(','));
console.log(values.toReversed().join(','), values.join(','));
console.log(values.toSpliced(0, 0, 8).join(','), values.join(','));
console.log(values.with(-4, 7).join(','), values.join(','));
const shifted = JSON.parse('[1,2,3]');
console.log(shifted.unshift(8, 9), shifted.join(','));
console.log(shifted.unshift(), shifted.join(','));
console.log(shifted.splice(1, 2, 4, 5, 6).join(','), shifted.join(','));
console.log(shifted.splice(-1, 1, 7).join(','), shifted.join(','));
const copied = JSON.parse('[0,1,2,3,4]');
console.log(copied.copyWithin(1, 3).join(','));
console.log(copied.copyWithin(-2, 0, 2).join(','));
console.log(copied.fill(6, -3, -1).join(','));
console.log(copied.fill(7, 0, 0).join(','));
const nested = JSON.parse('[1,[2,[3]],4]');
console.log(nested.flat(0).join(','));
console.log(nested.flat(1).join(','));
console.log(nested.flat(2).join(','));
console.log(JSON.stringify(nested));
+27
View File
@@ -0,0 +1,27 @@
const sealed = JSON.parse('[1,2]');
Object.seal(sealed);
console.log(Object.isExtensible(sealed), Object.isSealed(sealed), Object.isFrozen(sealed));
console.log(Object.getOwnPropertyDescriptor(sealed, '0').configurable);
console.log(Object.getOwnPropertyDescriptor(sealed, '0').writable);
sealed[0] = 4;
console.log(sealed[0]);
for (const operation of [() => sealed.pop(), () => sealed.shift(), () => sealed.push(3)]) {
try { operation(); } catch (error) { console.log(error.name); }
}
console.log(sealed.join(','));
const frozen = JSON.parse('[3,5]');
Object.freeze(frozen);
console.log(Object.isSealed(frozen), Object.isFrozen(frozen));
console.log(Object.getOwnPropertyDescriptor(frozen, '0').writable);
console.log(Object.getOwnPropertyDescriptor(frozen, 'length').writable);
for (const operation of [() => { frozen[0] = 9; }, () => frozen.reverse(), () => frozen.sort()]) {
try { operation(); } catch (error) { console.log(error.name); }
}
console.log(frozen.join(','));
const empty = JSON.parse('[]');
Object.preventExtensions(empty);
console.log(Object.isSealed(empty), Object.isFrozen(empty));
try { empty.push(1); } catch (error) { console.log(error.name); }
console.log(empty.length);
+19
View File
@@ -0,0 +1,19 @@
const values = JSON.parse('[1,2,3,4]');
values.length = 2;
console.log(values.length, values.join(','), Object.keys(values).join(','));
values.length = 2;
console.log(values.length, Object.hasOwn(values, '2'));
values[2] = 8;
console.log(values.join(','));
try { values.length = -1; } catch (error) { console.log(error.name); }
console.log(values.length);
const sealed = JSON.parse('[1,2,3]');
Object.seal(sealed);
try { sealed.length = 1; } catch (error) { console.log(error.name); }
console.log(sealed.length, sealed.join(','));
const frozen = JSON.parse('[1,2]');
Object.freeze(frozen);
try { frozen.length = 1; } catch (error) { console.log(error.name); }
console.log(frozen.length, frozen.join(','));
@@ -0,0 +1,21 @@
const values = JSON.parse('[3,5]');
values.extra = 7;
Object.defineProperty(values, 'hidden', { value: 9, writable: true, configurable: true });
console.log(values.extra, values.hidden, values.length);
console.log(Object.keys(values).join(','));
console.log(Object.getOwnPropertyNames(values).join(','));
console.log(Object.hasOwn(values, 'extra'), 'hidden' in values);
console.log(Object.getOwnPropertyDescriptor(values, 'hidden').enumerable);
console.log(Object.getOwnPropertyDescriptors(values).extra.value);
const keys = [];
for (const key in values) keys.push(key);
console.log(keys.join(','));
delete values.hidden;
console.log(Object.hasOwn(values, 'hidden'));
Object.seal(values);
console.log(Object.isSealed(values), Object.isFrozen(values));
console.log(Object.getOwnPropertyDescriptor(values, 'extra').configurable);
values.extra = 8;
console.log(values.extra);
Object.freeze(values);
console.log(Object.isFrozen(values), Object.getOwnPropertyDescriptor(values, 'extra').writable);
@@ -0,0 +1,18 @@
const values = JSON.parse('[1,2,3,4]');
console.log(values.reduce((a, b) => a + b));
console.log(values.reduceRight((a, b) => a - b));
console.log(values.reduce((a, b) => a + b, 10));
console.log(values.findLast(x => x < 4), values.findLastIndex(x => x < 4));
console.log(values.findLast(x => x > 9), values.findLastIndex(x => x > 9));
try { JSON.parse('[]').reduce((a, b) => a + b); } catch (error) { console.log(error.name); }
console.log(JSON.parse('[]').reduce((a, b) => a + b, 5));
console.log(JSON.parse('[7]').reduce((a, b) => a + b));
console.log(JSON.parse('[7]').reduceRight((a, b) => a + b));
const shrinking = JSON.parse('[1,2,3]');
const seen = [];
const found = shrinking.findLast((value, index) => {
seen.push(`${index}:${value}`);
if (index === 2) shrinking.length = 1;
return false;
});
console.log(found, seen.join(','), shrinking.join(','));
@@ -0,0 +1,11 @@
const values = JSON.parse('[1,2,1,3]');
console.log(values.indexOf(1), values.indexOf(1, 1), values.indexOf(1, -2));
console.log(values.indexOf(2, 3), values.indexOf(2, -9));
console.log(values.lastIndexOf(1), values.lastIndexOf(1, 1), values.lastIndexOf(1, -2));
console.log(values.lastIndexOf(3, -9), values.lastIndexOf(3, 99));
console.log(values.includes(1, 2), values.includes(1, 3), values.includes(3, -1));
console.log(values.includes(2, 8), values.includes(2, -8));
const empty = JSON.parse('[]');
let conversions = 0;
const from = { valueOf() { conversions++; return 0; } };
console.log(empty.indexOf(1, from), empty.lastIndexOf(1, from), empty.includes(1, from), conversions);
@@ -0,0 +1,18 @@
const values = JSON.parse('[0,1,2,3]');
let conversions = 0;
const start = { valueOf() { conversions++; return 1; } };
const count = { valueOf() { conversions++; return 2; } };
console.log(values.splice(start, count, 8, 9).join(','));
console.log(values.join(','), conversions);
console.log(values.splice(-1, 0, 7).join(','), values.join(','));
const sealed = JSON.parse('[1,2]');
Object.seal(sealed);
let sealedConversions = 0;
const sealedStart = { valueOf() { sealedConversions++; return 0; } };
console.log(sealed.splice(sealedStart, 1, 3).join(','), sealed.join(','), sealedConversions);
const frozen = JSON.parse('[4,5]');
Object.freeze(frozen);
try { frozen.splice(0, 1); } catch (error) { console.log(error.name); }
console.log(frozen.join(','));
+18
View File
@@ -0,0 +1,18 @@
const values = JSON.parse('[0,1,2,3,4]');
console.log(values.splice(1, 2, 8, 9).join(','), values.join(','));
console.log(values.splice(-2, 1).join(','), values.join(','));
console.log(values.splice(1, 0, 7).join(','), values.join(','));
console.log(values.splice(2).join(','), values.join(','));
console.log(values.splice(9, 2).join(','), values.join(','));
const nested = JSON.parse('[1,[2,[3,[4]]],5]');
console.log(JSON.stringify(nested.flat()));
console.log(JSON.stringify(nested.flat(2)));
console.log(JSON.stringify(nested.flat(0)));
console.log(JSON.stringify(nested));
const sealed = JSON.parse('[1,2,3]');
Object.seal(sealed);
console.log(sealed.splice(1, 1, 7).join(','), sealed.join(','));
try { sealed.splice(1, 1); } catch (error) { console.log(error.name); }
console.log(sealed.join(','));
+12
View File
@@ -0,0 +1,12 @@
const object = JSON.parse('{"first":1,"second":2}');
Object.defineProperty(object, 'hidden', { value: 3 });
const descriptors = Object.getOwnPropertyDescriptors(object);
console.log(Object.keys(descriptors).join(','));
console.log(descriptors.first.value, descriptors.first.writable, descriptors.first.enumerable, descriptors.first.configurable);
console.log(descriptors.hidden.value, descriptors.hidden.writable, descriptors.hidden.enumerable, descriptors.hidden.configurable);
console.log(Object.getOwnPropertyDescriptors(3));
console.log(Object.keys(Object.getOwnPropertyDescriptors('cat')).join(','));
const parent = JSON.parse('{"inherited":4}');
const child = Object.create(parent);
child.own = 5;
console.log(Object.keys(Object.getOwnPropertyDescriptors(child)).join(','));
@@ -0,0 +1,11 @@
const parent = JSON.parse('{"first":1,"second":2}');
const child = Object.create(parent);
console.log(Object.keys(parent).join(','));
console.log(Object.getOwnPropertyNames(parent).join(','));
console.log('first' in child);
const parentKeys = [];
for (const key in parent) parentKeys.push(key);
console.log(parentKeys.join(','));
const keys = [];
for (const key in child) keys.push(key);
console.log(keys.join(','));
@@ -0,0 +1,50 @@
const target = JSON.parse('{"visible":1}');
Object.defineProperty(target, 'hidden', { value: 2, writable: true, configurable: true });
console.log(Object.keys(target).join(','));
const absent = JSON.parse('null');
try { Object.prototype.propertyIsEnumerable.call(absent, 'x'); }
catch (error) { console.log(error.name); }
console.log(Object.getOwnPropertyNames(target).join(','));
console.log(target.hidden, Object.hasOwn(target, 'hidden'), target.propertyIsEnumerable('hidden'));
const first = Object.getOwnPropertyDescriptor(target, 'hidden');
console.log(first.value, first.writable, first.enumerable, first.configurable);
Object.defineProperty(target, 'hidden', { value: 3, enumerable: true });
const second = Object.getOwnPropertyDescriptor(target, 'hidden');
console.log(second.value, second.writable, second.enumerable, second.configurable);
console.log(Object.keys(target).join(','), Object.values(target).join(','));
const parent = JSON.parse('{"inherited":4}');
const child = Object.create(parent);
const omittedDescriptors = Object.create(parent, undefined);
console.log(omittedDescriptors.inherited, Object.getOwnPropertyNames(omittedDescriptors).length);
Object.defineProperty(child, 'own', { value: 5, enumerable: true, writable: true, configurable: true });
console.log(Object.keys(child).join(','), Object.getOwnPropertyNames(child).join(','));
console.log(Object.hasOwn(child, 'inherited'), 'inherited' in child);
const inherited = Object.getOwnPropertyDescriptor(child, 'inherited');
console.log(inherited === undefined, child.inherited, Object.getPrototypeOf(child) === parent);
const visited = [];
for (const key in child) visited.push(key);
console.log(visited.join(','));
Object.defineProperty(child, 'inherited', { value: 6, enumerable: false, writable: true });
const shadowed = [];
for (const key in child) shadowed.push(key);
console.log(child.inherited, parent.inherited, shadowed.join(','));
const fixed = JSON.parse('{"item":7}');
Object.freeze(fixed);
const frozen = Object.getOwnPropertyDescriptors(fixed);
console.log(frozen.item.value, frozen.item.writable, frozen.item.configurable);
console.log(Object.isExtensible(fixed), Object.isSealed(fixed), Object.isFrozen(fixed));
try { Object.defineProperty(fixed, 'item', { value: 8 }); }
catch (error) { console.log(error.name); }
try { Object.defineProperty(fixed, 'new', { value: 9 }); }
catch (error) { console.log(error.name); }
console.log(fixed.item, Object.hasOwn(fixed, 'new'));
const nullProto = Object.create(null);
nullProto.key = 10;
console.log(Object.getPrototypeOf(nullProto) === null, Object.keys(nullProto).join(','));
Object.preventExtensions(nullProto);
console.log(Object.isExtensible(nullProto), Object.isSealed(nullProto), Object.isFrozen(nullProto));
Object.seal(nullProto);
console.log(Object.isSealed(nullProto), Object.isFrozen(nullProto));
+44
View File
@@ -0,0 +1,44 @@
const parent = JSON.parse('{"value": 1}');
const child = Object.create(parent);
console.log(child.value, Object.keys(child).length, Object.hasOwn(child, 'value'), 'value' in child);
console.log(child.propertyIsEnumerable('value'), parent.propertyIsEnumerable('value'));
parent.value = 2;
console.log(child.value);
child.value = 3;
console.log(child.value, parent.value, Object.hasOwn(child, 'value'));
const grandchild = Object.create(child);
console.log(grandchild.value);
Object.defineProperty(parent, 'inherited', { get: function() { return this.value + 1; }, enumerable: true });
console.log(grandchild.inherited, Object.keys(grandchild).length);
console.log(grandchild.propertyIsEnumerable('inherited'), parent.propertyIsEnumerable('inherited'));
console.log(Object.getOwnPropertyNames(grandchild).length, Object.getOwnPropertyDescriptor(grandchild, 'value'));
console.log(Object.getPrototypeOf(child) === parent, Object.getPrototypeOf(grandchild) === child);
const inheritedKeys = [];
for (const key in grandchild) inheritedKeys.push(key);
console.log(inheritedKeys.join(','));
Object.defineProperty(child, 'value', { value: 5, enumerable: false, writable: true, configurable: true });
const shadowedKeys = [];
for (const key in grandchild) shadowedKeys.push(key);
console.log(grandchild.value, shadowedKeys.join(','));
const replacement = JSON.parse('{"replacement": 8}');
Object.setPrototypeOf(grandchild, replacement);
console.log(Object.getPrototypeOf(grandchild) === replacement, grandchild.replacement, 'value' in grandchild);
const descriptors = JSON.parse('{"own":{"value":11,"enumerable":true,"writable":true}}');
const described = Object.create(replacement, descriptors);
console.log(described.own, described.replacement, Object.keys(described).join(','));
const locked = JSON.parse('{"first":1,"second":2}');
console.log(Object.isExtensible(locked), Object.isSealed(locked), Object.isFrozen(locked));
Object.preventExtensions(locked);
console.log(Object.isExtensible(locked), Object.isSealed(locked), Object.isFrozen(locked));
try { locked.third = 3; } catch (error) { console.log(error.name); }
console.log(Object.hasOwn(locked, 'third'));
const sealed = JSON.parse('{"first":1}');
Object.seal(sealed);
console.log(Object.isSealed(sealed), Object.isFrozen(sealed), Object.getOwnPropertyDescriptor(sealed, 'first').configurable);
sealed.first = 4;
console.log(sealed.first);
const frozen = JSON.parse('{"first":1}');
Object.freeze(frozen);
console.log(Object.isSealed(frozen), Object.isFrozen(frozen), Object.getOwnPropertyDescriptor(frozen, 'first').writable);
try { frozen.first = 4; } catch (error) { console.log(error.name); }
console.log(frozen.first);
@@ -0,0 +1,14 @@
console.log(Object.getOwnPropertyDescriptor(true, 'x'));
console.log(Object.getOwnPropertyDescriptor(7, 'x'));
const text = Object.getOwnPropertyDescriptor('abc', '1');
console.log(text.value, text.writable, text.enumerable, text.configurable);
const length = Object.getOwnPropertyDescriptor('abc', 'length');
console.log(length.value, length.writable, length.enumerable, length.configurable);
function item(value) { return value; }
item[1] = 'first';
item[1] = 'second';
const own = Object.getOwnPropertyDescriptor(item, '1');
console.log(own.value, own.writable, own.enumerable, own.configurable);
Object.defineProperty(item, 'fixed', { value: 42, enumerable: false, writable: false, configurable: false });
const fixed = Object.getOwnPropertyDescriptor(item, 'fixed');
console.log(item['fixed'], fixed.value, fixed.writable, fixed.enumerable, fixed.configurable);
+2 -15
View File
@@ -1,17 +1,4 @@
// Object.create's fence battery: only the null-prototype dictionary
// lowers in a static build (Object.create(null) — corpus 2581/2582).
// Every other prototype is a NAMED fence, not an own-copy stand-in: Node
// lists NO own keys on the created object (Object.keys/inspect/JSON all
// answer empty) and mutating the prototype afterwards shows through it
// LIVE — observations a copy answers wrong silently.
// A static record prototype.
// Statically shaped records cannot serve as live checked-dynamic prototypes.
const base = { indent: 2 };
const viaRecord = Object.create(base);
// A checked-dynamic (dyn) prototype.
const dynProto: object = JSON.parse('{"a":1}');
const viaDyn = Object.create(dynProto);
// The properties-descriptor form.
const withDescriptors = Object.create(null, { a: { value: 1, enumerable: true } });
base.indent = 4;
@@ -1,26 +1,8 @@
object-create-fences.ts:10:19 - error SC2020: 'Object.create over '{ indent: number }' prototypes' is part of the standard library types but has no scriptc lowering yet
object-create-fences.ts:3:19 - error SC2020: 'Object.create over '{ indent: number }' prototypes' is part of the standard library types but has no scriptc lowering yet
9 | const base = { indent: 2 };
10 | const viaRecord = Object.create(base);
| ^~~~~~~~~~~~~~~~~~~
11 |
2 | const base = { indent: 2 };
3 | const viaRecord = Object.create(base);
| ^~~~~~~~~~~~~~~~~~~
4 | base.indent = 4;
hint: the compiled representations have no prototype chain, and an own-copy would answer wrong observably (Node lists NO own keys on the created object, and prototype mutations show through it live) — only Object.create(null) lowers
object-create-fences.ts:14:16 - error SC2020: 'Object.create over 'unknown' prototypes' is part of the standard library types but has no scriptc lowering yet
13 | const dynProto: object = JSON.parse('{"a":1}');
14 | const viaDyn = Object.create(dynProto);
| ^~~~~~~~~~~~~~~~~~~~~~~
15 |
hint: the compiled representations have no prototype chain, and an own-copy would answer wrong observably (Node lists NO own keys on the created object, and prototype mutations show through it live) — only Object.create(null) lowers
object-create-fences.ts:17:25 - error SC2020: 'Object.create with a properties-descriptor argument' is part of the standard library types but has no scriptc lowering yet
16 | // The properties-descriptor form.
17 | const withDescriptors = Object.create(null, { a: { value: 1, enumerable: true } });
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
18 |
hint: create first, then assign: const o = Object.create(null); o.k = v
hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)