Compile-Time Check Elimination
A common concern with runtime type enforcement is performance overhead. If every return statement, variable assignment, and property initialization were blindly wrapped in runtime assertions, high-frequency functions and tight loops would suffer from unnecessary Zend VM function-call overhead.
TypePHP solves this with Compile-Time Check Elimination (eliminate_statically_proven_checks => true). During file transformation, TypePHP analyzes AST expressions against declared DocBlock contracts. If an expression is mathematically, logically, or syntactically proven to satisfy its contract, TypePHP completely skips injecting runtime assertion wrappers for that statement.
The Performance Difference: Before vs. After
Consider a standard getter method returning a string literal:
/** @return non-empty-string */
public function getVersion(): string
{
return '2.0.0';
}Without Elimination (Standard Runtime Wrapping)
return ($__typephpRet = \TypePHP\Internal\RuntimeTypeChecker::checkReturn(__METHOD__, '2.0.0', $this)) instanceof \TypePHP\Internal\Diagnostic\ErrorMessage
? throw ...
: $__typephpRet;- Pushes arguments to the VM stack
invokes RuntimeTypeChecker::checkReturn(). - Looks up function contract in memory
validates string length > 0. - Executes
T_INSTANCEOFevaluates ternary branch. - Cost: ~50 Zend VM opcodes and stack frame allocations on every single call.
With Compile-Time Elimination (TypePHP Optimization)
Because '2.0.0' is proven to be a non-empty-string at compile time, TypePHP emits clean, unmodified PHP code:
return '2.0.0';- Cost: 1 single Zend VM opcode (
ZEND_RETURN). - Execution Speed: Runs at 100% native PHP C-speed (~15–30M calls/sec).
- JIT Inlining Synergy: Because there are no dynamic wrapper calls or exception branches, PHP 8's Tracing JIT compiler can inline the getter directly into caller machine code.
Interactive Demo: Inspecting Transformed Source Code
Click the Transformed button in any of the snippets below to inspect how TypePHP compiles the code. Notice that statements with proven literals, shapes, constants, or variables have zero assertion wrappers injected, while dynamic expressions maintain runtime protection.
Scalars, Numbers & Arithmetic Expressions
ScalarEvaluator and ArithmeticEvaluator analyze scalar literals, constants, string prefixes, and folded arithmetic expressions. Operations like 60 * 60 or "user_" . $id are evaluated statically:
Generating AST transformation...Class Constants, Wildcards & Enums
ConstantAndEnumEvaluator statically inspects class constants, wildcard patterns (Class::PREFIX_*), and PHP 8.1+ Enum cases and backing values:
Generating AST transformation...Literal Arrays, Lists & Shapes
ArrayLiteralEvaluator recursively inspects array structures. If every key and element expression in an array is statically proven, TypePHP skips injecting collection loops and shape assertion wrappers:
Generating AST transformation...Local Typed Variables (@var) & Assignments
Compile-time check elimination applies directly to local variable assignments and destructuring. When a variable is initialized with a proven literal, TypePHP emits a raw $x = ...; assignment without injecting RuntimeTypeChecker::checkVariable():
Generating AST transformation...Object Instantiations & Identity Returns ($this, new static)
ObjectEvaluator statically handles exact class instantiations and strict object identity:
Generating AST transformation...Preceding Local Assignment Propagation
TypePHP tracks immediate predecessor local variable assignments within function bodies. If a local variable was just assigned a proven literal, returning that variable benefits from compile-time elimination:
Generating AST transformation...Generator Yield & Send Optimization
GeneratorEvaluator statically optimizes generator bodies, eliminating yield, yield from, send, and return wrappers when contracts are proven:
Generating AST transformation...Elimination Safety Policy (EliminationSafetyPolicy)
To ensure that compile-time check elimination never compromises type safety or creates false negatives, TypePHP enforces strict safety policies.
When Class Methods Are Strictly Deferred to Runtime
Return checks on class methods are strictly deferred to runtime unless:
- The method is
private: Private methods cannot override parent contracts and cannot be overridden by child classes. - The class has no inheritance (
hasRealInheritance === false): Standalone classes with noextends,implements, oruse Traitcannot violate Liskov Substitution. - Standalone Functions & Closures: Global functions, anonymous closures, and arrow functions have zero inheritance risk and are always eligible for elimination.
All public and protected methods in classes with inheritance preserve runtime return checks to guarantee parent contracts cannot be bypassed.
Why Reified Generic Objects Are Never Eliminated
When an expression instantiates a generic class (new Collection([1, 2])), TypePHP never eliminates the check.
Runtime generic checks perform the critical side-effect of stamping template bindings (T = int) onto the object instance in \WeakMap memory. Omitting the check would prevent downstream methods ($col->add()) from knowing the instance's generic state!
(Note: Generic array containers like list<T> and array<K, V> carry no object state and are fully eligible for compile-time elimination).
Summary of Statically Eliminated Scenarios
| Expression Pattern | Target Contract | Elimination Result |
|---|---|---|
| Literal Scalars | return 42; vs @return positive-intreturn 'admin'; vs @return non-empty-string | Injected check is completely omitted. Statement remains clean return 'admin';. |
| Integer Ranges | return 50; vs @return int<1, 100> | Evaluated statically against range bounds. Check is omitted. |
| String Concatenations | return 'user_' . $id; vs @return non-empty-string | Prefix |
| Class Constants | return self::MODE_TEST; vs @return self::MODE_* | Constant wildcard prefix matched at compile time. Check is omitted. |
| Enum Cases & Values | return Status::PENDING; vs @return Statusreturn Status::PENDING->value; vs value-of<Status> | Case and backing values matched at compile time. Check is omitted. |
| Empty Array Literals | return []; vs @return list<T> or array<K, V> | In PHP, [] is always a valid list/array. Check is omitted. |
| Sequential Lists | return [1, 2, 3]; vs @return list<positive-int> | All keys verified sequential and values satisfy contract. Check is omitted. |
| Array Shapes | return ['id' => 1]; vs array{id: int} | All required keys present and values statically proven. Check is omitted. |
| Exact Class Instantiations | return new User('Alice'); vs @return User | FQCN matches without reflection. Check is omitted. |
| Identity Returns | return $this; vs @return $this / @return selfreturn new static(); vs @return static | Strict identity truth. Check is omitted; fluent chaining runs at native speed. |
| Property Defaults | public int $timeout = 30; vs @var positive-int | Default value is statically proven. No constructor assertions synthesized. |
Local @var Assignments | /** @var positive-int $x */ $x = 10; | Emits clean $x = 10; without checkVariable() wrapper. |
| Safe By-Ref Foreach | foreach ($scores as &$s) { $s += 5; } | Post-loop container assertion is omitted when loop body is free of opaque mutations. |
Configuration & Disabling Check Elimination
Compile-time check elimination is enabled by default (true). You can disable it for deep debugging, tracing every single check in stack traces, or benchmarking unoptimized AST instrumentation:
In typephp.php
return [
'eliminate_statically_proven_checks' => false,
];In composer.json
{
"extra": {
"typephp": {
"eliminate-statically-proven-checks": false
}
}
}Via Environment Variable
export TYPEPHP_ELIMINATE_STATICALLY_PROVEN_CHECKS=false