Skip to content
Prev Previous commit
Next Next commit
Shadow ConstantArrayType natively
The array-shape type: key/value/optional-key/next-auto-index bookkeeping,
every array operation with shape precision, isSuperTypeOf/accepts over
optional keys, list handling, unsealed shapes, describe in all forms,
generalize, template inference and toPhpDocNode, mirrored method by
method (the largest single port). The native class declares the twin's
twelve properties in its order, the $optionalKeySet memo included, so
its slots are the generated ones. The type-family differential gains 48
subjects (439k observations).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSCEwuXjDzsNw2wSNsDW6D
  • Loading branch information
ondrejmirtes and claude committed Sep 21, 2026
commit 9e026fa06ab453b94d30926d49ba5ae2b015b52d
6 changes: 6 additions & 0 deletions build/PHPStan/Build/TurboAttributeCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeUnsealedTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
Expand Down Expand Up @@ -138,6 +141,9 @@ final class TurboAttributeCollector
'callableTypeNode' => CallableTypeNode::class,
'callableTypeParameterNode' => CallableTypeParameterNode::class,
'templateTagValueNode' => TemplateTagValueNode::class,
'arrayShapeNode' => ArrayShapeNode::class,
'arrayShapeItemNode' => ArrayShapeItemNode::class,
'arrayShapeUnsealedTypeNode' => ArrayShapeUnsealedTypeNode::class,
];

private string $realRoot;
Expand Down
2 changes: 2 additions & 0 deletions src/DependencyInjection/BleedingEdgeToggle.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

use Generator;
use PHPStan\ShouldNotHappenException;
use PHPStan\Turbo\ReferencedByTurboExtension;

#[ReferencedByTurboExtension(key: 'bleedingEdgeToggle')]
final class BleedingEdgeToggle
{

Expand Down
4 changes: 2 additions & 2 deletions src/Type/Constant/ConstantArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
use PHPStan\Rules\Arrays\AllowedArrayKeysTypes;
use PHPStan\ShouldNotHappenException;
use PHPStan\TrinaryLogic;
use PHPStan\Turbo\ReferencedByTurboExtension;
use PHPStan\Turbo\ShadowedByTurboExtension;
use PHPStan\Type\AcceptsResult;
use PHPStan\Type\Accessory\AccessoryArrayListType;
use PHPStan\Type\Accessory\AccessoryLowercaseStringType;
Expand Down Expand Up @@ -101,7 +101,7 @@
* @api
*/
#[InstanceofDeprecated(insteadUse: 'Type::getConstantArrays()')]
#[ReferencedByTurboExtension(key: 'constantArrayType')]
#[ShadowedByTurboExtension(implementation: __DIR__ . '/../../../turbo-ext/src/ConstantArrayType.cpp')]
class ConstantArrayType implements Type
{

Expand Down
2 changes: 2 additions & 0 deletions src/Type/Constant/ConstantArrayTypeAndMethod.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@

use PHPStan\ShouldNotHappenException;
use PHPStan\TrinaryLogic;
use PHPStan\Turbo\ReferencedByTurboExtension;
use PHPStan\Type\Type;

/**
* @api
*/
#[ReferencedByTurboExtension(key: 'constantArrayTypeAndMethod')]
final class ConstantArrayTypeAndMethod
{

Expand Down
2 changes: 1 addition & 1 deletion turbo-ext/config.w32
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ if (PHP_PHPSTAN_TURBO != "no") {
// directory by splitting on backslashes only — with forward slashes the
// objects compile flat while the link list expects the subpath
EXTENSION("phpstan_turbo",
"src\\main.cpp src\\support.cpp src\\AcceptsResult.cpp src\\AccessoryArrayListType.cpp src\\AccessoryDecimalIntegerStringType.cpp src\\AccessoryLiteralStringType.cpp src\\AccessoryLowercaseStringType.cpp src\\AccessoryNonEmptyStringType.cpp src\\AccessoryNonFalsyStringType.cpp src\\AccessoryNumericStringType.cpp src\\AccessoryUppercaseStringType.cpp src\\ArenaCache.cpp src\\ArrayType.cpp src\\BooleanType.cpp src\\ClassStringType.cpp src\\CombinationsHelper.cpp src\\ConditionalExpressionHolder.cpp src\\ConstantBooleanType.cpp src\\ConstantFloatType.cpp src\\ConstantIntegerType.cpp src\\ConstantStringType.cpp src\\EnumCaseObjectType.cpp src\\ExpressionResultStorage.cpp src\\ExpressionTypeHolder.cpp src\\FloatType.cpp src\\GenericClassStringType.cpp src\\GenericObjectType.cpp src\\GenericStaticType.cpp src\\HasMethodType.cpp src\\HasOffsetType.cpp src\\HasOffsetValueType.cpp src\\HasPropertyType.cpp src\\IntegerRangeType.cpp src\\IntegerType.cpp src\\IsSuperTypeOfResult.cpp src\\MixedType.cpp src\\NeverType.cpp src\\NodeScanner.cpp src\\NodeTraverser.cpp src\\NonEmptyArrayType.cpp src\\NonexistentParentClassType.cpp src\\NullType.cpp src\\ObjectShapeType.cpp src\\ObjectType.cpp src\\ObjectWithoutClassType.cpp src\\OversizedArrayType.cpp src\\PharForkGuard.cpp src\\PhpFileCleaner.cpp src\\ScopeContext.cpp src\\ScopeOps.cpp src\\Shadow.cpp src\\StaticType.cpp src\\StrictMixedType.cpp src\\StringType.cpp src\\SymbolFinderInFiles.cpp src\\ThisType.cpp src\\TrinaryLogic.cpp src\\TrustedTypes.cpp src\\TypeCombinatorCache.cpp src\\TypeTraits.cpp src\\VoidType.cpp src\\IterableType.cpp src\\CallableType.cpp src\\ClosureType.cpp",
"src\\main.cpp src\\support.cpp src\\AcceptsResult.cpp src\\AccessoryArrayListType.cpp src\\AccessoryDecimalIntegerStringType.cpp src\\AccessoryLiteralStringType.cpp src\\AccessoryLowercaseStringType.cpp src\\AccessoryNonEmptyStringType.cpp src\\AccessoryNonFalsyStringType.cpp src\\AccessoryNumericStringType.cpp src\\AccessoryUppercaseStringType.cpp src\\ArenaCache.cpp src\\ArrayType.cpp src\\BooleanType.cpp src\\CallableType.cpp src\\ClassStringType.cpp src\\ClosureType.cpp src\\CombinationsHelper.cpp src\\ConditionalExpressionHolder.cpp src\\ConstantArrayType.cpp src\\ConstantBooleanType.cpp src\\ConstantFloatType.cpp src\\ConstantIntegerType.cpp src\\ConstantStringType.cpp src\\EnumCaseObjectType.cpp src\\ExpressionResultStorage.cpp src\\ExpressionTypeHolder.cpp src\\FloatType.cpp src\\GenericClassStringType.cpp src\\GenericObjectType.cpp src\\GenericStaticType.cpp src\\HasMethodType.cpp src\\HasOffsetType.cpp src\\HasOffsetValueType.cpp src\\HasPropertyType.cpp src\\IntegerRangeType.cpp src\\IntegerType.cpp src\\IsSuperTypeOfResult.cpp src\\IterableType.cpp src\\MixedType.cpp src\\NeverType.cpp src\\NodeScanner.cpp src\\NodeTraverser.cpp src\\NonEmptyArrayType.cpp src\\NonexistentParentClassType.cpp src\\NullType.cpp src\\ObjectShapeType.cpp src\\ObjectType.cpp src\\ObjectWithoutClassType.cpp src\\OversizedArrayType.cpp src\\PharForkGuard.cpp src\\PhpFileCleaner.cpp src\\ScopeContext.cpp src\\ScopeOps.cpp src\\Shadow.cpp src\\StaticType.cpp src\\StrictMixedType.cpp src\\StringType.cpp src\\SymbolFinderInFiles.cpp src\\ThisType.cpp src\\TrinaryLogic.cpp src\\TrustedTypes.cpp src\\TypeCombinatorCache.cpp src\\TypeTraits.cpp src\\VoidType.cpp",
true,
flags);
ADD_SOURCES(configure_module_dirname + "/src/parser",
Expand Down
4 changes: 2 additions & 2 deletions turbo-ext/src/AccessoryNonFalsyStringType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,10 @@ class AccessoryNonFalsyStringType
zv::Val falseType = zv::Val::adopt(falseRaw);
zv::Val emptyString = pt_type_new_constant_string("", 0);
if (UNEXPECTED(emptyString.isUndef())) return zv::Val();
zval emptyArrayArgs[2];
zval emptyArrayArgs[2], emptyArrayRaw;
ZVAL_EMPTY_ARRAY(&emptyArrayArgs[0]);
ZVAL_EMPTY_ARRAY(&emptyArrayArgs[1]);
zv::Val emptyArray = pt_type_new(PT_CLASS_CONSTANT_ARRAY_TYPE, 2, emptyArrayArgs);
zv::Val emptyArray = pt_constant_array_type_new(&emptyArrayRaw, &emptyArrayArgs[0], &emptyArrayArgs[1]) ? zv::Val::adopt(emptyArrayRaw) : zv::Val();
if (UNEXPECTED(emptyArray.isUndef())) return zv::Val();
zv::Args args{nullType.raw(), falseType.raw(), emptyString.raw(), emptyArray.raw()};
zv::Val dominated = pt_type_call_static(PT_CLASS_TYPE_COMBINATOR, PT_LC("union"), 4, args);
Expand Down
16 changes: 7 additions & 9 deletions turbo-ext/src/ArrayType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ static zv::Val combinator2(const char *lcname, size_t len, zval *a, zval *b)
/* new ConstantArrayType([], []) */
static zv::Val emptyConstantArray()
{
zval args[2];
ZVAL_EMPTY_ARRAY(&args[0]);
ZVAL_EMPTY_ARRAY(&args[1]);
return pt_type_new(PT_CLASS_CONSTANT_ARRAY_TYPE, 2, args);
zval empty, result;
ZVAL_EMPTY_ARRAY(&empty);
if (UNEXPECTED(!pt_constant_array_type_new(&result, &empty, &empty))) return zv::Val();
return zv::Val::adopt(result);
}

/* new IntersectionType($types) ($types consumed) */
Expand Down Expand Up @@ -242,8 +242,7 @@ class ArrayType
return pt_type_call(Z_OBJ_P(type), PT_LC("isacceptedby"), 2, args);
}

bool isConstantArray;
if (UNEXPECTED(!isInstance(type, PT_CLASS_CONSTANT_ARRAY_TYPE, isConstantArray))) return zv::Val();
bool isConstantArray = instanceof_function(Z_OBJCE_P(type), pt_ce_constant_array_type);
if (isConstantArray) {
zv::Val result = pt_type_accepts_result(PT_TRI_YES);
zval *thisKeyType = keyType();
Expand Down Expand Up @@ -302,9 +301,8 @@ class ArrayType
* pending exception */
zv::Val isSuperTypeOf(zval *type) const
{
bool isConstantArray = false;
bool isArray = instanceof_function(Z_OBJCE_P(type), pt_ce_array_type);
if (!isArray && UNEXPECTED(!isInstance(type, PT_CLASS_CONSTANT_ARRAY_TYPE, isConstantArray))) return zv::Val();
bool isConstantArray = instanceof_function(Z_OBJCE_P(type), pt_ce_constant_array_type);
if (isArray || isConstantArray) {
zv::Val itemType = thisGetItemType();
if (UNEXPECTED(itemType.isUndef())) return zv::Val();
Expand Down Expand Up @@ -1129,7 +1127,7 @@ class ArrayType
zv::Val truncateListToSize(zval *sizeType) const
{
/* [$min, $max] = ConstantArrayType::extractTruncateListBounds($sizeType) */
zv::Val bounds = pt_type_call_static(PT_CLASS_CONSTANT_ARRAY_TYPE, PT_LC("extracttruncatelistbounds"), 1, sizeType);
zv::Val bounds = pt_type_call_static_ce(pt_ce_constant_array_type, PT_LC("extracttruncatelistbounds"), 1, sizeType);
if (UNEXPECTED(bounds.isUndef())) return zv::Val();
if (UNEXPECTED(!zv::Ref(bounds.raw()).isArray())) {
zend_type_error("phpstan_turbo: ConstantArrayType::extractTruncateListBounds() must return array");
Expand Down
25 changes: 1 addition & 24 deletions turbo-ext/src/BooleanType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,30 +88,7 @@ class BooleanType
/* new ConstantArrayType([new ConstantIntegerType(0)], [$this], [1],
* isList: TrinaryLogic::createYes()) — the named argument skips
* $optionalKeys, whose default is [] */
zv::Val toArray() const
{
zv::Val zero = pt_type_new_constant_integer(0);
if (UNEXPECTED(zero.isUndef())) return zv::Val();
zv::Arr keyTypes = zv::Arr::create(1);
keyTypes.push(std::move(zero));
zv::Arr valueTypes = zv::Arr::create(1);
zval selfZv;
ZVAL_OBJ(&selfZv, self);
valueTypes.push(zv::Ref(&selfZv));
zv::Arr nextAutoIndexes = zv::Arr::create(1);
nextAutoIndexes.push(zv::Val::integer(1));
zval args[5];
args[0] = keyTypes.take();
args[1] = valueTypes.take();
args[2] = nextAutoIndexes.take();
ZVAL_EMPTY_ARRAY(&args[3]);
ZVAL_COPY_VALUE(&args[4], pt_trinary_singleton(PT_TRI_YES));
zv::Val result = pt_type_new(PT_CLASS_CONSTANT_ARRAY_TYPE, 5, args);
zval_ptr_dtor(&args[0]);
zval_ptr_dtor(&args[1]);
zval_ptr_dtor(&args[2]);
return result;
}
zv::Val toArray() const { return pt_type_scalar_to_array(self); }

/* new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]) */
static zv::Val toArrayKey() { return zeroOrOne(); }
Expand Down
Loading