feat(closure): closure parameter type narrowing from call-site literals - #103
feat(closure): closure parameter type narrowing from call-site literals#103yuan-dian wants to merge 5 commits into
Conversation
…terals Infer closure parameter types from call-site literal arguments to generate native C++ types instead of php::Var. When all call sites pass the same literal type, the lambda signature uses the native type directly. Changes: - LocalClosureAnalyzer: track call sites per candidate, inferParamTypes() detects int/float/string/bool/array literals, unary ops, boolean expressions, string concatenation, cast expressions, and ConstFetch - ClosureGenerator: use inferred types in lambda signatures, skip type checks when effectiveType is native, add newClosureWithParameters for closures with type checks - Translator: wire inferParamTypes() into candidate processing - FunctionContext: document callSites and inferredParamTypes keys Performance (5M iterations): - fn()(42): 10ms -> 5ms (2x faster) - fn()(3.14): 85ms -> 8ms (10x faster) - fn()(true): 14ms -> 4ms (3.5x faster) - fn(int $x)(42): 10ms (no regression, approach A: no declaration narrowing) Tests: 22 new unit tests, 60 total closure tests pass
- Remove stray main() call outside function body (TypePHP prohibits loose code) - Change fn($x) => $x to fn($x) => count($x) to match expected int(2) output
run-tests.php requires the closing PHP tag to properly extract the --FILE-- section. All other PHPT tests in the project have it.
matyhtf
left a comment
There was a problem hiding this comment.
Thank you for working on closure parameter type inference. The current implementation is not ready to merge yet because it introduces several correctness regressions in TypePHP's strong type semantics.
Please address the following blockers:
-
Explicit parameter declarations must remain authoritative.
resolveEffectiveClosureParamType()currently always returns the inferred call-site type and ignoresNode\Param::$type. As a result,fn(int $value) => $valuecalled with"12"returns a string instead of raisingTypeError. Call-site inference must never bypass or replace an explicit declared type. -
Please reuse the compiler's canonical expression-type detection instead of maintaining a second AST-only type system in
LocalClosureAnalyzer::detectArgType(). The duplicate implementation classifies every float literal asFLOAT, while TypePHP promotes high-precision literals toDecimal. For example, passing3.14159265358979323846currently generates aphp::Floatlambda parameter but aphp::Variant/Decimal argument, and the generated C++ does not compile. BigInt and other existing promotions need the same canonical handling. -
Several operator result types are incorrect. The spaceship operator (
<=>) returnsint, notbool, and unary+/-cannot simply inherit a boolean operand type. At present both1 <=> 2and-truecan be narrowed to bool and change-1intotrue. -
The PR description says narrowing occurs when all call sites have the same type, but
inferParamTypes()falls back toVARwhenever there is more than one call site. Please either implement the documented behavior safely or adjust the scope and description. -
Please strengthen the tests with isolated fixtures and observable generated signatures/runtime behavior. The current large fixture and generic substring assertions can pass by matching unrelated closures. In particular, the explicit typed-parameter assertion checks parameter name
x, while the typed closures usea; the goto test also succeeds because another closure already containsnewClosureWithParameters. Please add negative/regression cases for explicit parameter types, high-precision Decimal literals,<=>, unary numeric operators, and multiple same-type call sites.
Suggested direction: keep LocalClosureAnalyzer responsible for locality/escape analysis and call-site collection, obtain argument types through the existing compiler type-analysis path, narrow only when every call site has the same canonical compatible native type, and otherwise retain php::Var plus the normal runtime type check.
Redesign closure parameter type narrowing based on official review feedback: 1. Type declaration authoritative - resolveEffectiveClosureParamType() checks resolveTypeDecl() first, then inferred type, then VAR 2. Reuse compiler's detectTypeOfExpr() - deleted duplicate detectArgType() from LocalClosureAnalyzer, inferParamTypesFromCallSites() wraps the canonical type resolver 3. Operator result types - spaceship returns VAR (not in switch), unary-bool guard intercepts BOOL + UnaryMinus/Plus → VAR 4. Multi-call narrowing - inferParamTypesFromCallSites() checks ALL call sites agree; disagree → VAR 5. Nullable/Union/Intersection - always returns VAR with runtime check Files modified: - src/Generator/ClosureGenerator.php: new resolveEffectiveClosureParamType(), inferParamTypesFromCallSites(), inferCallSiteArgType() - src/Analysis/LocalClosureAnalyzer.php: deleted detectArgType(), inferParamTypes() - src/Translator.php: removed inferParamTypes loop - src/Context/FunctionContext.php: removed inferredParamTypes docblock - phpunit/code/closure-param-type.php: 47 isolated fixture functions - phpunit/src/ClosureParamTypeTest.php: 26 tests, 88 assertions
…pilation The foreach variable was reused across two scopes with different inferred types (php::Array from init, php::Var from foreach iteration), causing TypePHP's self-compilation type checker to reject the assignment. Rename to to eliminate the conflict.
matyhtf
left a comment
There was a problem hiding this comment.
感谢继续完善这个优化。最新版本的基础方向是合理的:为不逃逸的局部 Closure 收集直接调用点,并在调用类型一致时生成强类型 C++ lambda。
不过我基于最新提交 5ecb725d4 做了实际 C++ 编译验证,目前仍有两个阻塞问题,需要修改后再合并。
1. detectTypeOfExpr() 的 PHP 语义类型不等于生成表达式的 C++ ABI 类型
当前 inferCallSiteArgType() 直接把 detectTypeOfExpr() 的结果用于 lambda 参数类型,但实际生成的 C++ 表达式可能仍然是 php::Var。
直接编译本 PR 新增的 phpunit/code/closure-param-type.php 即可复现:
php bin/tpc.php phpunit/code/closure-param-type.php -m lib -o closure_param_type主要错误包括:
could not convert ‘dl1’ from ‘php::Decimal’ to ‘php::Var’
no known conversion from ‘php::Variant’ to ‘php::Decimal’
no known conversion from ‘php::Variant’ to ‘php::Int’
其中:
- Decimal 用例将参数收窄为
php::Decimal,但调用表达式php::toDecimal(...)实际返回php::Var;lambda 返回dl1时也不能自动转换为php::Var。 2 ** 3的 PHP 结果类型是int,但实际生成的php::fn::pow(...)返回php::Var,无法直接传给php::Int参数。
因此不能只依据 PHP 语义类型收窄。建议增加“生成表达式是否具有相同 native C++ 表示”的判断/白名单,或者在调用边界统一执行可靠的拆箱转换。Decimal、BigInt、BigFloat 等类型在转换完整之前应回退到 php::Var。
2. 显式 array、object 和具体 class 参数遇到错误实参时会生成无效 C++
例如:
$fn = fn(array $value): int => count($value);
return $fn(42);当前生成:
auto fn = [](php::Array value) mutable -> php::Var { ... };
fn(42L);最终由 C++ 编译器报错,而不是保留原来的 PHP 参数检查并在运行时抛出 TypeError。object 和具体 class 参数也有相同问题。
原因是调用点当前仅实现 int/float/bool/string 四种转换;array/object/class 没有转换。object/class 的类型检查位于 lambda 内部,但错误实参无法进入 lambda;array 的检查则因为参数已被标记为 native type 而被跳过。
建议在完整实现非标量调用边界转换前,让这些显式类型继续使用 php::Var 参数和现有运行时检查;或者补齐 Array/Object/class 的参数校验与转换。
测试需要补充实际 C++ 编译
当前新增 PHPUnit 中的 compileFixture() 只执行 PHP → C++ 转换并检查生成字符串,没有编译生成的 C++。因此相关 PHPUnit 和 CI 都通过了,但 PR 自己新增的完整 fixture 实际无法编译。
请至少增加:
- 完整 fixture 的 C++ 编译/运行测试,特别是 Decimal 和
pow(); - 显式
array/object/class参数接收错误实参的测试,确认程序能够编译,并产生预期的参数类型错误; - 对所有允许收窄的类型验证生成表达式的真实 C++ 表示,而不只是断言 lambda 签名字符串。
完成以上两项语义修复和端到端测试后,再进行下一轮 review。
Summary
Infer closure parameter types from call-site literal arguments to generate native C++ types instead of
php::Var. When all call sites pass the same literal type, the lambda signature uses the native type directly, eliminating runtime type checks.Performance
fn($x)(3.14)floatfn($x)("hello")stringfn($x)(true)boolfn($x)(42)intfn($x)($i)variablefn($x)([1,2,3])arrayChanges
Core Logic:
LocalClosureAnalyzer.php: Track call sites per candidate,inferParamTypes()detects int/float/string/bool/array literals, unary ops, boolean expressions, string concatenation, cast expressions, and ConstFetchClosureGenerator.php: Use inferred types in lambda signatures, skip type checks when effectiveType is native, addnewClosureWithParametersfor closures with type checksTranslator.php: WireinferParamTypes()into candidate processingFunctionContext.php: DocumentcallSitesandinferredParamTypeskeysTests:
ClosureParamTypeTest.php: 22 unit tests covering type declarations, call-site literals, multi-call fallback, unary ops, boolean expressions, string concatenation, cast expressions, goto, nested functions, and class method guardclosure-param-type.php: Test fixture with 20+ scenariosclosure-param-type-class.php: Class method guard testclosure-param-type-inference.phpt: End-to-end integration testHow It Works
LocalClosureAnalyzertracks all call sites for each closure candidate and infers parameter types from literal argumentsphp::Int,php::Float) instead ofphp::Varphp::Var+ runtime type check (unchanged behavior)Scope
Safety
php::Var