From dc413b22a589231e3170a18af0eff108cf181f91 Mon Sep 17 00:00:00 2001 From: ellainng <299958882+ellainng@users.noreply.github.com> Date: Mon, 26 Oct 2026 18:17:57 -0700 Subject: [PATCH 1/3] docs(INT-OBJECT-01C): specify construction and type protocols --- docs/interpreter-construction.md | 44 ++ tests/fixtures/interpreter/construction.json | 591 +++++++++++++++++++ tests/test_generator_contract.py | 2 +- 3 files changed, 636 insertions(+), 1 deletion(-) create mode 100644 docs/interpreter-construction.md create mode 100644 tests/fixtures/interpreter/construction.json diff --git a/docs/interpreter-construction.md b/docs/interpreter-construction.md new file mode 100644 index 0000000..51eda18 --- /dev/null +++ b/docs/interpreter-construction.md @@ -0,0 +1,44 @@ +# Construction, super and type relations + +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. +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. + +| Stage | Contract | +| --- | --- | +| O3 | Class calls select `__new__` through the class MRO, bind its descriptor with no instance, and pass the requested class plus original arguments. A Python function defined as `__new__` is implicitly static. `object.__new__` allocates an ordinary user instance. Only a returned instance of the requested class or its subclass is initialized, using the returned value's actual type and the original arguments. Foreign values pass through unchanged. `__init__` binds normally, can suspend, and must return None. Direct `object.__new__`/`object.__init__` validate receivers and extra arguments. Mutation, wrappers, callable descriptors, exceptions and generator returns obey the existing protocols. | +| O4 | `super(T, value)` follows the current C3 MRO after T, ignoring instance shadow attributes and binding descriptors to the original value and dynamic owner. `super(T)` and `super(T, None)` are unbound; stored unbound proxies bind as descriptors. Zero-argument `super()` reads the calling Python frame's first positional parameter and lexical `__class__` cell. Cells are filled before class decorators run, are shared with nested functions and suspended generators, and do not capture ordinary class locals. Proxy metadata `__thisclass__`, `__self__`, `__self_class__` and `__class__` is read-only. | +| O5 | `type(value)` returns canonical type identities for adopted values. `isinstance`/`issubclass` accept types and nested tuples with Python short-circuit/error order, bool/int ancestry and the adopted exception hierarchy. Three-argument `type(name, bases, namespace)` accepts a tuple of adopted bases and an ordinary dictionary with string keys. Finite metaclasses support inheritance from type, most-derived selection/conflicts, explicit `metaclass=`, and user `__new__`/`__init__` through ordinary frames and `type.__new__`/`type.__init__`. Class cells propagate through the namespace to `type.__new__`. Metadata, class decorators and `__set_name__` retain their order. | +| Future | Builtin instance layouts/subclassing, custom `__prepare__` and class-creation mappings, extra class keywords, `__init_subclass__`, `__mro_entries__`, custom instance/subclass checking, unions, custom attribute hooks and dynamic source admission require separate contracts. Broader dir/vars/globals/locals/id, coroutine/async-generator and host-adapter work remain open under #123 and #34. | + +O5 covers ordinary dictionary class namespaces. It does not imply an arbitrary +metaclass or builtin-subclass profile. Unsupported class-creation hooks must be +rejected before class-body or hook effects where the source/compiler can identify +them. Dynamic namespace inputs are bounded and validated before notification. +Dynamic class names are bounded strings, not necessarily Python identifiers. +Existing class-statement names remain normalized identifiers. + +All constructor/type candidates, arguments, keyword values, namespace contents, +class cells, super receivers and outer destinations are explicit owned edges or +continuation roots. Python callbacks execute on the common machine. Stepping and +collection preserve pending values; cancellation, recursion/work limits and real +callback allocation failure release all logical owners. Close is idempotent and +does not execute new user finalizers. The eight existing composed resource +observations retain their exact accounting; new records have explicit costs. + +Acceptance requires independently compiled source in Node and Chromium, draining +and instruction-boundary execution, callback/source-error coverage, collection +between instructions, cancellation and allocation failure. The offline console +must execute newly entered source after reload and support Stop/Restart in +constructor and descriptor callbacks. Complete engine assets and their receipt +hashes remain pinned. No dependency, service, host capability, publication or +deployment change is included. + +The semantic anchors are the Python 3.14 +[construction protocol](https://docs.python.org/3.14/reference/datamodel.html#object.__new__), +[class creation](https://docs.python.org/3.14/reference/datamodel.html#customizing-class-creation) +and [super builtin](https://docs.python.org/3.14/library/functions.html#super). +The pinned executable reference decides the finite cases when later patch-level +documentation differs. diff --git a/tests/fixtures/interpreter/construction.json b/tests/fixtures/interpreter/construction.json new file mode 100644 index 0000000..33d77fb --- /dev/null +++ b/tests/fixtures/interpreter/construction.json @@ -0,0 +1,591 @@ +{ + "schema_version": 1, + "suite": "interpreter-construction/1", + "profile": "classes-source/1", + "reference_version": "3.14.7", + "implemented_stages": [], + "cases": [ + { + "id": "new-original-arguments", + "stage": "O3", + "source": "class A:\n def __new__(cls,x,*,y):\n print(\"new\",cls.__name__,x,y)\n return object.__new__(cls)\n def __init__(self,x,*,y):\n print(\"init\",x,y)\n self.x=x+y\na=A(3,y=4)\nprint(a.x)\n", + "stdout": "new A 3 4\ninit 3 4\n7\n" + }, + { + "id": "new-foreign-return", + "stage": "O3", + "source": "class A:\n def __new__(cls,x): return [x]\n def __init__(self,x): print(\"never\")\nprint(A(7)[0])\n", + "stdout": "7\n" + }, + { + "id": "new-none-return", + "stage": "O3", + "source": "class A:\n def __new__(cls): return None\n def __init__(self): print(\"never\")\nprint(A())\n", + "stdout": "None\n" + }, + { + "id": "new-subclass-init", + "stage": "O3", + "source": "class A:\n def __new__(cls,x): return object.__new__(B)\n def __init__(self,x): print(\"base\")\nclass B(A):\n def __init__(self,x): print(\"child\",x)\na=A(8)\nprint(isinstance(a,B))\n", + "stdout": "child 8\nTrue\n" + }, + { + "id": "new-unrelated-instance", + "stage": "O3", + "source": "class B:\n def __init__(self): self.x=9\nclass A:\n def __new__(cls): return B()\n def __init__(self): print(\"never\")\nprint(A().x)\n", + "stdout": "9\n" + }, + { + "id": "new-inherited", + "stage": "O3", + "source": "class A:\n def __new__(cls,x):\n print(cls.__name__)\n return object.__new__(cls)\n def __init__(self,x): self.x=x\nclass B(A): pass\nprint(B(7).x)\n", + "stdout": "B\n7\n" + }, + { + "id": "new-static-definition", + "stage": "O3", + "source": "class A:\n def __new__(cls): return object.__new__(cls)\na=A()\nprint(A.__new__ is a.__new__,isinstance(a.__new__(A),A))\n", + "stdout": "True True\n" + }, + { + "id": "new-explicit-static", + "stage": "O3", + "source": "class A:\n @staticmethod\n def __new__(cls,x): return object.__new__(cls)\nprint(isinstance(A(3),A))\n", + "stdout": "True\n" + }, + { + "id": "new-classmethod", + "stage": "O3", + "source": "class A:\n @classmethod\n def __new__(bound,cls):\n print(bound is cls)\n return object.__new__(cls)\nA()\n", + "stdout": "True\n" + }, + { + "id": "new-descriptor", + "stage": "O3", + "source": "class D:\n def __get__(self,obj,owner):\n print(obj is None,owner.__name__)\n def create(cls,x): return object.__new__(cls)\n return create\nclass A:\n __new__=D()\n def __init__(self,x): self.x=x\nprint(A(4).x)\n", + "stdout": "True A\n4\n" + }, + { + "id": "new-runtime-assignment", + "stage": "O3", + "source": "class A: pass\ndef create(cls,x): return [x]\nA.__new__=create\nprint(A(6)[0])\ndel A.__new__\nprint(isinstance(A(),A))\n", + "stdout": "6\nTrue\n" + }, + { + "id": "new-cached-instance", + "stage": "O3", + "source": "class A:\n def __init__(self,x): self.x=x\na=object.__new__(A)\ndef create(cls,x): return a\nA.__new__=create\nprint(A(3) is A(4),a.x)\n", + "stdout": "True 4\n" + }, + { + "id": "new-init-selected-after-new", + "stage": "O3", + "source": "def initialize(self,x): print(\"changed\",x)\nclass A:\n def __new__(cls,x):\n cls.__init__=initialize\n return object.__new__(cls)\n def __init__(self,x): print(\"old\")\nA(5)\n", + "stdout": "changed 5\n" + }, + { + "id": "new-init-descriptor", + "stage": "O3", + "source": "class D:\n def __get__(self,obj,owner):\n def initialize(x): obj.x=x\n return initialize\nclass A:\n __init__=D()\nprint(A(7).x)\n", + "stdout": "7\n" + }, + { + "id": "new-init-result", + "stage": "O3", + "source": "class A:\n def __new__(cls): return object.__new__(cls)\n def __init__(self): return 1\nA()\n", + "stdout": "", + "error_type": "TypeError" + }, + { + "id": "new-body-error", + "stage": "O3", + "source": "class A:\n def __new__(cls): raise AttributeError(\"body\")\nA()\n", + "stdout": "", + "error_type": "AttributeError" + }, + { + "id": "new-binding-error", + "stage": "O3", + "source": "class D:\n def __get__(self,obj,owner): raise ValueError(\"binding\")\nclass A: __new__=D()\nA()\n", + "stdout": "", + "error_type": "ValueError" + }, + { + "id": "new-not-callable", + "stage": "O3", + "source": "class A: __new__=None\nA()\n", + "stdout": "", + "error_type": "TypeError" + }, + { + "id": "new-signature", + "stage": "O3", + "source": "class A:\n def __new__(cls,x): return object.__new__(cls)\nA()\n", + "stdout": "", + "error_type": "TypeError" + }, + { + "id": "new-generator-result", + "stage": "O3", + "source": "class A:\n def __new__(cls):\n yield 7\n def __init__(self): print(\"never\")\nprint(next(A()))\n", + "stdout": "7\n" + }, + { + "id": "object-new-direct", + "stage": "O3", + "source": "class A:\n def __init__(self,x): self.x=x\na=object.__new__(A,7)\nprint(isinstance(a,A),hasattr(a,\"x\"))\nA.__init__(a,8)\nprint(a.x)\n", + "stdout": "True False\n8\n" + }, + { + "id": "object-new-extra", + "stage": "O3", + "source": "class A: pass\nobject.__new__(A,3)\n", + "stdout": "", + "error_type": "TypeError" + }, + { + "id": "object-new-overridden-extra", + "stage": "O3", + "source": "class A:\n def __new__(cls,*args): return object.__new__(cls,*args)\nA(3)\n", + "stdout": "", + "error_type": "TypeError" + }, + { + "id": "object-new-type", + "stage": "O3", + "source": "object.__new__(7)\n", + "stdout": "", + "error_type": "TypeError" + }, + { + "id": "object-init-overrides", + "stage": "O3", + "source": "class A:\n def __new__(cls,x): return object.__new__(cls)\na=A(3)\nprint(object.__init__(a,4))\n", + "stdout": "None\n" + }, + { + "id": "object-init-default-extra", + "stage": "O3", + "source": "class A: pass\nobject.__init__(A(),3)\n", + "stdout": "", + "error_type": "TypeError" + }, + { + "id": "object-init-overridden-extra", + "stage": "O3", + "source": "class A:\n def __init__(self,x): pass\nobject.__init__(A(1),2)\n", + "stdout": "", + "error_type": "TypeError" + }, + { + "id": "object-init-arbitrary-value", + "stage": "O3", + "source": "print(object.__init__(7))\n", + "stdout": "None\n" + }, + { + "id": "super-diamond", + "stage": "O4", + "source": "class A:\n def f(self): return 1\nclass B(A):\n def f(self): return super().f()+2\nclass C(A):\n def f(self): return super().f()+4\nclass D(B,C):\n def f(self): return super().f()+8\nprint(D().f())\n", + "stdout": "15\n" + }, + { + "id": "super-explicit", + "stage": "O4", + "source": "class A:\n def f(self): return 7\nclass B(A): pass\nb=B()\nb.f=None\nprint(super(B,b).f(),super(B,b).__self__ is b,super(B,b).__self_class__ is B)\n", + "stdout": "7 True True\n" + }, + { + "id": "super-classmethod", + "stage": "O4", + "source": "class A:\n @classmethod\n def f(cls): return cls.__name__\nclass B(A):\n @classmethod\n def f(cls): return super().f()\nprint(B.f(),super(B,B).f())\n", + "stdout": "B B\n" + }, + { + "id": "super-property", + "stage": "O4", + "source": "class A:\n @property\n def x(self): return self.saved\nclass B(A):\n def f(self): return super().x\nb=B()\nb.saved=8\nb.x=9\nprint(b.f())\n", + "stdout": "", + "error_type": "AttributeError" + }, + { + "id": "super-property-binding", + "stage": "O4", + "source": "class A:\n @property\n def x(self): return self.saved\nclass B(A):\n def f(self): return super().x\nb=B()\nb.saved=8\nprint(b.f())\n", + "stdout": "8\n" + }, + { + "id": "super-descriptor-owner", + "stage": "O4", + "source": "class Desc:\n def __get__(self,obj,owner):\n print(owner.__name__,obj is None)\n return 9\nclass A: x=Desc()\nclass B(A): pass\nprint(super(B,B()).x,super(B,B).x)\n", + "stdout": "B False\nB True\n9 9\n" + }, + { + "id": "super-unbound", + "stage": "O4", + "source": "class A:\n def f(self): return 4\nclass B(A): pass\nu=super(B)\nprint(u.__thisclass__ is B,u.__self__ is None,u.__self_class__ is None)\nB.proxy=u\nb=B()\nprint(b.proxy.f(),B.proxy is u)\n", + "stdout": "True True True\n4 True\n" + }, + { + "id": "super-none", + "stage": "O4", + "source": "class A: pass\nu=super(A,None)\nprint(u.__self__ is None)\n", + "stdout": "True\n" + }, + { + "id": "super-missing", + "stage": "O4", + "source": "class A: pass\nsuper(A,A()).missing\n", + "stdout": "", + "error_type": "AttributeError" + }, + { + "id": "super-invalid-first", + "stage": "O4", + "source": "super(7,7)\n", + "stdout": "", + "error_type": "TypeError" + }, + { + "id": "super-invalid-receiver", + "stage": "O4", + "source": "class A: pass\nclass B: pass\nsuper(A,B())\n", + "stdout": "", + "error_type": "TypeError" + }, + { + "id": "super-no-frame-cell", + "stage": "O4", + "source": "def f(x): return super()\nf(7)\n", + "stdout": "", + "error_type": "RuntimeError" + }, + { + "id": "super-no-arguments", + "stage": "O4", + "source": "class A:\n @staticmethod\n def f(): return super()\nA.f()\n", + "stdout": "", + "error_type": "RuntimeError" + }, + { + "id": "super-deleted-first", + "stage": "O4", + "source": "class A:\n def f(self):\n del self\n return super()\nA().f()\n", + "stdout": "", + "error_type": "RuntimeError" + }, + { + "id": "super-replaced-first", + "stage": "O4", + "source": "class A:\n def f(self):\n self=7\n return super()\nA().f()\n", + "stdout": "", + "error_type": "TypeError" + }, + { + "id": "super-nested-first", + "stage": "O4", + "source": "class A:\n def f(self): return 3\nclass B(A):\n def f(self):\n def nested(value): return super().f()\n return nested(self)\nprint(B().f())\n", + "stdout": "3\n" + }, + { + "id": "super-generator", + "stage": "O4", + "source": "class A:\n def f(self): return 6\nclass B(A):\n def g(self):\n yield 1\n yield super().f()\ng=B().g()\nprint(next(g),next(g))\n", + "stdout": "1 6\n" + }, + { + "id": "super-new", + "stage": "O4", + "source": "class A:\n def __new__(cls,x): return super().__new__(cls)\n def __init__(self,x): self.x=x\nprint(A(8).x)\n", + "stdout": "8\n" + }, + { + "id": "class-cell-decorator", + "stage": "O4", + "source": "def decorate(cls):\n print(cls().owner() is cls)\n return cls\n@decorate\nclass A:\n def owner(self): return __class__\n", + "stdout": "True\n" + }, + { + "id": "class-cell-nested", + "stage": "O4", + "source": "class A:\n x=9\n def f(self):\n def inner(): return __class__\n return inner()\nprint(A().f() is A)\n", + "stdout": "True\n" + }, + { + "id": "class-cell-class-local", + "stage": "O4", + "source": "class A:\n x=9\n def f(self): return x\nA().f()\n", + "stdout": "", + "error_type": "NameError" + }, + { + "id": "class-cell-not-class-local-value", + "stage": "O4", + "source": "class A:\n __class__=7\n def f(self): return __class__\nprint(A().f() is A,A.__class__ is type)\n", + "stdout": "True True\n" + }, + { + "id": "class-cell-nonlocal", + "stage": "O4", + "source": "class A:\n def f(self):\n nonlocal __class__\n __class__=7\n def g(self): return __class__\na=A()\na.f()\nprint(a.g())\n", + "stdout": "7\n" + }, + { + "id": "class-cell-empty-before-completion", + "stage": "O4", + "source": "class A:\n def f(value): return __class__\n f(None)\n", + "stdout": "", + "error_type": "NameError" + }, + { + "id": "type-primitives", + "stage": "O5", + "source": "print(type(1) is int,type(True) is bool,type(1.5) is float,type(2j) is complex,type(\"x\") is str)\nprint(type([]) is list,type(()) is tuple,type({}) is dict,type(range(3)) is range)\n", + "stdout": "True True True True True\nTrue True True True\n" + }, + { + "id": "type-owned-values", + "stage": "O5", + "source": "print(type(set()) is set,type(frozenset()) is frozenset,type(b\"a\") is bytes,type(bytearray()) is bytearray,type(memoryview(b\"x\")) is memoryview,type(slice(1)) is slice)\n", + "stdout": "True True True True True True\n" + }, + { + "id": "type-classes", + "stage": "O5", + "source": "class A: pass\nprint(type(A) is type,type(A()) is A,type(type) is type,type(object) is type,type(object()) is object)\n", + "stdout": "True True True True True\n" + }, + { + "id": "type-named-values", + "stage": "O5", + "source": "def f(): pass\nclass A:\n def f(self): pass\nprint(type(None).__name__,type(f).__name__,type(A().f).__name__,type(print).__name__,type(NotImplemented).__name__)\n", + "stdout": "NoneType function method builtin_function_or_method NotImplementedType\n" + }, + { + "id": "type-exceptions", + "stage": "O5", + "source": "print(type(ValueError()) is ValueError,isinstance(KeyError(),LookupError),issubclass(UnicodeDecodeError,ValueError))\n", + "stdout": "True True True\n" + }, + { + "id": "type-signatures", + "stage": "O5", + "source": "type(1,2)\n", + "stdout": "", + "error_type": "TypeError" + }, + { + "id": "type-no-arguments", + "stage": "O5", + "source": "type()\n", + "stdout": "", + "error_type": "TypeError" + }, + { + "id": "relation-builtin-ancestry", + "stage": "O5", + "source": "print(issubclass(bool,int),issubclass(int,bool),issubclass(type,object),issubclass(str,object),issubclass(Exception,BaseException))\nprint(isinstance(True,int),isinstance(int,type),isinstance(object,type),isinstance(7,object))\n", + "stdout": "True False True True True\nTrue True True True\n" + }, + { + "id": "relation-nested-tuples", + "stage": "O5", + "source": "class A: pass\nclass B(A): pass\nprint(isinstance(B(),((str,int),(A,))),issubclass(B,((str,int),(A,))),isinstance(3,(int,7)),issubclass(bool,(int,7)))\n", + "stdout": "True True True True\n" + }, + { + "id": "relation-empty-tuples", + "stage": "O5", + "source": "print(isinstance(1,()),issubclass(1,()),issubclass(int,()))\n", + "stdout": "False False False\n" + }, + { + "id": "relation-invalid-classinfo", + "stage": "O5", + "source": "isinstance(1,(str,7,int))\n", + "stdout": "", + "error_type": "TypeError" + }, + { + "id": "relation-invalid-subject", + "stage": "O5", + "source": "issubclass(7,(int,str))\n", + "stdout": "", + "error_type": "TypeError" + }, + { + "id": "relation-native-metadata", + "stage": "O5", + "source": "print(int.__name__,bool.__bases__[0] is int,type.__bases__[0] is object,object.__bases__==(),len(bool.__mro__))\n", + "stdout": "int True True True 3\n" + }, + { + "id": "dynamic-type", + "stage": "O5", + "source": "A=type(\"A\",(),{\"x\":7})\nB=type(\"B\",(A,),{\"y\":8})\nprint(A.__name__,B().x,B.y,B.__bases__[0] is A,type(B) is type)\n", + "stdout": "A 7 8 True True\n" + }, + { + "id": "dynamic-name", + "stage": "O5", + "source": "A=type(\"two words\",(),{})\nprint(A.__name__)\n", + "stdout": "two words\n" + }, + { + "id": "dynamic-init", + "stage": "O5", + "source": "def init(self,x): self.x=x\nA=type(\"A\",(),{\"__init__\":init})\nprint(A(8).x)\n", + "stdout": "8\n" + }, + { + "id": "dynamic-notification", + "stage": "O5", + "source": "class D:\n def __set_name__(self,owner,name): print(owner.__name__,name)\nA=type(\"A\",(),{\"x\":D()})\n", + "stdout": "A x\n" + }, + { + "id": "dynamic-namespace-copy", + "stage": "O5", + "source": "ns={\"x\":7}\nA=type(\"A\",(),ns)\nns[\"x\"]=8\nprint(A.x)\n", + "stdout": "7\n" + }, + { + "id": "dynamic-namespace-type", + "stage": "O5", + "source": "type(\"A\",(),[])\n", + "stdout": "", + "error_type": "TypeError" + }, + { + "id": "dynamic-namespace-key", + "stage": "Future", + "source": "type(\"A\",(),{1:2})\n", + "stdout": "" + }, + { + "id": "dynamic-bases-type", + "stage": "O5", + "source": "type(\"A\",[],{})\n", + "stdout": "", + "error_type": "TypeError" + }, + { + "id": "dynamic-name-type", + "stage": "O5", + "source": "type(7,(),{})\n", + "stdout": "", + "error_type": "TypeError" + }, + { + "id": "metaclass-explicit-type", + "stage": "O5", + "source": "class A(metaclass=type): pass\nprint(type(A) is type)\n", + "stdout": "True\n" + }, + { + "id": "metaclass-new-init", + "stage": "O5", + "source": "class M(type):\n def __new__(mcls,name,bases,ns):\n print(\"new\",name)\n ns[\"x\"]=7\n return super().__new__(mcls,name,bases,ns)\n def __init__(cls,name,bases,ns):\n print(\"init\",name,cls.x)\n super().__init__(name,bases,ns)\nclass A(metaclass=M): pass\nprint(type(A) is M,A.x,isinstance(A,M),issubclass(M,type))\n", + "stdout": "new A\ninit A 7\nTrue 7 True True\n" + }, + { + "id": "metaclass-inheritance", + "stage": "O5", + "source": "class M(type): pass\nclass N(M): pass\nclass A(metaclass=M): pass\nclass B(A,metaclass=N): pass\nclass C(B): pass\nprint(type(A) is M,type(B) is N,type(C) is N)\n", + "stdout": "True True True\n" + }, + { + "id": "metaclass-conflict", + "stage": "O5", + "source": "class M(type): pass\nclass N(type): pass\nclass A(metaclass=M): pass\nclass B(metaclass=N): pass\nclass C(A,B): print(\"never\")\n", + "stdout": "", + "error_type": "TypeError" + }, + { + "id": "metaclass-selection", + "stage": "O5", + "source": "class M(type): pass\nclass N(M): pass\nclass A(metaclass=N): pass\nclass B(A,metaclass=M): pass\nprint(type(B) is N)\n", + "stdout": "True\n" + }, + { + "id": "metaclass-foreign-return", + "stage": "O5", + "source": "class M(type):\n def __new__(mcls,name,bases,ns): return 7\n def __init__(cls,*args): print(\"never\")\nclass A(metaclass=M): pass\nprint(A)\n", + "stdout": "7\n" + }, + { + "id": "metaclass-cell", + "stage": "O5", + "source": "class M(type):\n def __new__(mcls,name,bases,ns): return type.__new__(mcls,name,bases,ns)\nclass A(metaclass=M):\n def f(self): return __class__\nprint(A().f() is A)\n", + "stdout": "True\n" + }, + { + "id": "metaclass-notification-order", + "stage": "O5", + "source": "class D:\n def __set_name__(self,owner,name): print(\"name\",name)\nclass M(type):\n def __new__(mcls,name,bases,ns):\n print(\"before\")\n cls=type.__new__(mcls,name,bases,ns)\n print(\"after\")\n return cls\n def __init__(cls,*args): print(\"init\")\ndef deco(cls):\n print(\"deco\")\n return cls\n@deco\nclass A(metaclass=M): x=D()\n", + "stdout": "before\nname x\nafter\ninit\ndeco\n" + }, + { + "id": "metaclass-method", + "stage": "O5", + "source": "class M(type):\n def f(cls): return cls.__name__\nclass A(metaclass=M): pass\nprint(A.f(),A.f.__self__ is A)\n", + "stdout": "A True\n" + }, + { + "id": "metaclass-call", + "stage": "O5", + "source": "class M(type):\n def __call__(cls,x):\n print(\"call\",x)\n return super().__call__(x+1)\nclass A(metaclass=M):\n def __init__(self,x): self.x=x\nprint(A(4).x)\n", + "stdout": "call 4\n5\n" + }, + { + "id": "metaclass-call-foreign", + "stage": "O5", + "source": "class M(type):\n def __call__(cls,x): return [x]\nclass A(metaclass=M): pass\nprint(A(3)[0])\n", + "stdout": "3\n" + }, + { + "id": "metaclass-init-result", + "stage": "O5", + "source": "class M(type):\n def __init__(cls,*args): return 7\nclass A(metaclass=M): pass\n", + "stdout": "", + "error_type": "TypeError" + }, + { + "id": "metaclass-new-body-error", + "stage": "O5", + "source": "class M(type):\n def __new__(mcls,*args): raise ValueError(\"new\")\nclass A(metaclass=M): pass\n", + "stdout": "", + "error_type": "ValueError" + }, + { + "id": "future-prepare", + "stage": "Future", + "source": "class M(type):\n @classmethod\n def __prepare__(mcls,name,bases): return {}\nclass A(metaclass=M): pass\n", + "stdout": "" + }, + { + "id": "future-init-subclass", + "stage": "Future", + "source": "class A:\n def __init_subclass__(cls): print(\"hook\")\nclass B(A): pass\n", + "stdout": "hook\n" + }, + { + "id": "future-builtin-subclass", + "stage": "Future", + "source": "class A(list): pass\nprint(len(A()))\n", + "stdout": "0\n" + }, + { + "id": "future-instancecheck", + "stage": "Future", + "source": "class M(type):\n def __instancecheck__(cls,value): return True\nclass A(metaclass=M): pass\nprint(isinstance(7,A))\n", + "stdout": "True\n" + }, + { + "id": "future-class-keywords", + "stage": "Future", + "source": "class A:\n def __init_subclass__(cls,flag): print(flag)\nclass B(A,flag=7): pass\n", + "stdout": "7\n" + } + ] +} diff --git a/tests/test_generator_contract.py b/tests/test_generator_contract.py index 27980c6..8ddb723 100644 --- a/tests/test_generator_contract.py +++ b/tests/test_generator_contract.py @@ -10,7 +10,7 @@ class GeneratorObjectContractTests(unittest.TestCase): def test_pinned_reference_contracts(self): - for name in ("generators", "object-protocols"): + for name in ("generators", "object-protocols", "construction"): with self.subTest(corpus=name): path = ROOT / "tests/fixtures/interpreter" / (name + ".json") corpus = json.loads(path.read_text(encoding="utf-8")) From 0003b8794d40c4aac5047e4ee64473f1719617b4 Mon Sep 17 00:00:00 2001 From: ellainng <299958882+ellainng@users.noreply.github.com> Date: Mon, 26 Oct 2026 18:21:44 -0700 Subject: [PATCH 2/3] feat(INT-OBJECT-01D): execute owned instance construction --- docs/interpreter-construction.md | 1 + docs/interpreter-object-protocols.md | 5 +- runtime/class-policy.mjs | 2 +- runtime/class-policy.test.mjs | 2 +- runtime/class-values.mjs | 23 +++--- runtime/class-values.test.mjs | 2 - runtime/construction.test.mjs | 79 +++++++++++++++++++ runtime/operation-runtime.mjs | 51 +++++++++--- runtime/operations.mjs | 5 ++ src/pocket_python/_interpreter_lock.py | 10 +-- tests/fixtures/interpreter/construction.json | 4 +- tests/fixtures/interpreter/length-truth.json | 2 +- .../interpreter/object-protocols.json | 5 +- tests/fixtures/interpreter/special-calls.json | 2 +- 14 files changed, 157 insertions(+), 36 deletions(-) create mode 100644 runtime/construction.test.mjs diff --git a/docs/interpreter-construction.md b/docs/interpreter-construction.md index 51eda18..736ee8a 100644 --- a/docs/interpreter-construction.md +++ b/docs/interpreter-construction.md @@ -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. diff --git a/docs/interpreter-object-protocols.md b/docs/interpreter-object-protocols.md index b81c330..0042eb0 100644 --- a/docs/interpreter-object-protocols.md +++ b/docs/interpreter-object-protocols.md @@ -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 @@ -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 diff --git a/runtime/class-policy.mjs b/runtime/class-policy.mjs index dd080db..e68ecea 100644 --- a/runtime/class-policy.mjs +++ b/runtime/class-policy.mjs @@ -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("__"); diff --git a/runtime/class-policy.test.mjs b/runtime/class-policy.test.mjs index 86cc9cf..ab5dbff 100644 --- a/runtime/class-policy.test.mjs +++ b/runtime/class-policy.test.mjs @@ -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'}); diff --git a/runtime/class-values.mjs b/runtime/class-values.mjs index 4b73ab7..aaa2789 100644 --- a/runtime/class-values.mjs +++ b/runtime/class-values.mjs @@ -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"); @@ -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; } @@ -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); @@ -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 + "'."); @@ -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); diff --git a/runtime/class-values.test.mjs b/runtime/class-values.test.mjs index 4285633..d9c3ecd 100644 --- a/runtime/class-values.test.mjs +++ b/runtime/class-values.test.mjs @@ -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(); } }); diff --git a/runtime/construction.test.mjs b/runtime/construction.test.mjs new file mode 100644 index 0000000..e91fe3e --- /dev/null +++ b/runtime/construction.test.mjs @@ -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]]), [['', 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); +}); diff --git a/runtime/operation-runtime.mjs b/runtime/operation-runtime.mjs index 020ed8a..21e4365 100644 --- a/runtime/operation-runtime.mjs +++ b/runtime/operation-runtime.mjs @@ -1,6 +1,6 @@ import {ARITHMETIC_SLOTS} from "./class-policy.mjs"; import {GeneratorValue, FunctionValue, CallArguments, BuiltinValue, AppendValue, ValueMethod, ExceptionValue, EXCEPTIONS, matchesException, ClassValue, InstanceValue, BoundMethodValue, PropertyValue, MethodWrapperValue, PropertyAccessorValue, ProtocolIteratorValue, NotImplementedValue} from "./function-objects.mjs"; -import {completion as continuation, callTask, unaryTask, valueTask, itemTask, sliceTask, containsTask, compareTask, dictionaryTask, dictionaryBuildTask, arithmeticTask, numericTask, consumeTask, operationRoots} from "./operations.mjs"; +import {completion as continuation, completionRoots, callTask, unaryTask, valueTask, itemTask, sliceTask, containsTask, compareTask, dictionaryTask, dictionaryBuildTask, arithmeticTask, numericTask, consumeTask, operationRoots} from "./operations.mjs"; export class Raised { constructor(value) { this.value = value; } } @@ -335,6 +335,11 @@ export function operationRuntime({state, memory, values, classes, moduleContext, } completion = completion.after; } + if (completion?.kind === "new-result") { + state.temporaries.push(value, ...completionRoots(completion)); + if (!(value instanceof InstanceValue) || !classes.subclass(value.type, completion.type)) return valueTask(value, site, completion.after); + return initialize(value, completion.args, completion.keywords, completion.keywordValues, site, completion.after, completion.depth, true); + } if (completion?.kind === "arithmetic-result") { if (!(value instanceof NotImplementedValue)) return valueTask(value, site, completion.after); const inplace = completion.operation.startsWith("inplace-"); @@ -576,6 +581,17 @@ export function operationRuntime({state, memory, values, classes, moduleContext, } return deliver(type, null, site); }; + const initialize = (instance, args, keywords, keywordValues, site, after, depth, customNew = false) => { + const initializer = classes.lookup(instance.type, "__init__", false); + if (!initializer.found) { + if (!customNew && (args.length || keywords.length)) python("TypeError", instance.type.name + "() takes no arguments"); + return valueTask(instance, site, after); + } + const plan = customNew ? classes.bindAttribute(initializer.value, instance, instance.type) : classes.read(instance, "__init__"); + const target = continuation("init", {instance, after}); + return plan.kind === "value" ? callTask(plan.value, args, keywords, keywordValues, site, target, depth) : + callTask(plan.callable, plan.args, [], [], site, continuation("invoke", {args, keywords, keywordValues, after: target, depth}), depth); + }; const printNext = (args, index, site, after, depth, prefix = "") => { state.temporaries.push(...args); let text = prefix; @@ -1082,15 +1098,17 @@ export function operationRuntime({state, memory, values, classes, moduleContext, task = deliver(allocate(PropertyValue, ...fields), completion, site); continue; } if (callable instanceof ClassValue) { - const instance = classes.instance(callable); state.temporaries.push(instance); - const initializer = classes.lookup(callable, "__init__"); - if (!initializer.found) { - if (args.length || keywords.length) python("TypeError", callable.name + "() takes no arguments"); - task = deliver(instance, completion, site); continue; + if (classes.hasSlot(callable, "__new__")) { + if (++depth > maxDepth) python("RecursionError", "maximum constructor dispatch depth exceeded"); + const item = classes.lookup(callable, "__new__"), plan = classes.bindAttribute(item.value, null, callable); + const target = continuation("new-result", {type: callable, args, keywords, keywordValues, after: completion, depth}); + const positional = [callable, ...args]; + task = plan.kind === "value" ? callTask(plan.value, positional, keywords, keywordValues, site, target, depth) : + callTask(plan.callable, plan.args, [], [], site, continuation("invoke", {args: positional, keywords, keywordValues, after: target, depth}), depth); + continue; } - const method = classes.get(instance, "__init__"); - task = callTask(method, args, keywords, keywordValues, site, - continuation("init", {instance, after: completion}), depth); continue; + const instance = classes.instance(callable); state.temporaries.push(instance); + task = initialize(instance, args, keywords, keywordValues, site, completion, depth); continue; } if (callable instanceof FunctionValue) { const bindings = bind(callable, args, keywords, keywordValues); @@ -1100,6 +1118,21 @@ export function operationRuntime({state, memory, values, classes, moduleContext, enter(callable, bindings, site, completion); return; } if (callable instanceof BuiltinValue) { + if (callable.name === "object.__new__") { + if (!args.length || !(args[0] instanceof ClassValue)) python("TypeError", "object.__new__ requires a type"); + const type = args[0]; + if ((args.length > 1 || keywords.length) && (classes.hasSlot(type, "__new__") || !classes.hasSlot(type, "__init__"))) + python("TypeError", "object.__new__ received extra arguments"); + task = valueTask(classes.instance(type), site, completion); continue; + } + if (callable.name === "object.__init__") { + if (!args.length) python("TypeError", "object.__init__ requires an instance"); + const value = args[0]; + if ((args.length > 1 || keywords.length) && (!(value instanceof InstanceValue) || + classes.hasSlot(value.type, "__init__") || !classes.hasSlot(value.type, "__new__"))) + python("TypeError", "object.__init__ received extra arguments"); + task = valueTask(null, site, completion); continue; + } 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) diff --git a/runtime/operations.mjs b/runtime/operations.mjs index 8ab5539..654faa7 100644 --- a/runtime/operations.mjs +++ b/runtime/operations.mjs @@ -36,6 +36,10 @@ export function completion(kind, fields = {}) { switch (kind) { case "class": record = {kind, bases: operands(fields.bases), module: fields.module}; break; case "init": record = {kind, instance: fields.instance, after}; break; + case "new-result": + record = {kind, type: fields.type, args: operands(fields.args), keywords: operands(fields.keywords), + keywordValues: operands(fields.keywordValues), depth: fields.depth, after}; + depthCheck(record.depth); break; case "relay": record = {kind, receiver: fields.receiver, after}; break; case "item-result": record = {kind, receiver: fields.receiver, after}; break; case "index-result": record = {kind, receiver: fields.receiver, after}; break; @@ -150,6 +154,7 @@ function visit(record) { switch (record.kind) { case "class": case "condition": case "discard": case "for-iter": case "for-next": case "for-exhausted": return []; case "init": return [record.instance, ...completionRoots(record.after)]; + case "new-result": return [record.type, ...record.args, ...record.keywordValues, ...completionRoots(record.after)]; case "relay": case "protocol-result": case "optional-truth": case "format-result": case "index-result": case "item-result": return [record.receiver, ...completionRoots(record.after)]; case "hash-result": return [record.receiver, ...completionRoots(record.after)]; case "numeric-result": case "numeric-index": return [...record.args, ...completionRoots(record.after)]; diff --git a/src/pocket_python/_interpreter_lock.py b/src/pocket_python/_interpreter_lock.py index 4f9d14e..8ad4ab5 100644 --- a/src/pocket_python/_interpreter_lock.py +++ b/src/pocket_python/_interpreter_lock.py @@ -1,11 +1,11 @@ """Known source-console assets, canonical LF bytes; refresh with scripts/lock_interpreter.py.""" -ENGINE_FINGERPRINT = '0482efe29e46d5f1228ee6d7ce214f1f4168729a55241e4995502e9587cb3ec2' +ENGINE_FINGERPRINT = '27fbf19fccda89eed8d913ad2a5a10ef540ef838a614047248fbdc88e94bfd75' ENGINE_FILES = ( ('browser-protocol.mjs', 10589, '920e8d07c7070995636dff40fdf4333afc9bd4143483db5d8dff82ec1f28ff76'), ('bytecode.mjs', 6836, 'e76fabeea879bc1da8016283f7e74df5974479666b100199c2d3c31a53b8ecf4'), - ('class-policy.mjs', 1968, 'e3275e080c2d26584bb318e88d9a0278191815ac072c48e3054fd4c908cb771b'), - ('class-values.mjs', 15372, '67fc606a6c91f7aeb1d4309cc4a0be33778181cdccb51190e5f76d4969d9b43d'), + ('class-policy.mjs', 1995, '1e705bafb0c8d32a1bc49bb066d4284d1998db919fc5cfbc156afe06104b610b'), + ('class-values.mjs', 15409, '7c69d186e0a36bf85f974b6ef9ddd8c1e0399fd1d155b14daa9e52cdcd70de3f'), ('function-bytecode.mjs', 20438, 'ca6b3abd73d2be35422eea244b876a851bf98e38836e31a33fa6d18d4252ae68'), ('function-objects.mjs', 4277, '06af0ca936de1dd336cb7a2ac13f24efae960ea3fe448c93fb95a2e92213c4e2'), ('functions.mjs', 57726, '7d6e4322c31187b12387695dd094a0fd4dab2220cf28c1df2bb4856ccf7ae179'), @@ -20,8 +20,8 @@ ('module-registry.mjs', 10085, 'f23f8313c0804a052cba155e193df920b0221ef978a06f16c1a22f208dfbef91'), ('module-session.mjs', 1470, '08e4ccf16361299c15b99621fdfcef0d9ef93de50d28d738158c7bc85a7c23a3'), ('numeric.mjs', 13954, '2431f099a7d40aa38d6b3c68b556207752e4191211e01005acde94325bd91bad'), - ('operation-runtime.mjs', 93154, '488791c8e00d8e510feaf9bc654f06a6e0bcaed7f8ed6a2024a1a23140713315'), - ('operations.mjs', 23452, 'bd4c064811e6400576cae60bfcdbd35a76c829a25085794f95520d6307fad78e'), + ('operation-runtime.mjs', 95840, '09baff12598d313ae411fff8f3154521341b527b6e294db7f2ebdc9608c17fcd'), + ('operations.mjs', 23825, '4cf2afcb1d8fee89e4049c2fd176f230db17f57e6b4a4a6048b8004fedfe0684'), ('source-compiler.mjs', 35239, '4201a25a0c45b8fba4777c8e98b75e37f3277888708093a77e2827dec5ff7063'), ('source-identifiers.mjs', 25949, '1e4e6a90728a2d9393eb4b2055d4bd759542f4768d1870b508d629c6888e8914'), ('source-modules.mjs', 5115, '3f824a8cf79a9ab78c1b817ed1597d114d851748a6b0894a4ac6b4bdbe01cfef'), diff --git a/tests/fixtures/interpreter/construction.json b/tests/fixtures/interpreter/construction.json index 33d77fb..feaa9ad 100644 --- a/tests/fixtures/interpreter/construction.json +++ b/tests/fixtures/interpreter/construction.json @@ -3,7 +3,9 @@ "suite": "interpreter-construction/1", "profile": "classes-source/1", "reference_version": "3.14.7", - "implemented_stages": [], + "implemented_stages": [ + "O3" + ], "cases": [ { "id": "new-original-arguments", diff --git a/tests/fixtures/interpreter/length-truth.json b/tests/fixtures/interpreter/length-truth.json index e331527..3ac3aab 100644 --- a/tests/fixtures/interpreter/length-truth.json +++ b/tests/fixtures/interpreter/length-truth.json @@ -604,7 +604,7 @@ }, { "id": "future-new", - "stage": "C3-future", + "stage": "C3-length", "source": "class Index:\n def __new__(cls): return object()\nvalue=Index()\n", "stdout": "" } diff --git a/tests/fixtures/interpreter/object-protocols.json b/tests/fixtures/interpreter/object-protocols.json index 96e258c..62d1047 100644 --- a/tests/fixtures/interpreter/object-protocols.json +++ b/tests/fixtures/interpreter/object-protocols.json @@ -5,7 +5,8 @@ "reference_version": "3.14.7", "implemented_stages": [ "O1", - "O2" + "O2", + "O3" ], "cases": [ { @@ -88,7 +89,7 @@ }, { "id": "new-future", - "stage": "Future", + "stage": "O3", "source": "class A:\n def __new__(cls): return object.__new__(cls)\nprint(type(A()) is A)\n", "stdout": "True\n" }, diff --git a/tests/fixtures/interpreter/special-calls.json b/tests/fixtures/interpreter/special-calls.json index fc8902e..3bf6dcd 100644 --- a/tests/fixtures/interpreter/special-calls.json +++ b/tests/fixtures/interpreter/special-calls.json @@ -407,7 +407,7 @@ }, { "id": "future-new", - "stage": "C3-future", + "stage": "C3-call", "source": "class A:\n def __new__(cls): return 7\nprint(A())\n", "stdout": "7\n" }, From d329e6a01e8b065aaca29cd2857b3db9da9ea688 Mon Sep 17 00:00:00 2001 From: ellainng <299958882+ellainng@users.noreply.github.com> Date: Mon, 26 Oct 2026 18:26:33 -0700 Subject: [PATCH 3/3] feat(INT-OBJECT-01E): execute super and lexical class cells --- docs/interpreter-compatibility.json | 21 ++++---- docs/interpreter-compatibility.md | 4 +- docs/interpreter-construction.md | 2 +- docs/interpreter-object-protocols.md | 3 +- runtime/class-policy.mjs | 3 +- runtime/class-values.mjs | 31 ++++++++++-- runtime/classes-source.test.mjs | 3 +- runtime/construction.test.mjs | 48 +++++++++++++++++++ runtime/function-bytecode.mjs | 3 +- runtime/function-objects.mjs | 3 ++ runtime/functions.mjs | 18 +++++-- runtime/operation-runtime.mjs | 13 +++++ runtime/private-names.test.mjs | 2 +- runtime/source-compiler.mjs | 5 +- runtime/values.mjs | 9 ++-- src/pocket_python/_interpreter_lock.py | 18 +++---- tests/fixtures/interpreter/classes.json | 5 +- tests/fixtures/interpreter/compatibility.json | 8 ++-- tests/fixtures/interpreter/construction.json | 3 +- 19 files changed, 153 insertions(+), 49 deletions(-) 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": [ {