Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
100 changes: 84 additions & 16 deletions lib/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -1237,13 +1237,7 @@ protected function compileEnumBody(CfgBlock $block): Block
$result = new Block($block);
foreach ($block->children as $child) {
if ($child instanceof Op\Terminal\Const_) {
$this->compileOps($child->valueBlock->children, $result);
AttributeNames::assertNoDuplicates(AttributeNames::fromOp($child));
$result->addOpCode(new OpCode(
OpCode::TYPE_DECLARE_CLASS_CONST,
$this->compileOperand($child->name, $result, true),
$this->compileOperand($child->value, $result, true)
));
$this->compileClassConstDeclaration($child, $result);
continue;
}
if ($child instanceof Op\Stmt\ClassMethod) {
Expand Down Expand Up @@ -1504,15 +1498,7 @@ protected function compileClassBody(CfgBlock $block, int $type): Block {
) {
$this->throwCompileLogic('Class constants are only supported on classes, interfaces, and traits for now');
}
$this->compileOps($child->valueBlock->children, $result);
AttributeNames::assertNoDuplicates(AttributeNames::fromOp($child));
$constOp = new OpCode(
OpCode::TYPE_DECLARE_CLASS_CONST,
$this->compileOperand($child->name, $result, true),
$this->compileOperand($child->value, $result, true)
);
$constOp->deprecatedMetadata = DeprecatedMetadata::fromOp($child);
$result->addOpCode($constOp);
$this->compileClassConstDeclaration($child, $result);
break;
case Op\Stmt\TraitUse::class:
if (OpCode::TYPE_DECLARE_CLASS !== $type) {
Expand All @@ -1535,6 +1521,88 @@ protected function compileClassBody(CfgBlock $block, int $type): Block {
return $result;
}

protected function compileClassConstDeclaration(Op\Terminal\Const_ $child, Block $result): void
{
$valueSlot = $this->tryFoldClassConstValueSlot($child, $result);
if (null === $valueSlot) {
$this->compileOps($child->valueBlock->children, $result);
$valueSlot = $this->compileOperand($child->value, $result, true);
}
$typeSlot = null;
if (null !== $child->declaredType && $child->declaredType instanceof Op\Type\Literal) {
$declared = Type::fromDecl($child->declaredType->name);
if (Variable::TYPE_UNDEFINED !== Variable::mapFromType($declared)) {
$typeSlot = $this->compileTypeConstrainedVariable($result, $declared);
if (isset($result->constants[$valueSlot])) {
$this->verifyClassConstCompileTimeType($child->name, $result->constants[$valueSlot], $declared);
}
}
}
$constOp = new OpCode(
OpCode::TYPE_DECLARE_CLASS_CONST,
$this->compileOperand($child->name, $result, true),
$valueSlot,
$typeSlot
);
$constOp->deprecatedMetadata = DeprecatedMetadata::fromOp($child);
AttributeNames::assertNoDuplicates(AttributeNames::fromOp($child));
$result->addOpCode($constOp);
}

protected function tryFoldClassConstValueSlot(Op\Terminal\Const_ $terminal, Block $block): ?int
{
if (null !== $terminal->valueBlock && [] !== $terminal->valueBlock->children) {
$children = $terminal->valueBlock->children;
if (1 === \count($children) && $children[0] instanceof Op\Expr\Array_) {
$vm = $this->tryBuildCompileTimeArrayFromExpr($children[0]);
if (null !== $vm) {
return $block->registerConstant(new Operand\Temporary(), $vm);
}
}
}
$vm = $this->vmVariableFromCfgLiteralOperand($terminal->value);
if (null === $vm) {
return null;
}

return $block->registerConstant(new Operand\Temporary(), $vm);
}

protected function verifyClassConstCompileTimeType(Operand $nameOp, Variable $value, Type $declared): void
{
$mapped = Variable::mapFromType($declared);
if (Variable::TYPE_UNDEFINED === $mapped) {
return;
}
if ($value->type === $mapped) {
return;
}
$constName = $nameOp instanceof Operand\Literal ? (string) $nameOp->value : 'constant';
$expected = self::vmTypeName($mapped);
$given = self::vmTypeName($value->type);
throw new \TypeError("Cannot assign {$given} to class constant {$constName} of type {$expected}");
}

private static function vmTypeName(int $type): string
{
switch ($type) {
case Variable::TYPE_INTEGER:
return 'int';
case Variable::TYPE_FLOAT:
return 'float';
case Variable::TYPE_BOOLEAN:
return 'bool';
case Variable::TYPE_STRING:
return 'string';
case Variable::TYPE_NULL:
return 'null';
case Variable::TYPE_ARRAY:
return 'array';
default:
return 'mixed';
}
}

protected function isPromotedParam(Op\Expr\Param $param): bool
{
return property_exists($param, 'promotionFlags') && 0 !== $param->promotionFlags;
Expand Down
19 changes: 19 additions & 0 deletions lib/JIT/Builtin/Type/Object_.php
Original file line number Diff line number Diff line change
Expand Up @@ -1521,6 +1521,19 @@ public function definePropertyDefault(int $classId, string $name, VMVariable $va
public function defineClassConst(int $classId, string $name, VMVariable $value): void
{
$key = strtolower($name);
if (VMVariable::TYPE_ARRAY === $value->type) {
$table = $value->toArray();
if (!$table instanceof \PHPCompiler\VM\HashTable) {
throw new \LogicException('Class constant array must be a HashTable');
}
$this->classConstants[$classId][$key] = [
'type' => Variable::TYPE_HASHTABLE,
'value' => null,
'vmTable' => $table,
];

return;
}
$this->classConstants[$classId][$key] = [
'type' => Variable::fromVMVariable($value->type),
'value' => $this->compileTimeValueFromVm($value),
Expand Down Expand Up @@ -2037,6 +2050,12 @@ private function jitConstantFromEntry(array $entry): Variable
Variable::KIND_VARIABLE,
$slot
);
case Variable::TYPE_HASHTABLE:
if (!isset($entry['vmTable']) || !$entry['vmTable'] instanceof \PHPCompiler\VM\HashTable) {
throw new \LogicException('Missing VM table for class constant array');
}

return HashTableHelper::variableFromVmHashTable($this->context, $entry['vmTable']);
default:
throw new \LogicException('Unsupported class constant type for JIT');
}
Expand Down
78 changes: 78 additions & 0 deletions lib/JIT/HashTableHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,84 @@ public static function emptyVariable(Context $context): Variable
);
}

public static function variableFromVmHashTable(Context $context, \PHPCompiler\VM\HashTable $table): Variable
{
$ht = self::alloc($context);
$setLong = $context->lookupFunction('__hashtable__setLongAt');
$setStringAt = $context->lookupFunction('__hashtable__setStringAt');
$setStringKey = $context->lookupFunction('__hashtable__setStringKeyString');
foreach ($table->iterateKeyed(true) as [$keyVar, $valueVar]) {
$resolved = $valueVar->resolveIndirect();
if (\PHPCompiler\VM\Variable::TYPE_INTEGER === $keyVar->type) {
$idx = $context->constantFromInteger($keyVar->toInt(), 'size_t');
if (\PHPCompiler\VM\Variable::TYPE_INTEGER === $resolved->type) {
$context->builder->call(
$setLong,
$ht,
$idx,
$context->getTypeFromString('int64')->constInt($resolved->toInt(), false)
);
} elseif (\PHPCompiler\VM\Variable::TYPE_STRING === $resolved->type) {
$str = $context->builder->load(
$context->constantStringFromString($resolved->toString())
);
$context->builder->call($setStringAt, $ht, $idx, $str);
} elseif (\PHPCompiler\VM\Variable::TYPE_BOOLEAN === $resolved->type) {
$context->builder->call(
$setLong,
$ht,
$idx,
$context->getTypeFromString('int64')->constInt($resolved->toBool() ? 1 : 0, false)
);
} elseif (\PHPCompiler\VM\Variable::TYPE_NULL === $resolved->type) {
$nullSlot = JitValueBox::alloc($context);
$context->builder->call(
$context->lookupFunction('__value__writeNull'),
JitValueBox::pointer($context, $nullSlot)
);
self::setAtIndex($context, $ht, $idx, new Variable(
$context,
Variable::TYPE_VALUE,
Variable::KIND_VARIABLE,
$nullSlot
));
} else {
throw new \LogicException('Unsupported class constant array element type for JIT');
}

continue;
}
if (\PHPCompiler\VM\Variable::TYPE_STRING !== $keyVar->type) {
continue;
}
$key = $context->builder->load(
$context->constantStringFromString($keyVar->toString())
);
if (\PHPCompiler\VM\Variable::TYPE_STRING === $resolved->type) {
$str = $context->builder->load(
$context->constantStringFromString($resolved->toString())
);
$context->builder->call($setStringKey, $ht, $key, $str);
} elseif (\PHPCompiler\VM\Variable::TYPE_INTEGER === $resolved->type) {
$context->builder->call(
$setLong,
$ht,
$key,
$context->getTypeFromString('int64')->constInt($resolved->toInt(), false)
);
} else {
throw new \LogicException('Unsupported class constant array element type for JIT');
}
}

return new Variable(
$context,
Variable::TYPE_HASHTABLE,
Variable::KIND_VALUE,
$ht
);
}

/**
* Pack JIT call/recv arguments into a list hashtable (issue #197).
*
Expand Down
32 changes: 27 additions & 5 deletions lib/VM.php
Original file line number Diff line number Diff line change
Expand Up @@ -3312,11 +3312,16 @@ protected function defineClass(ClassEntry $entry, Block $block): void {
}
break;
}
$entry->constants[$name] = VM\ClassConstExpr::resolveValue(
$frame,
$block,
$op->arg2
);
$value = $this->resolveClassConstDefineValue($frame, $block, $op);
if (null !== $op->arg3 && isset($block->constants[$op->arg3])) {
$constraint = $block->constants[$op->arg3]->typeConstraint;
if (null !== $constraint) {
$check = new Variable();
$check->copyFrom($value);
TypeCheck::coerceReturn($check, true, $constraint);
}
}
$entry->constants[$name] = $value;
if (null !== $op->deprecatedMetadata) {
$entry->constDeprecated[$name] = $op->deprecatedMetadata;
}
Expand All @@ -3335,6 +3340,23 @@ protected function defineClass(ClassEntry $entry, Block $block): void {
}
}

private function resolveClassConstDefineValue(Frame $frame, Block $block, OpCode $op): Variable
{
if (isset($block->constants[$op->arg2])) {
$value = new Variable();
$value->copyFrom($block->constants[$op->arg2]);

return $value;
}
if (isset($frame->scope[$op->arg2])) {
$value = new Variable();
$value->copyFrom($frame->scope[$op->arg2]);

return $value;
}
throw new \LogicException('Class constant value must be a compile-time constant');
}

private function isClassBodyDefaultInitOpcode(int $type): bool
{
return OpCode::TYPE_INIT_ARRAY === $type
Expand Down
36 changes: 36 additions & 0 deletions patches/php-cfg-typed-class-const.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
--- vendor/ircmaxell/php-cfg/lib/PHPCfg/Op/Terminal/Const_.php
+++ vendor/ircmaxell/php-cfg/lib/PHPCfg/Op/Terminal/Const_.php
@@ -11,6 +11,7 @@ declare(strict_types=1);

namespace PHPCfg\Op\Terminal;

+use PHPCfg\Op\Type;
use PHPCfg\Block;
use PHPCfg\Op\Terminal;
use PHPCfg\Operand;
@@ -22,6 +23,9 @@ class Const_ extends Terminal

public $valueBlock;

+ /** PHP 8.3+ typed class constant declaration; null when untyped. */
+ public ?Type $declaredType = null;
+
public function __construct(Operand $name, Operand $value, Block $valueBlock, array $attributes = [])
{
parent::__construct($attributes);
--- vendor/ircmaxell/php-cfg/lib/PHPCfg/Parser.php
+++ vendor/ircmaxell/php-cfg/lib/PHPCfg/Parser.php
@@ -256,11 +256,13 @@ class Parser extends ParserAbstract
$value = $this->parseExprNode($const->value);
$this->block = $tmp;

- $this->block->children[] = new Op\Terminal\Const_(
+ $constOp = new Op\Terminal\Const_(
$this->parseExprNode($const->name),
$value, $valueBlock,
$this->mapAttributes($node)
);
+ $constOp->declaredType = null !== $node->type ? $this->parseTypeNode($node->type) : null;
+ $this->block->children[] = $constOp;
}
}
4 changes: 4 additions & 0 deletions script/apply-patches.sh
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,9 @@ patch_already_applied() {
grep -q 'public \\$type;' "$ROOT/vendor/ircmaxell/php-cfg/lib/PHPCfg/Op/Stmt/Property.php" 2>/dev/null \
&& grep -q 'function __construct(Operand \\$name, int \\$visibility' "$ROOT/vendor/ircmaxell/php-cfg/lib/PHPCfg/Op/Stmt/Property.php" 2>/dev/null
;;
php-cfg-typed-class-const.patch)
grep -q 'public ?Type \\$declaredType' "$ROOT/vendor/ircmaxell/php-cfg/lib/PHPCfg/Op/Terminal/Const_.php" 2>/dev/null
;;
php-cfg-assertion-expr-property.patch)
grep -q 'public \\$expr;' "$ROOT/vendor/ircmaxell/php-cfg/lib/PHPCfg/Op/Expr/Assertion.php" 2>/dev/null
;;
Expand Down Expand Up @@ -1861,6 +1864,7 @@ if [[ -d "$ROOT/vendor/ircmaxell/php-cfg" ]]; then
apply_patch "$PATCH_DIR/php-cfg-no-arrow-function.patch"
apply_patch "$PATCH_DIR/php-cfg-no-closure-preg-replace-callback.patch"
apply_patch "$PATCH_DIR/php-cfg-property-type.patch"
apply_patch "$PATCH_DIR/php-cfg-typed-class-const.patch"
apply_patch "$PATCH_DIR/php-cfg-assertion-expr-property.patch"
apply_patch "$PATCH_DIR/php-cfg-yield-from.patch"
apply_patch "$PATCH_DIR/php-cfg-incdec-expr.patch"
Expand Down
8 changes: 8 additions & 0 deletions script/capability-syntax-lib.php
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,14 @@ function syntaxRowDefinitions(): array
'notes' => ['MiniWebApp `Router::DEFAULT_CONTACT_NAME_MAX` (#2059)'],
'probe' => 'class C { private const M = 1; public function f(): int { return self::M; } } echo (new C())->f();',
],
[
'id' => 'typed_class_const',
'construct' => 'PHP 8.3 typed class constants (`const array X = [1,2];`, `const string S = \'a\';`)',
'opcodes' => ['TYPE_DECLARE_CLASS_CONST', 'TYPE_CLASS_CONST_FETCH', 'TYPE_ARRAY_DIM_FETCH'],
'issue' => 3592,
'notes' => ['Compile-time literal fold for array/scalar const values; typed mismatch is compile-time TypeError; JIT lowers immutable array constants (#3592)'],
'probe' => 'class C { public const array X = [1, 2]; } echo C::X[0];',
],
[
'id' => 'late_static_binding',
'construct' => 'Late static binding `static::method()` / `static::class`',
Expand Down
11 changes: 11 additions & 0 deletions test/compliance/cases/language/typed_class_const.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
--TEST--
Language: typed class constants — array and string (issue #3592, Zend zend_constants.c)
--FILE--
<?php
class C {
public const array X = [1, 2];
public const string S = 'a';
}
echo C::X[0], C::S, "\n";
--EXPECT--
1a
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
--TEST--
Language: typed class constant type mismatch — compile-time TypeError (#3592)
--FILE--
<?php
class C {
public const string S = 1;
}
--EXPECT_EXIT--
255