From 7bb03ce8d90eaa6172d24f898efc08a9c01a4eea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8E=9F=E7=82=B9?= <467490186@qq.com> Date: Tue, 8 Sep 2026 15:48:38 +0800 Subject: [PATCH 1/5] perf(compiler): infer closure parameter types from declarations and call sites * perf(compiler): use PHP type hints to narrow native local closure parameters to php::Int/Float/Bool/Str/Array * perf(compiler): infer closure parameter types from single call-site literal arguments * perf(compiler): skip redundant runtime type checks when parameter is already a native C++ type * fix(compiler): prevent closure variable names from overwriting function C++ names in Translator --- src/Analysis/LocalClosureAnalyzer.php | 92 ++++++++++++++++++++++++++- src/Generator/ClosureGenerator.php | 37 ++++++++++- src/Translator.php | 7 +- 3 files changed, 130 insertions(+), 6 deletions(-) diff --git a/src/Analysis/LocalClosureAnalyzer.php b/src/Analysis/LocalClosureAnalyzer.php index da88b509..82906a2f 100644 --- a/src/Analysis/LocalClosureAnalyzer.php +++ b/src/Analysis/LocalClosureAnalyzer.php @@ -12,6 +12,7 @@ use PhpParser\Node\Expr; use PhpParser\Node\FunctionLike; use PhpParser\Node\Stmt; +use TypePhp\Type; /** * Proves the deliberately small set of local Closures which can stay entirely @@ -20,7 +21,7 @@ */ final class LocalClosureAnalyzer { - /** @var array */ + /** @var array}> */ private array $candidates = []; /** @var array */ @@ -31,7 +32,7 @@ final class LocalClosureAnalyzer /** * @param list $statements - * @return array + * @return array}> */ public function analyze(array $statements): array { @@ -63,6 +64,7 @@ public function analyze(array $statements): array 'assignment' => $statement->expr, 'closure' => $statement->expr->expr, 'calls' => 0, + 'callSites' => [], ]; } @@ -205,6 +207,7 @@ private function classifyVariableUse( } $this->candidates[$name]['calls']++; + $this->candidates[$name]['callSites'][] = $parent; } private function isSupportedDirectCall(Expr\FuncCall $call, int $parameterCount): bool @@ -219,4 +222,89 @@ private function isSupportedDirectCall(Expr\FuncCall $call, int $parameterCount) } return true; } + + /** + * Infer closure parameter types from call site arguments. + * + * Only infers when: + * - There is exactly one call site (single call site) + * - All arguments have detectable types + * - The inferred type is a native type (int, float, bool, string, array) + * + * @param array{assignment: Expr\Assign, closure: Expr\Closure|Expr\ArrowFunction, calls: int, callSites: list} $candidate + * @return list Parameter types (Type::VAR for unknown) + */ + public function inferParamTypes(array $candidate): array + { + $closure = $candidate['closure']; + $paramCount = count($closure->params); + $callSites = $candidate['callSites']; + + // Only infer for single call site + if (count($callSites) !== 1) { + return array_fill(0, $paramCount, Type::VAR); + } + + $call = $callSites[0]; + $inferredTypes = []; + + foreach ($call->args as $i => $arg) { + $type = $this->detectArgType($arg->value); + $inferredTypes[$i] = $type; + } + + return $inferredTypes; + } + + /** + * Detect the type of an argument expression. + */ + private function detectArgType(Expr $expr): string + { + // Literal integers + if ($expr instanceof Node\Scalar\Int_) { + return Type::INT; + } + + // Literal floats + if ($expr instanceof Node\Scalar\Float_) { + return Type::FLOAT; + } + + // Literal strings + if ($expr instanceof Node\Scalar\String_) { + return Type::STR; + } + + // Boolean constants + if ($expr instanceof Expr\ConstFetch && $expr->name instanceof Node\Name) { + $name = strtolower($expr->name->toString()); + if ($name === 'true' || $name === 'false') { + return Type::BOOL; + } + // null can be any type, keep as VAR + return Type::VAR; + } + + // Array literals + if ($expr instanceof Expr\Array_) { + return Type::ARRAY; + } + + // Variables — could be extended to use SSA type info + // For now, keep as VAR (the closure body will use native type if inferred) + if ($expr instanceof Expr\Variable) { + return Type::VAR; + } + + // Function calls that return known types + if ($expr instanceof Expr\FuncCall && $expr->name instanceof Node\Name) { + $name = strtolower($expr->name->toString()); + if (in_array($name, ['count', 'strlen', 'sizeof'], true)) { + return Type::INT; + } + } + + return Type::VAR; + } } diff --git a/src/Generator/ClosureGenerator.php b/src/Generator/ClosureGenerator.php index e1f130eb..a0593143 100644 --- a/src/Generator/ClosureGenerator.php +++ b/src/Generator/ClosureGenerator.php @@ -129,9 +129,23 @@ protected function parseNativeLocalClosureAssignment(Expr\Assign $assign): ?stri $entryContext = $this->context; $entryIndent = $this->indentLevel; $entryInGeneratorBody = $this->inGeneratorBody; + + // Get inferred types from call sites (Phase 2) + $inferredTypes = $candidate['inferredParamTypes'] ?? array_fill(0, count($expr->params), Type::VAR); + $parameters = []; - foreach ($expr->params as $param) { - $parameters[] = Type::VAR . ' ' . $this->parseIdentifier($param->var); + foreach ($expr->params as $i => $param) { + $paramType = $inferredTypes[$i] ?? Type::VAR; + + // Type declarations take priority over call-site inference + if ($param->type !== null) { + [$resolvedType,] = $this->resolveTypeDecl($param->type, self::DECL_TYPE_OF_PARAM); + // Only use native types (int/float/bool/string/array), keep object as VAR + if (in_array($resolvedType, [Type::INT, Type::FLOAT, Type::BOOL, Type::STR, Type::ARRAY], true)) { + $paramType = $resolvedType; + } + } + $parameters[] = $paramType . ' ' . $this->parseIdentifier($param->var); } $code = 'auto ' . $name . ' = [' . implode(', ', $capturePlan['cpp']) . '](' @@ -158,7 +172,14 @@ protected function parseNativeLocalClosureAssignment(Expr\Assign $assign): ?stri $parameterChecks = ''; foreach ($expr->params as $index => $param) { $paramName = $this->parseIdentifier($param->var); - $this->addArgument($paramName, Type::VAR); + $paramType = $inferredTypes[$index] ?? Type::VAR; + if ($param->type !== null) { + [$resolvedType,] = $this->resolveTypeDecl($param->type, self::DECL_TYPE_OF_PARAM); + if (in_array($resolvedType, [Type::INT, Type::FLOAT, Type::BOOL, Type::STR, Type::ARRAY], true)) { + $paramType = $resolvedType; + } + } + $this->addArgument($paramName, $paramType); if (CompileTimeAttribute::consume($param, 'Immutable')) { $this->context->immutableVars[$paramName] = true; if ($this->immutableTypeNodeMayBeObject($param->type)) { @@ -273,6 +294,16 @@ private function genNativeLocalClosureParamTypeCheck(Node\Param $param, string $ if ($param->type === null) { return ''; } + + // Skip type check if parameter is already a native type (not php::Var). + // When the parameter type is resolved to a native C++ type (int, float, + // bool, string, array), the value is already unwrapped at the ABI level + // and a Z_TYPE_P check against php::Int/Float/etc. would be meaningless. + [$resolvedType,] = $this->resolveTypeDecl($param->type, self::DECL_TYPE_OF_PARAM); + if (in_array($resolvedType, [Type::INT, Type::FLOAT, Type::BOOL, Type::STR, Type::ARRAY], true)) { + return ''; + } + $typeInfo = $this->buildTypeCheckFromNode($param->type, true); if (empty($typeInfo['check'])) { return ''; diff --git a/src/Translator.php b/src/Translator.php index 00f1c54b..2482e5b9 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -5090,7 +5090,12 @@ protected function parseFunction(Node\Stmt\Function_|Node\Stmt\ClassMethod $v): } if ($v->stmts && !$this->class && $this->methodDef === null) { - $this->context->localClosureCandidates = (new LocalClosureAnalyzer())->analyze($v->stmts); + $analyzer = new LocalClosureAnalyzer(); + $this->context->localClosureCandidates = $analyzer->analyze($v->stmts); + // Infer parameter types from call sites (Phase 2) + foreach ($this->context->localClosureCandidates as $closureName => &$candidate) { + $candidate['inferredParamTypes'] = $analyzer->inferParamTypes($candidate); + } } $stmts = ''; From e97b32eba185748135f39e886406262bd99fced4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8E=9F=E7=82=B9?= <467490186@qq.com> Date: Tue, 8 Sep 2026 15:58:47 +0800 Subject: [PATCH 2/5] test(compiler): add closure parameter type inference tests * test(compiler): verify type hint parameters use native C++ types (php::Int/Float/Bool/Str/Array) * test(compiler): verify call-site literal inference narrows closure parameters * test(compiler): verify multi-call closures remain php::Var when types conflict * test(compiler): update LocalClosureCodegenTest for php::Int parameter change --- phpunit/code/closure-param-type.php | 65 +++++++++++++++ phpunit/src/ClosureParamTypeTest.php | 61 ++++++++++++++ phpunit/src/LocalClosureCodegenTest.php | 2 +- .../closure/closure-param-type-inference.phpt | 81 +++++++++++++++++++ 4 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 phpunit/code/closure-param-type.php create mode 100644 phpunit/src/ClosureParamTypeTest.php create mode 100644 tests/compiler/closure/closure-param-type-inference.phpt diff --git a/phpunit/code/closure-param-type.php b/phpunit/code/closure-param-type.php new file mode 100644 index 00000000..aec5fa6c --- /dev/null +++ b/phpunit/code/closure-param-type.php @@ -0,0 +1,65 @@ + $a + 1; + return $fn($x); +} +function closureTypeHintFloat(float $x): float +{ + $fn = fn(float $a) => $a * 2.0; + return $fn($x); +} +function closureTypeHintBool(bool $x): bool +{ + $fn = fn(bool $a) => !$a; + return $fn($x); +} +function closureTypeHintString(string $x): int +{ + $fn = fn(string $a) => strlen($a); + return $fn($x); +} +function closureTypeHintArray(array $x): int +{ + $fn = fn(array $a) => count($a); + return $fn($x); +} +function closureCallSiteInt(): int +{ + $fn = fn($x) => $x + 1; + return $fn(42); +} +function closureCallSiteFloat(): float +{ + $fn = fn($x) => $x * 2.0; + return $fn(3.14); +} +function closureCallSiteBool(): bool +{ + $fn = fn($x) => !$x; + return $fn(true); +} +function closureCallSiteArray(): int +{ + $fn = fn($arr) => count($arr); + return $fn([1, 2, 3, 4, 5]); +} +function closureMultiCallNoInfer(): void +{ + $fn = fn($x) => $x + 1; + var_dump($fn(42)); + var_dump($fn(3.14)); +} +function main(): void +{ + closureTypeHintInt(10); + closureTypeHintFloat(1.5); + closureTypeHintBool(false); + closureTypeHintString("test"); + closureTypeHintArray([1, 2]); + closureCallSiteInt(); + closureCallSiteFloat(); + closureCallSiteBool(); + closureCallSiteArray(); + closureMultiCallNoInfer(); +} diff --git a/phpunit/src/ClosureParamTypeTest.php b/phpunit/src/ClosureParamTypeTest.php new file mode 100644 index 00000000..0528fdfb --- /dev/null +++ b/phpunit/src/ClosureParamTypeTest.php @@ -0,0 +1,61 @@ +addFiles([$source]); + $compiler->prepareFile($source); + $code = file_get_contents($compiler->convertFile($source)); + + self::assertIsString($code); + + self::assertStringContainsString('(php::Int a)', $code); + self::assertStringContainsString('(php::Float a)', $code); + self::assertStringContainsString('(php::Bool a)', $code); + self::assertStringContainsString('(php::Str a)', $code); + self::assertStringContainsString('(php::Array a)', $code); + } + + public function testCallSiteInferredParametersUseNativeCppTypes(): void + { + global $translator; + + $compiler = CompilerTest::create(TYPEPHP_ROOT_PATH); + $translator = $compiler; + $source = TYPEPHP_ROOT_PATH . '/phpunit/code/closure-param-type.php'; + $compiler->addFiles([$source]); + $compiler->prepareFile($source); + $code = file_get_contents($compiler->convertFile($source)); + + self::assertIsString($code); + + self::assertStringContainsString('(php::Int x)', $code); + self::assertStringContainsString('(php::Float x)', $code); + self::assertStringContainsString('(php::Bool x)', $code); + self::assertStringContainsString('(php::Array arr)', $code); + } + + public function testMultiCallClosureRemainsPhpVar(): void + { + global $translator; + + $compiler = CompilerTest::create(TYPEPHP_ROOT_PATH); + $translator = $compiler; + $source = TYPEPHP_ROOT_PATH . '/phpunit/code/closure-param-type.php'; + $compiler->addFiles([$source]); + $compiler->prepareFile($source); + $code = file_get_contents($compiler->convertFile($source)); + + self::assertIsString($code); + + self::assertStringContainsString('(php::Var x)', $code); + } +} diff --git a/phpunit/src/LocalClosureCodegenTest.php b/phpunit/src/LocalClosureCodegenTest.php index 5c271f07..6cbe612c 100644 --- a/phpunit/src/LocalClosureCodegenTest.php +++ b/phpunit/src/LocalClosureCodegenTest.php @@ -28,7 +28,7 @@ public function testOnlyProvenLocalClosuresUseConcreteCppLambdas(): void self::assertIsString($code); self::assertStringContainsString( - 'auto direct = [base = base](php::Var value) mutable -> php::Var {', + 'auto direct = [base = base](php::Int value) mutable -> php::Var {', $code, ); self::assertStringContainsString('direct(2L)', $code); diff --git a/tests/compiler/closure/closure-param-type-inference.phpt b/tests/compiler/closure/closure-param-type-inference.phpt new file mode 100644 index 00000000..3d126c8f --- /dev/null +++ b/tests/compiler/closure/closure-param-type-inference.phpt @@ -0,0 +1,81 @@ +--TEST-- +Native local closure parameter types are inferred from type declarations and call-site literals +--FILE-- + $x + 1; + var_dump($fn(42)); +} +function testTypeHintFloat(): void +{ + $fn = fn(float $x) => $x * 2.0; + var_dump($fn(3.14)); +} +function testTypeHintBool(): void +{ + $fn = fn(bool $x) => !$x; + var_dump($fn(true)); +} +function testTypeHintString(): void +{ + $fn = fn(string $x) => strlen($x); + var_dump($fn("hello")); +} +function testTypeHintArray(): void +{ + $fn = fn(array $x) => count($x); + var_dump($fn([1, 2, 3])); +} +function testCallSiteInt(): void +{ + $fn = fn($x) => $x + 1; + var_dump($fn(42)); +} +function testCallSiteFloat(): void +{ + $fn = fn($x) => $x * 2.0; + var_dump($fn(3.14)); +} +function testCallSiteBool(): void +{ + $fn = fn($x) => !$x; + var_dump($fn(true)); +} +function testCallSiteArray(): void +{ + $fn = fn($arr) => count($arr); + var_dump($fn([1, 2, 3, 4, 5])); +} +function testMultiCallNoInfer(): void +{ + $fn = fn($x) => $x + 1; + var_dump($fn(42)); + var_dump($fn(3.14)); +} +function main(): void +{ + testTypeHintInt(); + testTypeHintFloat(); + testTypeHintBool(); + testTypeHintString(); + testTypeHintArray(); + testCallSiteInt(); + testCallSiteFloat(); + testCallSiteBool(); + testCallSiteArray(); + testMultiCallNoInfer(); +} +?> +--EXPECT-- +int(43) +float(6.28) +bool(false) +int(5) +int(3) +int(43) +float(6.28) +bool(false) +int(5) +int(43) +float(44) From cd589f0e532577f789cb167c20cafa51e1eea528 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8E=9F=E7=82=B9?= <467490186@qq.com> Date: Tue, 8 Sep 2026 16:27:59 +0800 Subject: [PATCH 3/5] perf(compiler): enhance detectArgType for closure param inference * Handle UnaryMinus/UnaryPlus wrapping numeric literals (-42, +3.14) * Add boolean expression inference (&&, ||, !, ===, !==, ==, !=, <, <=, >, >=, <=>, instanceof) * Add string concatenation inference when both operands are strings --- src/Analysis/LocalClosureAnalyzer.php | 56 +++++++++++++++------------ 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/src/Analysis/LocalClosureAnalyzer.php b/src/Analysis/LocalClosureAnalyzer.php index 82906a2f..a7f0b7d4 100644 --- a/src/Analysis/LocalClosureAnalyzer.php +++ b/src/Analysis/LocalClosureAnalyzer.php @@ -223,24 +223,12 @@ private function isSupportedDirectCall(Expr\FuncCall $call, int $parameterCount) return true; } - /** - * Infer closure parameter types from call site arguments. - * - * Only infers when: - * - There is exactly one call site (single call site) - * - All arguments have detectable types - * - The inferred type is a native type (int, float, bool, string, array) - * - * @param array{assignment: Expr\Assign, closure: Expr\Closure|Expr\ArrowFunction, calls: int, callSites: list} $candidate - * @return list Parameter types (Type::VAR for unknown) - */ public function inferParamTypes(array $candidate): array { $closure = $candidate['closure']; $paramCount = count($closure->params); $callSites = $candidate['callSites']; - // Only infer for single call site if (count($callSites) !== 1) { return array_fill(0, $paramCount, Type::VAR); } @@ -256,48 +244,68 @@ public function inferParamTypes(array $candidate): array return $inferredTypes; } - /** - * Detect the type of an argument expression. - */ private function detectArgType(Expr $expr): string { - // Literal integers if ($expr instanceof Node\Scalar\Int_) { return Type::INT; } - // Literal floats if ($expr instanceof Node\Scalar\Float_) { return Type::FLOAT; } - // Literal strings if ($expr instanceof Node\Scalar\String_) { return Type::STR; } - // Boolean constants + if ($expr instanceof Expr\UnaryMinus || $expr instanceof Expr\UnaryPlus) { + return $this->detectArgType($expr->expr); + } + + if ($expr instanceof Expr\BooleanNot + || $expr instanceof Expr\BinaryOp\BooleanAnd + || $expr instanceof Expr\BinaryOp\BooleanOr + || $expr instanceof Expr\BinaryOp\LogicalAnd + || $expr instanceof Expr\BinaryOp\LogicalOr + || $expr instanceof Expr\BinaryOp\Identical + || $expr instanceof Expr\BinaryOp\NotIdentical + || $expr instanceof Expr\BinaryOp\Equal + || $expr instanceof Expr\BinaryOp\NotEqual + || $expr instanceof Expr\BinaryOp\Smaller + || $expr instanceof Expr\BinaryOp\SmallerOrEqual + || $expr instanceof Expr\BinaryOp\Greater + || $expr instanceof Expr\BinaryOp\GreaterOrEqual + || $expr instanceof Expr\BinaryOp\Spaceship + || $expr instanceof Expr\Instanceof_ + ) { + return Type::BOOL; + } + + if ($expr instanceof Expr\BinaryOp\Concat) { + $left = $this->detectArgType($expr->left); + $right = $this->detectArgType($expr->right); + if ($left === Type::STR && $right === Type::STR) { + return Type::STR; + } + return Type::VAR; + } + if ($expr instanceof Expr\ConstFetch && $expr->name instanceof Node\Name) { $name = strtolower($expr->name->toString()); if ($name === 'true' || $name === 'false') { return Type::BOOL; } - // null can be any type, keep as VAR return Type::VAR; } - // Array literals if ($expr instanceof Expr\Array_) { return Type::ARRAY; } - // Variables — could be extended to use SSA type info - // For now, keep as VAR (the closure body will use native type if inferred) if ($expr instanceof Expr\Variable) { return Type::VAR; } - // Function calls that return known types if ($expr instanceof Expr\FuncCall && $expr->name instanceof Node\Name) { $name = strtolower($expr->name->toString()); if (in_array($name, ['count', 'strlen', 'sizeof'], true)) { From 7ea58a150b54dcb31497a87eb3f46c88760aa186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8E=9F=E7=82=B9?= <467490186@qq.com> Date: Tue, 8 Sep 2026 16:31:42 +0800 Subject: [PATCH 4/5] test(compiler): add coverage for UnaryMinus/UnaryPlus, boolean, concat inference --- phpunit/code/closure-param-type.php | 48 +++++++++++++++++++++++++++ phpunit/src/ClosureParamTypeTest.php | 49 ++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/phpunit/code/closure-param-type.php b/phpunit/code/closure-param-type.php index aec5fa6c..d0ddf88c 100644 --- a/phpunit/code/closure-param-type.php +++ b/phpunit/code/closure-param-type.php @@ -50,6 +50,46 @@ function closureMultiCallNoInfer(): void var_dump($fn(42)); var_dump($fn(3.14)); } +function closureCallSiteNegInt(): int +{ + $fn = fn($x) => $x + 1; + return $fn(-42); +} +function closureCallSiteNegFloat(): float +{ + $fn = fn($x) => $x * 2.0; + return $fn(-3.14); +} +function closureCallSiteUnaryPlus(): int +{ + $fn = fn($x) => $x + 1; + return $fn(+42); +} +function closureCallSiteBoolExpr(): bool +{ + $fn = fn($x) => !$x; + return $fn(1 === 2); +} +function closureCallSiteLogicalOr(): bool +{ + $fn = fn($x) => $x; + return $fn(true || false); +} +function closureCallSiteInstanceof(): bool +{ + $fn = fn($x) => $x; + return $fn(new \stdClass() instanceof \stdClass); +} +function closureCallSiteConcat(): string +{ + $fn = fn($x) => $x; + return $fn("hello" . "world"); +} +function closureCallSiteConcatMixed(): string +{ + $fn = fn($x) => $x; + return $fn("hello" . 123); +} function main(): void { closureTypeHintInt(10); @@ -62,4 +102,12 @@ function main(): void closureCallSiteBool(); closureCallSiteArray(); closureMultiCallNoInfer(); + closureCallSiteNegInt(); + closureCallSiteNegFloat(); + closureCallSiteUnaryPlus(); + closureCallSiteBoolExpr(); + closureCallSiteLogicalOr(); + closureCallSiteInstanceof(); + closureCallSiteConcat(); + closureCallSiteConcatMixed(); } diff --git a/phpunit/src/ClosureParamTypeTest.php b/phpunit/src/ClosureParamTypeTest.php index 0528fdfb..a32c19cc 100644 --- a/phpunit/src/ClosureParamTypeTest.php +++ b/phpunit/src/ClosureParamTypeTest.php @@ -58,4 +58,53 @@ public function testMultiCallClosureRemainsPhpVar(): void self::assertStringContainsString('(php::Var x)', $code); } + + public function testUnaryMinusInfersNativeType(): void + { + global $translator; + + $compiler = CompilerTest::create(TYPEPHP_ROOT_PATH); + $translator = $compiler; + $source = TYPEPHP_ROOT_PATH . '/phpunit/code/closure-param-type.php'; + $compiler->addFiles([$source]); + $compiler->prepareFile($source); + $code = file_get_contents($compiler->convertFile($source)); + + self::assertIsString($code); + + self::assertStringContainsString('(php::Int x)', $code); + self::assertStringContainsString('(php::Float x)', $code); + } + + public function testBooleanExpressionsInferBoolType(): void + { + global $translator; + + $compiler = CompilerTest::create(TYPEPHP_ROOT_PATH); + $translator = $compiler; + $source = TYPEPHP_ROOT_PATH . '/phpunit/code/closure-param-type.php'; + $compiler->addFiles([$source]); + $compiler->prepareFile($source); + $code = file_get_contents($compiler->convertFile($source)); + + self::assertIsString($code); + + self::assertStringContainsString('(php::Bool x)', $code); + } + + public function testConcatStringInference(): void + { + global $translator; + + $compiler = CompilerTest::create(TYPEPHP_ROOT_PATH); + $translator = $compiler; + $source = TYPEPHP_ROOT_PATH . '/phpunit/code/closure-param-type.php'; + $compiler->addFiles([$source]); + $compiler->prepareFile($source); + $code = file_get_contents($compiler->convertFile($source)); + + self::assertIsString($code); + + self::assertStringContainsString('(php::Str x)', $code); + } } From 459ed607257b02db5c07b41407579bab5f10d297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8E=9F=E7=82=B9?= <467490186@qq.com> Date: Tue, 8 Sep 2026 16:32:49 +0800 Subject: [PATCH 5/5] refactor(compiler): remove unnecessary Phase 2 comments --- src/Generator/ClosureGenerator.php | 1 - src/Translator.php | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Generator/ClosureGenerator.php b/src/Generator/ClosureGenerator.php index a0593143..98db63cd 100644 --- a/src/Generator/ClosureGenerator.php +++ b/src/Generator/ClosureGenerator.php @@ -130,7 +130,6 @@ protected function parseNativeLocalClosureAssignment(Expr\Assign $assign): ?stri $entryIndent = $this->indentLevel; $entryInGeneratorBody = $this->inGeneratorBody; - // Get inferred types from call sites (Phase 2) $inferredTypes = $candidate['inferredParamTypes'] ?? array_fill(0, count($expr->params), Type::VAR); $parameters = []; diff --git a/src/Translator.php b/src/Translator.php index 2482e5b9..22b3cefd 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -5092,7 +5092,6 @@ protected function parseFunction(Node\Stmt\Function_|Node\Stmt\ClassMethod $v): if ($v->stmts && !$this->class && $this->methodDef === null) { $analyzer = new LocalClosureAnalyzer(); $this->context->localClosureCandidates = $analyzer->analyze($v->stmts); - // Infer parameter types from call sites (Phase 2) foreach ($this->context->localClosureCandidates as $closureName => &$candidate) { $candidate['inferredParamTypes'] = $analyzer->inferParamTypes($candidate); }