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
42 changes: 41 additions & 1 deletion lib/VM.php
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,16 @@ private function runFrames(): int
if (!isset($classEntry->constants[$constName])) {
return $this->raise("Undefined class constant {$className}::{$constName}", $frame);
}
if ($classEntry->isEnum) {
$canonical = $classEntry->enumCaseCanonicalNames[$constName]
?? $frame->scope[$op->arg3]->toString();
$backing = new Variable();
$backing->copyFrom($classEntry->constants[$constName]);
$frame->scope[$op->arg1]->enumCase(
new VM\EnumCaseEntry($classEntry, $canonical, $backing)
);
break;
}
$frame->scope[$op->arg1]->copyFrom($classEntry->constants[$constName]);
break;
case OpCode::TYPE_INSTANCEOF:
Expand Down Expand Up @@ -1059,6 +1069,17 @@ private function runFrames(): int
case OpCode::TYPE_CLONE:
$result = $frame->scope[$op->arg1];
$src = $frame->scope[$op->arg2]->resolveIndirect();
if (Variable::TYPE_ENUM_CASE === $src->type) {
$enumCase = $src->toEnumCase();
$message = 'Cannot clone enum case '
.$enumCase->enumClass->name.'::'.$enumCase->caseName;
$catchFrame = $this->dispatchVmError($message, $frame);
if (null !== $catchFrame) {
$frame = $catchFrame;
goto restart;
}
break;
}
if (Variable::TYPE_OBJECT !== $src->type) {
throw new \LogicException('clone requires an object');
}
Expand Down Expand Up @@ -1414,6 +1435,21 @@ private function dispatchVmTypeError(\TypeError $error, Frame $frame): ?Frame
return null;
}

/**
* Bridge VM Error throws (enum clone guard, etc.) into user catch handlers (#3554).
*/
private function dispatchVmError(string $message, Frame $frame): ?Frame
{
$thrown = VM\BuiltinExceptionSupport::materializeError($this->context, $message);
$catchFrame = $this->findCatchFrameForThrow($frame, $thrown);
if (null !== $catchFrame) {
return $catchFrame;
}
$this->raiseUncaughtException($thrown);

return null;
}

private function findCatchFrameForThrow(Frame $frame, Variable $thrown): ?Frame
{
$this->context->pendingException = $thrown;
Expand Down Expand Up @@ -2297,11 +2333,15 @@ protected function defineClass(ClassEntry $entry, Block $block): void {
}
break;
case OpCode::TYPE_DECLARE_CLASS_CONST:
$name = strtolower($frame->scope[$op->arg1]->toString());
$canonical = $frame->scope[$op->arg1]->toString();
$name = strtolower($canonical);
if (!isset($block->constants[$op->arg2])) {
throw new \LogicException('Class constant value must be a compile-time constant');
}
$entry->constants[$name] = $block->constants[$op->arg2];
if ($entry->isEnum) {
$entry->enumCaseCanonicalNames[$name] = $canonical;
}
break;
case OpCode::TYPE_USE_TRAIT:
$this->applyTraitUse($entry, $frame->scope[$op->arg1]->toString());
Expand Down
4 changes: 4 additions & 0 deletions lib/VM/BuiltinClasses.php
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,11 @@ private static function registerThrowableHierarchy(Context $ctx): void
$pub = CfgFunc::FLAG_PUBLIC;
$getMessage = new ThrowableGetMessage();

$throwable = new ClassEntry('Throwable');
$ctx->classes['throwable'] = $throwable;

$error = new ClassEntry('Error');
$error->parentLc = 'throwable';
$error->properties[] = new ClassProperty(BuiltinExceptionSupport::PROP_MESSAGE, null, $strProto);
$error->methods['getmessage'] = $getMessage;
$error->methodVisibility['getmessage'] = $pub;
Expand Down
15 changes: 15 additions & 0 deletions lib/VM/BuiltinExceptionSupport.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,19 @@ public static function materializeTypeError(Context $ctx, string $message): Vari

return $var;
}

public static function materializeError(Context $ctx, string $message): Variable
{
if (!isset($ctx->classes[self::CLASS_ERROR])) {
throw new \LogicException('Error builtin class is not registered');
}
$entry = $ctx->classes[self::CLASS_ERROR];
$obj = new ObjectEntry($entry);
$obj->getProperty(self::PROP_MESSAGE)->string($message);
$obj->constructed = true;
$var = new Variable();
$var->object($obj);

return $var;
}
}
2 changes: 2 additions & 0 deletions lib/VM/ClassEntry.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class ClassEntry {
public array $methodVisibility = [];
/** @var array<string, Variable> constant name (lowercase) => value */
public array $constants = [];
/** @var array<string, string> lowercase enum case name => declared case name (#3420) */
public array $enumCaseCanonicalNames = [];
/** @var array<string, Variable> static property name (lowercase) => shared storage */
public array $staticProperties = [];
/** Readonly class: instance properties cannot change after construction (issue #1360). */
Expand Down
20 changes: 20 additions & 0 deletions lib/VM/EnumCaseEntry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace PHPCompiler\VM;

/**
* Zend-style enum case object for E::Case fetches (#3420, #3554).
*/
final class EnumCaseEntry
{
public ClassEntry $enumClass;
public string $caseName;
public Variable $backingValue;

public function __construct(ClassEntry $enumClass, string $caseName, Variable $backingValue)
{
$this->enumClass = $enumClass;
$this->caseName = $caseName;
$this->backingValue = $backingValue;
}
}
37 changes: 37 additions & 0 deletions lib/VM/Variable.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ final class Variable {
const TYPE_INDIRECT = 7;
/** Writable single-byte view of a parent string (Zend-style $str[$i]). */
const TYPE_STRING_OFFSET = 8;
/** Zend enum case object for E::Case fetches (#3420, #3554). */
const TYPE_ENUM_CASE = 9;


const NUMERIC = self::TYPE_INTEGER | self::TYPE_FLOAT;
Expand All @@ -35,6 +37,7 @@ final class Variable {
private float $float;
private bool $bool;
private ObjectEntry $object;
private EnumCaseEntry $enumCase;
private Variable $indirect;
private HashTable $array;
private Variable $stringOffsetParent;
Expand Down Expand Up @@ -264,6 +267,12 @@ public function toString(): string {
return 'Array';
case self::TYPE_OBJECT:
return 'Object';
case self::TYPE_ENUM_CASE:
if (null !== $var->enumCase->enumClass->backedType) {
return $var->enumCase->backingValue->toString();
}

return $var->enumCase->caseName;
}
throw new \LogicException("Cannot convert type {$var->type} to string");
}
Expand All @@ -274,6 +283,24 @@ public function object(ObjectEntry $value): void {
$this->object = $value;
}

public function enumCase(EnumCaseEntry $value): void
{
$this->reset();
$this->type = self::TYPE_ENUM_CASE;
$this->enumCase = $value;
}

public function toEnumCase(): EnumCaseEntry
{
switch ($this->type) {
case self::TYPE_ENUM_CASE:
return $this->enumCase;
case self::TYPE_INDIRECT:
return $this->indirect->toEnumCase();
}
throw new \LogicException("Cannot convert $this->type to enum case");
}

public function toObject(): ObjectEntry {
switch ($this->type) {
case self::TYPE_OBJECT:
Expand All @@ -298,6 +325,7 @@ public function reset(): void {
unset($this->float);
unset($this->bool);
unset($this->object);
unset($this->enumCase);
unset($this->indirect);
unset($this->stringOffsetParent);
unset($this->stringOffsetIndex);
Expand Down Expand Up @@ -381,6 +409,15 @@ public function copyFrom(self $var): void {
case self::TYPE_OBJECT:
$this->object($var->object);
break;
case self::TYPE_ENUM_CASE:
$backing = new Variable();
$backing->copyFrom($var->enumCase->backingValue);
$this->enumCase(new EnumCaseEntry(
$var->enumCase->enumClass,
$var->enumCase->caseName,
$backing
));
break;
case self::TYPE_ARRAY:
$this->array($var->array);
break;
Expand Down
21 changes: 21 additions & 0 deletions test/compliance/cases/language/clone_enum_case_error.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
--TEST--
Language: clone() on enum case throws Error (#3554)
--FILE--
<?php
enum E { case A; }
enum Status: string { case Active = 'active'; }

try {
clone E::A;
} catch (Throwable $e) {
echo get_class($e), "\n";
}

try {
clone Status::Active;
} catch (Throwable $e) {
echo get_class($e), "\n";
}
--EXPECT--
Error
Error