Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
44f8542
Implement static type evaluators for AST expressions
rcalicdan Oct 19, 2026
c3d31b4
Add comprehensive unit tests for StaticTypeEvaluator and related comp…
rcalicdan Oct 19, 2026
8fdfc00
Enhance ContractVisitor and FunctionContractInjector to support curre…
rcalicdan Oct 19, 2026
75ea10c
Refactor internal command classes to add @internal annotations and en…
rcalicdan Oct 19, 2026
a38a2d2
Fix phpstan errors
rcalicdan Oct 19, 2026
f16a2f0
improve aritmentic evaluation handling for statically provable types
rcalicdan Oct 19, 2026
e3b6493
fix autoloading deadlocks in compile time optimization for objects
rcalicdan Oct 19, 2026
acad4ee
Enhance FunctionContractInjector to tag preceding assignments and imp…
rcalicdan Oct 20, 2026
13f53f5
Refactor error handling in ContractVisitor to streamline type parsing…
rcalicdan Oct 20, 2026
5b3cc56
Update cache version prefix to v0.12.0
rcalicdan Oct 20, 2026
835a243
Implement generator evaluation and elimination checks
rcalicdan Oct 22, 2026
868b017
fix code styling
rcalicdan Oct 22, 2026
c3e51d5
Implement type deduction and reference assignment checks in ContractV…
rcalicdan Oct 23, 2026
43481b3
Implement property dimension assignment checks and validation; add te…
rcalicdan Oct 23, 2026
7624edc
Enhance MemberAssignChecker with caching improvements and reset funct…
rcalicdan Oct 23, 2026
c48c387
Add PropertyRefTargetFixture and SameFilePropertyRefFixture with test…
rcalicdan Oct 23, 2026
4bcd6c3
Enhance type inference and scope management by adding inferred variab…
rcalicdan Oct 23, 2026
fb2e80e
Refactor ContractVisitor to improve type handling and reduce code dup…
rcalicdan Oct 23, 2026
c6cfcd3
Refactor FunctionContractInjector and TemplateManager for improved lo…
rcalicdan Oct 25, 2026
8c5eaf2
Fix formatting in FunctionContractInjector by adding spaces and new l…
rcalicdan Oct 25, 2026
a6750ac
Enhance StreamWrapper by adding registration and unregistration aroun…
rcalicdan Oct 25, 2026
2dcad26
fix code styling
rcalicdan Oct 25, 2026
f3662f0
Enhance DocblockExtractor to merge bounds and defaults for template t…
rcalicdan Oct 25, 2026
07d7ef3
Enhance FunctionContractInjector and TypePHPPrinter for improved hand…
rcalicdan Oct 26, 2026
b07c7d9
Refactor StreamWrapper to simplify line number preservation logic and…
rcalicdan Oct 26, 2026
0c97dd7
Refactor FunctionContractInjector and TypePHPPrinter for improved han…
rcalicdan Oct 26, 2026
cdc7348
Add reset method to GenericValidator and enhance int-mask tests for e…
rcalicdan Oct 27, 2026
f3c3c3f
Fix phpstan errors and wndows specific failing test
rcalicdan Oct 28, 2026
06c6a15
Remove roave/security-advisories from require-dev in composer.json
rcalicdan Oct 28, 2026
e1f6a37
Improve test coverage
rcalicdan Oct 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ index.php
benchmark.php
.php-cs-fixer.cache
/app
/coverage-report
/coverage-report
/coverage
1 change: 0 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
"nikic/php-parser": "^5.3"
},
"require-dev": {
"roave/security-advisories": "dev-latest",
"laravel/pint": "^1.10",
"pestphp/pest": "^2.0 || ^3.0 || ^4.0",
"phpstan/phpstan": "^2.1",
Expand Down
724 changes: 541 additions & 183 deletions src/Internal/Ast/ContractVisitor.php

Large diffs are not rendered by default.

149 changes: 149 additions & 0 deletions src/Internal/Ast/Elimination/EliminationSafetyPolicy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php

declare(strict_types=1);

namespace TypePHP\Internal\Ast\Elimination;

use PhpParser\Node\Expr\ArrowFunction;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Function_;
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;

/**
* Enforces LSP safety rules and runtime state invariants to determine
* whether a check can be safely eliminated during AST transformation.
*
* @internal
*/
final class EliminationSafetyPolicy
{
/**
* O(1) hash map of generic containers that carry no object state or WeakMap template bindings.
*
* @var array<string, true>
*/
private const PERMISSIBLE_NON_OBJECT_GENERICS = [
'int' => true,
'integer' => true,
'int-mask' => true,
'int-mask-of' => true,
'value-of' => true,
'list' => true,
'non-empty-list' => true,
'array' => true,
'non-empty-array' => true,
'iterable' => true,
];

/**
* O(1) hash map of permissible generator return type containers.
*
* @var array<string, true>
*/
private const PERMISSIBLE_GENERATOR_GENERICS = [
'generator' => true,
'iterable' => true,
'traversable' => true,
'iterator' => true,
];

/**
* Determines whether a return check can be safely eliminated under LSP rules.
*
* Flow of logic:
* 1. Returns false if no return type is declared.
* 2. Rejects generic object classes (e.g. Collection<Dog>) to ensure runtime
* WeakMap template bindings are preserved. Permits array containers (list<T>, array<K, V>)
* and scalar generics (int<min, max>, int-mask) which carry no object state.
* 3. Authorizes standalone functions, closures, and arrow functions (zero inheritance risk).
* 4. For class methods:
* - Authorizes private methods (cannot override or be overridden).
* - Authorizes classes with no inheritance (`hasRealInheritance === false`).
* 5. Defers all other methods to runtime to prevent bypassing unanalyzed parent contracts.
*
* @param array{name?: ?string, hasInheritance?: bool, hasRealInheritance?: bool, hasPropertyWithDoc?: bool, isReadonly?: bool, isFinal?: bool, hasTemplates?: bool}|null $classContext
*/
public static function isReturnEliminationSafe(
ClassMethod|Function_|Closure|ArrowFunction $functionNode,
?array $classContext,
?TypeNode $returnTypeNode
): bool {
if ($returnTypeNode === null) {
return false;
}

if ($returnTypeNode instanceof GenericTypeNode) {
$baseName = strtolower($returnTypeNode->type->name);
if (! isset(self::PERMISSIBLE_NON_OBJECT_GENERICS[$baseName])) {
return false;
}
}

if ($classContext === null || $functionNode instanceof Function_ || $functionNode instanceof Closure || $functionNode instanceof ArrowFunction) {
return true;
}

if ($functionNode->isPrivate()) {
return true;
}

$hasRealInheritance = $classContext['hasRealInheritance'] ?? true;
if (! $hasRealInheritance) {
return true;
}

return false;
}

/**
* Determines whether generator yield, send, and return checks can be safely eliminated under LSP rules.
*
* @param array{name?: ?string, hasInheritance?: bool, hasRealInheritance?: bool, hasPropertyWithDoc?: bool, isReadonly?: bool, isFinal?: bool, hasTemplates?: bool}|null $classContext
*/
public static function isGeneratorEliminationSafe(
ClassMethod|Function_|Closure|ArrowFunction $functionNode,
?array $classContext,
?TypeNode $returnTypeNode
): bool {
if ($returnTypeNode === null) {
return false;
}

if ($returnTypeNode instanceof GenericTypeNode) {
$baseName = strtolower(ltrim($returnTypeNode->type->name, '\\'));
if (! isset(self::PERMISSIBLE_GENERATOR_GENERICS[$baseName])) {
return false;
}
} elseif ($returnTypeNode instanceof IdentifierTypeNode) {
$baseName = strtolower(ltrim($returnTypeNode->name, '\\'));
if (! isset(self::PERMISSIBLE_GENERATOR_GENERICS[$baseName])) {
return false;
}
} elseif (! ($returnTypeNode instanceof ArrayTypeNode)) {
return false;
}

if ($classContext === null || $functionNode instanceof Function_ || $functionNode instanceof Closure || $functionNode instanceof ArrowFunction) {
return true;
}

if ($classContext['hasTemplates'] ?? false) {
return false;
}

if ($functionNode->isPrivate()) {
return true;
}

$hasRealInheritance = $classContext['hasRealInheritance'] ?? true;
if (! $hasRealInheritance) {
return true;
}

return false;
}
}
118 changes: 118 additions & 0 deletions src/Internal/Ast/Elimination/Evaluator/ArithmeticEvaluator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

declare(strict_types=1);

namespace TypePHP\Internal\Ast\Elimination\Evaluator;

use PhpParser\Node\Expr;
use PhpParser\Node\Expr\BinaryOp;
use PhpParser\Node\Expr\UnaryMinus;
use PhpParser\Node\Expr\UnaryPlus;
use PhpParser\Node\Scalar\DNumber;
use PhpParser\Node\Scalar\LNumber;

/**
* Statically folds deterministic binary and unary arithmetic expressions at compile time,
* handling IEEE 754 floating-point calculations with epsilon tolerance.
*
* @internal
*/
final class ArithmeticEvaluator
{
/**
* Standard IEEE 754 comparison tolerance matching runtime ConstValidator.
*/
public const FLOAT_EPSILON = 1e-9;

/**
* Recursively folds an arithmetic expression into a deterministic int or float.
*
* Flow of logic:
* 1. Resolves integer and float scalar literal values.
* 2. Evaluates unary signs (+ and -) on folded sub-expressions.
* 3. Recursively evaluates left and right operands of binary arithmetic operations.
* 4. Guards against division and modulo by zero, aborting on invalid arithmetic.
* 5. Computes addition, subtraction, multiplication, division, modulo, and exponentiation.
* 6. Verifies that the resulting number is finite (not NAN or INF).
*/
public static function evaluate(Expr $expr): int|float|null
{
if ($expr instanceof LNumber) {
return $expr->value;
}

if ($expr instanceof DNumber) {
return $expr->value;
}

if ($expr instanceof UnaryMinus) {
$inner = self::evaluate($expr->expr);

return $inner !== null ? -$inner : null;
}

if ($expr instanceof UnaryPlus) {
return self::evaluate($expr->expr);
}

if ($expr instanceof BinaryOp) {
return self::evaluateBinaryOp($expr);
}

return null;
}

/**
* Computes the result of a binary arithmetic operation on folded operands.
*/
private static function evaluateBinaryOp(BinaryOp $expr): int|float|null
{
$left = self::evaluate($expr->left);
$right = self::evaluate($expr->right);

if ($left === null || $right === null) {
return null;
}

$result = match ($expr::class) {
BinaryOp\Plus::class => $left + $right,
BinaryOp\Minus::class => $left - $right,
BinaryOp\Mul::class => $left * $right,
BinaryOp\Div::class => self::divide($left, $right),
BinaryOp\Mod::class => self::modulo($left, $right),
BinaryOp\Pow::class => $left ** $right,
default => null,
};

if ($result === null || ! is_finite((float) $result)) {
return null;
}

return $result;
}

/**
* Safely executes division, guarding against division by zero.
*/
private static function divide(int|float $left, int|float $right): int|float|null
{
if (abs((float) $right) < self::FLOAT_EPSILON) {
return null;
}

return $left / $right;
}

/**
* Safely executes integer modulo arithmetic, guarding against modulo by zero.
*/
private static function modulo(int|float $left, int|float $right): int|null
{
$intRight = (int) $right;
if ($intRight === 0) {
return null;
}

return (int) $left % $intRight;
}
}
Loading
Loading