Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/interpreter-construction.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
INT-OBJECT-01 adopts the following finite construction slices against CPython
3.14.7. `construction.json` records independent source/reference expectations;
an authored reference is not evidence that the independent engine implements it.
O3 is implemented; O4/O5 remain reference-only until their changes are accepted.
Classes remain explicit in classes-source/1 and bytecode 5. The default
core-source/1 and bytecode 4 do not gain class syntax or reflective builtins.

Expand Down
5 changes: 3 additions & 2 deletions docs/interpreter-object-protocols.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ classes-source/1; core-source/1 keeps primitive arithmetic and generator support
| --- | --- |
| O1 | getattr(object, name[, default]), hasattr(object, name), setattr(object, name, value), delattr(object, name) on admitted class attributes. Existing descriptor precedence, Python callbacks and metadata rules apply. Names must be strings; only AttributeError is suppressed by defaults/hasattr. No lexical mangling is applied to dynamic strings. |
| O2 | Binary/reflected/in-place slots for +, -, *, /, //, %, **, shifts and bitwise operators; unary +, -, ~; divmod and two/three-argument pow. Strict right-subclass reflected overrides have priority, same-type reflected fallback is suppressed, NotImplemented advances dispatch, and in-place slots fall back to binary dispatch. Results can be arbitrary owned Python values. |
| Future | __new__, super, richer type/isinstance/issubclass, builtin subclasses, metaclasses, custom attribute hooks, ordering, dir/vars/globals/locals/id, and dynamic exec/eval/compile/import remain independently gated. Matrix multiplication, user-valued power modulus dispatch and ordinary list/tuple/text repetition require separate slices. Existing bytes repetition and list in-place repetition accept the adopted index hook. |
| O3 | Allocation and initialization are adopted in the [construction contract](interpreter-construction.md), including __new__ and conditional initialization of its returned value. |
| Future | super, richer type/isinstance/issubclass, builtin subclasses, metaclasses, custom attribute hooks, ordering, dir/vars/globals/locals/id, and dynamic exec/eval/compile/import remain independently gated. Matrix multiplication, user-valued power modulus dispatch and ordinary list/tuple/text repetition require separate slices. Existing bytes repetition and list in-place repetition accept the adopted index hook. |

Implicit operations consult type/MRO slots and descriptor binding, ignoring
instance shadow attributes. Signature and callback failures propagate; a method
Expand All @@ -28,7 +29,7 @@ and [attribute builtins](https://docs.python.org/3.14/library/functions.html#get

## Acceptance

The corpus contains 44 adopted comparisons and two explicit construction/hook
The corpus contains 45 adopted comparisons and one explicit attribute-hook
refusals. CI runs Node and Chromium through draining and instruction-boundary
drivers. Dedicated tests cover all twelve binary/reflected/in-place operator
families, unary hooks, subclass priority, descriptor binding, sequence index
Expand Down
2 changes: 1 addition & 1 deletion runtime/class-policy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const CLASS_SPECIAL_POLICY = Object.freeze(Object.fromEntries([
...Object.values(ARITHMETIC_SLOTS).flatMap(stem => ["", "r", ...(stem === "divmod" ? [] : ["i"])].map(prefix =>
["__" + prefix + stem + "__", Object.freeze({...ordinarySlot, implicit: true})])),
...["__neg__", "__pos__", "__invert__"].map(name => [name, Object.freeze({...ordinarySlot, implicit: true})]),
["__init__", ordinarySlot], ...DESCRIPTOR_HOOKS.map(name => [name, ordinarySlot]),
["__init__", ordinarySlot], ["__new__", ordinarySlot], ...DESCRIPTOR_HOOKS.map(name => [name, ordinarySlot]),
["__module__", Object.freeze({define: true})], ["__qualname__", Object.freeze({define: true})]
]));
export const isSpecialName = name => typeof name === "string" && name.startsWith("__") && name.endsWith("__");
Expand Down
2 changes: 1 addition & 1 deletion runtime/class-policy.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ test('shared policy is immutable and keeps metadata distinct from ordinary slot

test('source and direct class records reject unadopted protocols through the same policy', () => {
const runtime = new ClassRuntime(), root = runtime.root();
for (const name of ['__matmul__', '__format__', '__new__', '__getattribute__']) {
for (const name of ['__matmul__', '__format__', '__prepare__', '__getattribute__']) {
assert.equal(allowsClassSpecial(name, 'define'), false);
assert.throws(() => compileSource('print("never")\nclass A:\n '+name+'=None\n', profile), {code: 'UNSUPPORTED'});
assert.throws(() => runtime.create('A', 'A', 'app', [], new Map([[name, null]]), root), {code: 'UNSUPPORTED'});
Expand Down
23 changes: 12 additions & 11 deletions runtime/class-values.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ export class ClassRuntime {
if (!allowsClassSpecial(key, "define"))
this.fail("UnsupportedOperationError", "Custom special methods require a later class stage.", "UNSUPPORTED");
}
if (namespace.has("__init__")) this.initializer(namespace.get("__init__"));
if (namespace.get("__new__") instanceof FunctionValue)
namespace.set("__new__", this.make(MethodWrapperValue, "staticmethod", namespace.get("__new__")));
for (const [key, expected] of [["__module__", module], ["__qualname__", qualname]])
if (namespace.has(key) && this.values.text(namespace.get(key)) !== expected)
this.fail("UnsupportedOperationError", "Class metadata overrides are outside C1.", "UNSUPPORTED");
Expand All @@ -72,14 +73,20 @@ export class ClassRuntime {
if (!(type instanceof ClassValue)) throw new TypeError("An adopted class is required.");
this.touch(type); return this.make(InstanceValue, type);
}
lookup(type, name) {
hasSlot(type, name) {
// The bounded MRO is also the native slot-presence index. Default calls keep
// their original lookup/accounting path; only adopted overrides add phases.
return type.mro.some(base => !base.root && base.namespace.has(name));
}
lookup(type, name, native = true) {
this.key(name); this.touch(type);
if (!(type instanceof ClassValue)) throw new TypeError("An adopted class is required.");
for (const base of type.mro) {
this.charge(1);
if (!native && base.root && ["__new__", "__init__"].includes(name)) continue;
if (base.namespace.has(name)) return {found: true, value: base.namespace.get(name)};
if (base.root && name === "__hash__") {
const value = this.make(BuiltinValue, "object.__hash__");
if (native && base.root && ["__hash__", "__new__", "__init__"].includes(name)) {
const value = this.make(BuiltinValue, "object." + name);
base.namespace.set(name, value);
try { const layout = valueLayout(base); this.memory?.resize(base, layout.bytes, layout.children); }
catch (error) { base.namespace.delete(name); throw error; }
Expand Down Expand Up @@ -151,7 +158,7 @@ export class ClassRuntime {
if (value instanceof MethodWrapperValue) return {kind: "value", value: value.kind === "staticmethod" ?
value.callable : this.make(BoundMethodValue, value.callable, owner)};
return {kind: "value", value: receiver !== null && (value instanceof FunctionValue ||
value instanceof BuiltinValue && value.name === "object.__hash__") ? this.make(BoundMethodValue, value, receiver) : value};
value instanceof BuiltinValue && ["object.__hash__", "object.__init__"].includes(value.name)) ? this.make(BoundMethodValue, value, receiver) : value};
}
special(receiver, name) {
this.key(name); this.touch(receiver);
Expand Down Expand Up @@ -190,7 +197,6 @@ export class ClassRuntime {
this.fail("UnsupportedOperationError", "Attribute mutation requires a class or instance.", "UNSUPPORTED");
if (receiver instanceof ClassValue && receiver.root) this.fail("TypeError", "Cannot mutate the immutable object type.");
if (!allowsClassSpecial(name, "mutate")) this.fail("UnsupportedOperationError", "Special attribute mutation is outside classes-source/1.", "UNSUPPORTED");
if (receiver instanceof ClassValue && name === "__init__" && !remove) this.initializer(value);
const map = receiver instanceof ClassValue ? receiver.namespace : receiver.attributes;
const present = map.has(name), before = map.get(name);
if (remove && !present) this.fail("AttributeError", "Class/instance has no attribute '" + name + "'.");
Expand Down Expand Up @@ -223,11 +229,6 @@ export class ClassRuntime {
});
return this.make(PropertyValue, ...fields.map(name => assigned.has(name) ? assigned.get(name) : null));
}
initializer(value) {
if (value instanceof MethodWrapperValue) value = value.callable;
if (!(value instanceof FunctionValue || value instanceof BoundMethodValue && value.callable instanceof FunctionValue))
this.fail("UnsupportedOperationError", "Initializers require Python functions, retained Python bound methods or native method wrappers.", "UNSUPPORTED");
}
isinstance(value, type) {
if (!(type instanceof ClassValue)) this.fail("TypeError", "isinstance requires an adopted class.");
this.touch(type); this.touch(value);
Expand Down
2 changes: 0 additions & 2 deletions runtime/class-values.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,6 @@ test("identity/truth, metadata and explicit unsupported boundaries remain separa
assert.throws(() => s.runtime.mutate(A, "__bases__", null), {code: "UNSUPPORTED"});
assert.throws(() => s.runtime.mutate(s.root, "value", 7n), {name: "TypeError"});
assert.throws(() => s.make("Special", [], new Map([["__matmul__", method("f")]])), {code: "UNSUPPORTED"});
assert.throws(() => s.make("BadInit", [], new Map([["__init__", A]])), {code: "UNSUPPORTED"});
assert.throws(() => s.runtime.mutate(A, "__init__", s.runtime.make(BoundMethodValue, A, a)), {code: "UNSUPPORTED"});
assert.throws(() => s.runtime.subclass(a, A), {name: "TypeError"});
} finally { s.close(); }
});
Expand Down
79 changes: 79 additions & 0 deletions runtime/construction.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {runSourceRequest} from './source-request.mjs';
import {compileSourceModules} from './source-modules.mjs';
import {ModuleSession} from './module-session.mjs';
import {Execution} from './functions.mjs';
import {completion, completionRoots} from './operations.mjs';

const profile = {languageProfile: 'classes-source/1'};
const unit = source => ({name: 'app', package: false, imports: [], source, notice: 'Synthetic construction fixture'});
const run = (source, options = {}, memoryLimits) => runSourceRequest({modules: [unit(source)], compileOptions: profile, options, memoryLimits});
const closed = stats => { for (const key of ['bytes', 'objects', 'roots', 'frames', 'external']) assert.equal(stats[key], 0, key); };

test('allocation continuations retain the class, original operands and outer destinations', () => {
const type = {}, argument = {}, keyword = {}, outer = {}, after = completion('relay', {receiver: outer});
const target = completion('new-result', {type, args: [argument], keywords: ['x'], keywordValues: [keyword], depth: 1, after});
assert.deepEqual(completionRoots(target), [type, argument, keyword, outer]);
});

test('constructor callbacks and bound initializer descriptors survive collection, cancellation and close', async () => {
const sources = [
'class D:\n def __get__(self,obj,owner):\n print("entered")\n def create(cls,x,*,y): return object.__new__(cls)\n return create\nclass A:\n __new__=D()\n def __init__(self,x,*,y): self.x=x[0]+y[0]\nprint([1][0]+A([3],y=[4]).x)\n',
'class D:\n def __get__(self,obj,owner):\n print("entered")\n def initialize(x,*,y): obj.x=x[0]+y[0]\n return initialize\nclass A:\n def __new__(cls,*args,**kwargs): return object.__new__(cls)\n __init__=D()\nprint([1][0]+A([3],y=[4]).x)\n',
];
for (const source of sources) {
const {registry} = await compileSourceModules([unit(source)], profile);
try { for (const mode of ['finish', 'cancel', 'close']) {
const session = new ModuleSession(registry); let output = '', cancel = false, reached = false;
session.context.begin({shouldCancel: () => cancel}, text => { output += text; }); const execution = Execution.modules(session.context);
try {
while (execution.state !== 'completed') {
execution.advance(); session.memory.collect([], {cleanup: true});
if (output === 'entered\n') {
reached = true;
if (mode === 'cancel') { cancel = true; assert.throws(() => execution.advance(), {code: 'CANCELLED'}); break; }
if (mode === 'close') break;
}
}
assert.ok(reached); if (mode === 'finish') assert.equal(output, 'entered\n8\n');
} finally { execution.close(); execution.close(); session.context.finish(); session.close(); }
closed(session.stats());
} } finally { registry.close(); }
}
});

test('construction shares Python recursion, work and real allocation limits', async () => {
for (const slot of ['__new__', '__init__']) for (const [body, options, limits, expected] of [
[slot === '__new__' ? 'return cls()' : 'self.__init__()', {maxDepth: 12}, undefined, 'PYTHON'],
['while True: pass', {maxSteps: 200}, undefined, 'BUDGET'],
['a=[]\n while True: a.append([7])', {maxSteps: 100000}, {maxBytes: 22000}, 'OOM'],
]) {
const result = await run('class A:\n def '+slot+'('+(slot === '__new__' ? 'cls' : 'self')+'):\n '+body+'\nA()\n', options, limits);
assert.equal(result.error.code, expected); if (expected === 'PYTHON') assert.equal(result.error.type, 'RecursionError'); closed(result.closed);
}
});

test('constructor binding and body errors retain their original source frames', async () => {
for (const slot of ['__new__', '__init__']) {
const result = await run('class A:\n def '+slot+'(value):\n raise AttributeError("original")\nA()\n');
assert.equal(result.error.type, 'AttributeError');
assert.deepEqual(result.error.traceback.map(frame => [frame.function, frame.location.span[0]]), [['<module>', 4], [slot, 3]]);
closed(result.closed);
}
});

test('reading native constructors does not turn default class calls into custom slots', async () => {
const result = await run('new=object.__new__\ninit=object.__init__\nclass A: pass\ntry: A(1)\nexcept TypeError: print("arguments")\nprint(isinstance(A(),A))\n');
assert.equal(result.error, undefined, JSON.stringify(result.error)); assert.equal(result.stdout, 'arguments\nTrue\n'); closed(result.closed);
});

test('initializer callability is checked at invocation after allocation effects', async () => {
const result = await run('class A:\n def __new__(cls):\n print("allocated")\n return object.__new__(cls)\n __init__=None\nA()\n');
assert.equal(result.error.type, 'TypeError'); assert.equal(result.stdout, 'allocated\n'); closed(result.closed);
});

test('initialization after custom allocation ignores instance shadow attributes', async () => {
const result = await run('class A:\n def __new__(cls):\n value=object.__new__(cls)\n value.__init__=None\n return value\n def __init__(self): self.x=7\nprint(A().x)\n');
assert.equal(result.error, undefined, JSON.stringify(result.error)); assert.equal(result.stdout, '7\n'); closed(result.closed);
});
Loading
Loading