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/4] 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/4] 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/4] 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/4] 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" } ] }