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/7] 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/7] 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/7] 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": [ { From d7772617365fed866d5044e285c8841d275a64d6 Mon Sep 17 00:00:00 2001 From: ellainng <299958882+ellainng@users.noreply.github.com> Date: Mon, 26 Oct 2026 18:42:16 -0700 Subject: [PATCH 4/7] feat(INT-OBJECT-01F): execute finite type and metaclass protocols --- docs/interpreter-compatibility.json | 55 ++++--- docs/interpreter-compatibility.md | 12 +- docs/interpreter-construction.md | 9 +- docs/interpreter-object-protocols.md | 3 +- runtime/class-values.mjs | 138 +++++++++++++++--- runtime/classes-source.test.mjs | 2 +- runtime/construction.test.mjs | 71 ++++++++- runtime/function-bytecode.mjs | 8 +- runtime/function-objects.mjs | 3 +- runtime/functions.mjs | 36 +++-- runtime/operation-runtime.mjs | 108 ++++++++++++-- runtime/operations.mjs | 16 +- runtime/source-compiler.mjs | 7 +- runtime/source-parser.mjs | 16 +- runtime/values.mjs | 24 ++- src/pocket_python/_interpreter_lock.py | 20 +-- tests/fixtures/interpreter/classes.json | 5 +- tests/fixtures/interpreter/compatibility.json | 16 +- tests/fixtures/interpreter/construction.json | 135 ++++++++++++++++- 19 files changed, 567 insertions(+), 117 deletions(-) diff --git a/docs/interpreter-compatibility.json b/docs/interpreter-compatibility.json index 6e101d5..d5047f7 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: 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.", + "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. Finite metaclasses adopt type-derived inheritance, selection/conflicts, __new__/__init__/__call__, descriptor binding and class-cell propagation with ordinary dictionary namespaces.", + "gaps": "Builtin instance layouts, custom namespace preparation, further metaclass special slots, callable factories and additional class keywords remain separate.", "reference_version": "3.14.7", "targets": [ "node", @@ -254,7 +254,9 @@ ], "evidence": [ "tests/fixtures/interpreter/construction.json:new-subclass-init", - "tests/fixtures/interpreter/construction.json:super-diamond" + "tests/fixtures/interpreter/construction.json:super-diamond", + "tests/fixtures/interpreter/construction.json:metaclass-new-init", + "tests/fixtures/interpreter/construction.json:dynamic-winning-metaclass" ] }, { @@ -1087,9 +1089,9 @@ }, { "id": "dict", - "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": "No-argument dict() in classes-source/1; dictionary literals and existing owned operations retain their separate contracts.", + "gaps": "Populated dictionary construction and broader dictionary methods remain deferred.", "reference_version": "3.14.7", "targets": [ "node", @@ -1101,7 +1103,9 @@ "probes": [ "builtin-dict" ], - "evidence": [] + "evidence": [ + "tests/fixtures/interpreter/construction.json:type-primitives" + ] }, { "id": "dir", @@ -1443,8 +1447,8 @@ { "id": "isinstance", "status": "partial", - "scope": "isinstance(x, str/adopted exception class/user class).", - "gaps": "Tuple/union classinfo and unadopted builtin families.", + "scope": "Adopted type relationships, canonical native identities, bool/int and exception ancestry, nested class-info tuples with short-circuit/error order in classes-source/1.", + "gaps": "Unions and custom instance/subclass checks remain deferred; tuple nesting is bounded.", "reference_version": "3.14.7", "targets": [ "node", @@ -1456,13 +1460,16 @@ "probes": [ "builtin-isinstance" ], - "evidence": [] + "evidence": [ + "tests/fixtures/interpreter/construction.json:relation-builtin-ancestry", + "tests/fixtures/interpreter/construction.json:relation-nested-tuples" + ] }, { "id": "issubclass", "status": "partial", - "scope": "issubclass(user_class, user_class/object).", - "gaps": "Full builtin, tuple/union classinfo.", + "scope": "Adopted type relationships, canonical native identities, bool/int and exception ancestry, nested class-info tuples with short-circuit/error order in classes-source/1.", + "gaps": "Unions and custom instance/subclass checks remain deferred; tuple nesting is bounded.", "reference_version": "3.14.7", "targets": [ "node", @@ -1474,7 +1481,10 @@ "probes": [ "builtin-issubclass" ], - "evidence": [] + "evidence": [ + "tests/fixtures/interpreter/construction.json:relation-builtin-ancestry", + "tests/fixtures/interpreter/construction.json:relation-nested-tuples" + ] }, { "id": "iter", @@ -1656,8 +1666,8 @@ { "id": "object", "status": "partial", - "scope": "object() and root base in classes-source/1.", - "gaps": "Default displays, identity hashing, __new__ and richer object APIs.", + "scope": "Root object identity, no-argument construction and finite object.__new__/__init__ for ordinary user instances in classes-source/1.", + "gaps": "Default displays, builtin instance allocation and broader reflection remain separately gated.", "reference_version": "3.14.7", "targets": [ "node", @@ -1670,7 +1680,10 @@ "probes": [ "builtin-object" ], - "evidence": [] + "evidence": [ + "tests/fixtures/interpreter/construction.json:object-new-direct", + "tests/fixtures/interpreter/construction.json:object-init-overrides" + ] }, { "id": "oct", @@ -2029,8 +2042,8 @@ { "id": "type", "status": "partial", - "scope": "type(user_instance), one argument; classes-source/1.", - "gaps": "Full one-argument and three-argument type construction.", + "scope": "Canonical type identities for adopted values and three-argument type construction with tuple bases and a bounded string-key dictionary namespace; finite type.__new__/__init__/__call__ and metaclass selection.", + "gaps": "Custom namespace mappings, callable factories, builtin instance subclasses and further metaclass special slots remain gated.", "reference_version": "3.14.7", "targets": [ "node", @@ -2042,7 +2055,11 @@ "probes": [ "builtin-type" ], - "evidence": [] + "evidence": [ + "tests/fixtures/interpreter/construction.json:type-primitives", + "tests/fixtures/interpreter/construction.json:dynamic-type", + "tests/fixtures/interpreter/construction.json:metaclass-new-init" + ] }, { "id": "vars", diff --git a/docs/interpreter-compatibility.md b/docs/interpreter-compatibility.md index a10f62a..d1dca84 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: 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. | +| 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. Finite metaclasses adopt type-derived inheritance, selection/conflicts, __new__/__init__/__call__, descriptor binding and class-cell propagation with ordinary dictionary namespaces. Builtin instance layouts, custom namespace preparation, further metaclass special slots, callable factories and additional class keywords remain separate. | | 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. | @@ -103,7 +103,7 @@ issue families; availability alone never certifies an entire type or module. | compile | deferred | Not installed; resolving this builtin name raises Python NameError. | No implementation/API support credit from the negative availability probe. | | complex | partial | Owned immutable Cartesian values, literal/text/real/complex construction, real/imag keywords and attributes, finite basic arithmetic, equality/truth/hash and bounded integer powers. Owned conversion and index fallback callbacks run in ordinary VM frames under classes-source/1. | General powers, non-finite arithmetic/recovery families, numeric methods, formatting and builtin subclasses remain later boundaries. | | delattr | partial | Positional reflection on adopted class attributes and descriptors. | Custom hooks and broader reflection remain separate. | -| dict | deferred | Not installed; resolving this builtin name raises Python NameError. | No implementation/API support credit from the negative availability probe. | +| dict | partial | No-argument dict() in classes-source/1; dictionary literals and existing owned operations retain their separate contracts. | Populated dictionary construction and broader dictionary methods remain deferred. | | dir | deferred | Not installed; resolving this builtin name raises Python NameError. | No implementation/API support credit from the negative availability probe. | | divmod | partial | Positional-only primitive int/float quotient and remainder with owned tuple results and Python sign/zero rules. | General user arithmetic/reflected slots remain INT-OBJECT-01. | | enumerate | deferred | Not installed; resolving this builtin name raises Python NameError. | No implementation/API support credit from the negative availability probe. | @@ -122,8 +122,8 @@ issue families; availability alone never certifies an entire type or module. | id | deferred | Not installed; resolving this builtin name raises Python NameError. | No implementation/API support credit from the negative availability probe. | | input | host-restricted | Not installed; availability probe only, never invoked. | Interactive/process/filesystem behavior needs an explicit target capability contract. | | int | partial | Real primitive and Unicode text construction with exact signatures, base/underscore rules and bounded input/result work in both source profiles. Owned conversion and index fallback callbacks run in ordinary VM frames under classes-source/1. | Bytes (int/float), builtin subclasses, numeric methods and formatting remain later value/object stages. | -| isinstance | partial | isinstance(x, str/adopted exception class/user class). | Tuple/union classinfo and unadopted builtin families. | -| issubclass | partial | issubclass(user_class, user_class/object). | Full builtin, tuple/union classinfo. | +| isinstance | partial | Adopted type relationships, canonical native identities, bool/int and exception ancestry, nested class-info tuples with short-circuit/error order in classes-source/1. | Unions and custom instance/subclass checks remain deferred; tuple nesting is bounded. | +| issubclass | partial | Adopted type relationships, canonical native identities, bool/int and exception ancestry, nested class-info tuples with short-circuit/error order in classes-source/1. | Unions and custom instance/subclass checks remain deferred; tuple nesting is bounded. | | iter | partial | Primitive iter(value); classes-source adds user __iter__, indexed fallback and iter(callable, sentinel). | Generator/async protocols and broader sequence APIs remain separate. | | len | partial | len(admitted containers/str or classes-profile __len__). | Unadopted value families and builtin subclasses remain deferred; user index results are validated through owned conversion. | | list | partial | Zero or one positional adopted iterable; primitives in both profiles, owned user iteration in classes-source/1 without user length hints. Lists are fresh shallow containers. | User length hints, methods, builtin subclassing and broader reflection remain separate stages. | @@ -133,7 +133,7 @@ issue families; availability alone never certifies an entire type or module. | memoryview | partial | One-dimensional unsigned-byte views over owned bytes/bytearray, strided aliases, scalar writes, readonly/cached hashing, release, finite metadata and conversion methods. | Native buffers, other formats/dimensions/casts, slice mutation, context management and overflowing nested strides remain deferred. | | min | deferred | Not installed; resolving this builtin name raises Python NameError. | No implementation/API support credit from the negative availability probe. | | next | partial | Primitive next(iterator); classes-source adds user __next__ and next(iterator, default). | Generator/async protocols and iterator reflection remain separate. | -| object | partial | object() and root base in classes-source/1. | Default displays, identity hashing, __new__ and richer object APIs. | +| object | partial | Root object identity, no-argument construction and finite object.__new__/__init__ for ordinary user instances in classes-source/1. | Default displays, builtin instance allocation and broader reflection remain separately gated. | | oct | supported | Primitive numeric signatures in both source profiles and owned type-level callbacks in classes-source/1; see interpreter-numeric.md for the finite contract. | | | open | host-restricted | Not installed; availability probe only, never invoked. | Interactive/process/filesystem behavior needs an explicit target capability contract. | | ord | supported | One-character Unicode strings, including non-BMP code points and lone surrogate values; positional-only argument. One-byte bytes/bytearray arguments are also adopted. | | @@ -153,7 +153,7 @@ issue families; availability alone never certifies an entire type or module. | sum | 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. | +| type | partial | Canonical type identities for adopted values and three-argument type construction with tuple bases and a bounded string-key dictionary namespace; finite type.__new__/__init__/__call__ and metaclass selection. | Custom namespace mappings, callable factories, builtin instance subclasses and further metaclass special slots remain gated. | | vars | deferred | Not installed; resolving this builtin name raises Python NameError. | No implementation/API support credit from the negative availability probe. | | zip | deferred | Not installed; resolving this builtin name raises Python NameError. | No implementation/API support credit from the negative availability probe. | | __import__ | 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 b9f8ddc..bc7db97 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 and O4 are implemented; O5 remains reference-only until its changes are accepted. +O3, O4 and O5 are implemented with independently executed acceptance below. 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. @@ -12,7 +12,7 @@ core-source/1 and bytecode 4 do not gain class syntax or reflective builtins. | 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. | +| Future | Builtin instance layouts/subclassing, custom `__prepare__` and class-creation mappings, extra class keywords, `__init_subclass__`, `__mro_entries__`, callable metaclass factories, additional metaclass special slots beyond construction/call, custom instance/subclass checking, non-string namespace keys, 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 @@ -20,6 +20,11 @@ rejected before class-body or hook effects where the source/compiler can identif 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. +Type relations use bounded nesting of at most 32 class-info tuples. Reflection +returns canonical identities for the adopted value families; construction of +reflected implementation types is separately refused, except the no-argument +NoneType and NotImplementedType singletons. `dict()` is available without +arguments in classes-source/1; populated dictionary constructors remain deferred. All constructor/type candidates, arguments, keyword values, namespace contents, class cells, super receivers and outer destinations are explicit owned edges or diff --git a/docs/interpreter-object-protocols.md b/docs/interpreter-object-protocols.md index 1d7f328..2541748 100644 --- a/docs/interpreter-object-protocols.md +++ b/docs/interpreter-object-protocols.md @@ -12,7 +12,8 @@ classes-source/1; core-source/1 keeps primitive arithmetic and generator support | 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. | | 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. | +| O5 | Type relations and finite metaclass construction follow the [construction contract](interpreter-construction.md). | +| Future | builtin instance subclasses, further metaclass protocols, 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-values.mjs b/runtime/class-values.mjs index 178c614..a1603ce 100644 --- a/runtime/class-values.mjs +++ b/runtime/class-values.mjs @@ -1,9 +1,13 @@ -import {ClassValue, InstanceValue, BoundMethodValue, SuperValue, FunctionValue, BuiltinValue, EXCEPTIONS, PropertyValue, MethodWrapperValue, PropertyAccessorValue} from "./function-objects.mjs"; +import {ClassValue, InstanceValue, BoundMethodValue, SuperValue, FunctionValue, BuiltinValue, EXCEPTIONS, matchesException, PropertyValue, MethodWrapperValue, PropertyAccessorValue, GeneratorValue, ModuleValue, Cell, NotImplementedValue, ProtocolIteratorValue} from "./function-objects.mjs"; import {allowsClassSpecial, isSpecialName} from "./class-policy.mjs"; import {ValueRuntime, valueLayout} from "./values.mjs"; import {isPythonIdentifier} from "./source-identifiers.mjs"; export const CLASS_LIMITS = Object.freeze({bases: 16, mro: 64, attributes: 128, name: 256}); +const NATIVE_TYPES = new Set(["type", "bool", "int", "float", "complex", "str", "list", "tuple", "dict", "range", "slice", "set", "frozenset", "bytes", "bytearray", "memoryview", + "super", "property", "staticmethod", "classmethod", "NoneType", "NotImplementedType", "function", "method", "method-wrapper", "builtin_function_or_method", "generator", "module", "cell", + "list_iterator", "tuple_iterator", "str_iterator", "dict_keyiterator", "range_iterator", "set_iterator", "bytes_iterator", "bytearray_iterator", "memory_iterator", "iterator", "callable_iterator", ...Object.keys(EXCEPTIONS)]); +const META_SPECIALS = new Set(["__new__", "__init__", "__call__", "__module__", "__qualname__", "__class__"]); export class ClassRuntime { constructor({memory = null, charge = () => {}, fail = null, values = null, builtin = null} = {}) { @@ -23,20 +27,24 @@ export class ClassRuntime { if (typeof name !== "string" || name.length > CLASS_LIMITS.name) throw new TypeError("Invalid class attribute name."); } root() { return this.make(ClassValue, "object", "object", "builtins", [], [], new Map(), true); } - create(name, qualname, module, bases, namespace, root) { + create(name, qualname, module, bases, namespace, root, {dynamic = false, metaclass = null} = {}) { this.key(name); - if (!isPythonIdentifier(name)) throw new TypeError("Invalid class name."); + if (!dynamic && !isPythonIdentifier(name)) throw new TypeError("Invalid class name."); this.bounded(typeof qualname === "string" && qualname.length <= 256 && typeof module === "string" && module.length <= 192, "Class identity budget exceeded."); if (!(root instanceof ClassValue) || !root.root || !Array.isArray(bases) || !(namespace instanceof Map)) throw new TypeError("Invalid class construction record."); this.touch(root); this.bounded(bases.length <= CLASS_LIMITS.bases && namespace.size <= CLASS_LIMITS.attributes, "Class base/namespace budget exceeded."); bases = bases.length ? [...bases] : [root]; namespace = new Map(namespace); + const metaLayout = bases.some(base => base instanceof BuiltinValue && base.name === "type" || this.isMetaclass(base)); + if (metaLayout && [...namespace.keys()].some(key => isSpecialName(key) && !META_SPECIALS.has(key))) + this.fail("UnsupportedOperationError", "Further metaclass special protocols require a separate stage", "UNSUPPORTED"); if (namespace.has("__eq__") && !namespace.has("__hash__")) namespace.set("__hash__", null); this.bounded(namespace.size <= CLASS_LIMITS.attributes, "Class namespace budget exceeded."); for (const base of bases) { this.charge(1); - if (base instanceof BuiltinValue && (["list", "tuple", "str", "bool", "type", "property", "staticmethod", "classmethod"].includes(base.name) || Object.hasOwn(EXCEPTIONS, base.name))) + if (base instanceof BuiltinValue && base.name === "type") { this.touch(base); continue; } + if (base instanceof BuiltinValue && NATIVE_TYPES.has(base.name)) this.fail("UnsupportedOperationError", "Builtin subclassing is outside classes-source/1.", "UNSUPPORTED"); if (!(base instanceof ClassValue)) this.fail("TypeError", "Class bases must be adopted class values."); this.touch(base); @@ -52,7 +60,7 @@ export class ClassRuntime { 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"); - const pending = [...bases.map(base => [...base.mro]), [...bases]], parents = []; + const pending = [...bases.map(base => [...this.mro(base)]), [...bases]], parents = []; while (pending.some(items => items.length)) { this.charge(1); let candidate = null; for (const items of pending) { @@ -68,7 +76,7 @@ export class ClassRuntime { parents.push(candidate); for (const items of pending) if (items[0] === candidate) items.shift(); } - return this.make(ClassValue, name, qualname, module, bases, parents, namespace); + return this.make(ClassValue, name, qualname, module, bases, parents, namespace, false, metaclass); } instance(type) { if (!(type instanceof ClassValue)) throw new TypeError("An adopted class is required."); @@ -77,13 +85,51 @@ export class ClassRuntime { 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)); + return type.mro.some(base => base instanceof ClassValue && !base.root && base.namespace.has(name)); + } + isType(value) { return value instanceof ClassValue || value?.constructor === BuiltinValue && NATIVE_TYPES.has(value.name); } + isMetaclass(value) { return value instanceof ClassValue && value.mro.some(base => base instanceof BuiltinValue && base.name === "type"); } + mro(type) { + if (type instanceof ClassValue) return type.mro; + if (!this.isType(type)) this.fail("TypeError", "An adopted type is required"); + const result = [type]; + let name = type.name; + while (name === "bool" || EXCEPTIONS[name]) { name = name === "bool" ? "int" : EXCEPTIONS[name]; result.push(this.builtin(name)); } + result.push(this.builtin("object")); return result; + } + typeOf(value) { + this.touch(value); + if (value instanceof InstanceValue) return value.type; + if (value instanceof ClassValue) return value.metaclass || this.builtin("type"); + let name = this.values.typeName(value); + if (name === null) { + if (this.isType(value)) name = "type"; + else if (value instanceof BuiltinValue) name = "builtin_function_or_method"; + else if (value instanceof FunctionValue) name = "function"; + else if (value instanceof BoundMethodValue) name = value.callable instanceof BuiltinValue ? "method-wrapper" : "method"; + else if (value instanceof PropertyValue) name = "property"; + else if (value instanceof MethodWrapperValue) name = value.kind; + else if (value instanceof SuperValue) name = "super"; + else if (value instanceof GeneratorValue) name = "generator"; + else if (value instanceof ModuleValue) name = "module"; + else if (value instanceof Cell) name = "cell"; + else if (value instanceof NotImplementedValue) name = "NotImplementedType"; + else if (value instanceof ProtocolIteratorValue) name = value.kind === "callable" ? "callable_iterator" : "iterator"; + else if (Object.hasOwn(EXCEPTIONS, value?.name)) name = value.name; + } + if (name === null || name === undefined) this.fail("UnsupportedOperationError", "This reflective value type requires a separate stage", "UNSUPPORTED"); + return this.builtin(name); } 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.slice(start)) { + if (!this.isType(type)) throw new TypeError("An adopted class is required."); + for (const base of this.mro(type).slice(start)) { this.charge(1); + if (base instanceof BuiltinValue) { + if (native && base.name === "type" && ["__new__", "__init__", "__call__"].includes(name)) + return {found: true, value: this.builtin("type." + name)}; + continue; + } if (!native && base.root && ["__new__", "__init__"].includes(name)) continue; if (base.namespace.has(name)) return {found: true, value: base.namespace.get(name)}; if (native && base.root && ["__hash__", "__new__", "__init__"].includes(name)) { @@ -121,11 +167,23 @@ export class ClassRuntime { 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); + const item = this.lookup(receiver.selfClass, name, true, this.mro(receiver.selfClass).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 BuiltinValue) { + if (name === "__class__") return value(this.typeOf(receiver)); + if (["__name__", "__qualname__"].includes(name)) return value(this.values.string(Array.from(receiver.name, char => char.codePointAt(0)))); + if (this.isType(receiver)) { + if (name === "__module__") return value(this.values.string(Array.from("builtins", char => char.codePointAt(0)))); + if (name === "__mro__") return value(this.values.tuple(this.mro(receiver))); + if (name === "__bases__") return value(this.values.tuple([this.mro(receiver)[1]])); + const item = this.lookup(receiver, name); + if (item.found) return this.bindAttribute(item.value, null, receiver); + } + this.fail("AttributeError", "Builtin has no adopted 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)); @@ -145,19 +203,26 @@ 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 === "__class__") return value(receiver.metaclass || 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)); } if (!allowsClassSpecial(name, "attribute")) this.fail("UnsupportedOperationError", "This special attribute is outside classes-source/1.", "UNSUPPORTED"); const owner = receiver instanceof ClassValue ? receiver : receiver.type; + const metaItem = receiver instanceof ClassValue && receiver.metaclass ? this.lookup(receiver.metaclass, name) : null; + const metaDescriptor = metaItem?.found ? this.descriptor(metaItem.value) : null; + if (metaItem?.value instanceof PropertyValue || metaDescriptor?.get.found && (metaDescriptor.set.found || metaDescriptor.delete.found)) + return this.bindAttribute(metaItem.value, receiver, receiver.metaclass); const item = this.lookup(owner, name), descriptor = item.found ? this.descriptor(item.value) : null; if (receiver instanceof InstanceValue && (item.value instanceof PropertyValue || descriptor?.get.found && (descriptor.set.found || descriptor.delete.found))) return this.bindAttribute(item.value, receiver, owner); if (receiver instanceof InstanceValue && receiver.attributes.has(name)) return value(receiver.attributes.get(name)); - if (!item.found) this.fail("AttributeError", "Class/instance has no attribute '" + name + "'."); + if (!item.found) { + if (metaItem?.found) return this.bindAttribute(metaItem.value, receiver, receiver.metaclass); + this.fail("AttributeError", "Class/instance has no attribute '" + name + "'."); + } return this.bindAttribute(item.value, receiver instanceof ClassValue ? null : receiver, owner); } bindAttribute(value, receiver, owner) { @@ -172,7 +237,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 && ["object.__hash__", "object.__init__"].includes(value.name)) ? this.make(BoundMethodValue, value, receiver) : value}; + value instanceof BuiltinValue && ["object.__hash__", "object.__init__", "type.__init__", "type.__call__"].includes(value.name)) ? this.make(BoundMethodValue, value, receiver) : value}; } special(receiver, name) { this.key(name); this.touch(receiver); @@ -191,8 +256,8 @@ 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 (receiver instanceof InstanceValue || receiver instanceof ClassValue && receiver.metaclass) { + const item = this.lookup(receiver instanceof InstanceValue ? receiver.type : receiver.metaclass, name), descriptor = item.found ? this.descriptor(item.value) : null; if (item.value instanceof PropertyValue) { const callable = item.value[remove ? "fdel" : "fset"]; if (callable === null) this.fail("AttributeError", "Property has no " + (remove ? "deleter" : "setter") + "."); @@ -211,6 +276,8 @@ export class ClassRuntime { if (!(receiver instanceof ClassValue || receiver instanceof InstanceValue)) 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 (receiver instanceof ClassValue && this.isMetaclass(receiver) && isSpecialName(name) && !META_SPECIALS.has(name)) + this.fail("UnsupportedOperationError", "Further metaclass special protocols require a separate stage", "UNSUPPORTED"); if (!allowsClassSpecial(name, "mutate")) this.fail("UnsupportedOperationError", "Special attribute mutation is outside classes-source/1.", "UNSUPPORTED"); const map = receiver instanceof ClassValue ? receiver.namespace : receiver.attributes; const present = map.has(name), before = map.get(name); @@ -221,17 +288,22 @@ export class ClassRuntime { catch (error) { if (present) map.set(name, before); else map.delete(name); throw error; } return null; } - subclass(type, parent) { - if (!(type instanceof ClassValue) || !(parent instanceof ClassValue)) this.fail("TypeError", "issubclass requires adopted class values."); + subclass(type, parent, depth = 0) { + if (this.values.isSequenceType(parent, "tuple")) { + this.bounded(depth < 32, "Class-info tuple nesting budget exceeded"); + for (const item of parent.items) { this.charge(1); if (this.subclass(type, item, depth + 1)) return true; } + return false; + } + if (!this.isType(type) || !this.isType(parent)) this.fail("TypeError", "issubclass requires adopted type values."); this.touch(type); this.touch(parent); - for (const base of type.mro) { this.charge(1); if (base === parent) return true; } + for (const base of this.mro(type)) { 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"); + if (!this.isType(type)) 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; + let owner = receiver instanceof ClassValue && this.subclass(receiver, type) ? receiver : this.typeOf(receiver); if (owner === null || !this.subclass(owner, type)) this.fail("TypeError", "super receiver must be an instance or subtype of its first argument"); return this.make(SuperValue, type, receiver, owner); } @@ -252,9 +324,29 @@ export class ClassRuntime { }); return this.make(PropertyValue, ...fields.map(name => assigned.has(name) ? assigned.get(name) : null)); } - isinstance(value, type) { - if (!(type instanceof ClassValue)) this.fail("TypeError", "isinstance requires an adopted class."); + isinstance(value, type, depth = 0) { + if (this.values.isSequenceType(type, "tuple")) { + this.bounded(depth < 32, "Class-info tuple nesting budget exceeded"); + for (const item of type.items) { this.charge(1); if (this.isinstance(value, item, depth + 1)) return true; } + return false; + } + if (!this.isType(type)) this.fail("TypeError", "isinstance requires an adopted type."); this.touch(type); this.touch(value); - return type.root || value instanceof InstanceValue && this.subclass(value.type, type); + return type.root || this.subclass(this.typeOf(value), type); + } + selectMetaclass(bases, explicit = undefined) { + let selected = explicit === undefined ? this.builtin("type") : explicit; + if (!this.callable(selected)) this.fail("TypeError", "Metaclass must be callable"); + if (!this.isType(selected)) this.fail("UnsupportedOperationError", "Callable metaclass factories require a separate construction stage", "UNSUPPORTED"); + for (const base of bases) { + this.charge(1); + const candidate = this.typeOf(base); + if (this.subclass(selected, candidate)) continue; + if (this.subclass(candidate, selected)) selected = candidate; + else this.fail("TypeError", "Metaclass conflict"); + } + if (!(selected instanceof BuiltinValue && selected.name === "type") && !this.isMetaclass(selected)) + this.fail("UnsupportedOperationError", "Non-type metaclass constructors require a separate stage", "UNSUPPORTED"); + return selected; } } diff --git a/runtime/classes-source.test.mjs b/runtime/classes-source.test.mjs index 6d5980c..e5c67b9 100644 --- a/runtime/classes-source.test.mjs +++ b/runtime/classes-source.test.mjs @@ -44,7 +44,7 @@ test('metadata overrides and later class stages refuse before source effects',as 'class A:\n from helper import value as __module__\n', 'class A:\n def __matmul__(self,x): return 7\n', 'class A: pass\na=A()\na.__class__=A\n', - 'class A(metaclass=type): pass\n','class A[T]: pass\n','class A(*bases): pass\n', + 'class A(metaclass=type,extra=1): pass\n','class A[T]: pass\n','class A(*bases): pass\n', 'async def f(): pass\n' ]){ const result=await run("print('never')\n"+source); diff --git a/runtime/construction.test.mjs b/runtime/construction.test.mjs index 0d7d95f..30802a9 100644 --- a/runtime/construction.test.mjs +++ b/runtime/construction.test.mjs @@ -4,7 +4,7 @@ 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'; +import {completion, completionRoots, constructTask, operationRoots} from './operations.mjs'; import {compileSource} from './source-compiler.mjs'; import {verifyFunctions} from './function-bytecode.mjs'; @@ -125,3 +125,72 @@ test('super descriptor failures retain source frames and shared resource limits' assert.equal(result.error.code, expected); if (expected === 'PYTHON') assert.equal(result.error.type, 'RecursionError'); closed(result.closed); } }); + +test('metaclass invocation and notification continuations retain all pending owners', () => { + const type = {}, cell = {}, descriptor = {}, argument = {}, keyword = {}, outer = {}; + const after = completion('class-created', {cell, after: completion('relay', {receiver: outer})}); + assert.deepEqual(operationRoots(constructTask(type, [argument], ['x'], [keyword], null, after)), [type, argument, keyword, cell, outer]); + assert.deepEqual(completionRoots(completion('notify', {type, entries: [['x', descriptor]], index: 0, after})), [type, descriptor, cell, outer]); +}); + +test('temporary metaclasses, class cells and pending notifications survive collection and interruption', async () => { + const sources = [ + 'def factory():\n class M(type):\n def __new__(mcls,name,bases,ns):\n print("entered")\n return type.__new__(mcls,name,bases,ns)\n return M\nclass A(metaclass=factory()):\n def f(self): return __class__\nprint(A().f() is A)\n', + 'class D:\n def __set_name__(self,owner,name):\n print("entered")\n owner.x=[7]\nclass M(type):\n def __new__(mcls,name,bases,ns): return type.__new__(mcls,name,bases,ns)\nclass A(metaclass=M):\n field=D()\n def f(self): return __class__\nprint(A().f() is A,A.x[0])\n', + 'class M(type):\n def __new__(mcls,name,bases,ns): return type.__new__(mcls,name,bases,ns)\n def __init__(cls,name,bases,ns):\n print("entered")\n cls.x=ns["saved"]\nclass A(metaclass=M): saved=[7]\nprint(A.x[0])\n', + ]; + for (let index = 0; index < sources.length; index++) { + const {registry} = await compileSourceModules([unit(sources[index])], profile); + try { for (const mode of ['finish', 'cancel', 'close']) { + const session = new ModuleSession(registry); let output = '', cancel = false, reached = false; + session.context.begin({shouldCancel: () => cancel}, text => { output += text; }); const execution = Execution.modules(session.context); + try { + while (execution.state !== 'completed') { + execution.advance(); session.memory.collect([], {cleanup: true}); + if (output === 'entered\n') { + reached = true; + if (mode === 'cancel') { cancel = true; assert.throws(() => execution.advance(), {code: 'CANCELLED'}); break; } + if (mode === 'close') break; + } + } + assert.ok(reached); if (mode === 'finish') assert.equal(output, ['entered\nTrue\n', 'entered\nTrue 7\n', 'entered\n7\n'][index]); + } finally { execution.close(); execution.close(); session.context.finish(); session.close(); } + closed(session.stats()); + } } finally { registry.close(); } + } +}); + +test('metaclass callbacks and inherited type calls obey shared limits', async () => { + for (const slot of ['__new__', '__init__', '__call__']) for (const [body, options, limits, expected] of [ + [slot === '__init__' ? 'M.__init__(cls,*args)' : 'return cls(*args)', {maxDepth: 12}, undefined, 'PYTHON'], + ['while True: pass', {maxSteps: 200}, undefined, 'BUDGET'], + ['a=[]\n while True: a.append([7])', {maxSteps: 100000, maxWork: 100000000}, {maxBytes: 30000}, 'OOM'], + ]) { + const source = 'class M(type):\n def '+slot+'(cls,*args):\n '+body+'\nclass A(metaclass=M): pass\n'+(slot === '__call__' ? 'A()\n' : ''); + const result = await run(source, options, limits); + assert.equal(result.error.code, expected, JSON.stringify(result.error)); if (expected === 'PYTHON') assert.equal(result.error.type, 'RecursionError'); closed(result.closed); + } +}); + +test('class-info tuple nesting is bounded without host recursion and remains short-circuited', async () => { + const result = await run('t=()\ni=0\nwhile i<100:\n t=(t,)\n i+=1\nprint(isinstance(7,(int,t)))\nisinstance(7,t)\n'); + assert.equal(result.stdout, 'True\n'); assert.equal(result.error.code, 'BUDGET'); closed(result.closed); +}); + +test('class-meta bytecode verifies the extra operand and cannot enter the core profile', () => { + const raw = JSON.parse(JSON.stringify(compileSource('class A(metaclass=type): pass\n', profile).program)); + assert.ok(raw.functions[0].blocks[0].instructions.some(([op]) => op === 'class-meta')); + const underflow = JSON.parse(JSON.stringify(raw)); underflow.functions[0].blocks[0].instructions.shift(); + assert.throws(() => verifyFunctions(underflow), {code: 'BYTECODE'}); + const wrongTarget = JSON.parse(JSON.stringify(raw)); wrongTarget.functions[0].blocks[0].instructions.find(([op]) => op === 'class-meta')[1] = 0; + assert.throws(() => verifyFunctions(wrongTarget), {code: 'BYTECODE'}); + assert.throws(() => compileSource('class A(metaclass=type): pass\n'), {code: 'UNSUPPORTED'}); +}); + +test('dynamic class identities and namespace keys enforce budgets as owned failures', async () => { + const name = JSON.stringify('x'.repeat(257)); + for (const source of ['type('+name+',(),{})\n', 'type("A",(),{'+name+':7})\n']) { + const result = await run(source); + assert.equal(result.error.code, 'BUDGET'); closed(result.closed); + } +}); diff --git a/runtime/function-bytecode.mjs b/runtime/function-bytecode.mjs index cc459e9..8ed9092 100644 --- a/runtime/function-bytecode.mjs +++ b/runtime/function-bytecode.mjs @@ -122,7 +122,7 @@ export function verifyFunctions(program) { ![...p.defaults, ...Object.values(p.kwdefaults)].length || [...p.defaults, ...Object.values(p.kwdefaults)].some(value => value !== null)) fail("Runtime defaults do not match the target signature."); } - else if (classes && op === "class") { + else if (classes && ["class", "class-meta"].includes(op)) { length = 3; if (!integer(arg, program.functions.length - 1) || arg === 0 || program.functions[arg]?.kind !== "class" || !integer(extra, 16)) fail("Invalid class body or base count."); } @@ -167,7 +167,7 @@ export function verifyFunctions(program) { const heights = new Map([[0, 0]]), pending = [0]; for (let cursor = 0; cursor < pending.length; cursor++) { const at = pending[cursor], [op, arg, extra] = instructions[at], before = heights.get(at); - const needs = op === "dict-insert" ? 3 : ["list-push", "list-extend", "dict-merge", "set-insert", "set-extend"].includes(op) ? 2 : ["unpack", "list-to-tuple"].includes(op) ? 1 : op === "assertion-error" ? Number(arg) : ["call-argument", "call-star", "call-mapping", "call-keyword"].includes(op) ? 2 : ["call-start", "call-expanded"].includes(op) ? 1 : op === "function-defaults" ? extra + instructions[at][3].length : op === "class" ? extra : op === "attribute-set" ? 2 : BINARY.has(op) || op === "swap" ? 2 : ["setitem", "build-slice"].includes(op) ? 3 : op === "slice" ? 4 : + const needs = op === "dict-insert" ? 3 : ["list-push", "list-extend", "dict-merge", "set-insert", "set-extend"].includes(op) ? 2 : ["unpack", "list-to-tuple"].includes(op) ? 1 : op === "assertion-error" ? Number(arg) : ["call-argument", "call-star", "call-mapping", "call-keyword"].includes(op) ? 2 : ["call-start", "call-expanded"].includes(op) ? 1 : op === "function-defaults" ? extra + instructions[at][3].length : ["class", "class-meta"].includes(op) ? extra + Number(op === "class-meta") : op === "attribute-set" ? 2 : BINARY.has(op) || op === "swap" ? 2 : ["setitem", "build-slice"].includes(op) ? 3 : op === "slice" ? 4 : op === "call" ? 1 + arg + extra.length : CONSTRUCT.has(op) ? arg * (op === "build-dict" ? 2 : 1) : UNARY.has(op) || ["yield", "yield-from", "attribute-get", "attribute-delete", "module-get", "import-from", "store", "pop", "dup", "if", "jump-if-false", "return", "raise"].includes(op) ? 1 : 0; if (before < needs) fail("Reachable function stack underflow."); @@ -175,7 +175,7 @@ export function verifyFunctions(program) { if (before !== (op === "end" ? Number(block.kind === "expression") : needs)) fail("Block completion requires an exact stack height."); continue; } - const after = before + (op === "dict-insert" ? -2 : ["list-push", "list-extend", "dict-merge", "set-insert", "set-extend"].includes(op) ? -1 : op === "assertion-error" ? 1 - needs : ["call-argument", "call-star", "call-mapping", "call-keyword"].includes(op) ? -1 : ["class", "function-defaults"].includes(op) ? 1 - needs : op === "attribute-set" ? -1 : ["const", "load", "function", "dup", "import"].includes(op) ? 1 : + const after = before + (op === "dict-insert" ? -2 : ["list-push", "list-extend", "dict-merge", "set-insert", "set-extend"].includes(op) ? -1 : op === "assertion-error" ? 1 - needs : ["call-argument", "call-star", "call-mapping", "call-keyword"].includes(op) ? -1 : ["class", "class-meta", "function-defaults"].includes(op) ? 1 - needs : op === "attribute-set" ? -1 : ["const", "load", "function", "dup", "import"].includes(op) ? 1 : BINARY.has(op) || CONSTRUCT.has(op) || ["call", "setitem", "slice", "build-slice"].includes(op) ? 1 - needs : ["store", "pop", "if", "jump-if-false"].includes(op) ? -1 : 0); if (after > FUNCTION_LIMITS.stack) fail("Function stack exceeds 256 values."); @@ -227,7 +227,7 @@ export function verifyFunctions(program) { for (const fn of functions) for (const block of fn.blocks) for (const [op, target] of block.instructions) { 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 => + if (["function", "function-defaults", "class", "class-meta"].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."); } diff --git a/runtime/function-objects.mjs b/runtime/function-objects.mjs index 860e3cf..00d9eff 100644 --- a/runtime/function-objects.mjs +++ b/runtime/function-objects.mjs @@ -37,10 +37,11 @@ export class CallArguments { constructor(callable) { this.callable = callable; this.args = []; this.keywords = []; this.keywordValues = []; this.keywordMap = null; } } export class ClassValue { - constructor(name, qualname, module, bases, parents, namespace, root = false) { + constructor(name, qualname, module, bases, parents, namespace, root = false, metaclass = null) { this.name = name; this.qualname = qualname; this.module = module; this.root = root; this.bases = Object.freeze([...bases]); this.mro = Object.freeze([this, ...parents]); this.namespace = new Map(namespace); + this.metaclass = metaclass; } } export class InstanceValue { diff --git a/runtime/functions.mjs b/runtime/functions.mjs index 8d9070a..18dc94d 100644 --- a/runtime/functions.mjs +++ b/runtime/functions.mjs @@ -79,8 +79,9 @@ function activation(program, {maxSteps = 10_000, maxWork = 1_000_000, maxDepth = hard(code, name, message); }}); const nativeBuiltin = name => { - if (moduleContext) return moduleContext.builtin(name, () => allocate(BuiltinValue, name)); - if (!state.builtins.has(name)) state.builtins.set(name, allocate(BuiltinValue, name)); + const make = () => name === "object" ? classes.root() : allocate(BuiltinValue, name); + if (moduleContext) return moduleContext.builtin(name, make); + if (!state.builtins.has(name)) state.builtins.set(name, make()); return state.builtins.get(name); }; const classes = new ClassRuntime({memory, charge, values, fail: values.fail, builtin: nativeBuiltin}); @@ -104,7 +105,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 && ["super", "getattr", "setattr", "delattr", "hasattr", "type", "issubclass", "property", "staticmethod", "classmethod", "callable", "slice", "hash"].includes(key))) { + (frame.callable.pool.code.version === 5 && ["dict", "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); @@ -194,7 +195,7 @@ function activation(program, {maxSteps = 10_000, maxWork = 1_000_000, maxDepth = }; const {dispatch, call, deliver, notifyNext, recover, argumentOwner, appendArgument, appendKeyword, expandArguments, keywordArgument, expandMapping, finishArguments, expandDisplay, expandUnpack} = operationRuntime({state, memory, values, classes, moduleContext, maxDepth, allocate, mutate, python, hard, charge, enter, bind, block, store, onOutput, - resumeGenerator: (...args) => resumeGenerator(...args), yieldGenerator: value => yieldGenerator(value), + setCell, resumeGenerator: (...args) => resumeGenerator(...args), yieldGenerator: value => yieldGenerator(value), delegateResult: (...args) => delegateResult(...args), injectGenerator: value => injectGenerator(value)}); const leaveGenerator = (frame, status) => { const generator = frame.generator, completion = frame.completion; @@ -439,10 +440,23 @@ 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); + if (completion.metaclass) state.temporaries.push(completion.metaclass); 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(); + if (completion.invalidMetaclass) python("TypeError", "Metaclass is not callable"); + if (completion.metaclass instanceof ClassValue) { + const dictionary = values.dict([]); state.temporaries.push(dictionary); + for (const [key, value] of [...namespace, ...(classCell ? [["__classcell__", classCell]] : [])]) { + const name = values.string(Array.from(key, char => char.codePointAt(0))); state.temporaries.push(name); + values.set(dictionary, name, value); + } + const name = values.string(Array.from(frame.fn.name, char => char.codePointAt(0))); state.temporaries.push(name); + const bases = values.tuple(completion.bases); state.temporaries.push(bases); + call(completion.metaclass, [name, bases, dictionary], [], [], frame.callsite, + classCell ? continuation("class-created", {cell: classCell}) : null); return; + } 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; @@ -482,18 +496,21 @@ function activation(program, {maxSteps = 10_000, maxWork = 1_000_000, maxDepth = } store(frame, arg, null, false); } - else if (op === "function" || op === "function-defaults" || op === "class") { + else if (op === "function" || op === "function-defaults" || op === "class" || op === "class-meta") { const owner = frame.callable.pool; const closure = new Map(owner.code.functions[arg].free.map(key => [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); + if (op !== "class" && op !== "class-meta") stack.push(callable); else { + const explicit = op === "class-meta" ? stack.pop() : undefined; const bases = stack.splice(stack.length - extra), name = values.text(frame.callable.globals.cells.get("__name__")?.value); state.temporaries.push(callable); if (name === null) hard("UNSUPPORTED", "UnsupportedOperationError", "Class module metadata requires text in C1."); - enter(callable, new Map(), summary(frame, cursor), continuation("class", {bases, module: name})); + const invalidMetaclass = op === "class-meta" && !classes.isType(explicit) && !classes.callable(explicit); + const metaclass = !invalidMetaclass && (op === "class-meta" || bases.some(base => base instanceof ClassValue && base.metaclass !== null)) ? classes.selectMetaclass(bases, explicit) : null; + enter(callable, new Map(), summary(frame, cursor), continuation("class", {bases, module: name, metaclass, invalidMetaclass})); store(state.frames.at(-1), "__module__", values.string(Array.from(name, char => char.codePointAt(0)))); store(state.frames.at(-1), "__qualname__", values.string(Array.from(owner.code.functions[arg].qualname, char => char.codePointAt(0)))); } @@ -533,7 +550,8 @@ function activation(program, {maxSteps = 10_000, maxWork = 1_000_000, maxDepth = inheritedException: frame.activeException}; else { const receiver = stack.pop(); - if (receiver instanceof GeneratorValue) { + if (op === "attribute-get" && arg === "__class__") stack.push(classes.typeOf(receiver)); + else if (receiver instanceof GeneratorValue) { if (!["send", "throw", "close", "__iter__", "__next__"].includes(arg)) hard("UNSUPPORTED", "UnsupportedOperationError", "Generator reflection requires a separate stage."); stack.push(allocate(ValueMethod, receiver, arg)); } else if (receiver instanceof ExceptionValue && arg === "value" && receiver.name === "StopIteration") stack.push(receiver.args[0] ?? null); @@ -544,7 +562,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 SuperValue || 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 BuiltinValue || 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 1ee8218..518872f 100644 --- a/runtime/operation-runtime.mjs +++ b/runtime/operation-runtime.mjs @@ -1,13 +1,13 @@ 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, completionRoots, callTask, unaryTask, valueTask, itemTask, sliceTask, containsTask, compareTask, dictionaryTask, dictionaryBuildTask, arithmeticTask, numericTask, consumeTask, operationRoots} from "./operations.mjs"; +import {GeneratorValue, FunctionValue, CallArguments, Cell, BuiltinValue, AppendValue, ValueMethod, ExceptionValue, EXCEPTIONS, matchesException, ClassValue, InstanceValue, BoundMethodValue, PropertyValue, MethodWrapperValue, PropertyAccessorValue, ProtocolIteratorValue, NotImplementedValue} from "./function-objects.mjs"; +import {completion as continuation, completionRoots, callTask, constructTask, unaryTask, valueTask, itemTask, sliceTask, containsTask, compareTask, dictionaryTask, dictionaryBuildTask, arithmeticTask, numericTask, consumeTask, operationRoots} from "./operations.mjs"; export class Raised { constructor(value) { this.value = value; } } // Fixed phases of the existing machine. Python callbacks enter its ordinary // frames; destinations use its block stack and single unwinder. export function operationRuntime({state, memory, values, classes, moduleContext, maxDepth, - allocate, mutate, python, hard, charge, enter, bind, block, store, onOutput, resumeGenerator, yieldGenerator, delegateResult, injectGenerator}) { + allocate, mutate, python, hard, charge, enter, bind, block, store, onOutput, setCell, resumeGenerator, yieldGenerator, delegateResult, injectGenerator}) { const count = tag => { state.transitions[tag] = (state.transitions[tag] || 0) + 1; }; const display = value => { if (value instanceof NotImplementedValue) return "NotImplemented"; @@ -87,6 +87,7 @@ export function operationRuntime({state, memory, values, classes, moduleContext, } if (moduleContext && callable.name === "isinstance") { if (args.length !== 2) python("TypeError", "isinstance expected 2 arguments, got " + args.length); + if (state.frames.at(-1).callable.pool.code.version === 5) return classes.isinstance(args[0], args[1]); const type = args[1]; if (type instanceof ClassValue) return classes.isinstance(args[0], type); if (type instanceof BuiltinValue && type.name === "memoryview") return values.isView(args[0]); @@ -105,8 +106,8 @@ export function operationRuntime({state, memory, values, classes, moduleContext, return classes.subclass(args[0], args[1]); } if (callable.name === "type") { - if (args.length !== 1 || !(args[0] instanceof InstanceValue)) hard("UNSUPPORTED", "UnsupportedOperationError", "type supports one user instance in C1."); - return args[0].type; + if (args.length !== 1) python("TypeError", "type requires one or three arguments"); + return classes.typeOf(args[0]); } if (callable.name === "str" && args.length === 0) return values.string([]); if (args.length !== 1) python("TypeError", callable.name + "() requires one argument in this profile"); @@ -337,9 +338,16 @@ export function operationRuntime({state, memory, values, classes, moduleContext, } 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); + if (!classes.subclass(classes.typeOf(value), 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 === "class-created") { + if (value instanceof ClassValue) { + if (!completion.cell.bound) python("RuntimeError", "__class__ cell was not propagated to type.__new__"); + if (completion.cell.value !== value) python("TypeError", "__class__ cell refers to a different class"); + } + return valueTask(value, site, completion.after); + } if (completion?.kind === "arithmetic-result") { if (!(value instanceof NotImplementedValue)) return valueTask(value, site, completion.after); const inplace = completion.operation.startsWith("inplace-"); @@ -552,7 +560,7 @@ export function operationRuntime({state, memory, values, classes, moduleContext, completion.after, completion.depth); if (completion?.kind === "invoke") return callTask(value, completion.args, completion.keywords, completion.keywordValues, site, completion.after, completion.depth); - if (completion?.kind === "notify") return notifyNext(completion.type, completion.entries, completion.index, site); + if (completion?.kind === "notify") return notifyNext(completion.type, completion.entries, completion.index, site, completion.after); if (completion?.kind === "condition") { const frame = state.frames.at(-1), task = frame.tasks.at(-1); if (completion.control === "if") block(frame, value ? completion.body : completion.otherwise); @@ -566,7 +574,7 @@ export function operationRuntime({state, memory, values, classes, moduleContext, state.frames.at(-1).tasks.at(-1).stack.push(completion?.kind === "discard" ? null : value); return null; }; - const notifyNext = (type, entries, index, site) => { + const notifyNext = (type, entries, index, site, after = null) => { state.temporaries.push(type, ...entries.map(entry => entry[1])); while (index < entries.length) { charge(1); @@ -577,17 +585,18 @@ export function operationRuntime({state, memory, values, classes, moduleContext, const plan = classes.notification(value, type, nameValue); if (!plan) continue; return callTask(plan.callable, plan.args, [], [], site, - continuation("notify", {type, entries, index})); + continuation("notify", {type, entries, index, after})); } - return deliver(type, null, site); + return deliver(type, after, site); }; const initialize = (instance, args, keywords, keywordValues, site, after, depth, customNew = false) => { - const initializer = classes.lookup(instance.type, "__init__", false); + const type = instance instanceof InstanceValue ? instance.type : classes.typeOf(instance); + const initializer = classes.lookup(type, "__init__", instance instanceof ClassValue); 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 plan = customNew ? classes.bindAttribute(initializer.value, 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); @@ -1098,7 +1107,15 @@ export function operationRuntime({state, memory, values, classes, moduleContext, task = deliver(allocate(PropertyValue, ...fields), completion, site); continue; } if (callable instanceof ClassValue) { - if (classes.hasSlot(callable, "__new__")) { + if (task.kind !== "construct" && callable.metaclass && classes.hasSlot(callable.metaclass, "__call__")) { + if (++depth > maxDepth) python("RecursionError", "maximum metaclass call dispatch depth exceeded"); + const item = classes.lookup(callable.metaclass, "__call__"), plan = classes.bindAttribute(item.value, callable, callable.metaclass); + const target = continuation("relay", {receiver: callable, after: completion}); + task = 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); + continue; + } + if (classes.hasSlot(callable, "__new__") || classes.isMetaclass(callable)) { 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}); @@ -1118,6 +1135,68 @@ export function operationRuntime({state, memory, values, classes, moduleContext, enter(callable, bindings, site, completion); return; } if (callable instanceof BuiltinValue) { + if (callable.name === "dict") { + if (args.length || keywords.length) hard("UNSUPPORTED", "UnsupportedOperationError", "Populated dictionary constructors require their separate sequence stage."); + task = valueTask(values.dict([]), site, completion); continue; + } + if (callable.name === "type.__call__") { + if (!args.length || !classes.isType(args[0])) python("TypeError", "type.__call__ requires a type"); + task = constructTask(args[0], args.slice(1), keywords, keywordValues, site, completion, depth); continue; + } + if (callable.name === "type.__init__") { + if (!args.length || !classes.isType(args[0]) || keywords.length || ![2, 4].includes(args.length)) + python("TypeError", "type.__init__ requires one or three arguments after its receiver"); + task = valueTask(null, site, completion); continue; + } + if (callable.name === "type" && args.length === 3) { + const target = continuation("new-result", {type: callable, args, keywords, keywordValues, after: completion, depth}); + task = callTask(classes.builtin("type.__new__"), [callable, ...args], keywords, keywordValues, site, target, depth); continue; + } + if (callable.name === "type.__new__") { + if (keywords.length) hard("UNSUPPORTED", "UnsupportedOperationError", "Class creation keywords require their separate construction stage."); + if (args.length !== 4) python("TypeError", "type.__new__ requires a metaclass, name, bases and namespace"); + let [metaclass, nameValue, basesValue, namespaceValue] = args; + if (!(metaclass instanceof BuiltinValue && metaclass.name === "type") && !classes.isMetaclass(metaclass)) python("TypeError", "type.__new__ requires a subtype of type"); + const name = values.text(nameValue); + if (name === null || !values.isSequenceType(basesValue, "tuple") || !values.isDict(namespaceValue)) python("TypeError", "type requires a string name, tuple bases and dictionary namespace"); + if (name.length > 256 || basesValue.items.length > 16) hard("BUDGET", "ExecutionLimitError", "Class identity/base budget exceeded."); + if (name.includes("\0")) python("ValueError", "type name must not contain null characters"); + const bases = basesValue.items, namespace = new Map(); + for (const entry of namespaceValue.entries) { + charge(1); const key = values.text(entry.key); + if (key === null) hard("UNSUPPORTED", "UnsupportedOperationError", "Non-string class namespace keys require a separate construction stage."); + if (key.length > 256) hard("BUDGET", "ExecutionLimitError", "Class attribute name budget exceeded."); + classes.key(key); namespace.set(key, entry.value); + } + const frame = state.frames.at(-1), currentModule = values.text(frame.callable.globals.cells.get("__name__")?.value) || "builtins"; + const module = namespace.has("__module__") ? values.text(namespace.get("__module__")) : currentModule; + const qualname = namespace.has("__qualname__") ? values.text(namespace.get("__qualname__")) : name; + if (module === null || qualname === null) python("TypeError", "Class metadata requires strings"); + const cell = namespace.get("__classcell__"); namespace.delete("__classcell__"); + if (cell !== undefined && !(cell instanceof Cell)) python("TypeError", "__classcell__ must be a cell"); + const selected = classes.selectMetaclass(bases, metaclass); + if (selected !== metaclass && selected instanceof ClassValue && classes.hasSlot(selected, "__new__")) { + if (++depth > maxDepth) python("RecursionError", "maximum metaclass construction depth exceeded"); + const item = classes.lookup(selected, "__new__"), plan = classes.bindAttribute(item.value, null, selected); + const positional = [selected, nameValue, basesValue, namespaceValue]; + task = plan.kind === "value" ? callTask(plan.value, positional, [], [], site, completion, depth) : + callTask(plan.callable, plan.args, [], [], site, continuation("invoke", {args: positional, keywords: [], keywordValues: [], after: completion, depth}), depth); + continue; + } + metaclass = selected; + const type = classes.create(name, qualname, module, bases, namespace, classes.builtin("object"), + {dynamic: true, metaclass: metaclass instanceof ClassValue ? metaclass : null}); + state.temporaries.push(type); + if (cell) setCell(cell, type); + task = notifyNext(type, [...namespace], 0, site, completion); continue; + } + if (classes.isType(callable) && !["type", "super", "property", "staticmethod", "classmethod", "slice", "dict", "bool", "int", "float", "complex", "str", "list", "tuple", "range", "set", "frozenset", "bytes", "bytearray", "memoryview", ...Object.keys(EXCEPTIONS)].includes(callable.name)) { + if (["NoneType", "NotImplementedType"].includes(callable.name)) { + if (args.length || keywords.length) python("TypeError", callable.name + " takes no arguments"); + task = valueTask(callable.name === "NoneType" ? null : moduleContext.builtin("NotImplemented", () => allocate(NotImplementedValue)), site, completion); continue; + } + hard("UNSUPPORTED", "UnsupportedOperationError", "Construction of reflected implementation types requires a separate stage."); + } if (callable.name === "super") { if (keywords.length || args.length > 2) python("TypeError", "super received invalid arguments"); let type = args[0], receiver = args[1] ?? null; @@ -1126,7 +1205,7 @@ export function operationRuntime({state, memory, values, classes, moduleContext, 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"); + if (!cell?.bound || !classes.isType(cell.value)) python("RuntimeError", "super(): missing or invalid __class__ cell"); type = cell.value; receiver = parameter.value; } task = valueTask(classes.superProxy(type, receiver), site, completion); continue; @@ -1134,6 +1213,7 @@ export function operationRuntime({state, memory, values, classes, moduleContext, if (callable.name === "object.__new__") { if (!args.length || !(args[0] instanceof ClassValue)) python("TypeError", "object.__new__ requires a type"); const type = args[0]; + if (classes.isMetaclass(type)) python("TypeError", "object.__new__ cannot allocate a type layout"); 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; diff --git a/runtime/operations.mjs b/runtime/operations.mjs index 654faa7..f0a1eb8 100644 --- a/runtime/operations.mjs +++ b/runtime/operations.mjs @@ -34,7 +34,9 @@ export function completion(kind, fields = {}) { if (height > 1024) throw new TypeError("Operation continuation bound exceeded."); let record; switch (kind) { - case "class": record = {kind, bases: operands(fields.bases), module: fields.module}; break; + case "class": record = {kind, bases: operands(fields.bases), module: fields.module, + ...(fields.metaclass ? {metaclass: fields.metaclass} : {}), ...(fields.invalidMetaclass ? {invalidMetaclass: true} : {})}; break; + case "class-created": record = {kind, cell: fields.cell, after}; 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), @@ -136,7 +138,7 @@ export function completion(kind, fields = {}) { case "delegate-bind": record = {kind, receiver: fields.receiver, args: operands(fields.args), after}; break; case "invoke": record = {kind, args: operands(fields.args), keywords: operands(fields.keywords), keywordValues: operands(fields.keywordValues), depth: fields.depth, fallback: fields.fallback ?? null, after}; break; - case "notify": record = {kind, type: fields.type, entries: Object.freeze(fields.entries.map(entry => Object.freeze([...entry]))), index: fields.index}; + case "notify": record = {kind, type: fields.type, entries: Object.freeze(fields.entries.map(entry => Object.freeze([...entry]))), index: fields.index, after}; if (record.entries.length > 128) throw new TypeError("Invalid notification snapshot."); break; case "condition": @@ -155,6 +157,7 @@ function visit(record) { 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 "class-created": return [record.cell, ...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)]; @@ -187,7 +190,7 @@ function visit(record) { case "attribute-result": return [record.receiver, record.default, ...completionRoots(record.after)]; case "delegate-bind": return [record.receiver, ...record.args, ...completionRoots(record.after)]; case "invoke": return [...record.args, ...record.keywordValues, ...completionRoots(record.after)]; - case "notify": return [record.type, ...record.entries.map(entry => entry[1])]; + case "notify": return [record.type, ...record.entries.map(entry => entry[1]), ...completionRoots(record.after)]; default: throw new TypeError("Unknown operation root owner."); } } @@ -197,7 +200,7 @@ export function completionRoots(record, frame = false) { if (!records.has(record)) throw new TypeError("Unknown operation root owner."); const retained = roots.get(record); if (!frame) return retained; - if (record.kind === "class") return record.bases; + if (record.kind === "class") return record.metaclass ? [...record.bases, record.metaclass] : record.bases; if (record.kind === "init") return [record.instance, ...retained]; return retained; } @@ -218,6 +221,9 @@ export function callTask(callable, args, keywords, keywordValues, site, after = return Object.freeze({kind: "call", callable, args: operands(args), keywords: operands(keywords), keywordValues: operands(keywordValues), site, completion: after, depth}); } +export function constructTask(callable, args, keywords, keywordValues, site, after = null, depth = 0) { + return Object.freeze({...callTask(callable, args, keywords, keywordValues, site, after, depth), kind: "construct"}); +} export function unaryTask(operation, value, site, after = null, depth = 0, optionalLength = false) { depthCheck(depth); completionRoots(after); return Object.freeze({kind: "unary", operation, value, site, completion: after, depth, optionalLength}); @@ -272,7 +278,7 @@ export function dictionaryBuildTask(receiver, parts, site, after = null, depth = } export function operationRoots(task) { switch (task.kind) { - case "call": return [task.callable, ...task.args, ...task.keywordValues, ...completionRoots(task.completion)]; + case "construct": case "call": return [task.callable, ...task.args, ...task.keywordValues, ...completionRoots(task.completion)]; case "arithmetic": case "numeric": return [...task.args, ...completionRoots(task.completion)]; case "consume": return [task.receiver, ...(task.result === null ? [] : [task.result]), ...completionRoots(task.completion)]; diff --git a/runtime/source-compiler.mjs b/runtime/source-compiler.mjs index 51d8134..16610f6 100644 --- a/runtime/source-compiler.mjs +++ b/runtime/source-compiler.mjs @@ -46,6 +46,7 @@ class Compiler { this.charge(value); if (value.kind === "Class") return {...value, bindingName: name(value.name, owner, value), bases: rewrite(value.bases, owner), body: rewrite(value.body, value.name), + ...(value.metaclass ? {metaclass: rewrite(value.metaclass, owner)} : {}), ...(value.decorators ? {decorators: rewrite(value.decorators, owner)} : {})}; if (["Function", "Lambda"].includes(value.kind)) { const params = Object.fromEntries(Object.entries(value.params).map(([key, group]) => [key, @@ -106,7 +107,7 @@ class Compiler { if (["Function", "Class", "Lambda"].includes(value.kind)) { (value.decorators || []).forEach(visit); if (value.kind !== "Lambda") binding(value.bindingName ?? value.name, value); - if (value.kind === "Class") value.bases.forEach(visit); + if (value.kind === "Class") { value.bases.forEach(visit); if (value.metaclass) visit(value.metaclass); } else for (const group of Object.values(value.params)) for (const param of group) if (param.value) visit(param.value); this.bind(value, scope); return; } @@ -434,7 +435,9 @@ class Compiler { if (value.kind === "Function") makeFunction(value); else { if (value.bases.length > 16) this.fail("Class base budget exceeded.", value, "COMPILE_BUDGET"); - value.bases.forEach(expr); emit(value, "class", this.scopeOf.get(value).index, value.bases.length); + value.bases.forEach(expr); + if (value.metaclass) expr(value.metaclass); + emit(value, value.metaclass ? "class-meta" : "class", this.scopeOf.get(value).index, value.bases.length); } if (decorators.length) { const result = this.temporary(scope, value); emit(value, "store", result); diff --git a/runtime/source-parser.mjs b/runtime/source-parser.mjs index b3b6cd1..8979a24 100644 --- a/runtime/source-parser.mjs +++ b/runtime/source-parser.mjs @@ -313,12 +313,20 @@ class Parser { } classDefinition(decorators = []) { const start = this.expect("class"), name = this.name().value, bases = []; + let metaclass = null; if (this.at("[")) this.unsupported("Generic classes are not adopted."); if (this.eat("(")) { while (!this.at(")")) { - if (this.at("*") || this.at("**") || this.kind("name") && this.peek(1).value === "=") - this.unsupported("Class keywords, metaclasses and expanded bases are not adopted."); - bases.push(this.expression()); + if (this.at("*") || this.at("**")) this.unsupported("Expanded class bases and keywords require a separate stage."); + if (this.kind("name") && this.peek(1).value === "=") { + const keyword = this.name(); this.expect("="); + if (keyword.value !== "metaclass") this.unsupported("Extra class keywords require a separate construction stage."); + if (metaclass !== null) this.fail("Duplicate metaclass keyword."); + metaclass = this.expression(); + } else { + if (metaclass !== null) this.fail("Positional class bases follow a keyword."); + bases.push(this.expression()); + } if (!this.eat(",")) break; } this.expect(")"); @@ -326,7 +334,7 @@ class Parser { const before = [this.functionDepth, this.loopDepth]; this.functionDepth = 0; this.loopDepth = 0; let body; try { body = this.suite(); } finally { [this.functionDepth, this.loopDepth] = before; } - return this.node("Class", start, {name, bases, body, ...(decorators.length ? {decorators} : {})}, body.at(-1)); + return this.node("Class", start, {name, bases, body, ...(metaclass ? {metaclass} : {}), ...(decorators.length ? {decorators} : {})}, body.at(-1)); } branch(keyword = "if") { const start = this.expect(keyword), test = this.namedExpression(), body = this.suite(); diff --git a/runtime/values.mjs b/runtime/values.mjs index aa85bd6..b8ac533 100644 --- a/runtime/values.mjs +++ b/runtime/values.mjs @@ -338,6 +338,26 @@ export class ValueRuntime { return name === "list" ? value instanceof ListValue : name === "tuple" ? value instanceof TupleValue : name === "range" && value instanceof RangeValue; } + typeName(value) { + this.charge(1); + if (value === null) return "NoneType"; + if (typeof value === "boolean") return "bool"; + if (typeof value === "bigint") return "int"; + if (value instanceof FloatValue) return "float"; + if (value instanceof ComplexValue) return "complex"; + if (value instanceof StringValue) return "str"; + if (value instanceof ListValue) return "list"; + if (value instanceof TupleValue) return "tuple"; + if (value instanceof DictValue) return "dict"; + if (value instanceof RangeValue) return "range"; + if (value instanceof SliceValue) return "slice"; + if (value instanceof SetValue) return value.frozen ? "frozenset" : "set"; + if (value instanceof BinaryValue) return value.mutable ? "bytearray" : "bytes"; + if (value instanceof MemoryViewValue) return "memoryview"; + if (value instanceof IteratorValue) return {list: "list_iterator", tuple: "tuple_iterator", str: "str_iterator", dict: "dict_keyiterator", + range: "range_iterator", set: "set_iterator", frozenset: "set_iterator", bytes: "bytes_iterator", bytearray: "bytearray_iterator", memoryview: "memory_iterator"}[this.typeName(value.value)]; + return null; + } checkConstructionHint(value) { const range = value instanceof IteratorValue && value.value instanceof RangeValue ? value.value : value; if (!(range instanceof RangeValue)) return; @@ -1258,8 +1278,8 @@ export function valueLayout(value) { children: [value.callable, ...value.args, ...value.keywordValues, ...(value.keywordMap === null ? [] : [value.keywordMap])]}; if (value instanceof FunctionValue) return {bytes: 80 + 8 * (value.closure.size + (value.defaults?.length ?? 0)), children: [value.pool, value.globals, ...value.closure.values(), ...(value.defaults ?? [])]}; if (value instanceof ClassValue) return {bytes: 128 + 2 * (value.name.length + value.qualname.length + value.module.length) + - 8 * (value.bases.length + value.mro.length) + [...value.namespace.keys()].reduce((sum, key) => sum + 32 + 2 * key.length, 0), - children: [...value.bases, ...value.mro.slice(1), ...value.namespace.values()]}; + 8 * (value.bases.length + value.mro.length + (value.metaclass === null ? 0 : 1)) + [...value.namespace.keys()].reduce((sum, key) => sum + 32 + 2 * key.length, 0), + children: [...value.bases, ...value.mro.slice(1), ...value.namespace.values(), ...(value.metaclass === null ? [] : [value.metaclass])]}; 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]}; diff --git a/src/pocket_python/_interpreter_lock.py b/src/pocket_python/_interpreter_lock.py index 6392d96..60414ef 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 = 'd0aa48046dad7db59b2b21c91bb0556807598f3550df50101af44b24b634fc65' +ENGINE_FINGERPRINT = 'ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826' ENGINE_FILES = ( ('browser-protocol.mjs', 10589, '920e8d07c7070995636dff40fdf4333afc9bd4143483db5d8dff82ec1f28ff76'), ('bytecode.mjs', 6836, 'e76fabeea879bc1da8016283f7e74df5974479666b100199c2d3c31a53b8ecf4'), ('class-policy.mjs', 2043, '38fd2e79653b9219bbf1b98672dcd36cc9846d7d0d15c27864500ea3af8a7a6d'), - ('class-values.mjs', 17126, 'd52488156e37e5321905cf037859dd1cb10648e31e740628c4e9acfd941ade67'), - ('function-bytecode.mjs', 20591, '03c89d0a19582524a88227950fc50b9928647f6b4fee55b28d8794cb4477e8b7'), - ('function-objects.mjs', 4446, '7643672838459122b6128fcdf12615df12e3dbc2d68e02fc395e1b25ea5e5108'), - ('functions.mjs', 58375, 'e01da834c2209d2904347b799ef3ac5e8b3b222acec826340eedfa0cb39b83aa'), + ('class-values.mjs', 23879, '06d53f86f5d3bfa5f0b3d244302cf23cf1b022a403bdbfd0b2c9b6b8199bd002'), + ('function-bytecode.mjs', 20693, '601e2bc41d4f5936b4eca9485d152f2962c3b4072829e7b12a9c7c5cbe888c69'), + ('function-objects.mjs', 4496, '46d9d7bdd6acedb77cbbfa4f18b3ec57dda15aaab58cb8352c98cf9544944d69'), + ('functions.mjs', 60030, 'fdd226f04ef617fe31845ae5eab34c639a74329c86bd4dfbd5993cf37040f95e'), ('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', 96811, '24fce4a6b335de34ab39aaf058394665f903db50b19b02bae4a4b05a4657f87e'), - ('operations.mjs', 23825, '4cf2afcb1d8fee89e4049c2fd176f230db17f57e6b4a4a6048b8004fedfe0684'), - ('source-compiler.mjs', 35378, '7ca4fc06290189a6740e87efca563d6fefdab10ad99b56700e9706701fada519'), + ('operation-runtime.mjs', 103962, 'b6fa15c4a30e1e17b40410615449d8aa2b853942111ec4bd70f1f55557126753'), + ('operations.mjs', 24452, '8d7045d5373de4b579da5203cf7075e8734e7e722fd56958bf026e9d7c1dc220'), + ('source-compiler.mjs', 35610, '8896968c4e0ca48bdc64821d4f0b047f6cf27e7a63e280d6cd585f598a92709b'), ('source-identifiers.mjs', 25949, '1e4e6a90728a2d9393eb4b2055d4bd759542f4768d1870b508d629c6888e8914'), ('source-modules.mjs', 5115, '3f824a8cf79a9ab78c1b817ed1597d114d851748a6b0894a4ac6b4bdbe01cfef'), - ('source-parser.mjs', 33396, '578c6663213e46049c51ba23a52427af8c301943ef67433054a6bf3fb2218ed2'), + ('source-parser.mjs', 33874, '6edfb99f92e9aa559dfec27d80e0ffeede84d6e1dcdaf904e0eefe46e29b2fd0'), ('source-tokens.mjs', 15061, 'e044276920224f8ed788f4e3445bee926698444e6cf0b9e8573bd15fec94ad54'), ('string-printability.mjs', 13287, '355a086a142e06822eff821dd0ac6a2ca70c4e06f4ec659236dc3cea1ddd639f'), - ('values.mjs', 85411, '7aeb384e833285017df398bf0bd90169473a7de84fe4719a0567a1fa5a5555e7'), + ('values.mjs', 86696, '9939130b873388651d61e7a450be83cf6f5687d65fc0322f85f5a8dc29ba5371'), ('vm.mjs', 6080, '796eaac2848ee7aa2d740797eb2afbdb3e577918866a88a9f25e5adbc55e985d'), ) ENGINE_IMPORTS = ( diff --git a/tests/fixtures/interpreter/classes.json b/tests/fixtures/interpreter/classes.json index 5c40ae9..e06a200 100644 --- a/tests/fixtures/interpreter/classes.json +++ b/tests/fixtures/interpreter/classes.json @@ -151,7 +151,7 @@ }, { "id": "metaclass", - "stage": "deferred", + "stage": "O5", "source": "class Meta(type): pass\nclass A(metaclass=Meta): pass\n" }, { @@ -246,6 +246,7 @@ "C2-decorator", "C3-length", "C3-hash", - "O4" + "O4", + "O5" ] } diff --git a/tests/fixtures/interpreter/compatibility.json b/tests/fixtures/interpreter/compatibility.json index 62d08bc..26f8cd3 100644 --- a/tests/fixtures/interpreter/compatibility.json +++ b/tests/fixtures/interpreter/compatibility.json @@ -613,16 +613,14 @@ { "id": "metaclass", "source": "class M(type): pass\nclass A(metaclass=M): pass\nprint(7)\n", - "classification": "deferred", + "classification": "supported", "reference": { "stdout": "7\n", "error_type": null }, "engine": { - "stdout": "", - "error_type": null, - "code": "UNSUPPORTED", - "phase": "parse" + "stdout": "7\n", + "error_type": null } }, { @@ -896,16 +894,14 @@ { "id": "builtin-dict", "source": "print(callable(dict))\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 dad02a9..c5a6d57 100644 --- a/tests/fixtures/interpreter/construction.json +++ b/tests/fixtures/interpreter/construction.json @@ -5,7 +5,8 @@ "reference_version": "3.14.7", "implemented_stages": [ "O3", - "O4" + "O4", + "O5" ], "cases": [ { @@ -589,6 +590,138 @@ "stage": "Future", "source": "class A:\n def __init_subclass__(cls,flag): print(flag)\nclass B(A,flag=7): pass\n", "stdout": "7\n" + }, + { + "id": "dynamic-winning-metaclass", + "stage": "O5", + "source": "class M(type):\n def __new__(mcls,name,bases,ns):\n print(\"new\",name)\n return type.__new__(mcls,name,bases,ns)\n def __init__(cls,name,bases,ns): print(\"init\",name)\nclass A(metaclass=M): pass\nB=type(\"B\",(A,),{})\nprint(type(B) is M)\n", + "stdout": "new A\ninit A\nnew B\ninit B\nTrue\n" + }, + { + "id": "native-type-new-without-init", + "stage": "O5", + "source": "class M(type):\n def __init__(cls,*args): print(\"never\")\nA=type.__new__(M,\"A\",(),{})\nprint(type(A) is M)\n", + "stdout": "True\n" + }, + { + "id": "native-type-init", + "stage": "O5", + "source": "print(type.__init__(int,7),type.__init__(object,\"O\",(),{}))\n", + "stdout": "None None\n" + }, + { + "id": "native-type-new-selection", + "stage": "O5", + "source": "class M(type): pass\nclass N(M):\n def __new__(mcls,name,bases,ns):\n print(\"chosen\",name)\n return type.__new__(mcls,name,bases,ns)\nclass A(metaclass=N): pass\nB=type.__new__(M,\"B\",(A,),{})\nprint(type(B) is N)\n", + "stdout": "chosen A\nchosen B\nTrue\n" + }, + { + "id": "metaclass-data-descriptor", + "stage": "O5", + "source": "class M(type):\n @property\n def x(cls): return cls.saved\n @x.setter\n def x(cls,value): cls.saved=value\n @x.deleter\n def x(cls): del cls.saved\nclass A(metaclass=M):\n saved=4\n x=0\nprint(A.x)\nA.x=8\nprint(A.x)\ndel A.x\nprint(hasattr(A,\"saved\"))\n", + "stdout": "4\n8\nFalse\n" + }, + { + "id": "metaclass-nondata-descriptor", + "stage": "O5", + "source": "class M(type):\n def f(cls): return 7\nclass A(metaclass=M): f=8\nprint(A.f)\ndel A.f\nprint(A.f())\n", + "stdout": "8\n7\n" + }, + { + "id": "metaclass-temporary", + "stage": "O5", + "source": "def factory():\n class M(type):\n def __new__(mcls,name,bases,ns): return type.__new__(mcls,name,bases,ns)\n return M\nclass A(metaclass=factory()):\n def f(self): return __class__\nprint(A().f() is A,type(A).__name__)\n", + "stdout": "True M\n" + }, + { + "id": "metaclass-cell-missing", + "stage": "O5", + "source": "class M(type):\n def __new__(mcls,name,bases,ns):\n del ns[\"__classcell__\"]\n return type.__new__(mcls,name,bases,ns)\nclass A(metaclass=M):\n def f(self): return __class__\n", + "stdout": "", + "error_type": "RuntimeError" + }, + { + "id": "metaclass-cell-different", + "stage": "O5", + "source": "class M(type):\n def __new__(mcls,name,bases,ns):\n other=type.__new__(mcls,\"Other\",bases,ns)\n return type.__new__(mcls,name,bases,{})\nclass A(metaclass=M):\n def f(self): return __class__\n", + "stdout": "", + "error_type": "TypeError" + }, + { + "id": "metaclass-cell-foreign-return", + "stage": "O5", + "source": "class M(type):\n def __new__(mcls,*args): return 7\nclass A(metaclass=M):\n def f(self): return __class__\nprint(A)\n", + "stdout": "7\n" + }, + { + "id": "class-cell-before-notification", + "stage": "O5", + "source": "class D:\n def __set_name__(self,owner,name): print(owner().f() is owner)\nclass A:\n x=D()\n def f(self): return __class__\n", + "stdout": "True\n" + }, + { + "id": "metaclass-cell-before-notification", + "stage": "O5", + "source": "class D:\n def __set_name__(self,owner,name): print(owner().f() is owner)\nclass M(type): pass\nclass A(metaclass=M):\n x=D()\n def f(self): return __class__\n", + "stdout": "True\n" + }, + { + "id": "metaclass-none", + "stage": "O5", + "source": "class A(metaclass=None): print(\"never\")\n", + "stdout": "never\n", + "error_type": "TypeError" + }, + { + "id": "type-singleton-constructors", + "stage": "O5", + "source": "print(type(None)() is None,type(NotImplemented)() is NotImplemented)\n", + "stdout": "True True\n" + }, + { + "id": "type-none-constructor-arguments", + "stage": "O5", + "source": "type(None)(7)\n", + "stdout": "", + "error_type": "TypeError" + }, + { + "id": "type-class-attribute", + "stage": "O5", + "source": "class M(type): pass\nclass A(metaclass=M): pass\na=A()\nprint(A.__class__ is M,a.__class__ is A,(7).__class__ is int,True.__class__ is bool)\n", + "stdout": "True True True True\n" + }, + { + "id": "dynamic-name-null", + "stage": "O5", + "source": "type(\"bad\\x00name\",(),{})\n", + "stdout": "", + "error_type": "ValueError" + }, + { + "id": "relation-error-order", + "stage": "O5", + "source": "print(issubclass(7,((),())),isinstance(7,((),int)))\n", + "stdout": "False True\n" + }, + { + "id": "object-new-metaclass-layout", + "stage": "O5", + "source": "class M(type): pass\nobject.__new__(M)\n", + "stdout": "", + "error_type": "TypeError" + }, + { + "id": "future-metaclass-equality", + "stage": "Future", + "source": "class M(type):\n def __eq__(cls,other): return True\nclass A(metaclass=M): pass\nprint(A==7)\n", + "stdout": "True\n" + }, + { + "id": "future-metaclass-factory", + "stage": "Future", + "source": "def factory(name,bases,ns): return 7\nclass A(metaclass=factory): pass\nprint(A)\n", + "stdout": "7\n" } ] } From bc7cf48d60bd2f4e7f8e7d33464a7d680f560837 Mon Sep 17 00:00:00 2001 From: ellainng <299958882+ellainng@users.noreply.github.com> Date: Mon, 26 Oct 2026 18:48:40 -0700 Subject: [PATCH 5/7] test(INT-OBJECT-01G): accept construction protocols offline --- .github/workflows/ci.yml | 4 + IMPLEMENTATION_PLAN_INTERPRETER.md | 22 + README.md | 8 +- docs/interpreter-construction.md | 22 + docs/interpreter-object-protocols.md | 6 +- .../measurements/2026-10-26-construction.json | 388 ++++++++++++++++++ examples/interpreter_construction.py | 46 +++ scripts/classes_console_browser_smoke.py | 28 +- src/pocket_python/interpreter_templates.py | 2 +- 9 files changed, 520 insertions(+), 6 deletions(-) create mode 100644 docs/measurements/2026-10-26-construction.json create mode 100644 examples/interpreter_construction.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41630f2..fc62cfd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,6 +93,8 @@ jobs: - run: uv run --no-sync python runtime/check_classes.py --target browser --corpus tests/fixtures/interpreter/generators.json --stepped --report build/generators-browser-stepped-report.json - run: uv run --no-sync python runtime/check_classes.py --target browser --corpus tests/fixtures/interpreter/generators.json --profile core-source/1 --report build/generators-core-browser-report.json - run: uv run --no-sync python runtime/check_classes.py --target browser --corpus tests/fixtures/interpreter/generators.json --profile core-source/1 --stepped --report build/generators-core-browser-stepped-report.json + - run: uv run --no-sync python runtime/check_classes.py --target browser --corpus tests/fixtures/interpreter/construction.json --report build/construction-browser-report.json + - run: uv run --no-sync python runtime/check_classes.py --target browser --corpus tests/fixtures/interpreter/construction.json --stepped --report build/construction-browser-stepped-report.json - run: uv run --no-sync python runtime/check_classes.py --target browser --corpus tests/fixtures/interpreter/object-protocols.json --report build/object-protocols-browser-report.json - run: uv run --no-sync python runtime/check_classes.py --target browser --corpus tests/fixtures/interpreter/object-protocols.json --stepped --report build/object-protocols-browser-stepped-report.json - run: uv run --no-sync python runtime/check_classes.py --target browser --report build/classes-browser-report.json @@ -199,6 +201,8 @@ jobs: - run: python runtime/check_classes.py --target node --corpus tests/fixtures/interpreter/generators.json --stepped --report build/generators-node-stepped-report.json - run: python runtime/check_classes.py --target node --corpus tests/fixtures/interpreter/generators.json --profile core-source/1 --report build/generators-core-node-report.json - run: python runtime/check_classes.py --target node --corpus tests/fixtures/interpreter/generators.json --profile core-source/1 --stepped --report build/generators-core-node-stepped-report.json + - run: python runtime/check_classes.py --target node --corpus tests/fixtures/interpreter/construction.json --report build/construction-node-report.json + - run: python runtime/check_classes.py --target node --corpus tests/fixtures/interpreter/construction.json --stepped --report build/construction-node-stepped-report.json - run: python runtime/check_classes.py --target node --corpus tests/fixtures/interpreter/object-protocols.json --report build/object-protocols-node-report.json - run: python runtime/check_classes.py --target node --corpus tests/fixtures/interpreter/object-protocols.json --stepped --report build/object-protocols-node-stepped-report.json - run: python runtime/check_classes.py --target node --report build/classes-node-report.json diff --git a/IMPLEMENTATION_PLAN_INTERPRETER.md b/IMPLEMENTATION_PLAN_INTERPRETER.md index 84b2dd7..014a0a4 100644 --- a/IMPLEMENTATION_PLAN_INTERPRETER.md +++ b/IMPLEMENTATION_PLAN_INTERPRETER.md @@ -815,3 +815,25 @@ relations, metaclasses, builtin subclasses, custom attribute hooks, dynamic source, comprehension assignment expressions and async remain separately gated. The broader #120/#123 and #33/#34 milestones remain open; this checkpoint does not select a publication, deployment, dependency or host capability. + +### Construction and type checkpoint (2026-10-26) + +The finite O3/O4/O5 [construction contract](docs/interpreter-construction.md) +adopts owned allocation/initialization, explicit and zero-argument super, +lexical class cells, canonical adopted types and tuple type relations. +Type-derived metaclasses support selection/conflicts, construction/call hooks, +descriptors, ordinary dictionary namespaces and class-cell propagation through +type.__new__. The additive class-meta opcode stays in explicit bytecode 5; +core-source/1 remains bytecode 4. + +Independent Node/Chromium draining/stepped references, collection at instruction +boundaries, callback resource failures and offline source/Stop/Restart acceptance +are required. All eight existing composed resource observations remain unchanged. +The [construction console](examples/interpreter_construction.py) composes +metaclasses and super with suspended generators. + +Builtin instance layouts, custom preparation/mappings, extra class keywords, +callable metaclass factories, additional metaclass special slots, broader +reflection, dynamic source and async remain separately tracked in #123/#34. +The parent milestones remain open. No publication, dependency, service or host +capability is selected by this construction checkpoint. diff --git a/README.md b/README.md index 32f9ecd..ad258c2 100644 --- a/README.md +++ b/README.md @@ -172,8 +172,12 @@ functions, yield-from delegation, next/send/throw/close and scoped list/dict/set generator comprehensions in both profiles. [Object protocols](docs/interpreter-object-protocols.md) add attribute reflection and forward/reflected/in-place arithmetic in the class profile. Run `python -m examples.interpreter_generators`, adding `--core` for -generators without classes. Construction, richer reflection and async retain -separate contracts. +generators without classes. The finite [construction profile](docs/interpreter-construction.md) +adds owned __new__/__init__, super and lexical __class__ cells, type relations +and type-derived metaclasses with ordinary dictionary namespaces. Run +`python -m examples.interpreter_construction` for a console composing these +protocols with suspended generators. Builtin instance layouts, custom namespace +preparation, further reflection and async retain separate contracts. The [continuing interpreter roadmap](docs/interpreter-roadmap.md) targets broad Python 3.14+ compatibility through finite supported profiles. Execution ownership diff --git a/docs/interpreter-construction.md b/docs/interpreter-construction.md index bc7db97..aaf1858 100644 --- a/docs/interpreter-construction.md +++ b/docs/interpreter-construction.md @@ -48,3 +48,25 @@ The semantic anchors are the Python 3.14 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. + +## Accepted evidence + +The 113-case matrix has 105 adopted comparisons and eight explicit future +refusals. Node and Chromium each pass draining and instruction-boundary execution: +452 observations, including 436 measured zero logical teardown records. Source, +corpus and driver hashes are checked against the retained reports. Dedicated +tests cover callback source frames, temporary metaclasses, missing/conflicting +class cells, notification destinations, suspended generators, every-instruction +collection, cancellation/close and real recursion/work/allocation failures. +All 385 Node tests pass, including the eight unchanged composed resource cases. + +Installed offline console acceptance has 487 program comparisons, 44 acceptance +groups and 98 Stop/Restart cycles. New source composes construction, super, +metaclasses and generators after offline reload. Constructor/metaclass errors +retain their Python source diagnostics; custom preparation remains a refusal. +The waiting class-to-core update still removes class/library authority. + +The [payload and source record](measurements/2026-10-26-construction.json) +contains all 38 file sizes and hashes for the 592,822-byte construction console, +including the receipt and deterministic compression observations. This is a +complete local payload measurement, with no timing, device or package claim. diff --git a/docs/interpreter-object-protocols.md b/docs/interpreter-object-protocols.md index 2541748..001eb98 100644 --- a/docs/interpreter-object-protocols.md +++ b/docs/interpreter-object-protocols.md @@ -41,5 +41,7 @@ outer-destination roots; logical teardown must be zero. The generator console composes these protocols with suspended source functions and comprehensions. Offline acceptance includes freshly entered source, callback -errors and Stop/Restart. INT-OBJECT-01 remains open for its construction, super, -type-relation, metaclass, builtin-subclass and broader reflection slices. +errors and Stop/Restart. The finite [construction, super, type-relation and +metaclass slices](interpreter-construction.md) are also adopted. INT-OBJECT-01 +remains open for builtin instance layouts, additional metaclass protocols, +custom attribute hooks and broader reflection. diff --git a/docs/measurements/2026-10-26-construction.json b/docs/measurements/2026-10-26-construction.json new file mode 100644 index 0000000..2384f60 --- /dev/null +++ b/docs/measurements/2026-10-26-construction.json @@ -0,0 +1,388 @@ +{ + "schema_version": 1, + "scope": "Finite O3/O4/O5 construction, super/class cells, type relations and metaclasses. No whole-Python, package, device or performance claim.", + "baseline_commit": "a760a5bceb16e3d210c15edc79f1fbd2594e639c", + "measurement_source_revision": "d7772617365fed866d5044e285c8841d275a64d6", + "working_tree_modified": true, + "engine_fingerprint": "ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826", + "corpus": { + "path": "tests/fixtures/interpreter/construction.json", + "sha256": "4261c46b4bf4affc28960bbb833075b7b5245e7cd125d54e4460d673cb74bf86", + "cases": 113, + "implemented_stages": [ + "O3", + "O4", + "O5" + ] + }, + "source_reports": [ + { + "path": "build/construction-node-report.json", + "sha256": "2b20b6804a7cee79d707b4b1ffe74acba527c178c031459c877169f8495cf968", + "target": "node", + "driver": "drain", + "host": "v24.19.0", + "summary": { + "pass": 113, + "fail": 0 + }, + "zero_teardown_records": 109, + "input_hashes_verified": 55 + }, + { + "path": "build/construction-node-stepped-report.json", + "sha256": "58755d9ae8c5bfc94b932d955660475a256bb9428c14f98712116f2328700950", + "target": "node", + "driver": "instruction-boundary", + "host": "v24.19.0", + "summary": { + "pass": 113, + "fail": 0 + }, + "zero_teardown_records": 109, + "input_hashes_verified": 55 + }, + { + "path": "build/construction-browser-report.json", + "sha256": "4314efe918b36d0916e4330015278e1ad224367d4d532e49d7badad56b042a41", + "target": "browser", + "driver": "drain", + "host": "156.0.8078.4", + "summary": { + "pass": 113, + "fail": 0 + }, + "zero_teardown_records": 109, + "input_hashes_verified": 55 + }, + { + "path": "build/construction-browser-stepped-report.json", + "sha256": "beb1c0f9f36757898ad4ea36bea29eabc51eceeb47b089ec253bb57ed47accdd", + "target": "browser", + "driver": "instruction-boundary", + "host": "156.0.8078.4", + "summary": { + "pass": 113, + "fail": 0 + }, + "zero_teardown_records": 109, + "input_hashes_verified": 55 + } + ], + "resource_baseline": { + "path": "tests/fixtures/interpreter/execution-resources.json", + "sha256": "1efa15cde7472dc4f3346a825a244f15cadc39cfc9b0b82f2458d9da1529ed72", + "unchanged_cases": 8 + }, + "console_acceptance": { + "script": "scripts/classes_console_browser_smoke.py", + "script_sha256": "5bbf2d6730e9b75e2741dc3478aa21f2dd9c928aa72ff16107046e4711f8506c", + "browser": "156.0.8078.4", + "programs": 487, + "groups": 44, + "stop_restart_cycles": 98, + "summary": { + "pass": 531, + "fail": 0 + } + }, + "payload": { + "example": "examples/interpreter_construction.py", + "engine": { + "backend": "javascript-vm", + "bytecode_version": 5, + "entrypoint": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/interpreter-worker.mjs", + "fingerprint": "ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826", + "implementation": "pocket-python-source", + "language_profile": "classes-source/1", + "version": "1" + }, + "sizes": { + "engine_bytes": 509482, + "bridge_bytes": 30458, + "library_bytes": 0, + "application_source_bytes": 878, + "generated_module_bytes": 0, + "helper_bytes": 0, + "source_map_bytes": 0, + "notice_bytes": 4127, + "data_bytes": 5428, + "assembly_module_bytes": 16526, + "receipt_bytes": 25923, + "complete_payload_bytes": 592822, + "gzip_file_transfer_bytes": 144491, + "canonical_tar_bytes": 624640, + "gzip_tar_bytes": 135267, + "gzip_tar_sha256": "072a05b71f489adedd4ba27e619d14fe0f9c26e4e302f4f7beda3458648af4f0" + }, + "files": [ + { + "path": "POCKET-PYTHON.json", + "sha256": "3c5a3a74cc077cf911426d41d502d6308fc74f2b1db42b16943713cb4ad80cf5", + "size_bytes": 25923, + "bucket": "receipt_bytes", + "gzip_bytes": 3526 + }, + { + "path": "app-c0e82f714f46ab4ded8fd1af1d17703e7342651c686f84af7e312cc901837f13.js", + "sha256": "c0e82f714f46ab4ded8fd1af1d17703e7342651c686f84af7e312cc901837f13", + "size_bytes": 7733, + "bucket": "assembly_module_bytes", + "gzip_bytes": 2873 + }, + { + "path": "app-config-70ea2cc667a983a5d1ff50a6639cad450961f08fb26160a57d83bc1d3ada414e.json", + "sha256": "70ea2cc667a983a5d1ff50a6639cad450961f08fb26160a57d83bc1d3ada414e", + "size_bytes": 1213, + "bucket": "data_bytes", + "gzip_bytes": 561 + }, + { + "path": "application/46b0ef1d9c68ea9c454d41806138dec64299ff1fa2864c14e4774fc2907f3c43/APPLICATION.json", + "sha256": "46b0ef1d9c68ea9c454d41806138dec64299ff1fa2864c14e4774fc2907f3c43", + "size_bytes": 1430, + "bucket": "data_bytes", + "gzip_bytes": 711 + }, + { + "path": "application/46b0ef1d9c68ea9c454d41806138dec64299ff1fa2864c14e4774fc2907f3c43/LICENSES/application.txt", + "sha256": "30989a02b7e73aa97c349440d9323d2486033a88a2b37461f3142bbb30515e85", + "size_bytes": 1066, + "bucket": "notice_bytes", + "gzip_bytes": 646 + }, + { + "path": "application/46b0ef1d9c68ea9c454d41806138dec64299ff1fa2864c14e4774fc2907f3c43/sources/app.py", + "sha256": "644216f90d60cc7df24fa4be23f208eabdaee5cea7c1d26371ac47dc1587cab9", + "size_bytes": 878, + "bucket": "application_source_bytes", + "gzip_bytes": 369 + }, + { + "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/browser-protocol.mjs", + "sha256": "920e8d07c7070995636dff40fdf4333afc9bd4143483db5d8dff82ec1f28ff76", + "size_bytes": 10589, + "bucket": "bridge_bytes", + "gzip_bytes": 3464 + }, + { + "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/bytecode.mjs", + "sha256": "e76fabeea879bc1da8016283f7e74df5974479666b100199c2d3c31a53b8ecf4", + "size_bytes": 6836, + "bucket": "engine_bytes", + "gzip_bytes": 2355 + }, + { + "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/class-policy.mjs", + "sha256": "38fd2e79653b9219bbf1b98672dcd36cc9846d7d0d15c27864500ea3af8a7a6d", + "size_bytes": 2043, + "bucket": "engine_bytes", + "gzip_bytes": 862 + }, + { + "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/class-values.mjs", + "sha256": "06d53f86f5d3bfa5f0b3d244302cf23cf1b022a403bdbfd0b2c9b6b8199bd002", + "size_bytes": 23879, + "bucket": "engine_bytes", + "gzip_bytes": 5532 + }, + { + "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/function-bytecode.mjs", + "sha256": "601e2bc41d4f5936b4eca9485d152f2962c3b4072829e7b12a9c7c5cbe888c69", + "size_bytes": 20693, + "bucket": "engine_bytes", + "gzip_bytes": 5535 + }, + { + "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/function-objects.mjs", + "sha256": "46d9d7bdd6acedb77cbbfa4f18b3ec57dda15aaab58cb8352c98cf9544944d69", + "size_bytes": 4496, + "bucket": "engine_bytes", + "gzip_bytes": 1400 + }, + { + "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/functions.mjs", + "sha256": "fdd226f04ef617fe31845ae5eab34c639a74329c86bd4dfbd5993cf37040f95e", + "size_bytes": 60030, + "bucket": "engine_bytes", + "gzip_bytes": 13836 + }, + { + "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/interpreter-host.mjs", + "sha256": "0bb0a64faffcf77e0064258e4cfcaa7a937c4372129849f76901bee91b5a5ae2", + "size_bytes": 7019, + "bucket": "bridge_bytes", + "gzip_bytes": 2417 + }, + { + "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/interpreter-worker.mjs", + "sha256": "937a97cc49d987492de0bc16021a4a804ef51e4ea5b398f79c92e5d30ac82558", + "size_bytes": 8282, + "bucket": "bridge_bytes", + "gzip_bytes": 2739 + }, + { + "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/library-catalog.mjs", + "sha256": "0adcaae0f6f07b4e1acb22e46493ec99f7247a5828b426c703530d8b2736032c", + "size_bytes": 907, + "bucket": "bridge_bytes", + "gzip_bytes": 504 + }, + { + "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/library-selection.mjs", + "sha256": "b1e2c756afef5a45a1a71fb09fa284290c8f999a1dfb854f2f536fe183119a8a", + "size_bytes": 3661, + "bucket": "bridge_bytes", + "gzip_bytes": 1469 + }, + { + "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/licenses/POCKET-PYTHON.txt", + "sha256": "30989a02b7e73aa97c349440d9323d2486033a88a2b37461f3142bbb30515e85", + "size_bytes": 1066, + "bucket": "notice_bytes", + "gzip_bytes": 646 + }, + { + "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/licenses/UNICODE.txt", + "sha256": "e7a93b009565cfce55919a381437ac4db883e9da2126fa28b91d12732bc53d96", + "size_bytes": 1995, + "bucket": "notice_bytes", + "gzip_bytes": 1063 + }, + { + "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/memory.mjs", + "sha256": "240775fe3521e88dc2f004f6a7af4f3507fb573d1dc0b7551c5b70bc91df884e", + "size_bytes": 7591, + "bucket": "engine_bytes", + "gzip_bytes": 2424 + }, + { + "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/module-context.mjs", + "sha256": "f700ecd289f51c6ae98c1c1a448d8eeebb6ea0853bf1f8c71661af549f58adca", + "size_bytes": 8319, + "bucket": "engine_bytes", + "gzip_bytes": 2697 + }, + { + "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/module-registry.mjs", + "sha256": "f23f8313c0804a052cba155e193df920b0221ef978a06f16c1a22f208dfbef91", + "size_bytes": 10085, + "bucket": "engine_bytes", + "gzip_bytes": 3403 + }, + { + "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/module-session.mjs", + "sha256": "08e4ccf16361299c15b99621fdfcef0d9ef93de50d28d738158c7bc85a7c23a3", + "size_bytes": 1470, + "bucket": "engine_bytes", + "gzip_bytes": 612 + }, + { + "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/numeric.mjs", + "sha256": "2431f099a7d40aa38d6b3c68b556207752e4191211e01005acde94325bd91bad", + "size_bytes": 13954, + "bucket": "engine_bytes", + "gzip_bytes": 4311 + }, + { + "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/operation-runtime.mjs", + "sha256": "b6fa15c4a30e1e17b40410615449d8aa2b853942111ec4bd70f1f55557126753", + "size_bytes": 103962, + "bucket": "engine_bytes", + "gzip_bytes": 18699 + }, + { + "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/operations.mjs", + "sha256": "8d7045d5373de4b579da5203cf7075e8734e7e722fd56958bf026e9d7c1dc220", + "size_bytes": 24452, + "bucket": "engine_bytes", + "gzip_bytes": 4347 + }, + { + "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/source-compiler.mjs", + "sha256": "8896968c4e0ca48bdc64821d4f0b047f6cf27e7a63e280d6cd585f598a92709b", + "size_bytes": 35610, + "bucket": "engine_bytes", + "gzip_bytes": 8263 + }, + { + "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/source-identifiers.mjs", + "sha256": "1e4e6a90728a2d9393eb4b2055d4bd759542f4768d1870b508d629c6888e8914", + "size_bytes": 25949, + "bucket": "engine_bytes", + "gzip_bytes": 6247 + }, + { + "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/source-modules.mjs", + "sha256": "3f824a8cf79a9ab78c1b817ed1597d114d851748a6b0894a4ac6b4bdbe01cfef", + "size_bytes": 5115, + "bucket": "engine_bytes", + "gzip_bytes": 1866 + }, + { + "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/source-parser.mjs", + "sha256": "6edfb99f92e9aa559dfec27d80e0ffeede84d6e1dcdaf904e0eefe46e29b2fd0", + "size_bytes": 33874, + "bucket": "engine_bytes", + "gzip_bytes": 7911 + }, + { + "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/source-tokens.mjs", + "sha256": "e044276920224f8ed788f4e3445bee926698444e6cf0b9e8573bd15fec94ad54", + "size_bytes": 15061, + "bucket": "engine_bytes", + "gzip_bytes": 4470 + }, + { + "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/string-printability.mjs", + "sha256": "355a086a142e06822eff821dd0ac6a2ca70c4e06f4ec659236dc3cea1ddd639f", + "size_bytes": 13287, + "bucket": "engine_bytes", + "gzip_bytes": 3903 + }, + { + "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/values.mjs", + "sha256": "9939130b873388651d61e7a450be83cf6f5687d65fc0322f85f5a8dc29ba5371", + "size_bytes": 86696, + "bucket": "engine_bytes", + "gzip_bytes": 18358 + }, + { + "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/vm.mjs", + "sha256": "796eaac2848ee7aa2d740797eb2afbdb3e577918866a88a9f25e5adbc55e985d", + "size_bytes": 6080, + "bucket": "engine_bytes", + "gzip_bytes": 1920 + }, + { + "path": "icon.svg", + "sha256": "860b74abf34f2b7662d477c7b216f54ba6e861ddcf28fd3b005ce787e5cf9691", + "size_bytes": 214, + "bucket": "data_bytes", + "gzip_bytes": 183 + }, + { + "path": "index.html", + "sha256": "7ce3c3f54dd9b019c8ab4e6effe88e5d7691669e0196b493e30e66c3a08fd89c", + "size_bytes": 2234, + "bucket": "data_bytes", + "gzip_bytes": 1140 + }, + { + "path": "manifest.webmanifest", + "sha256": "f0bf72f6e0317669b7ec5b37c3dea322d8737e52983560afabcc256a2106a3ee", + "size_bytes": 337, + "bucket": "data_bytes", + "gzip_bytes": 209 + }, + { + "path": "service-worker.js", + "sha256": "8d816c367244d0de40e54017c18d1bce6924d137a0406a98f78e83eb39eada7c", + "size_bytes": 8793, + "bucket": "assembly_module_bytes", + "gzip_bytes": 3020 + } + ], + "compression": "Sorted USTAR mode0644 mtime0, gzip level9 mtime0; local payload bytes only." + } +} diff --git a/examples/interpreter_construction.py b/examples/interpreter_construction.py new file mode 100644 index 0000000..68cd631 --- /dev/null +++ b/examples/interpreter_construction.py @@ -0,0 +1,46 @@ +"""Build a checkout console demonstrating construction, super and metaclasses.""" + +import argparse +from pathlib import Path + +from examples.interpreter_console import write_source +from pocket_python.interpreter import build_interpreter, read_interpreter + +ROOT = Path(__file__).resolve().parents[1] +SOURCE = '''class ModelType(type): + def __new__(meta, name, bases, namespace): + namespace["category"] = "offline" + return super().__new__(meta, name, bases, namespace) + +class Base(metaclass=ModelType): + def __new__(cls, value): + instance = super().__new__(cls) + instance.original = value + return instance + def __init__(self, value): self.value = value + +class Note(Base): + def __init__(self, value): super().__init__(value + 1) + def values(self): + yield self.original + yield self.value + yield __class__.__name__ + +note = Note(4) +print("construction", type(note) is Note, isinstance(Note, ModelType), Note.category) +stream = note.values() +print("values", next(stream), next(stream), next(stream)) +stream.close() +Temporary = type("Temporary", (Base,), {}) +print("dynamic", type(Temporary) is ModelType, Temporary.category) +''' + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--source', type=Path, default=Path('build/construction-console-source')) + parser.add_argument('--output', type=Path, default=Path('build/construction-console')) + args = parser.parse_args() + receipt = build_interpreter(write_source(args.source, source=SOURCE), + read_interpreter(ROOT / 'runtime'), args.output, + language_profile='classes-source/1', title='Python Construction Console') + print(f"Created {args.output.resolve()} ({receipt['size']['artifact_bytes']} bytes)") diff --git a/scripts/classes_console_browser_smoke.py b/scripts/classes_console_browser_smoke.py index e252ff1..8fbe205 100644 --- a/scripts/classes_console_browser_smoke.py +++ b/scripts/classes_console_browser_smoke.py @@ -24,6 +24,7 @@ from examples.interpreter_syntax import SOURCE as SYNTAX_SOURCE, CORE_SOURCE as CORE_SYNTAX_SOURCE from examples.interpreter_values import SOURCE as VALUES_SOURCE, CORE_SOURCE as CORE_VALUES_SOURCE from examples.interpreter_generators import SOURCE as GENERATOR_SOURCE, CORE_SOURCE as GENERATOR_CORE_SOURCE +from examples.interpreter_construction import SOURCE as CONSTRUCTION_SOURCE def smoke(channel=None): @@ -77,6 +78,7 @@ def refused(source, prefix): assert 'iteration and containment' in page.locator('#language').inner_text() assert 'equality and hashing' in page.locator('#language').inner_text() assert 'sets, bytes, bytearrays and memoryviews' in page.locator('#language').inner_text() + assert 'construction, super and finite metaclasses' in page.locator('#language').inner_text() assert page.locator('#input').input_value() == SOURCE checks.append('explicit-class-profile-ready-and-initial-source') corpus = json.loads((ROOT / 'tests/fixtures/interpreter/classes.json').read_bytes()) @@ -155,10 +157,12 @@ def refused(source, prefix): programs.append(VALUES_SOURCE) generators = json.loads((ROOT / 'tests/fixtures/interpreter/generators.json').read_bytes()) objects = json.loads((ROOT / 'tests/fixtures/interpreter/object-protocols.json').read_bytes()) - for corpus in [generators, objects]: + construction = json.loads((ROOT / 'tests/fixtures/interpreter/construction.json').read_bytes()) + for corpus in [generators, objects, construction]: programs.extend(c['source'] for c in corpus['cases'] if c['stage'] in corpus['implemented_stages'] and 'error_type' not in c) programs.append(GENERATOR_SOURCE) + programs.append(CONSTRUCTION_SOURCE) for program in programs: expected = subprocess.check_output([sys.executable, '-I', '-S', '-X', 'utf8', '-c', program], text=True, encoding='utf-8', timeout=5) @@ -353,6 +357,18 @@ def refused(source, prefix): refused('class A:\n @property\n def x(self): raise ValueError("body")\nhasattr(A(),"x")\n', 'ValueError:') refused('class A:\n def __add__(self,x): raise KeyError("body")\nA()+1\n', 'KeyError:') checks.append('generator-and-object-body-errors-retain-offline-diagnostics') + run(CONSTRUCTION_SOURCE, 'construction True True offline\nvalues 4 5 Note\ndynamic True offline\n') + run('class OfflineBase:\n def __new__(cls,x): return super().__new__(cls)\n def __init__(self,x): self.x=x\nclass OfflineChild(OfflineBase):\n def __init__(self,x): super().__init__(x+1)\n def values(self):\n yield self.x\n yield __class__.__name__\na=OfflineChild(7)\ng=a.values()\nprint(next(g),next(g),isinstance(a,(str,OfflineBase)),issubclass(OfflineChild,OfflineBase))\n', '8 OfflineChild True True\n') + checks.append('new-construction-super-metaclass-and-generator-source-offline') + refused('class A:\n def __new__(cls): raise ValueError("new")\nA()\n', 'ValueError:') + assert '__new__ at sources/app.py:2' in page.locator('#diagnostic').inner_text() + refused('class A:\n def __init__(self): return 7\nA()\n', 'TypeError:') + refused('class M(type):\n def __new__(meta,name,bases,ns): raise KeyError("meta")\nclass A(metaclass=M): pass\n', 'KeyError:') + assert '__new__ at sources/app.py:2' in page.locator('#diagnostic').inner_text() + refused('class M(type):\n def __new__(meta,name,bases,ns):\n del ns["__classcell__"]\n return type.__new__(meta,name,bases,ns)\nclass A(metaclass=M):\n def f(self): return __class__\n', 'RuntimeError:') + refused('class A:\n def f(self): return super().missing\nA().f()\n', 'AttributeError:') + refused('class M(type):\n @classmethod\n def __prepare__(meta,name,bases): return {}\n', 'UnsupportedSyntaxError:') + checks.append('constructor-super-metaclass-errors-and-future-hooks-retain-offline-diagnostics') busy = [ 'class Spin:\n def __call__(self):\n while True: pass\nSpin()()\n', 'class Spin:\n @property\n def __call__(self):\n while True: pass\nSpin()()\n', @@ -445,6 +461,16 @@ def refused(source, prefix): 'class Spin:\n @property\n def __radd__(self):\n while True: pass\n[7]+Spin()\n', 'class Spin:\n @property\n def x(self):\n while True: pass\ngetattr(Spin(),"x",[7])\n' ]) + busy.extend([ + 'class Spin:\n def __new__(cls):\n while True: pass\nSpin()\n', + 'class D:\n def __get__(self,obj,owner):\n while True: pass\nclass Spin: __new__=D()\nSpin()\n', + 'class Spin:\n def __new__(cls): return object.__new__(cls)\n def __init__(self):\n while True: pass\nSpin()\n', + 'class Base:\n @property\n def value(self):\n while True: pass\nclass Spin(Base):\n def read(self): return super().value\nSpin().read()\n', + 'class M(type):\n def __new__(meta,name,bases,ns):\n while True: pass\nclass Spin(metaclass=M): pass\n', + 'class M(type):\n def __init__(cls,*args):\n while True: pass\nclass Spin(metaclass=M): pass\n', + 'class M(type):\n def __call__(cls):\n while True: pass\nclass Spin(metaclass=M): pass\nSpin()\n', + 'class D:\n def __set_name__(self,owner,name):\n while True: pass\nclass M(type): pass\nclass Spin(metaclass=M): value=D()\n' + ]) for cycle in range(len(busy)): page.fill('#input', busy[cycle % len(busy)]) page.click('#run') diff --git a/src/pocket_python/interpreter_templates.py b/src/pocket_python/interpreter_templates.py index b11494a..2bd9944 100644 --- a/src/pocket_python/interpreter_templates.py +++ b/src/pocket_python/interpreter_templates.py @@ -77,7 +77,7 @@ prepareOffline().catch(error=>{byId('offline').textContent='Offline unavailable: '+error.message;}); try{ config=JSON.parse(await verified('./__CONFIG__','__CONFIG_SHA__',65536)); - const descriptions={'classes-source/1':'Language profile: classes-source/1. Supports the core subset plus classes, inheritance, attributes, methods, private names, descriptors, properties, decorators, callable objects, length/truth, repr/str, integer indices and item/slice access and mutation, iteration and containment, equality and hashing. Ranges, list/tuple construction and all/any support adopted user protocols; user length hints are a later stage. Functions support computed/mutable defaults, global/nonlocal state, variadic parameters and starred calls. Assertions, assignment expressions, lambdas, unpacking targets and starred sequence/dictionary displays are available. Finite sets, bytes, bytearrays and memoryviews, with strict UTF-8/ASCII/Latin-1 codecs, are available in both profiles. Bytearray resizing, buffer casts and native buffers are later stages. Numeric operators, helpers and real/complex values are available in both profiles; numeric conversion and abs/round hooks use class type lookup. Generators, yield from, send/throw/close and scoped comprehensions are available. Attribute reflection and arithmetic/reflected/in-place hooks use ordinary class descriptors. Float/container/default-object formatting, ordering protocols and async are later stages.', + const descriptions={'classes-source/1':'Language profile: classes-source/1. Supports the core subset plus classes, inheritance, attributes, methods, private names, descriptors, properties, decorators, callable objects, length/truth, repr/str, integer indices and item/slice access and mutation, iteration and containment, equality and hashing. Ranges, list/tuple construction and all/any support adopted user protocols; user length hints are a later stage. Functions support computed/mutable defaults, global/nonlocal state, variadic parameters and starred calls. Assertions, assignment expressions, lambdas, unpacking targets and starred sequence/dictionary displays are available. Finite sets, bytes, bytearrays and memoryviews, with strict UTF-8/ASCII/Latin-1 codecs, are available in both profiles. Bytearray resizing, buffer casts and native buffers are later stages. Numeric operators, helpers and real/complex values are available in both profiles; numeric conversion and abs/round hooks use class type lookup. Generators, yield from, send/throw/close and scoped comprehensions are available. Attribute reflection and arithmetic/reflected/in-place hooks use ordinary class descriptors. Owned construction, super and finite metaclasses with ordinary dictionary namespaces are available. Type relations accept nested tuples of adopted types. Builtin instance subclasses and custom class namespace preparation are later stages. Float/container/default-object formatting, ordering protocols and async are later stages.', 'core-source/1':'Language profile: core-source/1. Supports values, loops, functions with computed/mutable defaults, global/nonlocal state, variadic parameters and starred calls, assertions, assignment expressions, lambdas, unpacking targets and starred sequence/dictionary displays, exceptions, primitive repr/str, ranges, primitive list/tuple construction, all/any, numeric operators/helpers and real/complex values. Finite sets, bytes, bytearrays and memoryviews, with strict UTF-8/ASCII/Latin-1 codecs, are available. Bytearray resizing, buffer casts and native buffers are later stages. Generators, yield from, send/throw/close and scoped comprehensions are available. Classes require the explicit class profile; async and external packages are later stages.'}; if(!Object.hasOwn(descriptions,config.application.engine.language_profile))throw new Error('Source language profile is unavailable.'); byId('language').textContent=descriptions[config.application.engine.language_profile]; From c2c6ec51f7204c033b74506c86ce43c866eb2a04 Mon Sep 17 00:00:00 2001 From: ellainng <299958882+ellainng@users.noreply.github.com> Date: Mon, 26 Oct 2026 18:50:58 -0700 Subject: [PATCH 6/7] test(INT-OBJECT-01G): run construction resource regressions in CI --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc62cfd..46cadae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -167,6 +167,7 @@ jobs: - run: node --test runtime/syntax.test.mjs - run: node --test runtime/function-binding.test.mjs - run: node --test runtime/object-protocols.test.mjs + - run: node --test runtime/construction.test.mjs - run: node --test runtime/generators.test.mjs - run: node --test runtime/functions.test.mjs - run: node --test runtime/execution.test.mjs From 68766a8b8a0c6821d36af06581217fa1b0754bf0 Mon Sep 17 00:00:00 2001 From: ellainng <299958882+ellainng@users.noreply.github.com> Date: Mon, 26 Oct 2026 18:57:54 -0700 Subject: [PATCH 7/7] fix(INT-OBJECT-01H): refuse deferred advertised class lookup --- docs/interpreter-construction.md | 15 +- .../measurements/2026-10-26-construction.json | 156 +++++++++--------- runtime/class-values.mjs | 18 +- runtime/construction.test.mjs | 13 ++ runtime/functions.mjs | 2 +- scripts/classes_console_browser_smoke.py | 5 + src/pocket_python/_interpreter_lock.py | 6 +- tests/fixtures/interpreter/construction.json | 30 ++++ 8 files changed, 155 insertions(+), 90 deletions(-) diff --git a/docs/interpreter-construction.md b/docs/interpreter-construction.md index aaf1858..ce6a2b8 100644 --- a/docs/interpreter-construction.md +++ b/docs/interpreter-construction.md @@ -51,22 +51,27 @@ documentation differs. ## Accepted evidence -The 113-case matrix has 105 adopted comparisons and eight explicit future +The 118-case matrix has 106 adopted comparisons and twelve explicit future refusals. Node and Chromium each pass draining and instruction-boundary execution: -452 observations, including 436 measured zero logical teardown records. Source, +472 observations, including 456 measured zero logical teardown records. Source, corpus and driver hashes are checked against the retained reports. Dedicated tests cover callback source frames, temporary metaclasses, missing/conflicting class cells, notification destinations, suspended generators, every-instruction collection, cancellation/close and real recursion/work/allocation failures. -All 385 Node tests pass, including the eight unchanged composed resource cases. +All 387 Node tests pass, including the eight unchanged composed resource cases. -Installed offline console acceptance has 487 program comparisons, 44 acceptance +Installed offline console acceptance has 488 program comparisons, 45 acceptance groups and 98 Stop/Restart cycles. New source composes construction, super, metaclasses and generators after offline reload. Constructor/metaclass errors retain their Python source diagnostics; custom preparation remains a refusal. The waiting class-to-core update still removes class/library authority. The [payload and source record](measurements/2026-10-26-construction.json) -contains all 38 file sizes and hashes for the 592,822-byte construction console, +contains all 38 file sizes and hashes for the 593,610-byte construction console, including the receipt and deterministic compression observations. This is a complete local payload measurement, with no timing, device or package claim. + +Advertised `__class__` identities remain an explicit reflection boundary. Type +queries and super refuse a required advertised-class lookup before callback +effects. `type(value)`, successful actual-type checks and real-subtype super +binding continue without consulting these hooks. diff --git a/docs/measurements/2026-10-26-construction.json b/docs/measurements/2026-10-26-construction.json index 2384f60..bfce099 100644 --- a/docs/measurements/2026-10-26-construction.json +++ b/docs/measurements/2026-10-26-construction.json @@ -2,13 +2,13 @@ "schema_version": 1, "scope": "Finite O3/O4/O5 construction, super/class cells, type relations and metaclasses. No whole-Python, package, device or performance claim.", "baseline_commit": "a760a5bceb16e3d210c15edc79f1fbd2594e639c", - "measurement_source_revision": "d7772617365fed866d5044e285c8841d275a64d6", + "measurement_source_revision": "c2c6ec51f7204c033b74506c86ce43c866eb2a04", "working_tree_modified": true, - "engine_fingerprint": "ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826", + "engine_fingerprint": "5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490", "corpus": { "path": "tests/fixtures/interpreter/construction.json", - "sha256": "4261c46b4bf4affc28960bbb833075b7b5245e7cd125d54e4460d673cb74bf86", - "cases": 113, + "sha256": "a4c2774783a01a6ae0e7082fe037bcc14ce82bd669a24189787dd83bc7e43a46", + "cases": 118, "implemented_stages": [ "O3", "O4", @@ -18,54 +18,54 @@ "source_reports": [ { "path": "build/construction-node-report.json", - "sha256": "2b20b6804a7cee79d707b4b1ffe74acba527c178c031459c877169f8495cf968", + "sha256": "6a303ab128f92a3e644f032e27f5630fcefc5f0df189b1c8717b08f8018a3367", "target": "node", "driver": "drain", "host": "v24.19.0", "summary": { - "pass": 113, + "pass": 118, "fail": 0 }, - "zero_teardown_records": 109, + "zero_teardown_records": 114, "input_hashes_verified": 55 }, { "path": "build/construction-node-stepped-report.json", - "sha256": "58755d9ae8c5bfc94b932d955660475a256bb9428c14f98712116f2328700950", + "sha256": "53a4f90976a2057283e4bf0f365651c6cfc0b3eabb27763ae37308d5056e4663", "target": "node", "driver": "instruction-boundary", "host": "v24.19.0", "summary": { - "pass": 113, + "pass": 118, "fail": 0 }, - "zero_teardown_records": 109, + "zero_teardown_records": 114, "input_hashes_verified": 55 }, { "path": "build/construction-browser-report.json", - "sha256": "4314efe918b36d0916e4330015278e1ad224367d4d532e49d7badad56b042a41", + "sha256": "fb59315e8e3a4ac0fe01e313c18de309328b5b598950993bf22b9825e632e06c", "target": "browser", "driver": "drain", "host": "156.0.8078.4", "summary": { - "pass": 113, + "pass": 118, "fail": 0 }, - "zero_teardown_records": 109, + "zero_teardown_records": 114, "input_hashes_verified": 55 }, { "path": "build/construction-browser-stepped-report.json", - "sha256": "beb1c0f9f36757898ad4ea36bea29eabc51eceeb47b089ec253bb57ed47accdd", + "sha256": "dfc5286f9c42f5122ff2e42393641ab86a28aa083dc7592674da576ea55db09d", "target": "browser", "driver": "instruction-boundary", "host": "156.0.8078.4", "summary": { - "pass": 113, + "pass": 118, "fail": 0 }, - "zero_teardown_records": 109, + "zero_teardown_records": 114, "input_hashes_verified": 55 } ], @@ -76,13 +76,13 @@ }, "console_acceptance": { "script": "scripts/classes_console_browser_smoke.py", - "script_sha256": "5bbf2d6730e9b75e2741dc3478aa21f2dd9c928aa72ff16107046e4711f8506c", + "script_sha256": "f758de099ba4c2e0cb1eb7a1f06cd27430d8c7c92a21d243f4449a39d97ade44", "browser": "156.0.8078.4", - "programs": 487, - "groups": 44, + "programs": 488, + "groups": 45, "stop_restart_cycles": 98, "summary": { - "pass": 531, + "pass": 533, "fail": 0 } }, @@ -91,14 +91,14 @@ "engine": { "backend": "javascript-vm", "bytecode_version": 5, - "entrypoint": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/interpreter-worker.mjs", - "fingerprint": "ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826", + "entrypoint": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/interpreter-worker.mjs", + "fingerprint": "5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490", "implementation": "pocket-python-source", "language_profile": "classes-source/1", "version": "1" }, "sizes": { - "engine_bytes": 509482, + "engine_bytes": 510270, "bridge_bytes": 30458, "library_bytes": 0, "application_source_bytes": 878, @@ -109,246 +109,246 @@ "data_bytes": 5428, "assembly_module_bytes": 16526, "receipt_bytes": 25923, - "complete_payload_bytes": 592822, - "gzip_file_transfer_bytes": 144491, + "complete_payload_bytes": 593610, + "gzip_file_transfer_bytes": 144555, "canonical_tar_bytes": 624640, - "gzip_tar_bytes": 135267, - "gzip_tar_sha256": "072a05b71f489adedd4ba27e619d14fe0f9c26e4e302f4f7beda3458648af4f0" + "gzip_tar_bytes": 135340, + "gzip_tar_sha256": "8d6b57249c8d080c315cfeb3fa0dac484164fcb96f066e159e92cf35bc23104b" }, "files": [ { "path": "POCKET-PYTHON.json", - "sha256": "3c5a3a74cc077cf911426d41d502d6308fc74f2b1db42b16943713cb4ad80cf5", + "sha256": "08831fa92733437ecd87db91ba89300d04bd4b0ac9e93227178c7b81bf8a1b07", "size_bytes": 25923, "bucket": "receipt_bytes", - "gzip_bytes": 3526 + "gzip_bytes": 3514 }, { - "path": "app-c0e82f714f46ab4ded8fd1af1d17703e7342651c686f84af7e312cc901837f13.js", - "sha256": "c0e82f714f46ab4ded8fd1af1d17703e7342651c686f84af7e312cc901837f13", + "path": "app-2ba1e350815e96dedcaa7f2c2f9a5be86e18f0b56d501827e9360d0084d0449f.js", + "sha256": "2ba1e350815e96dedcaa7f2c2f9a5be86e18f0b56d501827e9360d0084d0449f", "size_bytes": 7733, "bucket": "assembly_module_bytes", - "gzip_bytes": 2873 + "gzip_bytes": 2875 }, { - "path": "app-config-70ea2cc667a983a5d1ff50a6639cad450961f08fb26160a57d83bc1d3ada414e.json", - "sha256": "70ea2cc667a983a5d1ff50a6639cad450961f08fb26160a57d83bc1d3ada414e", + "path": "app-config-19d75f78bc92626d7de0e5ce8fe4583011f428369e2fcd7f12b552ba1151acaf.json", + "sha256": "19d75f78bc92626d7de0e5ce8fe4583011f428369e2fcd7f12b552ba1151acaf", "size_bytes": 1213, "bucket": "data_bytes", - "gzip_bytes": 561 + "gzip_bytes": 560 }, { - "path": "application/46b0ef1d9c68ea9c454d41806138dec64299ff1fa2864c14e4774fc2907f3c43/APPLICATION.json", - "sha256": "46b0ef1d9c68ea9c454d41806138dec64299ff1fa2864c14e4774fc2907f3c43", + "path": "application/399aca0b12ef5bb0c6d401b7b915532d9c127594c58eaf21bc84d4242d77ceb9/APPLICATION.json", + "sha256": "399aca0b12ef5bb0c6d401b7b915532d9c127594c58eaf21bc84d4242d77ceb9", "size_bytes": 1430, "bucket": "data_bytes", - "gzip_bytes": 711 + "gzip_bytes": 712 }, { - "path": "application/46b0ef1d9c68ea9c454d41806138dec64299ff1fa2864c14e4774fc2907f3c43/LICENSES/application.txt", + "path": "application/399aca0b12ef5bb0c6d401b7b915532d9c127594c58eaf21bc84d4242d77ceb9/LICENSES/application.txt", "sha256": "30989a02b7e73aa97c349440d9323d2486033a88a2b37461f3142bbb30515e85", "size_bytes": 1066, "bucket": "notice_bytes", "gzip_bytes": 646 }, { - "path": "application/46b0ef1d9c68ea9c454d41806138dec64299ff1fa2864c14e4774fc2907f3c43/sources/app.py", + "path": "application/399aca0b12ef5bb0c6d401b7b915532d9c127594c58eaf21bc84d4242d77ceb9/sources/app.py", "sha256": "644216f90d60cc7df24fa4be23f208eabdaee5cea7c1d26371ac47dc1587cab9", "size_bytes": 878, "bucket": "application_source_bytes", "gzip_bytes": 369 }, { - "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/browser-protocol.mjs", + "path": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/browser-protocol.mjs", "sha256": "920e8d07c7070995636dff40fdf4333afc9bd4143483db5d8dff82ec1f28ff76", "size_bytes": 10589, "bucket": "bridge_bytes", "gzip_bytes": 3464 }, { - "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/bytecode.mjs", + "path": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/bytecode.mjs", "sha256": "e76fabeea879bc1da8016283f7e74df5974479666b100199c2d3c31a53b8ecf4", "size_bytes": 6836, "bucket": "engine_bytes", "gzip_bytes": 2355 }, { - "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/class-policy.mjs", + "path": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/class-policy.mjs", "sha256": "38fd2e79653b9219bbf1b98672dcd36cc9846d7d0d15c27864500ea3af8a7a6d", "size_bytes": 2043, "bucket": "engine_bytes", "gzip_bytes": 862 }, { - "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/class-values.mjs", - "sha256": "06d53f86f5d3bfa5f0b3d244302cf23cf1b022a403bdbfd0b2c9b6b8199bd002", - "size_bytes": 23879, + "path": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/class-values.mjs", + "sha256": "47bc0508b412f95e0325cbcd30ec2d21f3b6b1bd538f9df82529f6c19b9f7002", + "size_bytes": 24659, "bucket": "engine_bytes", - "gzip_bytes": 5532 + "gzip_bytes": 5618 }, { - "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/function-bytecode.mjs", + "path": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/function-bytecode.mjs", "sha256": "601e2bc41d4f5936b4eca9485d152f2962c3b4072829e7b12a9c7c5cbe888c69", "size_bytes": 20693, "bucket": "engine_bytes", "gzip_bytes": 5535 }, { - "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/function-objects.mjs", + "path": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/function-objects.mjs", "sha256": "46d9d7bdd6acedb77cbbfa4f18b3ec57dda15aaab58cb8352c98cf9544944d69", "size_bytes": 4496, "bucket": "engine_bytes", "gzip_bytes": 1400 }, { - "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/functions.mjs", - "sha256": "fdd226f04ef617fe31845ae5eab34c639a74329c86bd4dfbd5993cf37040f95e", - "size_bytes": 60030, + "path": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/functions.mjs", + "sha256": "35cff7e041738b20a27aafedf4c04797da257bc3f6f5bb8b3135c940376cff1b", + "size_bytes": 60038, "bucket": "engine_bytes", - "gzip_bytes": 13836 + "gzip_bytes": 13835 }, { - "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/interpreter-host.mjs", + "path": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/interpreter-host.mjs", "sha256": "0bb0a64faffcf77e0064258e4cfcaa7a937c4372129849f76901bee91b5a5ae2", "size_bytes": 7019, "bucket": "bridge_bytes", "gzip_bytes": 2417 }, { - "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/interpreter-worker.mjs", + "path": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/interpreter-worker.mjs", "sha256": "937a97cc49d987492de0bc16021a4a804ef51e4ea5b398f79c92e5d30ac82558", "size_bytes": 8282, "bucket": "bridge_bytes", "gzip_bytes": 2739 }, { - "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/library-catalog.mjs", + "path": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/library-catalog.mjs", "sha256": "0adcaae0f6f07b4e1acb22e46493ec99f7247a5828b426c703530d8b2736032c", "size_bytes": 907, "bucket": "bridge_bytes", "gzip_bytes": 504 }, { - "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/library-selection.mjs", + "path": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/library-selection.mjs", "sha256": "b1e2c756afef5a45a1a71fb09fa284290c8f999a1dfb854f2f536fe183119a8a", "size_bytes": 3661, "bucket": "bridge_bytes", "gzip_bytes": 1469 }, { - "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/licenses/POCKET-PYTHON.txt", + "path": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/licenses/POCKET-PYTHON.txt", "sha256": "30989a02b7e73aa97c349440d9323d2486033a88a2b37461f3142bbb30515e85", "size_bytes": 1066, "bucket": "notice_bytes", "gzip_bytes": 646 }, { - "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/licenses/UNICODE.txt", + "path": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/licenses/UNICODE.txt", "sha256": "e7a93b009565cfce55919a381437ac4db883e9da2126fa28b91d12732bc53d96", "size_bytes": 1995, "bucket": "notice_bytes", "gzip_bytes": 1063 }, { - "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/memory.mjs", + "path": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/memory.mjs", "sha256": "240775fe3521e88dc2f004f6a7af4f3507fb573d1dc0b7551c5b70bc91df884e", "size_bytes": 7591, "bucket": "engine_bytes", "gzip_bytes": 2424 }, { - "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/module-context.mjs", + "path": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/module-context.mjs", "sha256": "f700ecd289f51c6ae98c1c1a448d8eeebb6ea0853bf1f8c71661af549f58adca", "size_bytes": 8319, "bucket": "engine_bytes", "gzip_bytes": 2697 }, { - "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/module-registry.mjs", + "path": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/module-registry.mjs", "sha256": "f23f8313c0804a052cba155e193df920b0221ef978a06f16c1a22f208dfbef91", "size_bytes": 10085, "bucket": "engine_bytes", "gzip_bytes": 3403 }, { - "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/module-session.mjs", + "path": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/module-session.mjs", "sha256": "08e4ccf16361299c15b99621fdfcef0d9ef93de50d28d738158c7bc85a7c23a3", "size_bytes": 1470, "bucket": "engine_bytes", "gzip_bytes": 612 }, { - "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/numeric.mjs", + "path": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/numeric.mjs", "sha256": "2431f099a7d40aa38d6b3c68b556207752e4191211e01005acde94325bd91bad", "size_bytes": 13954, "bucket": "engine_bytes", "gzip_bytes": 4311 }, { - "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/operation-runtime.mjs", + "path": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/operation-runtime.mjs", "sha256": "b6fa15c4a30e1e17b40410615449d8aa2b853942111ec4bd70f1f55557126753", "size_bytes": 103962, "bucket": "engine_bytes", "gzip_bytes": 18699 }, { - "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/operations.mjs", + "path": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/operations.mjs", "sha256": "8d7045d5373de4b579da5203cf7075e8734e7e722fd56958bf026e9d7c1dc220", "size_bytes": 24452, "bucket": "engine_bytes", "gzip_bytes": 4347 }, { - "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/source-compiler.mjs", + "path": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/source-compiler.mjs", "sha256": "8896968c4e0ca48bdc64821d4f0b047f6cf27e7a63e280d6cd585f598a92709b", "size_bytes": 35610, "bucket": "engine_bytes", "gzip_bytes": 8263 }, { - "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/source-identifiers.mjs", + "path": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/source-identifiers.mjs", "sha256": "1e4e6a90728a2d9393eb4b2055d4bd759542f4768d1870b508d629c6888e8914", "size_bytes": 25949, "bucket": "engine_bytes", "gzip_bytes": 6247 }, { - "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/source-modules.mjs", + "path": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/source-modules.mjs", "sha256": "3f824a8cf79a9ab78c1b817ed1597d114d851748a6b0894a4ac6b4bdbe01cfef", "size_bytes": 5115, "bucket": "engine_bytes", "gzip_bytes": 1866 }, { - "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/source-parser.mjs", + "path": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/source-parser.mjs", "sha256": "6edfb99f92e9aa559dfec27d80e0ffeede84d6e1dcdaf904e0eefe46e29b2fd0", "size_bytes": 33874, "bucket": "engine_bytes", "gzip_bytes": 7911 }, { - "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/source-tokens.mjs", + "path": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/source-tokens.mjs", "sha256": "e044276920224f8ed788f4e3445bee926698444e6cf0b9e8573bd15fec94ad54", "size_bytes": 15061, "bucket": "engine_bytes", "gzip_bytes": 4470 }, { - "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/string-printability.mjs", + "path": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/string-printability.mjs", "sha256": "355a086a142e06822eff821dd0ac6a2ca70c4e06f4ec659236dc3cea1ddd639f", "size_bytes": 13287, "bucket": "engine_bytes", "gzip_bytes": 3903 }, { - "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/values.mjs", + "path": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/values.mjs", "sha256": "9939130b873388651d61e7a450be83cf6f5687d65fc0322f85f5a8dc29ba5371", "size_bytes": 86696, "bucket": "engine_bytes", "gzip_bytes": 18358 }, { - "path": "engine/ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826/vm.mjs", + "path": "engine/5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490/vm.mjs", "sha256": "796eaac2848ee7aa2d740797eb2afbdb3e577918866a88a9f25e5adbc55e985d", "size_bytes": 6080, "bucket": "engine_bytes", @@ -363,10 +363,10 @@ }, { "path": "index.html", - "sha256": "7ce3c3f54dd9b019c8ab4e6effe88e5d7691669e0196b493e30e66c3a08fd89c", + "sha256": "5407942c5a8d89f83ddf78d9712efb66d1803e17f3e22d7b0ba31db8438eb5a3", "size_bytes": 2234, "bucket": "data_bytes", - "gzip_bytes": 1140 + "gzip_bytes": 1138 }, { "path": "manifest.webmanifest", @@ -377,10 +377,10 @@ }, { "path": "service-worker.js", - "sha256": "8d816c367244d0de40e54017c18d1bce6924d137a0406a98f78e83eb39eada7c", + "sha256": "43dda732373f2f95aca47ad1b09a6072f116bd428b74bcc8805d98502305b2a8", "size_bytes": 8793, "bucket": "assembly_module_bytes", - "gzip_bytes": 3020 + "gzip_bytes": 3011 } ], "compression": "Sorted USTAR mode0644 mtime0, gzip level9 mtime0; local payload bytes only." diff --git a/runtime/class-values.mjs b/runtime/class-values.mjs index a1603ce..a55ea28 100644 --- a/runtime/class-values.mjs +++ b/runtime/class-values.mjs @@ -120,6 +120,11 @@ export class ClassRuntime { if (name === null || name === undefined) this.fail("UnsupportedOperationError", "This reflective value type requires a separate stage", "UNSUPPORTED"); return this.builtin(name); } + classAttribute(value) { + if (value instanceof InstanceValue && this.hasSlot(value.type, "__class__")) + this.fail("UnsupportedOperationError", "Advertised __class__ identities require their separate reflection stage", "UNSUPPORTED"); + return this.typeOf(value); + } lookup(type, name, native = true, start = 0) { this.key(name); this.touch(type); if (!this.isType(type)) throw new TypeError("An adopted class is required."); @@ -201,7 +206,7 @@ export class ClassRuntime { } if (!(receiver instanceof ClassValue || receiver instanceof InstanceValue)) this.fail("UnsupportedOperationError", "Attribute receiver is outside the class profile.", "UNSUPPORTED"); - if (receiver instanceof InstanceValue && name === "__class__") return value(receiver.type); + if (receiver instanceof InstanceValue && name === "__class__") return value(this.classAttribute(receiver)); if (receiver instanceof ClassValue) { if (name === "__class__") return value(receiver.metaclass || this.builtin("type")); if (["__name__", "__qualname__", "__module__"].includes(name)) return value(this.values.string(Array.from( @@ -304,7 +309,11 @@ export class ClassRuntime { this.touch(type); if (receiver === null) return this.make(SuperValue, type, null, null); let owner = receiver instanceof ClassValue && this.subclass(receiver, type) ? receiver : this.typeOf(receiver); - if (owner === null || !this.subclass(owner, type)) this.fail("TypeError", "super receiver must be an instance or subtype of its first argument"); + if (owner === null || !this.subclass(owner, type)) { + if (receiver instanceof InstanceValue && this.hasSlot(receiver.type, "__class__")) + this.fail("UnsupportedOperationError", "Advertised __class__ identities require their separate reflection stage", "UNSUPPORTED"); + this.fail("TypeError", "super receiver must be an instance or subtype of its first argument"); + } return this.make(SuperValue, type, receiver, owner); } callable(value) { @@ -332,7 +341,10 @@ export class ClassRuntime { } if (!this.isType(type)) this.fail("TypeError", "isinstance requires an adopted type."); this.touch(type); this.touch(value); - return type.root || this.subclass(this.typeOf(value), type); + if (type.root || this.subclass(this.typeOf(value), type)) return true; + if (value instanceof InstanceValue && this.hasSlot(value.type, "__class__")) + this.fail("UnsupportedOperationError", "Advertised __class__ identities require their separate reflection stage", "UNSUPPORTED"); + return false; } selectMetaclass(bases, explicit = undefined) { let selected = explicit === undefined ? this.builtin("type") : explicit; diff --git a/runtime/construction.test.mjs b/runtime/construction.test.mjs index 30802a9..8cc5315 100644 --- a/runtime/construction.test.mjs +++ b/runtime/construction.test.mjs @@ -194,3 +194,16 @@ test('dynamic class identities and namespace keys enforce budgets as owned failu assert.equal(result.error.code, 'BUDGET'); closed(result.closed); } }); + +test('deferred advertised class lookup refuses before descriptor effects', async () => { + const prefix = 'class A: pass\nclass B:\n @property\n def __class__(self):\n print("hook")\n return A\nb=B()\n'; + for (const expression of ['b.__class__', 'getattr(b,"__class__")', 'isinstance(b,A)', 'super(A,b)']) { + const result = await run(prefix+expression+'\n'); + assert.equal(result.error.code, 'UNSUPPORTED'); assert.equal(result.stdout, ''); closed(result.closed); + } +}); + +test('real type identity and successful actual checks do not consult advertised classes', async () => { + const result = await run('class A:\n @property\n def __class__(self):\n print("never")\n return int\na=A()\nprint(type(a) is A,isinstance(a,A),isinstance(a,object))\nclass B(A): pass\nprint(super(A,B()).__self_class__ is B)\n'); + assert.equal(result.error, undefined, JSON.stringify(result.error)); assert.equal(result.stdout, 'True True True\nTrue\n'); closed(result.closed); +}); diff --git a/runtime/functions.mjs b/runtime/functions.mjs index 18dc94d..e6a7433 100644 --- a/runtime/functions.mjs +++ b/runtime/functions.mjs @@ -550,7 +550,7 @@ function activation(program, {maxSteps = 10_000, maxWork = 1_000_000, maxDepth = inheritedException: frame.activeException}; else { const receiver = stack.pop(); - if (op === "attribute-get" && arg === "__class__") stack.push(classes.typeOf(receiver)); + if (op === "attribute-get" && arg === "__class__") stack.push(classes.classAttribute(receiver)); else if (receiver instanceof GeneratorValue) { if (!["send", "throw", "close", "__iter__", "__next__"].includes(arg)) hard("UNSUPPORTED", "UnsupportedOperationError", "Generator reflection requires a separate stage."); stack.push(allocate(ValueMethod, receiver, arg)); diff --git a/scripts/classes_console_browser_smoke.py b/scripts/classes_console_browser_smoke.py index 8fbe205..759f1f6 100644 --- a/scripts/classes_console_browser_smoke.py +++ b/scripts/classes_console_browser_smoke.py @@ -369,6 +369,11 @@ def refused(source, prefix): refused('class A:\n def f(self): return super().missing\nA().f()\n', 'AttributeError:') refused('class M(type):\n @classmethod\n def __prepare__(meta,name,bases): return {}\n', 'UnsupportedSyntaxError:') checks.append('constructor-super-metaclass-errors-and-future-hooks-retain-offline-diagnostics') + identity = 'class A: pass\nclass B:\n @property\n def __class__(self):\n print("hook")\n return A\nb=B()\n' + for expression in ['b.__class__', 'getattr(b,"__class__")', 'isinstance(b,A)', 'super(A,b)']: + refused(identity + expression + '\n', 'UnsupportedOperationError:') + assert page.locator('#result').inner_text() == '' + checks.append('deferred-advertised-class-identities-refuse-before-offline-hook-effects') busy = [ 'class Spin:\n def __call__(self):\n while True: pass\nSpin()()\n', 'class Spin:\n @property\n def __call__(self):\n while True: pass\nSpin()()\n', diff --git a/src/pocket_python/_interpreter_lock.py b/src/pocket_python/_interpreter_lock.py index 60414ef..61bdd58 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 = 'ceef442b7cd873d0d07d191ce9bc2d44a68417787af3d27caf8abb376c728826' +ENGINE_FINGERPRINT = '5954f421b6b5344fe19d1afe87e20a8087d7f96a2a3d7815f18b1e14074e9490' ENGINE_FILES = ( ('browser-protocol.mjs', 10589, '920e8d07c7070995636dff40fdf4333afc9bd4143483db5d8dff82ec1f28ff76'), ('bytecode.mjs', 6836, 'e76fabeea879bc1da8016283f7e74df5974479666b100199c2d3c31a53b8ecf4'), ('class-policy.mjs', 2043, '38fd2e79653b9219bbf1b98672dcd36cc9846d7d0d15c27864500ea3af8a7a6d'), - ('class-values.mjs', 23879, '06d53f86f5d3bfa5f0b3d244302cf23cf1b022a403bdbfd0b2c9b6b8199bd002'), + ('class-values.mjs', 24659, '47bc0508b412f95e0325cbcd30ec2d21f3b6b1bd538f9df82529f6c19b9f7002'), ('function-bytecode.mjs', 20693, '601e2bc41d4f5936b4eca9485d152f2962c3b4072829e7b12a9c7c5cbe888c69'), ('function-objects.mjs', 4496, '46d9d7bdd6acedb77cbbfa4f18b3ec57dda15aaab58cb8352c98cf9544944d69'), - ('functions.mjs', 60030, 'fdd226f04ef617fe31845ae5eab34c639a74329c86bd4dfbd5993cf37040f95e'), + ('functions.mjs', 60038, '35cff7e041738b20a27aafedf4c04797da257bc3f6f5bb8b3135c940376cff1b'), ('interpreter-host.mjs', 7019, '0bb0a64faffcf77e0064258e4cfcaa7a937c4372129849f76901bee91b5a5ae2'), ('interpreter-worker.mjs', 8282, '937a97cc49d987492de0bc16021a4a804ef51e4ea5b398f79c92e5d30ac82558'), ('library-catalog.mjs', 907, '0adcaae0f6f07b4e1acb22e46493ec99f7247a5828b426c703530d8b2736032c'), diff --git a/tests/fixtures/interpreter/construction.json b/tests/fixtures/interpreter/construction.json index c5a6d57..299a52c 100644 --- a/tests/fixtures/interpreter/construction.json +++ b/tests/fixtures/interpreter/construction.json @@ -722,6 +722,36 @@ "stage": "Future", "source": "def factory(name,bases,ns): return 7\nclass A(metaclass=factory): pass\nprint(A)\n", "stdout": "7\n" + }, + { + "id": "future-advertised-class-value", + "stage": "Future", + "source": "class A:\n __class__=int\nprint(A().__class__ is int)\n", + "stdout": "True\n" + }, + { + "id": "future-advertised-class-getter", + "stage": "Future", + "source": "class A:\n @property\n def __class__(self):\n print(\"hook\")\n return int\nprint(A().__class__ is int)\n", + "stdout": "hook\nTrue\n" + }, + { + "id": "future-advertised-class-relation", + "stage": "Future", + "source": "class A:\n @property\n def __class__(self):\n print(\"hook\")\n return int\nprint(isinstance(A(),int))\n", + "stdout": "hook\nTrue\n" + }, + { + "id": "future-advertised-class-super", + "stage": "Future", + "source": "class A: pass\nclass B:\n __class__=A\nprint(super(A,B()).__self_class__ is A)\n", + "stdout": "True\n" + }, + { + "id": "actual-class-before-advertised", + "stage": "O5", + "source": "class A:\n @property\n def __class__(self):\n print(\"never\")\n return int\na=A()\nprint(type(a) is A,isinstance(a,A),isinstance(a,object))\n", + "stdout": "True True True\n" } ] }