diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 963475c..41630f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,6 +93,8 @@ jobs: - run: uv run --no-sync python runtime/check_classes.py --target browser --corpus tests/fixtures/interpreter/generators.json --stepped --report build/generators-browser-stepped-report.json - run: uv run --no-sync python runtime/check_classes.py --target browser --corpus tests/fixtures/interpreter/generators.json --profile core-source/1 --report build/generators-core-browser-report.json - run: uv run --no-sync python runtime/check_classes.py --target browser --corpus tests/fixtures/interpreter/generators.json --profile core-source/1 --stepped --report build/generators-core-browser-stepped-report.json + - run: uv run --no-sync python runtime/check_classes.py --target browser --corpus tests/fixtures/interpreter/object-protocols.json --report build/object-protocols-browser-report.json + - run: uv run --no-sync python runtime/check_classes.py --target browser --corpus tests/fixtures/interpreter/object-protocols.json --stepped --report build/object-protocols-browser-stepped-report.json - run: uv run --no-sync python runtime/check_classes.py --target browser --report build/classes-browser-report.json - run: uv run --no-sync python runtime/check_classes.py --target browser --corpus tests/fixtures/interpreter/value-families.json --report build/value-families-browser-report.json - run: uv run --no-sync python runtime/check_classes.py --target browser --corpus tests/fixtures/interpreter/value-families.json --stepped --report build/value-families-browser-stepped-report.json @@ -162,6 +164,7 @@ jobs: - run: node --test runtime/memory.test.mjs - run: node --test runtime/syntax.test.mjs - run: node --test runtime/function-binding.test.mjs + - run: node --test runtime/object-protocols.test.mjs - run: node --test runtime/generators.test.mjs - run: node --test runtime/functions.test.mjs - run: node --test runtime/execution.test.mjs @@ -196,6 +199,8 @@ jobs: - run: python runtime/check_classes.py --target node --corpus tests/fixtures/interpreter/generators.json --stepped --report build/generators-node-stepped-report.json - run: python runtime/check_classes.py --target node --corpus tests/fixtures/interpreter/generators.json --profile core-source/1 --report build/generators-core-node-report.json - run: python runtime/check_classes.py --target node --corpus tests/fixtures/interpreter/generators.json --profile core-source/1 --stepped --report build/generators-core-node-stepped-report.json + - run: python runtime/check_classes.py --target node --corpus tests/fixtures/interpreter/object-protocols.json --report build/object-protocols-node-report.json + - run: python runtime/check_classes.py --target node --corpus tests/fixtures/interpreter/object-protocols.json --stepped --report build/object-protocols-node-stepped-report.json - run: python runtime/check_classes.py --target node --report build/classes-node-report.json - run: python runtime/check_classes.py --target node --corpus tests/fixtures/interpreter/value-families.json --report build/value-families-node-report.json - run: python runtime/check_classes.py --target node --corpus tests/fixtures/interpreter/value-families.json --stepped --report build/value-families-node-stepped-report.json diff --git a/docs/interpreter-compatibility.json b/docs/interpreter-compatibility.json index 0e72196..658d879 100644 --- a/docs/interpreter-compatibility.json +++ b/docs/interpreter-compatibility.json @@ -588,9 +588,9 @@ }, { "id": "dynamic-code-reflection", - "status": "deferred", - "scope": "compile/eval/exec, dynamic imports and broader globals/locals/type/attribute reflection.", - "gaps": "Source admission, namespace/identity and target authority need separate explicit contracts.", + "status": "partial", + "scope": "getattr/setattr/delattr/hasattr on adopted class attributes through ordinary owned descriptor callbacks.", + "gaps": "Dynamic source admission, namespaces, richer types and broader reflection remain deferred.", "reference_version": "3.14.7", "targets": [ "node", @@ -1065,9 +1065,9 @@ }, { "id": "delattr", - "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": "Positional attribute reflection on adopted class/instance attributes, preserving descriptor precedence, arbitrary bounded string names and AttributeError-only suppression.", + "gaps": "Custom attribute hooks, reflective namespaces and unadopted receivers/metadata remain separately gated.", "reference_version": "3.14.7", "targets": [ "node", @@ -1079,7 +1079,9 @@ "probes": [ "builtin-delattr" ], - "evidence": [] + "evidence": [ + "tests/fixtures/interpreter/object-protocols.json:attribute-defaults" + ] }, { "id": "dict", @@ -1267,9 +1269,9 @@ }, { "id": "getattr", - "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": "Positional attribute reflection on adopted class/instance attributes, preserving descriptor precedence, arbitrary bounded string names and AttributeError-only suppression.", + "gaps": "Custom attribute hooks, reflective namespaces and unadopted receivers/metadata remain separately gated.", "reference_version": "3.14.7", "targets": [ "node", @@ -1281,7 +1283,9 @@ "probes": [ "builtin-getattr" ], - "evidence": [] + "evidence": [ + "tests/fixtures/interpreter/object-protocols.json:attribute-defaults" + ] }, { "id": "globals", @@ -1303,9 +1307,9 @@ }, { "id": "hasattr", - "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": "Positional attribute reflection on adopted class/instance attributes, preserving descriptor precedence, arbitrary bounded string names and AttributeError-only suppression.", + "gaps": "Custom attribute hooks, reflective namespaces and unadopted receivers/metadata remain separately gated.", "reference_version": "3.14.7", "targets": [ "node", @@ -1317,7 +1321,9 @@ "probes": [ "builtin-hasattr" ], - "evidence": [] + "evidence": [ + "tests/fixtures/interpreter/object-protocols.json:attribute-defaults" + ] }, { "id": "hash", @@ -1862,9 +1868,9 @@ }, { "id": "setattr", - "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": "Positional attribute reflection on adopted class/instance attributes, preserving descriptor precedence, arbitrary bounded string names and AttributeError-only suppression.", + "gaps": "Custom attribute hooks, reflective namespaces and unadopted receivers/metadata remain separately gated.", "reference_version": "3.14.7", "targets": [ "node", @@ -1876,7 +1882,9 @@ "probes": [ "builtin-setattr" ], - "evidence": [] + "evidence": [ + "tests/fixtures/interpreter/object-protocols.json:attribute-defaults" + ] }, { "id": "slice", diff --git a/docs/interpreter-compatibility.md b/docs/interpreter-compatibility.md index b2c3e7c..4b28868 100644 --- a/docs/interpreter-compatibility.md +++ b/docs/interpreter-compatibility.md @@ -102,7 +102,7 @@ issue families; availability alone never certifies an entire type or module. | classmethod | partial | classmethod(callable), classes-source/1 and accessed-owner binding. | Unrestricted wrapper introspection. | | compile | deferred | Not installed; resolving this builtin name raises Python NameError. | No implementation/API support credit from the negative availability probe. | | complex | partial | Owned immutable Cartesian values, literal/text/real/complex construction, real/imag keywords and attributes, finite basic arithmetic, equality/truth/hash and bounded integer powers. Owned conversion and index fallback callbacks run in ordinary VM frames under classes-source/1. | General powers, non-finite arithmetic/recovery families, numeric methods, formatting and builtin subclasses remain later boundaries. | -| delattr | deferred | Not installed; resolving this builtin name raises Python NameError. | No implementation/API support credit from the negative availability probe. | +| delattr | partial | Positional reflection on adopted class attributes and descriptors. | Custom hooks and broader reflection remain separate. | | dict | deferred | Not installed; resolving this builtin name raises Python NameError. | No implementation/API support credit from the negative availability probe. | | dir | deferred | Not installed; resolving this builtin name raises Python NameError. | No implementation/API support credit from the negative availability probe. | | divmod | partial | Positional-only primitive int/float quotient and remainder with owned tuple results and Python sign/zero rules. | General user arithmetic/reflected slots remain INT-OBJECT-01. | @@ -113,9 +113,9 @@ issue families; availability alone never certifies an entire type or module. | float | partial | Real primitive and Unicode text construction with exact signatures, base/underscore rules and bounded input/result work in both source profiles. Owned conversion and index fallback callbacks run in ordinary VM frames under classes-source/1. | Bytes (int/float), builtin subclasses, numeric methods and formatting remain later value/object stages. | | format | deferred | Not installed; resolving this builtin name raises Python NameError. | No implementation/API support credit from the negative availability probe. | | frozenset | partial | Zero/one positional iterable construction, set displays, membership/equality/iteration, finite methods and primitive algebra; owned iterator/hash/equality callbacks in classes-source/1. | Broader methods, set-algebra callbacks, recursive user-key frozen hashing and comprehensions remain deferred. | -| getattr | deferred | Not installed; resolving this builtin name raises Python NameError. | No implementation/API support credit from the negative availability probe. | +| getattr | partial | Positional reflection on adopted class attributes and descriptors. | Custom hooks and broader reflection remain separate. | | globals | deferred | Not installed; resolving this builtin name raises Python NameError. | No implementation/API support credit from the negative availability probe. | -| hasattr | deferred | Not installed; resolving this builtin name raises Python NameError. | No implementation/API support credit from the negative availability probe. | +| hasattr | partial | Positional reflection on adopted class attributes and descriptors. | Custom hooks and broader reflection remain separate. | | hash | partial | hash(value), positional-only in classes-source/1; numeric/string/primitive tuple hashes, user __hash__ and owned default identities. | Tuple-with-instance, bound-method and slice hash families remain deferred; exact string/object hash numbers are not a cross-process guarantee. | | help | host-restricted | Not installed; availability probe only, never invoked. | Interactive/process/filesystem behavior needs an explicit target capability contract. | | hex | supported | Primitive numeric signatures in both source profiles and owned type-level callbacks in classes-source/1; see interpreter-numeric.md for the finite contract. | | @@ -145,7 +145,7 @@ issue families; availability alone never certifies an entire type or module. | reversed | deferred | Not installed; resolving this builtin name raises Python NameError. | No implementation/API support credit from the negative availability probe. | | round | supported | Primitive numeric signatures in both source profiles and owned type-level callbacks in classes-source/1; see interpreter-numeric.md for the finite contract. | | | set | partial | Zero/one positional iterable construction, set displays, membership/equality/iteration, finite methods and primitive algebra; owned iterator/hash/equality callbacks in classes-source/1. | Broader methods, set-algebra callbacks, recursive user-key frozen hashing and comprehensions remain deferred. | -| setattr | deferred | Not installed; resolving this builtin name raises Python NameError. | No implementation/API support credit from the negative availability probe. | +| setattr | partial | Positional reflection on adopted class attributes and descriptors. | Custom hooks and broader reflection remain separate. | | slice | partial | classes-source/1: slice(stop), slice(start,stop), slice(start,stop,step), positional only; immutable raw start/stop/step fields. | Core constructor, slice.indices, repr, equality/hash and other reflection are deferred. | | sorted | deferred | Not installed; resolving this builtin name raises Python NameError. | No implementation/API support credit from the negative availability probe. | | staticmethod | partial | staticmethod(value) and existing wrapper binding/direct-call rules; classes-source/1. | Unrestricted wrapper introspection. | diff --git a/docs/interpreter-object-protocols.md b/docs/interpreter-object-protocols.md index c8909a4..75a4d01 100644 --- a/docs/interpreter-object-protocols.md +++ b/docs/interpreter-object-protocols.md @@ -2,7 +2,8 @@ INT-OBJECT-01 adopts finite slices against CPython 3.14.7. The authored `object-protocols.json` reference must also execute independently in Node and -Chromium, draining and instruction-boundary drivers. Classes remain explicit in +Chromium, draining and instruction-boundary drivers. O1 is implemented; O2 is the next stage. Attribute names can be arbitrary strings +within the 256-code-unit name quota. Classes remain explicit in classes-source/1; core-source/1 keeps primitive arithmetic and generator support. | Stage | Finite contract | diff --git a/runtime/class-values.mjs b/runtime/class-values.mjs index b00cc3a..4b73ab7 100644 --- a/runtime/class-values.mjs +++ b/runtime/class-values.mjs @@ -19,11 +19,12 @@ export class ClassRuntime { bounded(condition, message) { if (!condition) this.fail("ExecutionLimitError", message, "BUDGET"); } key(name) { this.charge(typeof name === "string" ? name.length + 1 : 1); - if (typeof name !== "string" || name.length > CLASS_LIMITS.name || !isPythonIdentifier(name)) throw new TypeError("Invalid class attribute name."); + if (typeof name !== "string" || name.length > CLASS_LIMITS.name) throw new TypeError("Invalid class attribute name."); } root() { return this.make(ClassValue, "object", "object", "builtins", [], [], new Map(), true); } create(name, qualname, module, bases, namespace, root) { this.key(name); + if (!isPythonIdentifier(name)) throw new TypeError("Invalid class name."); this.bounded(typeof qualname === "string" && qualname.length <= 256 && typeof module === "string" && module.length <= 192, "Class identity budget exceeded."); if (!(root instanceof ClassValue) || !root.root || !Array.isArray(bases) || !(namespace instanceof Map)) throw new TypeError("Invalid class construction record."); diff --git a/runtime/functions.mjs b/runtime/functions.mjs index 77548e6..9b4b6bd 100644 --- a/runtime/functions.mjs +++ b/runtime/functions.mjs @@ -98,7 +98,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 && ["type", "issubclass", "property", "staticmethod", "classmethod", "callable", "slice", "hash"].includes(key))) { + (frame.callable.pool.code.version === 5 && ["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); diff --git a/runtime/object-protocols.test.mjs b/runtime/object-protocols.test.mjs new file mode 100644 index 0000000..28af1d5 --- /dev/null +++ b/runtime/object-protocols.test.mjs @@ -0,0 +1,69 @@ +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 object protocol 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('attribute reflection retains defaults, receiver and outer destinations', () => { + const receiver = {}, fallback = {}, outer = {}, after = completion('relay', {receiver: outer}); + assert.deepEqual(completionRoots(completion('attribute-result', {receiver, default: fallback, mode: 'getattr', after})), [receiver, fallback, outer]); + assert.deepEqual(completionRoots(completion('attribute-write', {receiver, after})), [receiver, outer]); + assert.throws(() => completion('attribute-result', {receiver, default: null, mode: 'setattr'}), TypeError); +}); + +test('dynamic attribute names preserve arbitrary text without identifier normalization or mangling', async () => { + const result = await run('class A: pass\na=A()\nsetattr(a,"",1)\nsetattr(a,"two words",2)\nsetattr(a,"K",3)\nsetattr(a,"K",4)\nprint(getattr(a,""),getattr(a,"two words"),getattr(a,"K"),getattr(a,"K"))\n'); + assert.equal(result.error, undefined, JSON.stringify(result.error)); assert.equal(result.stdout, '1 2 3 4\n'); closed(result.closed); +}); + +test('reflection suppresses only AttributeError and validates signatures before callbacks', async () => { + for (const [expression, type] of [['getattr(A(),7)', 'TypeError'], ['hasattr(A(),"x")', 'StopIteration'], ['getattr(A(),"x",9)', 'StopIteration'], ['setattr(A(),"x")', 'TypeError']]) { + const result = await run('class A:\n @property\n def x(self): raise StopIteration("body")\n'+expression+'\n'); + assert.equal(result.error.type, type); closed(result.closed); + } + const result = await run('class A:\n @property\n def x(self): raise AttributeError("body")\na=A()\nprint(getattr(a,"x",[4])[0],hasattr(a,"x"))\n'); + assert.equal(result.stdout, '4 False\n'); assert.equal(result.error, undefined); closed(result.closed); +}); + +test('descriptor getters and setters keep temporary operands across collection, cancellation and close', async () => { + const sources = [ + 'class A:\n @property\n def x(self):\n print("entered")\n raise AttributeError("missing")\nprint(getattr(A(),"x",[7])[0])\n', + 'class A:\n @property\n def x(self): return 0\n @x.setter\n def x(self,value):\n print("entered")\n self.saved=value\na=A()\nprint(setattr(a,"x",[7]),a.saved[0])\n', + ]; + for (let index = 0; index < sources.length; index++) { + const {registry} = await compileSourceModules([unit(sources[index])], 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, index ? 'entered\nNone 7\n' : 'entered\n7\n'); + } finally { execution.close(); execution.close(); session.context.finish(); session.close(); } + closed(session.stats()); + } } finally { registry.close(); } + } +}); + +test('reflection callbacks share recursion, work and memory budgets', async () => { + const recursive = await run('class A:\n @property\n def x(self): return getattr(self,"x")\ngetattr(A(),"x")\n'); + assert.equal(recursive.error.type, 'RecursionError'); closed(recursive.closed); + const budget = await run('class A:\n @property\n def x(self):\n while True: pass\ngetattr(A(),"x",9)\n', {maxSteps: 200}); + assert.equal(budget.error.code, 'BUDGET'); closed(budget.closed); + const oom = await run('class A:\n @property\n def x(self):\n a=[]\n while True: a.append([7])\ngetattr(A(),"x")\n', {maxSteps: 100000}, {maxBytes: 20000}); + assert.equal(oom.error.code, 'OOM'); closed(oom.closed); +}); diff --git a/runtime/operation-runtime.mjs b/runtime/operation-runtime.mjs index 94acc90..d1d9d17 100644 --- a/runtime/operation-runtime.mjs +++ b/runtime/operation-runtime.mjs @@ -286,6 +286,7 @@ export function operationRuntime({state, memory, values, classes, moduleContext, const binding = bindingRecovery(value, completion, site); if (binding) return binding; for (let current = completion; current; current = current.after) { + if (current.kind === "attribute-result" && value.name === "AttributeError") return valueTask(current.mode === "hasattr" ? false : current.default, site, current.after); if (current.kind === "delegate-bind" && value.name === "AttributeError") return unaryTask("delegate-raise", current.receiver, site); if (current.kind.startsWith("call-map-") && (value.name === "AttributeError" || value.name === "KeyError" && current.mode !== "display")) return value.name === "KeyError" ? duplicateKeyword(current.receiver, value.args[0] ?? current.key, site, current.after) : @@ -333,6 +334,8 @@ export function operationRuntime({state, memory, values, classes, moduleContext, } completion = completion.after; } + if (completion?.kind === "attribute-write") return valueTask(null, site, completion.after); + if (completion?.kind === "attribute-result") return valueTask(completion.mode === "hasattr" ? true : value, site, completion.after); if (completion?.kind === "delegate-bind") return callTask(value, completion.args, [], [], site, completion.after); if (completion?.kind.startsWith("delegate-")) return delegateResult(completion.kind, value, site); count("deliver:" + (completion?.kind ?? "stack")); @@ -1033,6 +1036,27 @@ export function operationRuntime({state, memory, values, classes, moduleContext, enter(callable, bindings, site, completion); return; } if (callable instanceof BuiltinValue) { + if (["getattr", "setattr", "delattr", "hasattr"].includes(callable.name)) { + const name = callable.name, writing = name === "setattr" || name === "delattr"; + if (keywords.length || args.length < 2 || args.length > (name === "getattr" || name === "setattr" ? 3 : 2) || name === "setattr" && args.length !== 3) + python("TypeError", name + "() received invalid arguments"); + const attribute = values.text(args[1]); + if (attribute === null) python("TypeError", "attribute name must be string"); + if (attribute.length > 256) hard("BUDGET", "ExecutionLimitError", "Attribute name budget exceeded."); + const receiver = args[0], optional = name === "hasattr" || name === "getattr" && args.length === 3; + const target = writing ? continuation("attribute-write", {receiver, after: completion}) : optional ? + continuation("attribute-result", {receiver, default: args[2] ?? null, mode: name, after: completion}) : completion; + let plan; + try { plan = writing ? classes.write(receiver, attribute, args[2] ?? null, name === "delattr") : classes.read(receiver, attribute); } + catch (error) { + const recovery = error instanceof Raised && recover(error.value, target, site); + if (!recovery) throw error; + task = recovery; continue; + } + task = plan.kind === "value" ? valueTask(writing ? null : name === "hasattr" ? true : plan.value, site, completion) : + callTask(plan.callable, plan.args, [], [], site, target, depth); + continue; + } if (callable instanceof ValueMethod && callable.receiver instanceof GeneratorValue) { if (keywords.length || args.length !== (callable.name === "close" || callable.name === "__iter__" || callable.name === "__next__" ? 0 : 1)) { if (callable.name === "throw" && args.length > 1) hard("UNSUPPORTED", "UnsupportedOperationError", "Multi-argument generator.throw requires a separate stage."); diff --git a/runtime/operations.mjs b/runtime/operations.mjs index 4b72b5e..107f24d 100644 --- a/runtime/operations.mjs +++ b/runtime/operations.mjs @@ -122,6 +122,9 @@ export function completion(kind, fields = {}) { break; case "protocol-result": record = {kind, receiver: fields.receiver, operation: fields.operation, slot: fields.slot, depth: fields.depth ?? 0, after}; break; case "optional-truth": record = {kind, receiver: fields.receiver, operation: fields.operation, depth: fields.depth, after}; break; + case "attribute-write": record = {kind, receiver: fields.receiver, after}; break; + case "attribute-result": record = {kind, receiver: fields.receiver, default: fields.default, mode: fields.mode, after}; + if (!["getattr", "hasattr"].includes(record.mode)) throw new TypeError("Invalid attribute result mode."); break; case "delegate-bind": record = {kind, receiver: fields.receiver, args: operands(fields.args), after}; break; case "invoke": record = {kind, args: operands(fields.args), keywords: operands(fields.keywords), keywordValues: operands(fields.keywordValues), depth: fields.depth, fallback: fields.fallback ?? null, after}; break; @@ -170,6 +173,8 @@ function visit(record) { case "index-key": return [record.receiver, record.key, ...(record.operation === "set" ? [record.item] : []), ...completionRoots(record.after)]; case "slice-index": return [record.receiver, ...record.parts, ...(record.operation === "set" ? [record.item] : []), ...completionRoots(record.after)]; case "print-argument": return [...record.args, ...completionRoots(record.after)]; + case "attribute-write": return [record.receiver, ...completionRoots(record.after)]; + case "attribute-result": return [record.receiver, record.default, ...completionRoots(record.after)]; case "delegate-bind": return [record.receiver, ...record.args, ...completionRoots(record.after)]; case "invoke": return [...record.args, ...record.keywordValues, ...completionRoots(record.after)]; case "notify": return [record.type, ...record.entries.map(entry => entry[1])]; diff --git a/src/pocket_python/_interpreter_lock.py b/src/pocket_python/_interpreter_lock.py index ca8f5f1..dfa6532 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 = 'c7abf587469c1597dd8b941e8eae46ac8ac034a11704da24dca12d09cdd01108' +ENGINE_FINGERPRINT = '03d4255b4396e0016f2b34d86c3968d12f5f9573781a72d37ccd95f244a36d9f' ENGINE_FILES = ( ('browser-protocol.mjs', 10589, '920e8d07c7070995636dff40fdf4333afc9bd4143483db5d8dff82ec1f28ff76'), ('bytecode.mjs', 6836, 'e76fabeea879bc1da8016283f7e74df5974479666b100199c2d3c31a53b8ecf4'), ('class-policy.mjs', 1362, '586d8800afd0e5d5ec7d4d3e758dbf56942f7d74207c111c0b32fb4483b0d1f6'), - ('class-values.mjs', 15322, 'f4f682f07b74c4f8d36d6e30ba7cfbacfde1e81ba4b5b1ebec0a36c378901b43'), + ('class-values.mjs', 15372, '67fc606a6c91f7aeb1d4309cc4a0be33778181cdccb51190e5f76d4969d9b43d'), ('function-bytecode.mjs', 20438, 'ca6b3abd73d2be35422eea244b876a851bf98e38836e31a33fa6d18d4252ae68'), ('function-objects.mjs', 4277, '06af0ca936de1dd336cb7a2ac13f24efae960ea3fe448c93fb95a2e92213c4e2'), - ('functions.mjs', 56408, 'c6e8c6d0bbb719db0376ec93f3417015a4225fff1d6784acc5f5f322b245708e'), + ('functions.mjs', 56452, '3f60a5034ff614f6bc12743194bbcd46096da549a677afb58b00fa5fed8da527'), ('interpreter-host.mjs', 7019, '0bb0a64faffcf77e0064258e4cfcaa7a937c4372129849f76901bee91b5a5ae2'), ('interpreter-worker.mjs', 8282, '937a97cc49d987492de0bc16021a4a804ef51e4ea5b398f79c92e5d30ac82558'), ('library-catalog.mjs', 907, '0adcaae0f6f07b4e1acb22e46493ec99f7247a5828b426c703530d8b2736032c'), @@ -20,8 +20,8 @@ ('module-registry.mjs', 10085, 'f23f8313c0804a052cba155e193df920b0221ef978a06f16c1a22f208dfbef91'), ('module-session.mjs', 1470, '08e4ccf16361299c15b99621fdfcef0d9ef93de50d28d738158c7bc85a7c23a3'), ('numeric.mjs', 13954, '2431f099a7d40aa38d6b3c68b556207752e4191211e01005acde94325bd91bad'), - ('operation-runtime.mjs', 85085, '7767bd13309e4a326cf807c387dffff8ddd31b52edd7cf591e762162fe848c8c'), - ('operations.mjs', 21566, 'a05c7535ed1312c42d2961d2cae3ac2d60c7f65133f86b90f8fb215fa066433b'), + ('operation-runtime.mjs', 87150, 'fab6eed0c35813348cd17b6524ab006b51af6d0bbad7405752de939e65fe354e'), + ('operations.mjs', 22088, 'cd872da96bc09f14c105ebb977ea3b5ed1441ec43f538df6d208966dc82bf014'), ('source-compiler.mjs', 35239, '4201a25a0c45b8fba4777c8e98b75e37f3277888708093a77e2827dec5ff7063'), ('source-identifiers.mjs', 25949, '1e4e6a90728a2d9393eb4b2055d4bd759542f4768d1870b508d629c6888e8914'), ('source-modules.mjs', 5115, '3f824a8cf79a9ab78c1b817ed1597d114d851748a6b0894a4ac6b4bdbe01cfef'), diff --git a/tests/fixtures/interpreter/compatibility.json b/tests/fixtures/interpreter/compatibility.json index d6ecb1d..a2e3602 100644 --- a/tests/fixtures/interpreter/compatibility.json +++ b/tests/fixtures/interpreter/compatibility.json @@ -883,16 +883,14 @@ { "id": "builtin-delattr", "source": "print(callable(delattr))\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 } }, { @@ -1042,16 +1040,14 @@ { "id": "builtin-getattr", "source": "print(callable(getattr))\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 } }, { @@ -1072,16 +1068,14 @@ { "id": "builtin-hasattr", "source": "print(callable(hasattr))\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 } }, { @@ -1480,16 +1474,14 @@ { "id": "builtin-setattr", "source": "print(callable(setattr))\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/object-protocols.json b/tests/fixtures/interpreter/object-protocols.json index b679de0..31c3c10 100644 --- a/tests/fixtures/interpreter/object-protocols.json +++ b/tests/fixtures/interpreter/object-protocols.json @@ -3,7 +3,9 @@ "suite": "interpreter-object-protocols/1", "profile": "classes-source/1", "reference_version": "3.14.7", - "implemented_stages": [], + "implemented_stages": [ + "O1" + ], "cases": [ { "id": "attribute-defaults",