From 5b4dd01eefdff02addc15c5e25658fd135093a67 Mon Sep 17 00:00:00 2001 From: PurHur Date: Sun, 24 May 2026 15:30:32 +0200 Subject: [PATCH] feat(lang): VM stub for WeakReference and WeakMap (#1366) Register builtin WeakReference and WeakMap on the VM with create/get and ArrayAccess-style methods. WeakReference tracks targets via indirect slots so unset() clears get(); documented as partial until real GC weak refs exist. Co-authored-by: Cursor --- docs/capabilities-syntax.md | 3 +- lib/JIT/Builtin/Type/Object_.php | 6 ++ lib/VM/Builtin/VmClassMethod.php | 21 +++++ lib/VM/Builtin/WeakMapConstruct.php | 27 ++++++ lib/VM/Builtin/WeakMapCount.php | 29 +++++++ lib/VM/Builtin/WeakMapOffsetExists.php | 34 ++++++++ lib/VM/Builtin/WeakMapOffsetGet.php | 45 ++++++++++ lib/VM/Builtin/WeakMapOffsetSet.php | 28 ++++++ lib/VM/Builtin/WeakMapOffsetUnset.php | 31 +++++++ lib/VM/Builtin/WeakReferenceConstruct.php | 22 +++++ lib/VM/Builtin/WeakReferenceCreate.php | 42 +++++++++ lib/VM/Builtin/WeakReferenceGet.php | 34 ++++++++ lib/VM/BuiltinClasses.php | 76 ++++++++++++++++ lib/VM/ClassEntry.php | 101 +++++++++++----------- lib/VM/Context.php | 1 + lib/VM/ObjectEntry.php | 2 +- lib/VM/WeakRefSupport.php | 73 ++++++++++++++++ script/capability-syntax-lib.php | 12 +++ test/unit/WeakReferenceWeakMapTest.php | 101 ++++++++++++++++++++++ 19 files changed, 636 insertions(+), 52 deletions(-) create mode 100644 lib/VM/Builtin/VmClassMethod.php create mode 100644 lib/VM/Builtin/WeakMapConstruct.php create mode 100644 lib/VM/Builtin/WeakMapCount.php create mode 100644 lib/VM/Builtin/WeakMapOffsetExists.php create mode 100644 lib/VM/Builtin/WeakMapOffsetGet.php create mode 100644 lib/VM/Builtin/WeakMapOffsetSet.php create mode 100644 lib/VM/Builtin/WeakMapOffsetUnset.php create mode 100644 lib/VM/Builtin/WeakReferenceConstruct.php create mode 100644 lib/VM/Builtin/WeakReferenceCreate.php create mode 100644 lib/VM/Builtin/WeakReferenceGet.php create mode 100644 lib/VM/BuiltinClasses.php create mode 100644 lib/VM/WeakRefSupport.php create mode 100644 test/unit/WeakReferenceWeakMapTest.php diff --git a/docs/capabilities-syntax.md b/docs/capabilities-syntax.md index b96802fed7f..f759875f8f7 100644 --- a/docs/capabilities-syntax.md +++ b/docs/capabilities-syntax.md @@ -39,12 +39,13 @@ Tracking issues: [#58](https://github.com/PurHur/php-compiler/issues/58), [#145] | Variable variables (`$$name`) | yes | yes | yes | [#1226](https://github.com/PurHur/php-compiler/issues/1226) | php-cfg nests Operand\Variable name; VM resolves runtime local by name; JIT compile-time name fold (#1226); compliance PHPT | | Variable function calls (`$fn()`) | yes | yes | yes | [#56](https://github.com/PurHur/php-compiler/issues/56) | VM resolves callee at runtime; compiler folds literal assignment; JIT uses compile-time string | | Invokable objects (`$obj()` / `__invoke`) | yes | yes | yes | [#1232](https://github.com/PurHur/php-compiler/issues/1232) | Object-typed FuncCall lowered to __invoke method dispatch; VM runtime fallback | -| First-class callable syntax (`foo(...)`, `Class::m(...)`) | yes | yes | yes | [#1230](https://github.com/PurHur/php-compiler/issues/1230) | php-cfg Expr_FirstClassCallable; VM stores string or [obj, method] array; JIT via compileTimeString | +| First-class callable syntax (`foo(...)`, `Class::m(...)`) | yes | yes | yes | [#1363](https://github.com/PurHur/php-compiler/issues/1363) | php-cfg Expr_FirstClassCallable (#1230); VM stores string or [obj, method] array; JIT folds strlen(...) / Class::m(...) via compileTimeString assign chains (#1363) | | `never` return type | yes | yes | yes | [#1358](https://github.com/PurHur/php-compiler/issues/1358) | php-cfg Op\Type\Never_; any `return` in body is a compile error; normal completion via throw/exit | | Intersection types (`A&B`) | yes | yes | no | [#1357](https://github.com/PurHur/php-compiler/issues/1357) | php-cfg Op\Type\Intersection; VM checks object implements each interface at call | | Array/argument unpack `...$x` | yes | yes | yes | [#1361](https://github.com/PurHur/php-compiler/issues/1361) | php-cfg spread.patch (#141); VM HashTable::spreadFrom; JIT HashTableHelper::spreadInto + mergeCallArgEntries; compliance PHPT | | `__serialize` / `__unserialize` magic methods | no | no | yes | [#1365](https://github.com/PurHur/php-compiler/issues/1365) | serialize()/unserialize() call __serialize/__unserialize when present; VM via VmSerialize | | Multi-type catch `catch (A|B $e)` | yes | yes | yes | [#1362](https://github.com/PurHur/php-compiler/issues/1362) | php-cfg records union types per catch; VM filters TYPE_CATCH via OpCode.catchTypes; compliance PHPT | +| WeakReference / WeakMap | yes | no | yes | [#1366](https://github.com/PurHur/php-compiler/issues/1366) | VM stub: WeakReference::create/get via indirect target slot (unset clears get); not cycle-collecting GC weak refs; WeakMap uses object-id string keys; JIT may compile references but method bodies are VM-only; VM-only lowering | _Syntax AOT column reflects `Runtime::MODE_AOT` compile probes unless a row pins AOT (e.g. native user-class link)._ ## Web north-star (`examples/003-MiniWebApp`) diff --git a/lib/JIT/Builtin/Type/Object_.php b/lib/JIT/Builtin/Type/Object_.php index f1719788731..55af0df1016 100755 --- a/lib/JIT/Builtin/Type/Object_.php +++ b/lib/JIT/Builtin/Type/Object_.php @@ -708,6 +708,12 @@ private function registerExternalClass(string $lcname): void $this->splObjectStorageClassId = $id; $this->defineProperty($id, '__spl_ht', Variable::TYPE_HASHTABLE); } + if ('weakreference' === $lcname) { + $this->defineProperty($id, '__weak_target', Variable::TYPE_NULL); + } + if ('weakmap' === $lcname) { + $this->defineProperty($id, '__weak_map', Variable::TYPE_HASHTABLE); + } if ('phpcompiler\\vm\\variable' === $lcname) { foreach ([ 'type_undefined' => \PHPCompiler\VM\Variable::TYPE_UNDEFINED, diff --git a/lib/VM/Builtin/VmClassMethod.php b/lib/VM/Builtin/VmClassMethod.php new file mode 100644 index 00000000000..55f91db7360 --- /dev/null +++ b/lib/VM/Builtin/VmClassMethod.php @@ -0,0 +1,21 @@ +calledArgs) < 1) { + throw new \LogicException('WeakMap::__construct() called without $this'); + } + $receiver = WeakRefSupport::requireObject($frame->calledArgs[0], 'WeakMap'); + WeakRefSupport::initMapBacking($receiver->toObject()); + } +} diff --git a/lib/VM/Builtin/WeakMapCount.php b/lib/VM/Builtin/WeakMapCount.php new file mode 100644 index 00000000000..b06f6c7edfa --- /dev/null +++ b/lib/VM/Builtin/WeakMapCount.php @@ -0,0 +1,29 @@ +calledArgs) < 1) { + throw new \LogicException('WeakMap::count() called without $this'); + } + if (null === $frame->returnVar) { + return; + } + $receiver = WeakRefSupport::requireObject($frame->calledArgs[0], 'WeakMap'); + $frame->returnVar->int(WeakRefSupport::mapTable($receiver->toObject())->getNumElements()); + } +} diff --git a/lib/VM/Builtin/WeakMapOffsetExists.php b/lib/VM/Builtin/WeakMapOffsetExists.php new file mode 100644 index 00000000000..cb3a1206412 --- /dev/null +++ b/lib/VM/Builtin/WeakMapOffsetExists.php @@ -0,0 +1,34 @@ +calledArgs) < 2) { + throw new \LogicException('WeakMap::offsetExists() expects object key'); + } + if (null === $frame->returnVar) { + return; + } + $receiver = WeakRefSupport::requireObject($frame->calledArgs[0], 'WeakMap'); + $key = WeakRefSupport::objectKey($frame->calledArgs[1]); + $ht = WeakRefSupport::mapTable($receiver->toObject()); + $keyVar = new Variable(Variable::TYPE_STRING); + $keyVar->string($key); + $frame->returnVar->bool($ht->keyExists($keyVar)); + } +} diff --git a/lib/VM/Builtin/WeakMapOffsetGet.php b/lib/VM/Builtin/WeakMapOffsetGet.php new file mode 100644 index 00000000000..0af1b1be27c --- /dev/null +++ b/lib/VM/Builtin/WeakMapOffsetGet.php @@ -0,0 +1,45 @@ +calledArgs) < 2) { + throw new \LogicException('WeakMap::offsetGet() expects object key'); + } + if (null === $frame->returnVar) { + return; + } + $receiver = WeakRefSupport::requireObject($frame->calledArgs[0], 'WeakMap'); + $key = WeakRefSupport::objectKey($frame->calledArgs[1]); + $ht = WeakRefSupport::mapTable($receiver->toObject()); + $keyVar = new Variable(Variable::TYPE_STRING); + $keyVar->string($key); + if (!$ht->keyExists($keyVar)) { + $frame->returnVar->null(); + + return; + } + $slot = $ht->findVariable($keyVar, false); + if (null === $slot) { + $frame->returnVar->null(); + + return; + } + $frame->returnVar->copyFrom($slot); + } +} diff --git a/lib/VM/Builtin/WeakMapOffsetSet.php b/lib/VM/Builtin/WeakMapOffsetSet.php new file mode 100644 index 00000000000..3e97990d3c8 --- /dev/null +++ b/lib/VM/Builtin/WeakMapOffsetSet.php @@ -0,0 +1,28 @@ +calledArgs) < 3) { + throw new \LogicException('WeakMap::offsetSet() expects object key and value'); + } + $receiver = WeakRefSupport::requireObject($frame->calledArgs[0], 'WeakMap'); + $key = WeakRefSupport::objectKey($frame->calledArgs[1]); + $ht = WeakRefSupport::mapTable($receiver->toObject()); + $ht->add($key, $frame->calledArgs[2]); + } +} diff --git a/lib/VM/Builtin/WeakMapOffsetUnset.php b/lib/VM/Builtin/WeakMapOffsetUnset.php new file mode 100644 index 00000000000..2383ffdbac0 --- /dev/null +++ b/lib/VM/Builtin/WeakMapOffsetUnset.php @@ -0,0 +1,31 @@ +calledArgs) < 2) { + throw new \LogicException('WeakMap::offsetUnset() expects object key'); + } + $receiver = WeakRefSupport::requireObject($frame->calledArgs[0], 'WeakMap'); + $key = WeakRefSupport::objectKey($frame->calledArgs[1]); + $ht = WeakRefSupport::mapTable($receiver->toObject()); + $keyVar = new Variable(Variable::TYPE_STRING); + $keyVar->string($key); + $ht->offsetUnset($keyVar); + } +} diff --git a/lib/VM/Builtin/WeakReferenceConstruct.php b/lib/VM/Builtin/WeakReferenceConstruct.php new file mode 100644 index 00000000000..eab19675498 --- /dev/null +++ b/lib/VM/Builtin/WeakReferenceConstruct.php @@ -0,0 +1,22 @@ +calledArgs) < 1) { + throw new \LogicException('WeakReference::create() expects exactly 1 argument'); + } + if (null === $frame->vmContext) { + throw new \LogicException('WeakReference::create() requires VM context'); + } + $object = WeakRefSupport::requireObject($frame->calledArgs[0], 'WeakReference::create() argument #1'); + $class = $frame->vmContext->classes['weakreference'] ?? null; + if (null === $class) { + throw new \LogicException('WeakReference is not registered in this compiler build'); + } + $entry = new ObjectEntry($class); + $ref = new Variable(Variable::TYPE_OBJECT); + $ref->object($entry); + WeakRefSupport::targetSlot($entry)->indirect($object); + if (null !== $frame->returnVar) { + $frame->returnVar->copyFrom($ref); + } + } +} diff --git a/lib/VM/Builtin/WeakReferenceGet.php b/lib/VM/Builtin/WeakReferenceGet.php new file mode 100644 index 00000000000..b173f5fa978 --- /dev/null +++ b/lib/VM/Builtin/WeakReferenceGet.php @@ -0,0 +1,34 @@ +calledArgs) < 1) { + throw new \LogicException('WeakReference::get() called without $this'); + } + $receiver = WeakRefSupport::requireObject($frame->calledArgs[0], 'WeakReference'); + if (null === $frame->returnVar) { + return; + } + WeakRefSupport::copyAliveTarget( + $frame->returnVar, + WeakRefSupport::targetSlot($receiver->toObject()) + ); + } +} diff --git a/lib/VM/BuiltinClasses.php b/lib/VM/BuiltinClasses.php new file mode 100644 index 00000000000..4f4062f8cb3 --- /dev/null +++ b/lib/VM/BuiltinClasses.php @@ -0,0 +1,76 @@ +properties[] = new ClassProperty( + WeakRefSupport::TARGET_PROPERTY, + null, + $nullProto + ); + $pub = CfgFunc::FLAG_PUBLIC; + $entry->methods['create'] = new WeakReferenceCreate(); + $entry->methodVisibility['create'] = $pub; + $entry->methods['get'] = new WeakReferenceGet(); + $entry->methodVisibility['get'] = $pub; + $entry->constructor = new WeakReferenceConstruct(); + $entry->methods['__construct'] = $entry->constructor; + $entry->methodVisibility['__construct'] = $pub; + $ctx->classes['weakreference'] = $entry; + } + + private static function registerWeakMap(Context $ctx): void + { + $entry = new ClassEntry('WeakMap'); + $arrayProto = new Variable(Variable::TYPE_ARRAY); + $entry->properties[] = new ClassProperty( + WeakRefSupport::MAP_PROPERTY, + null, + $arrayProto + ); + $pub = CfgFunc::FLAG_PUBLIC; + $entry->constructor = new WeakMapConstruct(); + $entry->methods['__construct'] = $entry->constructor; + $entry->methodVisibility['__construct'] = $pub; + foreach ( + [ + 'offsetset' => new WeakMapOffsetSet(), + 'offsetget' => new WeakMapOffsetGet(), + 'offsetexists' => new WeakMapOffsetExists(), + 'offsetunset' => new WeakMapOffsetUnset(), + 'count' => new WeakMapCount(), + ] as $name => $method + ) { + $entry->methods[$name] = $method; + $entry->methodVisibility[$name] = $pub; + } + $ctx->classes['weakmap'] = $entry; + } +} diff --git a/lib/VM/ClassEntry.php b/lib/VM/ClassEntry.php index 700f762e24f..40392124106 100755 --- a/lib/VM/ClassEntry.php +++ b/lib/VM/ClassEntry.php @@ -1,50 +1,51 @@ - */ - public array $interfaces = []; - public ?Func\PHP $constructor = null; - public array $properties = []; - /** @var array method name (lowercase) => callable */ - public array $methods = []; - /** @var array method name (lowercase) => PHPCfg visibility flags */ - public array $methodVisibility = []; - /** @var array constant name (lowercase) => value */ - public array $constants = []; - /** @var array static property name (lowercase) => shared storage */ - public array $staticProperties = []; - - public function __construct(string $name) { - $this->name = $name; - } - - public function getProperties(array $properties, int $reason): array { - // todo: implement __debug_info - return $properties; - } - -} + */ + public array $interfaces = []; + /** User method or VM builtin handler (issue #1366). */ + public ?Func $constructor = null; + public array $properties = []; + /** @var array method name (lowercase) => callable */ + public array $methods = []; + /** @var array method name (lowercase) => PHPCfg visibility flags */ + public array $methodVisibility = []; + /** @var array constant name (lowercase) => value */ + public array $constants = []; + /** @var array static property name (lowercase) => shared storage */ + public array $staticProperties = []; + + public function __construct(string $name) { + $this->name = $name; + } + + public function getProperties(array $properties, int $reason): array { + // todo: implement __debug_info + return $properties; + } + +} diff --git a/lib/VM/Context.php b/lib/VM/Context.php index 99a35a98ddc..b7c098bc017 100755 --- a/lib/VM/Context.php +++ b/lib/VM/Context.php @@ -47,6 +47,7 @@ public function __construct(Runtime $runtime) { $this->runtime = $runtime; $this->errors = new ErrorReporter(); $this->scriptStack = new ScriptStack(); + BuiltinClasses::register($this); } public function constantFetch(string $name): ?Variable { diff --git a/lib/VM/ObjectEntry.php b/lib/VM/ObjectEntry.php index 598a133d7c1..7d25b8bb98f 100755 --- a/lib/VM/ObjectEntry.php +++ b/lib/VM/ObjectEntry.php @@ -20,7 +20,7 @@ class ObjectEntry { public ClassEntry $class; public int $id; private array $properties = []; - public ?Func\PHP $constructor = null; + public ?Func $constructor = null; public function __construct(ClassEntry $class) { $this->class = $class; diff --git a/lib/VM/WeakRefSupport.php b/lib/VM/WeakRefSupport.php new file mode 100644 index 00000000000..9bb38ab9093 --- /dev/null +++ b/lib/VM/WeakRefSupport.php @@ -0,0 +1,73 @@ +resolveIndirect(); + if (Variable::TYPE_OBJECT !== $var->type) { + throw new \TypeError("{$label} must be of type object"); + } + + return $var; + } + + public static function objectKey(Variable $key): string + { + $key = self::requireObject($key, 'WeakMap key'); + + return 'o:'.$key->toObject()->id; + } + + public static function targetSlot(ObjectEntry $weakRef): Variable + { + return $weakRef->getProperty(self::TARGET_PROPERTY); + } + + public static function mapTable(ObjectEntry $weakMap): HashTable + { + $slot = $weakMap->getProperty(self::MAP_PROPERTY)->resolveIndirect(); + if (Variable::TYPE_ARRAY !== $slot->type) { + throw new \LogicException('WeakMap backing store is missing in this compiler build'); + } + + return $slot->toArray(); + } + + public static function initMapBacking(ObjectEntry $weakMap): void + { + $slot = $weakMap->getProperty(self::MAP_PROPERTY); + $slot->newArray(); + } + + public static function isTargetAlive(Variable $target): bool + { + $target = $target->resolveIndirect(); + + return !$target->isUndefined() && Variable::TYPE_NULL !== $target->type; + } + + public static function copyAliveTarget(Variable $dst, Variable $target): void + { + $target = $target->resolveIndirect(); + if (!self::isTargetAlive($target)) { + $dst->null(); + + return; + } + $dst->copyFrom($target); + } +} diff --git a/script/capability-syntax-lib.php b/script/capability-syntax-lib.php index c4f38cd864a..7064483dfd6 100644 --- a/script/capability-syntax-lib.php +++ b/script/capability-syntax-lib.php @@ -316,6 +316,18 @@ function syntaxRowDefinitions(): array 'notes' => ['php-cfg records union types per catch; VM filters TYPE_CATCH via OpCode.catchTypes'], 'probe' => 'class A {} class B {} try { throw new A(); } catch (A|B $e) { echo "ok"; }', ], + [ + 'id' => 'weak_reference_weak_map', + 'construct' => 'WeakReference / WeakMap', + 'opcodes' => [], + 'issue' => 1366, + 'jit' => false, + 'notes' => [ + 'VM stub: WeakReference::create/get via indirect target slot (unset clears get); not cycle-collecting GC weak refs', + 'WeakMap uses object-id string keys; JIT may compile references but method bodies are VM-only', + ], + 'probe' => 'class Box {} $o = new Box(); $r = WeakReference::create($o); unset($o); echo $r->get() === null ? "1" : "0";', + ], ]; } diff --git a/test/unit/WeakReferenceWeakMapTest.php b/test/unit/WeakReferenceWeakMapTest.php new file mode 100644 index 00000000000..db6518111e1 --- /dev/null +++ b/test/unit/WeakReferenceWeakMapTest.php @@ -0,0 +1,101 @@ +get() === null ? '1' : '0'; +PHP; + ob_start(); + $runtime->run($runtime->parseAndCompile($code, 'weakref_unset.php')); + $this->assertSame('1', ob_get_clean()); + } + + public function testWeakReferenceGetReturnsObjectWhileAlive(): void + { + $runtime = new Runtime(); + $code = <<<'PHP' +get() === $o ? '1' : '0'; +PHP; + ob_start(); + $runtime->run($runtime->parseAndCompile($code, 'weakref_alive.php')); + $this->assertSame('1', ob_get_clean()); + } + + public function testClassExistsWeakReferenceAndWeakMap(): void + { + $runtime = new Runtime(); + $code = <<<'PHP' +run($runtime->parseAndCompile($code, 'weak_classes.php')); + $this->assertSame('1', ob_get_clean()); + } + + public function testWeakMapOffsetSetAndGet(): void + { + $runtime = new Runtime(); + $code = <<<'PHP' +offsetSet($k, 42); +echo $m->offsetGet($k); +PHP; + ob_start(); + $runtime->run($runtime->parseAndCompile($code, 'weakmap_basic.php')); + $this->assertSame('42', ob_get_clean()); + } + + public function testWeakMapCountAndOffsetExists(): void + { + $runtime = new Runtime(); + $code = <<<'PHP' +offsetSet($k, 1); +echo $m->count(); +echo $m->offsetExists($k) ? '1' : '0'; +PHP; + ob_start(); + $runtime->run($runtime->parseAndCompile($code, 'weakmap_count.php')); + $this->assertSame('11', ob_get_clean()); + } + + public function testWeakReferenceParsesAndCompiles(): void + { + $runtime = new Runtime(); + $code = <<<'PHP' +parseAndCompile($code, 'weakref_parse.php'); + $this->addToAssertionCount(1); + } +}