diff --git a/docs/interpreter-compatibility.json b/docs/interpreter-compatibility.json index 2f2ffec..6e101d5 100644 --- a/docs/interpreter-compatibility.json +++ b/docs/interpreter-compatibility.json @@ -237,8 +237,8 @@ { "id": "class-construction", "status": "partial", - "scope": "Explicit classes-source/1 only: user classes, bounded C3 MRO, __init__, ordinary attributes/deletion and bound methods.", - "gaps": "Builtin subclassing, __new__, super, metaclasses, implicit __class__ cells and full reflective behavior are not adopted. Builtin list/tuple bases are explicitly refused with UNSUPPORTED.", + "scope": "Explicit classes-source/1: user classes, bounded C3 MRO, owned __new__/__init__, conditional runtime-type initialization, descriptors, bound methods, lexical __class__ cells and explicit/zero-argument super.", + "gaps": "Metaclasses, builtin instance subclassing and broader type/reflection remain separate construction stages.", "reference_version": "3.14.7", "targets": [ "node", @@ -253,9 +253,8 @@ "metaclass" ], "evidence": [ - "tests/fixtures/interpreter/classes.json:diamond-mro", - "tests/fixtures/interpreter/classes.json:class-body-order", - "tests/fixtures/interpreter/classes.json:implicit-class-cell" + "tests/fixtures/interpreter/construction.json:new-subclass-init", + "tests/fixtures/interpreter/construction.json:super-diamond" ] }, { @@ -1986,9 +1985,9 @@ }, { "id": "super", - "status": "deferred", - "scope": "Not installed; resolving this builtin name raises Python NameError.", - "gaps": "No implementation/API support credit from the negative availability probe.", + "status": "partial", + "scope": "Explicit bound/unbound and zero-argument super with lexical class cells, current C3 lookup, descriptors and proxy metadata in classes-source/1.", + "gaps": "Builtin instance layouts, custom __class__ hooks and proxy reinitialization remain separate.", "reference_version": "3.14.7", "targets": [ "node", @@ -2000,7 +1999,11 @@ "probes": [ "builtin-super" ], - "evidence": [] + "evidence": [ + "tests/fixtures/interpreter/construction.json:super-diamond", + "tests/fixtures/interpreter/construction.json:super-unbound", + "tests/fixtures/interpreter/construction.json:super-generator" + ] }, { "id": "tuple", diff --git a/docs/interpreter-compatibility.md b/docs/interpreter-compatibility.md index f151988..a10f62a 100644 --- a/docs/interpreter-compatibility.md +++ b/docs/interpreter-compatibility.md @@ -59,7 +59,7 @@ issue families; availability alone never certifies an entire type or module. | ordinary-control | supported | if/elif/else, while/for over adopted values, loop else/break/continue, return and finally control transfer. Generator, context-manager, match and async control are separate rows. | | function-binding | partial | def, recursion, positional-only/ordinary/keyword-only parameters; literal and definition-time computed/mutable defaults and lexical closures. Global/nonlocal writes and deletion share the defining module and lexical cells. Variadic positional/keyword parameters pack fresh owned values. Positional starred calls consume owned iterables. Keyword call expansion admits owned dictionaries and keys/getitem mappings with ordered duplicate checks. Construction-hint forms and annotations remain deferred; signatures and error-message comparisons are scoped by interpreter-function-binding.md. | | ordinary-syntax-gaps | partial | Assertions and assignment expressions in both source profiles, with owned truth callbacks and lexical scope writes. Lambda functions use ordinary parameters/defaults and lexical cells. Tuple/list/starred assignments, loop targets, and starred sequence/dictionary displays use owned iteration/mapping phases. Construction hints remain with their sequence task; assertion message formatting and comprehensions retain separate gates. | -| class-construction | partial | Explicit classes-source/1 only: user classes, bounded C3 MRO, __init__, ordinary attributes/deletion and bound methods. Builtin subclassing, __new__, super, metaclasses, implicit __class__ cells and full reflective behavior are not adopted. Builtin list/tuple bases are explicitly refused with UNSUPPORTED. | +| class-construction | partial | Explicit classes-source/1: user classes, bounded C3 MRO, owned __new__/__init__, conditional runtime-type initialization, descriptors, bound methods, lexical __class__ cells and explicit/zero-argument super. Metaclasses, builtin instance subclassing and broader reflection retain their separate construction stages. | | private-names | supported | Lexical private-name mangling in admitted class bodies, nested code, parameters/imports and metadata. No additional syntax beyond the explicit source profile. | | descriptors-decorators | partial | Functions, properties, static/class methods, custom get/set/delete/name-notification descriptors and decorators in classes-source/1. Descriptor-valued setter/deleter/name-notification hooks and unrestricted reflection remain refused. | | call-length-truth | supported | Type-level __call__/__len__/__bool__, callable(), len(), bool(), not and control tests in classes-source/1, including adopted descriptor binding and optional truth fallback. Length uses the explicit signed-64-bit contract; later protocols remain separate. | @@ -151,7 +151,7 @@ issue families; availability alone never certifies an entire type or module. | staticmethod | partial | staticmethod(value) and existing wrapper binding/direct-call rules; classes-source/1. | Unrestricted wrapper introspection. | | str | partial | str() and str(x) for None/bool/int/str/adopted exceptions and user slots; existing/returned strings retain identity. Bytes/bytearray use finite binary representations. | Float/container/default-object displays, object= keyword and encoding forms. | | sum | deferred | Not installed; resolving this builtin name raises Python NameError. | No implementation/API support credit from the negative availability probe. | -| super | deferred | Not installed; resolving this builtin name raises Python NameError. | No implementation/API support credit from the negative availability probe. | +| super | partial | Explicit bound/unbound and zero-argument super with lexical class cells, current C3 lookup, descriptors and proxy metadata in classes-source/1. | Builtin instance layouts, custom __class__ hooks and proxy reinitialization remain separate. | | tuple | partial | Zero or one positional adopted iterable; primitives in both profiles, owned user iteration in classes-source/1 without user length hints. Existing tuple identity and shallow elements are preserved. | User length hints, methods, builtin subclassing and broader reflection remain separate stages. | | type | partial | type(user_instance), one argument; classes-source/1. | Full one-argument and three-argument type construction. | | vars | deferred | Not installed; resolving this builtin name raises Python NameError. | No implementation/API support credit from the negative availability probe. | diff --git a/docs/interpreter-construction.md b/docs/interpreter-construction.md index 736ee8a..b9f8ddc 100644 --- a/docs/interpreter-construction.md +++ b/docs/interpreter-construction.md @@ -3,7 +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. +O3 and O4 are implemented; O5 remains reference-only until its 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. diff --git a/docs/interpreter-object-protocols.md b/docs/interpreter-object-protocols.md index 0042eb0..1d7f328 100644 --- a/docs/interpreter-object-protocols.md +++ b/docs/interpreter-object-protocols.md @@ -11,7 +11,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. | | 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. | +| O4 | Explicit/zero-argument super and lexical class cells follow the [construction contract](interpreter-construction.md). | +| Future | 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 diff --git a/runtime/class-policy.mjs b/runtime/class-policy.mjs index e68ecea..f82fa21 100644 --- a/runtime/class-policy.mjs +++ b/runtime/class-policy.mjs @@ -10,7 +10,8 @@ export const CLASS_SPECIAL_POLICY = Object.freeze(Object.fromEntries([ ["__" + prefix + stem + "__", Object.freeze({...ordinarySlot, implicit: true})])), ...["__neg__", "__pos__", "__invert__"].map(name => [name, Object.freeze({...ordinarySlot, implicit: true})]), ["__init__", ordinarySlot], ["__new__", ordinarySlot], ...DESCRIPTOR_HOOKS.map(name => [name, ordinarySlot]), - ["__module__", Object.freeze({define: true})], ["__qualname__", Object.freeze({define: true})] + ["__module__", Object.freeze({define: true})], ["__qualname__", Object.freeze({define: true})], + ["__class__", Object.freeze({define: true})] ])); export const isSpecialName = name => typeof name === "string" && name.startsWith("__") && name.endsWith("__"); export function allowsClassSpecial(name, context) { diff --git a/runtime/class-values.mjs b/runtime/class-values.mjs index aaa2789..178c614 100644 --- a/runtime/class-values.mjs +++ b/runtime/class-values.mjs @@ -1,4 +1,4 @@ -import {ClassValue, InstanceValue, BoundMethodValue, FunctionValue, BuiltinValue, EXCEPTIONS, PropertyValue, MethodWrapperValue, PropertyAccessorValue} from "./function-objects.mjs"; +import {ClassValue, InstanceValue, BoundMethodValue, SuperValue, FunctionValue, BuiltinValue, EXCEPTIONS, PropertyValue, MethodWrapperValue, PropertyAccessorValue} from "./function-objects.mjs"; import {allowsClassSpecial, isSpecialName} from "./class-policy.mjs"; import {ValueRuntime, valueLayout} from "./values.mjs"; import {isPythonIdentifier} from "./source-identifiers.mjs"; @@ -6,10 +6,11 @@ import {isPythonIdentifier} from "./source-identifiers.mjs"; export const CLASS_LIMITS = Object.freeze({bases: 16, mro: 64, attributes: 128, name: 256}); export class ClassRuntime { - constructor({memory = null, charge = () => {}, fail = null, values = null} = {}) { + constructor({memory = null, charge = () => {}, fail = null, values = null, builtin = null} = {}) { this.memory = memory; this.charge = charge; this.fail = fail || ((name, message, code = "PYTHON") => { throw Object.assign(new Error(message), {name, code}); }); this.values = values || new ValueRuntime({memory, charge, fail: this.fail}); + this.builtin = builtin || (name => this.make(BuiltinValue, name)); } make(Type, ...args) { const value = new Type(...args), layout = valueLayout(value); @@ -78,10 +79,10 @@ export class ClassRuntime { // 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) { + lookup(type, name, native = true, start = 0) { this.key(name); this.touch(type); if (!(type instanceof ClassValue)) throw new TypeError("An adopted class is required."); - for (const base of type.mro) { + for (const base of type.mro.slice(start)) { 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)}; @@ -115,6 +116,16 @@ export class ClassRuntime { read(receiver, name) { this.key(name); this.touch(receiver); const value = value => ({kind: "value", value}); + if (receiver instanceof SuperValue) { + if (name === "__class__") return value(this.builtin("super")); + if (["__thisclass__", "__self__", "__self_class__"].includes(name)) + return value(receiver[{__thisclass__: "thisclass", __self__: "self", __self_class__: "selfClass"}[name]]); + if (receiver.selfClass !== null) { + const item = this.lookup(receiver.selfClass, name, true, receiver.selfClass.mro.indexOf(receiver.thisclass) + 1); + if (item.found) return this.bindAttribute(item.value, receiver.self === receiver.selfClass ? null : receiver.self, receiver.selfClass); + } + this.fail("AttributeError", "super has no attribute '" + name + "'."); + } if (receiver instanceof PropertyValue) { if (["fget", "fset", "fdel"].includes(name)) return value(receiver[name]); if (["getter", "setter", "deleter"].includes(name)) return value(this.make(PropertyAccessorValue, name, receiver)); @@ -134,6 +145,7 @@ export class ClassRuntime { this.fail("UnsupportedOperationError", "Attribute receiver is outside the class profile.", "UNSUPPORTED"); if (receiver instanceof InstanceValue && name === "__class__") return value(receiver.type); if (receiver instanceof ClassValue) { + if (name === "__class__") return value(this.builtin("type")); if (["__name__", "__qualname__", "__module__"].includes(name)) return value(this.values.string(Array.from( receiver[{__name__: "name", __qualname__: "qualname", __module__: "module"}[name]], char => char.codePointAt(0)))); if (["__bases__", "__mro__"].includes(name)) return value(this.values.tuple(name === "__bases__" ? receiver.bases : receiver.mro)); @@ -149,6 +161,8 @@ export class ClassRuntime { return this.bindAttribute(item.value, receiver instanceof ClassValue ? null : receiver, owner); } bindAttribute(value, receiver, owner) { + if (value instanceof SuperValue && value.self === null && receiver !== null) + return {kind: "value", value: this.superProxy(value.thisclass, receiver)}; if (receiver !== null && value instanceof PropertyValue) { if (value.fget === null) this.fail("AttributeError", "Property has no getter."); return {kind: "call", callable: value.fget, args: [receiver]}; @@ -176,6 +190,7 @@ export class ClassRuntime { } write(receiver, name, value, remove = false) { this.key(name); this.touch(receiver); + if (receiver instanceof SuperValue) this.fail("AttributeError", "super attributes are read-only"); if (receiver instanceof InstanceValue) { const item = this.lookup(receiver.type, name), descriptor = item.found ? this.descriptor(item.value) : null; if (item.value instanceof PropertyValue) { @@ -212,6 +227,14 @@ export class ClassRuntime { for (const base of type.mro) { this.charge(1); if (base === parent) return true; } return false; } + superProxy(type, receiver = null) { + if (!(type instanceof ClassValue)) this.fail("TypeError", "super requires an adopted type"); + this.touch(type); + if (receiver === null) return this.make(SuperValue, type, null, null); + const owner = receiver instanceof ClassValue ? receiver : receiver instanceof InstanceValue ? receiver.type : null; + if (owner === null || !this.subclass(owner, type)) this.fail("TypeError", "super receiver must be an instance or subtype of its first argument"); + return this.make(SuperValue, type, receiver, owner); + } callable(value) { this.touch(value); if (value instanceof InstanceValue) return this.lookup(value.type, "__call__").found; diff --git a/runtime/classes-source.test.mjs b/runtime/classes-source.test.mjs index 9e54061..6d5980c 100644 --- a/runtime/classes-source.test.mjs +++ b/runtime/classes-source.test.mjs @@ -37,9 +37,8 @@ test('class imports share object identity while inherited methods retain their d assert.equal(result.closed.bytes,0);assert.equal(result.closed.roots,0); }); -test('metadata overrides and implicit class cells refuse before source effects',async()=>{ +test('metadata overrides and later class stages refuse before source effects',async()=>{ for(const source of [ - 'class A:\n def f(self): return __class__\n', 'class A:\n __module__="custom"\n', 'class A:\n def __qualname__(self): pass\n', 'class A:\n from helper import value as __module__\n', diff --git a/runtime/construction.test.mjs b/runtime/construction.test.mjs index e91fe3e..0d7d95f 100644 --- a/runtime/construction.test.mjs +++ b/runtime/construction.test.mjs @@ -5,6 +5,8 @@ import {compileSourceModules} from './source-modules.mjs'; import {ModuleSession} from './module-session.mjs'; import {Execution} from './functions.mjs'; import {completion, completionRoots} from './operations.mjs'; +import {compileSource} from './source-compiler.mjs'; +import {verifyFunctions} from './function-bytecode.mjs'; const profile = {languageProfile: 'classes-source/1'}; const unit = source => ({name: 'app', package: false, imports: [], source, notice: 'Synthetic construction fixture'}); @@ -77,3 +79,49 @@ test('initialization after custom allocation ignores instance shadow attributes' 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); }); + +test('lexical class cells are private, shared and retained even after method deletion', async () => { + const result = await run('class A:\n def deleted(self): return __class__\n del deleted\nclass B:\n def f(self):\n return lambda: __class__\nf=B().f()\nprint(f() is B)\n'); + assert.equal(result.error, undefined, JSON.stringify(result.error)); assert.equal(result.stdout, 'True\n'); closed(result.closed); + const compiled = compileSource('class A:\n def f(self): return __class__\n', profile); + const raw = JSON.parse(JSON.stringify(compiled.program)); + raw.functions[1].locals = raw.functions[1].locals.filter(name => name !== '@source:class'); + assert.throws(() => verifyFunctions(raw), {code: 'BYTECODE'}); +}); + +test('super descriptors and suspended generators retain the dynamic owner across collection', async () => { + const source = 'class A:\n @property\n def f(self):\n print("entered")\n return [self.x]\nclass B(A):\n def g(self):\n yield 1\n yield super().f[0]\nb=B()\nb.x=7\ng=b.g()\nprint(next(g))\ndel b\nprint(next(g))\n'; + 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 === '1\nentered\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, '1\nentered\n7\n'); + } finally { execution.close(); execution.close(); session.context.finish(); session.close(); } + closed(session.stats()); + } } finally { registry.close(); } +}); + +test('super reads the current base namespace, bypasses shadows and exposes immutable proxy metadata', async () => { + const result = await run('class A:\n def f(self): return 1\nclass B(A): pass\nb=B()\np=super(B,b)\ndef changed(self): return 7\nA.f=changed\nb.f=None\nprint(p.f(),bool(p),p.__class__ is super,hash(p)==hash(p))\ntry: setattr(p,"__self__",None)\nexcept AttributeError: print("readonly")\n'); + assert.equal(result.error, undefined, JSON.stringify(result.error)); assert.equal(result.stdout, '7 True True True\nreadonly\n'); closed(result.closed); +}); + +test('super descriptor failures retain source frames and shared resource limits', async () => { + for (const [body, options, limits, expected] of [ + ['return self.f', {maxDepth: 12}, undefined, 'PYTHON'], + ['while True: pass', {maxSteps: 200}, undefined, 'BUDGET'], + ['a=[]\n while True: a.append([7])', {maxSteps: 100000}, {maxBytes: 26000}, 'OOM'], + ]) { + const result = await run('class A:\n @property\n def f(self):\n '+body+'\nclass B(A):\n def g(self): return super().f\nB().g()\n', options, limits); + assert.equal(result.error.code, expected); if (expected === 'PYTHON') assert.equal(result.error.type, 'RecursionError'); closed(result.closed); + } +}); diff --git a/runtime/function-bytecode.mjs b/runtime/function-bytecode.mjs index 30d2f9c..cc459e9 100644 --- a/runtime/function-bytecode.mjs +++ b/runtime/function-bytecode.mjs @@ -57,7 +57,7 @@ export function verifyFunctions(program) { params[key] = fn.params[key]; } const allParams = [...params.posonly, ...params.positional, ...params.kwonly, ...variadic.map(key => params[key])]; - const locals = names(fn.locals, storageName), free = names(fn.free, validName); + const locals = names(fn.locals, key => storageName(key) || kind === "class" && key === "@source:class"), free = names(fn.free, validName); const globals = names(Object.hasOwn(fn, "globals") ? fn.globals : [], validName), nonlocals = names(Object.hasOwn(fn, "nonlocals") ? fn.nonlocals : [], validName); if (globals.some(key => locals.includes(key) || nonlocals.includes(key) || kind !== "class" && free.includes(key)) || nonlocals.some(key => !free.includes(key) || locals.includes(key)) || functionIndex === 0 && nonlocals.length) @@ -228,6 +228,7 @@ export function verifyFunctions(program) { if (op === "function" && [...functions[target].params.defaults, ...Object.values(functions[target].params.kwdefaults)].some(value => value === null)) fail("A runtime-default function requires definition values."); if (["function", "function-defaults", "class"].includes(op) && functions[target].free.some(key => + !(fn.kind === "class" && key === "__class__" && fn.locals.includes("@source:class")) && (fn.kind === "class" || !fn.locals.includes(key)) && !fn.free.includes(key))) fail("Function/class capture has no lexical cell."); } const code = Object.freeze({format: program.format, version: program.version, constants, functions: Object.freeze(functions)}); diff --git a/runtime/function-objects.mjs b/runtime/function-objects.mjs index 36976e4..860e3cf 100644 --- a/runtime/function-objects.mjs +++ b/runtime/function-objects.mjs @@ -49,6 +49,9 @@ export class InstanceValue { export class BoundMethodValue { constructor(callable, receiver) { this.callable = callable; this.receiver = receiver; } } +export class SuperValue { + constructor(thisclass, self, selfClass) { this.thisclass = thisclass; this.self = self; this.selfClass = selfClass; Object.freeze(this); } +} export {DESCRIPTOR_HOOKS}; export class BuiltinValue { constructor(name) { this.name = name; } } export class PropertyValue { diff --git a/runtime/functions.mjs b/runtime/functions.mjs index 66a26a2..8d9070a 100644 --- a/runtime/functions.mjs +++ b/runtime/functions.mjs @@ -1,6 +1,6 @@ import {ARITHMETIC_SLOTS} from "./class-policy.mjs"; import {verifyFunctions} from "./function-bytecode.mjs"; -import {GeneratorValue, Cell, Environment, CodePool, ModuleValue, FunctionValue, CallArguments, BuiltinValue, AppendValue, ValueMethod, ExceptionValue, EXCEPTIONS, matchesException, ClassValue, InstanceValue, BoundMethodValue, PropertyValue, MethodWrapperValue, ProtocolIteratorValue, NotImplementedValue} from "./function-objects.mjs"; +import {GeneratorValue, Cell, Environment, CodePool, ModuleValue, FunctionValue, CallArguments, BuiltinValue, AppendValue, ValueMethod, ExceptionValue, EXCEPTIONS, matchesException, ClassValue, InstanceValue, BoundMethodValue, SuperValue, PropertyValue, MethodWrapperValue, ProtocolIteratorValue, NotImplementedValue} from "./function-objects.mjs"; import {ClassRuntime} from "./class-values.mjs"; import {ValueRuntime, valueLayout, IteratorValue} from "./values.mjs"; import {MemoryArena} from "./memory.mjs"; @@ -78,7 +78,12 @@ function activation(program, {maxSteps = 10_000, maxWork = 1_000_000, maxDepth = if (["PYTHON", "ZERO_DIVISION"].includes(code)) python(name, message); hard(code, name, message); }}); - const classes = new ClassRuntime({memory, charge, values, fail: values.fail}); + const nativeBuiltin = name => { + if (moduleContext) return moduleContext.builtin(name, () => allocate(BuiltinValue, name)); + if (!state.builtins.has(name)) state.builtins.set(name, allocate(BuiltinValue, name)); + return state.builtins.get(name); + }; + const classes = new ClassRuntime({memory, charge, values, fail: values.fail, builtin: nativeBuiltin}); const objectType = () => moduleContext.builtin("object", () => classes.root()); const setCell = (cell, value, bound = true) => mutate(cell, () => { const before = [cell.value, cell.bound]; cell.value = value; cell.bound = bound; @@ -99,7 +104,7 @@ function activation(program, {maxSteps = 10_000, maxWork = 1_000_000, maxDepth = if (frame.callable.pool.code.version === 5 && key === "NotImplemented") return moduleContext.builtin(key, () => allocate(NotImplementedValue)); if (builtinNames.has(key) || (moduleContext && ["isinstance", "bool", "iter", "next"].includes(key)) || (frame.callable.pool.code.version >= 4 && ["memoryview", "bytes", "bytearray", "set", "frozenset", "all", "any", "list", "tuple", "range", "int", "float", "complex", "abs", "divmod", "pow", "round", "bin", "oct", "hex", "chr", "ord"].includes(key)) || - (frame.callable.pool.code.version === 5 && ["getattr", "setattr", "delattr", "hasattr", "type", "issubclass", "property", "staticmethod", "classmethod", "callable", "slice", "hash"].includes(key))) { + (frame.callable.pool.code.version === 5 && ["super", "getattr", "setattr", "delattr", "hasattr", "type", "issubclass", "property", "staticmethod", "classmethod", "callable", "slice", "hash"].includes(key))) { if (moduleContext) return moduleContext.builtin(key, () => allocate(BuiltinValue, key)); if (!state.builtins.has(key)) state.builtins.set(key, allocate(BuiltinValue, key)); return state.builtins.get(key); @@ -434,9 +439,12 @@ function activation(program, {maxSteps = 10_000, maxWork = 1_000_000, maxDepth = .map(key => [key, frame.locals.get(key).value])); const completion = frame.completion; state.temporaries.push(...namespace.values(), ...completion.bases); + const classCell = frame.locals.get("@source:class"); + if (classCell) state.temporaries.push(classCell); state.current = {...frame.callsite, instruction: frame.returnInstruction}; frame.leave(); state.frames.pop(); const type = classes.create(frame.fn.name, frame.fn.qualname, completion.module, completion.bases, namespace, objectType()); + if (classCell) { state.temporaries.push(type, classCell); setCell(classCell, type); } dispatch(notifyNext(type, [...namespace], 0, frame.callsite)); return; } const completion = frame.completion; @@ -477,7 +485,7 @@ function activation(program, {maxSteps = 10_000, maxWork = 1_000_000, maxDepth = else if (op === "function" || op === "function-defaults" || op === "class") { const owner = frame.callable.pool; const closure = new Map(owner.code.functions[arg].free.map(key => [key, - frame.fn.kind === "class" ? frame.closure.get(key) : frame.locals.get(key) || frame.closure.get(key)])); + frame.fn.kind === "class" ? key === "__class__" && frame.locals.has("@source:class") ? frame.locals.get("@source:class") : frame.closure.get(key) : frame.locals.get(key) || frame.closure.get(key)])); const defaults = op === "function-defaults" ? stack.splice(stack.length - extra - fourth.length) : null; const callable = allocate(FunctionValue, owner, arg, frame.callable.globals, closure, defaults); if (op !== "class") stack.push(callable); @@ -536,7 +544,7 @@ function activation(program, {maxSteps = 10_000, maxWork = 1_000_000, maxDepth = else if (values.isBinary(receiver)) { values.binaryMethod(receiver, arg); stack.push(allocate(ValueMethod, receiver, arg)); } else if (arg === "encode" && values.isString(receiver)) stack.push(allocate(ValueMethod, receiver, arg)); else if (op === "attribute-get" && values.isSlice(receiver)) stack.push(values.sliceAttribute(receiver, arg)); - else if (op === "attribute-get" && (receiver instanceof ClassValue || receiver instanceof InstanceValue || receiver instanceof BoundMethodValue || receiver instanceof FunctionValue || receiver instanceof PropertyValue || receiver instanceof MethodWrapperValue)) { + else if (op === "attribute-get" && (receiver instanceof SuperValue || receiver instanceof ClassValue || receiver instanceof InstanceValue || receiver instanceof BoundMethodValue || receiver instanceof FunctionValue || receiver instanceof PropertyValue || receiver instanceof MethodWrapperValue)) { const plan = classes.read(receiver, arg); if (plan.kind === "value") stack.push(plan.value); else { state.temporaries.push(plan.callable, ...plan.args); call(plan.callable, plan.args, [], [], summary(frame, cursor)); } diff --git a/runtime/operation-runtime.mjs b/runtime/operation-runtime.mjs index 21e4365..1ee8218 100644 --- a/runtime/operation-runtime.mjs +++ b/runtime/operation-runtime.mjs @@ -1118,6 +1118,19 @@ export function operationRuntime({state, memory, values, classes, moduleContext, enter(callable, bindings, site, completion); return; } if (callable instanceof BuiltinValue) { + if (callable.name === "super") { + if (keywords.length || args.length > 2) python("TypeError", "super received invalid arguments"); + let type = args[0], receiver = args[1] ?? null; + if (!args.length) { + const frame = state.frames.at(-1), first = [...frame.fn.params.posonly, ...frame.fn.params.positional][0]; + if (first === undefined) python("RuntimeError", "super(): no arguments"); + const parameter = frame.locals.get(first), cell = frame.closure.get("__class__"); + if (!parameter?.bound) python("RuntimeError", "super(): first argument was deleted"); + if (!cell?.bound || !(cell.value instanceof ClassValue)) python("RuntimeError", "super(): missing or invalid __class__ cell"); + type = cell.value; receiver = parameter.value; + } + task = valueTask(classes.superProxy(type, receiver), site, completion); continue; + } if (callable.name === "object.__new__") { if (!args.length || !(args[0] instanceof ClassValue)) python("TypeError", "object.__new__ requires a type"); const type = args[0]; diff --git a/runtime/private-names.test.mjs b/runtime/private-names.test.mjs index beaabf3..4a7875a 100644 --- a/runtime/private-names.test.mjs +++ b/runtime/private-names.test.mjs @@ -73,7 +73,7 @@ test('default core syntax and later class features keep their existing refusal b assert.equal(compileSource('__x=7\nprint(__x)\n').program.version, 4); assert.throws(() => compileSource('class A:\n __x=7\n'), {code: 'UNSUPPORTED'}); for (const source of ['class A:\n def __matmul__(self,other): return 7\n', - 'async def f(): pass\n', 'class A:\n def read(self): return __class__\n']) { + 'async def f(): pass\n', 'class A:\n def __prepare__(self): return {}\n']) { const actual = await run("print('never')\n" + source); assert.equal(actual.error.code, 'UNSUPPORTED'); assert.equal(actual.stdout, ''); diff --git a/runtime/source-compiler.mjs b/runtime/source-compiler.mjs index 1a42a07..51d8134 100644 --- a/runtime/source-compiler.mjs +++ b/runtime/source-compiler.mjs @@ -111,8 +111,8 @@ class Compiler { this.bind(value, scope); return; } if (value.kind === "Name") { - if (scope.classContext && value.name === "__class__") this.fail("Implicit class cells require a later class stage.", value); scope.uses.add(value.name); + if (this.classes && value.name === "super" && ["Function", "Lambda"].includes(node.kind)) scope.uses.add("__class__"); } if (value.kind === "NamedExpression") binding(value.name, value); if (["Assign", "Delete"].includes(value.kind)) value.targets.forEach(target); @@ -175,6 +175,9 @@ class Compiler { let owner = scope.parent; while (owner?.parent) { this.charge(scope.node); + if (this.classes && name === "__class__" && owner.node.kind === "Class") { + owner.locals.add("@source:class"); break; + } if (owner.node.kind !== "Class") { if (owner.globals.has(name)) { owner = null; break; } if (owner.locals.has(name)) break; diff --git a/runtime/values.mjs b/runtime/values.mjs index d33aa60..aa85bd6 100644 --- a/runtime/values.mjs +++ b/runtime/values.mjs @@ -1,5 +1,5 @@ // Independent Python values. Native Number is used only inside explicit floats. -import {GeneratorValue, generatorRoots, Cell, Environment, CodePool, ModuleValue, FunctionValue, CallArguments, BuiltinValue, AppendValue, ValueMethod, ExceptionValue, ClassValue, InstanceValue, BoundMethodValue, PropertyValue, MethodWrapperValue, PropertyAccessorValue, ProtocolIteratorValue, NotImplementedValue} from "./function-objects.mjs"; +import {GeneratorValue, generatorRoots, Cell, Environment, CodePool, ModuleValue, FunctionValue, CallArguments, BuiltinValue, AppendValue, ValueMethod, ExceptionValue, ClassValue, InstanceValue, BoundMethodValue, SuperValue, PropertyValue, MethodWrapperValue, PropertyAccessorValue, ProtocolIteratorValue, NotImplementedValue} from "./function-objects.mjs"; import {isPrintable} from "./string-printability.mjs"; import {divideIntegers, powerIntegers, powerReals, modularPower, roundInteger, roundReal, parseInteger, parseReal, parseComplex, complexArithmetic, complexPower} from "./numeric.mjs"; export const VALUE_LIMITS = Object.freeze({digits: 4096, text: 65536, items: 1024, depth: 32}); @@ -531,7 +531,7 @@ export class ValueRuntime { if (value instanceof SetValue) return value.storage.entries.length !== 0; if (value instanceof GeneratorValue || value instanceof IteratorValue || value instanceof ProtocolIteratorValue || value instanceof SliceValue) return true; if (value instanceof FunctionValue || value instanceof BuiltinValue || value instanceof ExceptionValue || value instanceof ModuleValue || - value instanceof ClassValue || value instanceof InstanceValue || value instanceof BoundMethodValue || value instanceof PropertyValue || value instanceof MethodWrapperValue || value instanceof PropertyAccessorValue) return true; + value instanceof ClassValue || value instanceof InstanceValue || value instanceof BoundMethodValue || value instanceof SuperValue || value instanceof PropertyValue || value instanceof MethodWrapperValue || value instanceof PropertyAccessorValue) return true; this.fail("TypeError", "Truthiness is outside the adopted values."); } equal(left, right, depth = 0) { @@ -632,7 +632,7 @@ export class ValueRuntime { if (value instanceof MemoryViewValue) return 'p:' + this.pythonHash(value, depth); if (value instanceof BinaryValue) return 'p:' + this.pythonHash(value, depth); if (value instanceof SetValue) return 'p:' + this.pythonHash(value, depth); - if (value instanceof ClassValue || value instanceof InstanceValue || value instanceof BoundMethodValue || value instanceof PropertyValue || value instanceof MethodWrapperValue || value instanceof PropertyAccessorValue) + if (value instanceof ClassValue || value instanceof InstanceValue || value instanceof BoundMethodValue || value instanceof SuperValue || value instanceof PropertyValue || value instanceof MethodWrapperValue || value instanceof PropertyAccessorValue) this.fail("UnsupportedOperationError", "Object dictionary keys require the later identity-hash profile.", "UNSUPPORTED"); if (value === null) return "none"; if (value instanceof ComplexValue) { @@ -751,7 +751,7 @@ export class ValueRuntime { while (pending.length) { const item = pending.pop(); if (item instanceof GeneratorValue || item instanceof InstanceValue || item instanceof ClassValue || item instanceof FunctionValue || item instanceof BuiltinValue || - item instanceof BoundMethodValue || item instanceof PropertyValue || item instanceof MethodWrapperValue || + item instanceof BoundMethodValue || item instanceof SuperValue || item instanceof PropertyValue || item instanceof MethodWrapperValue || item instanceof NotImplementedValue || item instanceof ModuleValue || item instanceof ProtocolIteratorValue || item instanceof IteratorValue || item instanceof ExceptionValue || item instanceof FloatValue && Number.isNaN(item.value) || item instanceof ComplexValue && (Number.isNaN(item.real) || Number.isNaN(item.imag))) return true; @@ -1263,6 +1263,7 @@ export function valueLayout(value) { if (value instanceof InstanceValue) return {bytes: 80 + [...value.attributes.keys()].reduce((sum, key) => sum + 32 + 2 * key.length, 0), children: [value.type, ...value.attributes.values()]}; if (value instanceof BoundMethodValue) return {bytes: 64, children: [value.callable, value.receiver]}; + if (value instanceof SuperValue) return {bytes: 72, children: [value.thisclass, value.self, value.selfClass]}; if (value instanceof PropertyValue) return {bytes: 80, children: [value.fget, value.fset, value.fdel]}; if (value instanceof MethodWrapperValue) return {bytes: 64, children: [value.callable]}; if (value instanceof PropertyAccessorValue) return {bytes: 64, children: [value.property]}; diff --git a/src/pocket_python/_interpreter_lock.py b/src/pocket_python/_interpreter_lock.py index 8ad4ab5..6392d96 100644 --- a/src/pocket_python/_interpreter_lock.py +++ b/src/pocket_python/_interpreter_lock.py @@ -1,14 +1,14 @@ """Known source-console assets, canonical LF bytes; refresh with scripts/lock_interpreter.py.""" -ENGINE_FINGERPRINT = '27fbf19fccda89eed8d913ad2a5a10ef540ef838a614047248fbdc88e94bfd75' +ENGINE_FINGERPRINT = 'd0aa48046dad7db59b2b21c91bb0556807598f3550df50101af44b24b634fc65' ENGINE_FILES = ( ('browser-protocol.mjs', 10589, '920e8d07c7070995636dff40fdf4333afc9bd4143483db5d8dff82ec1f28ff76'), ('bytecode.mjs', 6836, 'e76fabeea879bc1da8016283f7e74df5974479666b100199c2d3c31a53b8ecf4'), - ('class-policy.mjs', 1995, '1e705bafb0c8d32a1bc49bb066d4284d1998db919fc5cfbc156afe06104b610b'), - ('class-values.mjs', 15409, '7c69d186e0a36bf85f974b6ef9ddd8c1e0399fd1d155b14daa9e52cdcd70de3f'), - ('function-bytecode.mjs', 20438, 'ca6b3abd73d2be35422eea244b876a851bf98e38836e31a33fa6d18d4252ae68'), - ('function-objects.mjs', 4277, '06af0ca936de1dd336cb7a2ac13f24efae960ea3fe448c93fb95a2e92213c4e2'), - ('functions.mjs', 57726, '7d6e4322c31187b12387695dd094a0fd4dab2220cf28c1df2bb4856ccf7ae179'), + ('class-policy.mjs', 2043, '38fd2e79653b9219bbf1b98672dcd36cc9846d7d0d15c27864500ea3af8a7a6d'), + ('class-values.mjs', 17126, 'd52488156e37e5321905cf037859dd1cb10648e31e740628c4e9acfd941ade67'), + ('function-bytecode.mjs', 20591, '03c89d0a19582524a88227950fc50b9928647f6b4fee55b28d8794cb4477e8b7'), + ('function-objects.mjs', 4446, '7643672838459122b6128fcdf12615df12e3dbc2d68e02fc395e1b25ea5e5108'), + ('functions.mjs', 58375, 'e01da834c2209d2904347b799ef3ac5e8b3b222acec826340eedfa0cb39b83aa'), ('interpreter-host.mjs', 7019, '0bb0a64faffcf77e0064258e4cfcaa7a937c4372129849f76901bee91b5a5ae2'), ('interpreter-worker.mjs', 8282, '937a97cc49d987492de0bc16021a4a804ef51e4ea5b398f79c92e5d30ac82558'), ('library-catalog.mjs', 907, '0adcaae0f6f07b4e1acb22e46493ec99f7247a5828b426c703530d8b2736032c'), @@ -20,15 +20,15 @@ ('module-registry.mjs', 10085, 'f23f8313c0804a052cba155e193df920b0221ef978a06f16c1a22f208dfbef91'), ('module-session.mjs', 1470, '08e4ccf16361299c15b99621fdfcef0d9ef93de50d28d738158c7bc85a7c23a3'), ('numeric.mjs', 13954, '2431f099a7d40aa38d6b3c68b556207752e4191211e01005acde94325bd91bad'), - ('operation-runtime.mjs', 95840, '09baff12598d313ae411fff8f3154521341b527b6e294db7f2ebdc9608c17fcd'), + ('operation-runtime.mjs', 96811, '24fce4a6b335de34ab39aaf058394665f903db50b19b02bae4a4b05a4657f87e'), ('operations.mjs', 23825, '4cf2afcb1d8fee89e4049c2fd176f230db17f57e6b4a4a6048b8004fedfe0684'), - ('source-compiler.mjs', 35239, '4201a25a0c45b8fba4777c8e98b75e37f3277888708093a77e2827dec5ff7063'), + ('source-compiler.mjs', 35378, '7ca4fc06290189a6740e87efca563d6fefdab10ad99b56700e9706701fada519'), ('source-identifiers.mjs', 25949, '1e4e6a90728a2d9393eb4b2055d4bd759542f4768d1870b508d629c6888e8914'), ('source-modules.mjs', 5115, '3f824a8cf79a9ab78c1b817ed1597d114d851748a6b0894a4ac6b4bdbe01cfef'), ('source-parser.mjs', 33396, '578c6663213e46049c51ba23a52427af8c301943ef67433054a6bf3fb2218ed2'), ('source-tokens.mjs', 15061, 'e044276920224f8ed788f4e3445bee926698444e6cf0b9e8573bd15fec94ad54'), ('string-printability.mjs', 13287, '355a086a142e06822eff821dd0ac6a2ca70c4e06f4ec659236dc3cea1ddd639f'), - ('values.mjs', 85194, '3a9b349575ecccd4c933aa9d8fc7877590290812e60074869ef45f4c1abbbdda'), + ('values.mjs', 85411, '7aeb384e833285017df398bf0bd90169473a7de84fe4719a0567a1fa5a5555e7'), ('vm.mjs', 6080, '796eaac2848ee7aa2d740797eb2afbdb3e577918866a88a9f25e5adbc55e985d'), ) ENGINE_IMPORTS = ( diff --git a/tests/fixtures/interpreter/classes.json b/tests/fixtures/interpreter/classes.json index 9dfd5cb..5c40ae9 100644 --- a/tests/fixtures/interpreter/classes.json +++ b/tests/fixtures/interpreter/classes.json @@ -204,7 +204,7 @@ }, { "id": "implicit-class-cell", - "stage": "deferred", + "stage": "O4", "source": "class A:\n def f(self): return __class__\nprint(A().f() is A)\n", "stdout": "True\n" }, @@ -245,6 +245,7 @@ "C2-descriptor", "C2-decorator", "C3-length", - "C3-hash" + "C3-hash", + "O4" ] } diff --git a/tests/fixtures/interpreter/compatibility.json b/tests/fixtures/interpreter/compatibility.json index a2e3602..62d08bc 100644 --- a/tests/fixtures/interpreter/compatibility.json +++ b/tests/fixtures/interpreter/compatibility.json @@ -1556,16 +1556,14 @@ { "id": "builtin-super", "source": "print(callable(super))\n", - "classification": "deferred", + "classification": "supported", "reference": { "stdout": "True\n", "error_type": null }, "engine": { - "stdout": "", - "error_type": "NameError", - "code": "PYTHON", - "phase": "execute" + "stdout": "True\n", + "error_type": null } }, { diff --git a/tests/fixtures/interpreter/construction.json b/tests/fixtures/interpreter/construction.json index feaa9ad..dad02a9 100644 --- a/tests/fixtures/interpreter/construction.json +++ b/tests/fixtures/interpreter/construction.json @@ -4,7 +4,8 @@ "profile": "classes-source/1", "reference_version": "3.14.7", "implemented_stages": [ - "O3" + "O3", + "O4" ], "cases": [ {