Skip to content
15 changes: 10 additions & 5 deletions docs/interpreter-construction.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,22 +51,27 @@ documentation differs.

## Accepted evidence

The 113-case matrix has 105 adopted comparisons and eight explicit future
The 118-case matrix has 106 adopted comparisons and twelve explicit future
refusals. Node and Chromium each pass draining and instruction-boundary execution:
452 observations, including 436 measured zero logical teardown records. Source,
472 observations, including 456 measured zero logical teardown records. Source,
corpus and driver hashes are checked against the retained reports. Dedicated
tests cover callback source frames, temporary metaclasses, missing/conflicting
class cells, notification destinations, suspended generators, every-instruction
collection, cancellation/close and real recursion/work/allocation failures.
All 385 Node tests pass, including the eight unchanged composed resource cases.
All 387 Node tests pass, including the eight unchanged composed resource cases.

Installed offline console acceptance has 487 program comparisons, 44 acceptance
Installed offline console acceptance has 488 program comparisons, 45 acceptance
groups and 98 Stop/Restart cycles. New source composes construction, super,
metaclasses and generators after offline reload. Constructor/metaclass errors
retain their Python source diagnostics; custom preparation remains a refusal.
The waiting class-to-core update still removes class/library authority.

The [payload and source record](measurements/2026-10-26-construction.json)
contains all 38 file sizes and hashes for the 592,822-byte construction console,
contains all 38 file sizes and hashes for the 593,610-byte construction console,
including the receipt and deterministic compression observations. This is a
complete local payload measurement, with no timing, device or package claim.

Advertised `__class__` identities remain an explicit reflection boundary. Type
queries and super refuse a required advertised-class lookup before callback
effects. `type(value)`, successful actual-type checks and real-subtype super
binding continue without consulting these hooks.
156 changes: 78 additions & 78 deletions docs/measurements/2026-10-26-construction.json

Large diffs are not rendered by default.

18 changes: 15 additions & 3 deletions runtime/class-values.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ export class ClassRuntime {
if (name === null || name === undefined) this.fail("UnsupportedOperationError", "This reflective value type requires a separate stage", "UNSUPPORTED");
return this.builtin(name);
}
classAttribute(value) {
if (value instanceof InstanceValue && this.hasSlot(value.type, "__class__"))
this.fail("UnsupportedOperationError", "Advertised __class__ identities require their separate reflection stage", "UNSUPPORTED");
return this.typeOf(value);
}
lookup(type, name, native = true, start = 0) {
this.key(name); this.touch(type);
if (!this.isType(type)) throw new TypeError("An adopted class is required.");
Expand Down Expand Up @@ -201,7 +206,7 @@ export class ClassRuntime {
}
if (!(receiver instanceof ClassValue || receiver instanceof InstanceValue))
this.fail("UnsupportedOperationError", "Attribute receiver is outside the class profile.", "UNSUPPORTED");
if (receiver instanceof InstanceValue && name === "__class__") return value(receiver.type);
if (receiver instanceof InstanceValue && name === "__class__") return value(this.classAttribute(receiver));
if (receiver instanceof ClassValue) {
if (name === "__class__") return value(receiver.metaclass || this.builtin("type"));
if (["__name__", "__qualname__", "__module__"].includes(name)) return value(this.values.string(Array.from(
Expand Down Expand Up @@ -304,7 +309,11 @@ export class ClassRuntime {
this.touch(type);
if (receiver === null) return this.make(SuperValue, type, null, null);
let owner = receiver instanceof ClassValue && this.subclass(receiver, type) ? receiver : this.typeOf(receiver);
if (owner === null || !this.subclass(owner, type)) this.fail("TypeError", "super receiver must be an instance or subtype of its first argument");
if (owner === null || !this.subclass(owner, type)) {
if (receiver instanceof InstanceValue && this.hasSlot(receiver.type, "__class__"))
this.fail("UnsupportedOperationError", "Advertised __class__ identities require their separate reflection stage", "UNSUPPORTED");
this.fail("TypeError", "super receiver must be an instance or subtype of its first argument");
}
return this.make(SuperValue, type, receiver, owner);
}
callable(value) {
Expand Down Expand Up @@ -332,7 +341,10 @@ export class ClassRuntime {
}
if (!this.isType(type)) this.fail("TypeError", "isinstance requires an adopted type.");
this.touch(type); this.touch(value);
return type.root || this.subclass(this.typeOf(value), type);
if (type.root || this.subclass(this.typeOf(value), type)) return true;
if (value instanceof InstanceValue && this.hasSlot(value.type, "__class__"))
this.fail("UnsupportedOperationError", "Advertised __class__ identities require their separate reflection stage", "UNSUPPORTED");
return false;
}
selectMetaclass(bases, explicit = undefined) {
let selected = explicit === undefined ? this.builtin("type") : explicit;
Expand Down
13 changes: 13 additions & 0 deletions runtime/construction.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,16 @@ test('dynamic class identities and namespace keys enforce budgets as owned failu
assert.equal(result.error.code, 'BUDGET'); closed(result.closed);
}
});

test('deferred advertised class lookup refuses before descriptor effects', async () => {
const prefix = 'class A: pass\nclass B:\n @property\n def __class__(self):\n print("hook")\n return A\nb=B()\n';
for (const expression of ['b.__class__', 'getattr(b,"__class__")', 'isinstance(b,A)', 'super(A,b)']) {
const result = await run(prefix+expression+'\n');
assert.equal(result.error.code, 'UNSUPPORTED'); assert.equal(result.stdout, ''); closed(result.closed);
}
});

test('real type identity and successful actual checks do not consult advertised classes', async () => {
const result = await run('class A:\n @property\n def __class__(self):\n print("never")\n return int\na=A()\nprint(type(a) is A,isinstance(a,A),isinstance(a,object))\nclass B(A): pass\nprint(super(A,B()).__self_class__ is B)\n');
assert.equal(result.error, undefined, JSON.stringify(result.error)); assert.equal(result.stdout, 'True True True\nTrue\n'); closed(result.closed);
});
2 changes: 1 addition & 1 deletion runtime/functions.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,7 @@ function activation(program, {maxSteps = 10_000, maxWork = 1_000_000, maxDepth =
inheritedException: frame.activeException};
else {
const receiver = stack.pop();
if (op === "attribute-get" && arg === "__class__") stack.push(classes.typeOf(receiver));
if (op === "attribute-get" && arg === "__class__") stack.push(classes.classAttribute(receiver));
else if (receiver instanceof GeneratorValue) {
if (!["send", "throw", "close", "__iter__", "__next__"].includes(arg)) hard("UNSUPPORTED", "UnsupportedOperationError", "Generator reflection requires a separate stage.");
stack.push(allocate(ValueMethod, receiver, arg));
Expand Down
5 changes: 5 additions & 0 deletions scripts/classes_console_browser_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,11 @@ def refused(source, prefix):
refused('class A:\n def f(self): return super().missing\nA().f()\n', 'AttributeError:')
refused('class M(type):\n @classmethod\n def __prepare__(meta,name,bases): return {}\n', 'UnsupportedSyntaxError:')
checks.append('constructor-super-metaclass-errors-and-future-hooks-retain-offline-diagnostics')
identity = 'class A: pass\nclass B:\n @property\n def __class__(self):\n print("hook")\n return A\nb=B()\n'
for expression in ['b.__class__', 'getattr(b,"__class__")', 'isinstance(b,A)', 'super(A,b)']:
refused(identity + expression + '\n', 'UnsupportedOperationError:')
assert page.locator('#result').inner_text() == ''
checks.append('deferred-advertised-class-identities-refuse-before-offline-hook-effects')
busy = [
'class Spin:\n def __call__(self):\n while True: pass\nSpin()()\n',
'class Spin:\n @property\n def __call__(self):\n while True: pass\nSpin()()\n',
Expand Down
6 changes: 3 additions & 3 deletions src/pocket_python/_interpreter_lock.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
"""Known source-console assets, canonical LF bytes; refresh with scripts/lock_interpreter.py."""

ENGINE_FINGERPRINT = 'ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826'
ENGINE_FINGERPRINT = '5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490'
ENGINE_FILES = (
('browser-protocol.mjs', 10589, '920e8d07c7070995636dff40fdf4333afc9bd4143483db5d8dff82ec1f28ff76'),
('bytecode.mjs', 6836, 'e76fabeea879bc1da8016283f7e74df5974479666b100199c2d3c31a53b8ecf4'),
('class-policy.mjs', 2043, '38fd2e79653b9219bbf1b98672dcd36cc9846d7d0d15c27864500ea3af8a7a6d'),
('class-values.mjs', 23879, '06d53f86f5d3bfa5f0b3d244302cf23cf1b022a403bdbfd0b2c9b6b8199bd002'),
('class-values.mjs', 24659, '47bc0508b412f95e0325cbcd30ec2d21f3b6b1bd538f9df82529f6c19b9f7002'),
('function-bytecode.mjs', 20693, '601e2bc41d4f5936b4eca9485d152f2962c3b4072829e7b12a9c7c5cbe888c69'),
('function-objects.mjs', 4496, '46d9d7bdd6acedb77cbbfa4f18b3ec57dda15aaab58cb8352c98cf9544944d69'),
('functions.mjs', 60030, 'fdd226f04ef617fe31845ae5eab34c639a74329c86bd4dfbd5993cf37040f95e'),
('functions.mjs', 60038, '35cff7e041738b20a27aafedf4c04797da257bc3f6f5bb8b3135c940376cff1b'),
('interpreter-host.mjs', 7019, '0bb0a64faffcf77e0064258e4cfcaa7a937c4372129849f76901bee91b5a5ae2'),
('interpreter-worker.mjs', 8282, '937a97cc49d987492de0bc16021a4a804ef51e4ea5b398f79c92e5d30ac82558'),
('library-catalog.mjs', 907, '0adcaae0f6f07b4e1acb22e46493ec99f7247a5828b426c703530d8b2736032c'),
Expand Down
30 changes: 30 additions & 0 deletions tests/fixtures/interpreter/construction.json
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,36 @@
"stage": "Future",
"source": "def factory(name,bases,ns): return 7\nclass A(metaclass=factory): pass\nprint(A)\n",
"stdout": "7\n"
},
{
"id": "future-advertised-class-value",
"stage": "Future",
"source": "class A:\n __class__=int\nprint(A().__class__ is int)\n",
"stdout": "True\n"
},
{
"id": "future-advertised-class-getter",
"stage": "Future",
"source": "class A:\n @property\n def __class__(self):\n print(\"hook\")\n return int\nprint(A().__class__ is int)\n",
"stdout": "hook\nTrue\n"
},
{
"id": "future-advertised-class-relation",
"stage": "Future",
"source": "class A:\n @property\n def __class__(self):\n print(\"hook\")\n return int\nprint(isinstance(A(),int))\n",
"stdout": "hook\nTrue\n"
},
{
"id": "future-advertised-class-super",
"stage": "Future",
"source": "class A: pass\nclass B:\n __class__=A\nprint(super(A,B()).__self_class__ is A)\n",
"stdout": "True\n"
},
{
"id": "actual-class-before-advertised",
"stage": "O5",
"source": "class A:\n @property\n def __class__(self):\n print(\"never\")\n return int\na=A()\nprint(type(a) is A,isinstance(a,A),isinstance(a,object))\n",
"stdout": "True True True\n"
}
]
}
Loading