Skip to content

feat(closure): closure parameter type narrowing from call-site literals - #103

Open
yuan-dian wants to merge 5 commits into
swoole:masterfrom
yuan-dian:closure-type-narrowing-v2
Open

feat(closure): closure parameter type narrowing from call-site literals#103
yuan-dian wants to merge 5 commits into
swoole:masterfrom
yuan-dian:closure-type-narrowing-v2

Conversation

@yuan-dian

Copy link
Copy Markdown
Contributor

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

Scenario Before After Speedup
fn($x)(3.14) float 28ms 4ms 7x
fn($x)("hello") string 140ms 22ms 6x
fn($x)(true) bool 10ms 3ms 3x
fn($x)(42) int 6ms 3ms 2x
fn($x)($i) variable 35ms 35ms no change
fn($x)([1,2,3]) array 360ms 360ms no change

Changes

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 ConstFetch
  • ClosureGenerator.php: Use inferred types in lambda signatures, skip type checks when effectiveType is native, add newClosureWithParameters for closures with type checks
  • Translator.php: Wire inferParamTypes() into candidate processing
  • FunctionContext.php: Document callSites and inferredParamTypes keys

Tests:

  • 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 guard
  • closure-param-type.php: Test fixture with 20+ scenarios
  • closure-param-type-class.php: Class method guard test
  • closure-param-type-inference.phpt: End-to-end integration test

How It Works

  1. Analysis phase: LocalClosureAnalyzer tracks all call sites for each closure candidate and infers parameter types from literal arguments
  2. Code generation: If all call sites pass the same type (or type declaration exists), the lambda uses native C++ type (e.g., php::Int, php::Float) instead of php::Var
  3. Fallback: Multiple call sites with different types → php::Var + runtime type check (unchanged behavior)

Scope

  • ✅ Scalar type declarations (int, float, string, bool)
  • ✅ Array type declarations
  • ✅ Call-site literal inference (all scalar types + array)
  • ✅ Unary expressions (-42, +42)
  • ✅ Boolean expressions (===, ||, instanceof)
  • ✅ String concatenation ("hello" . "world")
  • ✅ Cast expressions ((int)"42")
  • ✅ ConstFetch (true, false)
  • ✅ FuncCall (count, strlen, sizeof)
  • ❌ Class method closures (guard: stays as Zend closure)
  • ❌ Closures with goto (invalidated)
  • ❌ Closures with nested functions (invalidated)

Safety

  • No behavior change for existing code
  • Multi-call closures with different types fall back to php::Var
  • Class method closures remain as Zend closures
  • Closures with goto/labels are invalidated
  • 60 closure tests pass, full test suite 1987 tests (8 pre-existing failures unchanged)

…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 matyhtf left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Explicit parameter declarations must remain authoritative. resolveEffectiveClosureParamType() currently always returns the inferred call-site type and ignores Node\Param::$type. As a result, fn(int $value) => $value called with "12" returns a string instead of raising TypeError. Call-site inference must never bypass or replace an explicit declared type.

  2. 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 as FLOAT, while TypePHP promotes high-precision literals to Decimal. For example, passing 3.14159265358979323846 currently generates a php::Float lambda parameter but a php::Variant/Decimal argument, and the generated C++ does not compile. BigInt and other existing promotions need the same canonical handling.

  3. Several operator result types are incorrect. The spaceship operator (<=>) returns int, not bool, and unary +/- cannot simply inherit a boolean operand type. At present both 1 <=> 2 and -true can be narrowed to bool and change -1 into true.

  4. The PR description says narrowing occurs when all call sites have the same type, but inferParamTypes() falls back to VAR whenever there is more than one call site. Please either implement the documented behavior safely or adjust the scope and description.

  5. 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 use a; the goto test also succeeds because another closure already contains newClosureWithParameters. 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 matyhtf left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

感谢继续完善这个优化。最新版本的基础方向是合理的:为不逃逸的局部 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. 显式 arrayobject 和具体 class 参数遇到错误实参时会生成无效 C++

例如:

$fn = fn(array $value): int => count($value);
return $fn(42);

当前生成:

auto fn = [](php::Array value) mutable -> php::Var { ... };
fn(42L);

最终由 C++ 编译器报错,而不是保留原来的 PHP 参数检查并在运行时抛出 TypeErrorobject 和具体 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 实际无法编译。

请至少增加:

  1. 完整 fixture 的 C++ 编译/运行测试,特别是 Decimal 和 pow()
  2. 显式 array/object/class 参数接收错误实参的测试,确认程序能够编译,并产生预期的参数类型错误;
  3. 对所有允许收窄的类型验证生成表达式的真实 C++ 表示,而不只是断言 lambda 签名字符串。

完成以上两项语义修复和端到端测试后,再进行下一轮 review。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants