diff --git a/lib/Compiler.php b/lib/Compiler.php index e2da6e10..e5cfe665 100755 --- a/lib/Compiler.php +++ b/lib/Compiler.php @@ -150,11 +150,15 @@ protected function compileTypeConstrainedVariable(Block $block, Type $type): int protected function compileParam(Op\Expr\Param $param, Block $block, int $paramIdx): OpCode { assert(false === $param->byRef); assert(false === $param->variadic); - assert(null === $param->defaultBlock); + $defaultConst = null; + if (null !== $param->defaultVar) { + $defaultConst = $this->compileOperand($param->defaultVar, $block, true); + } return new OpCode( OpCode::TYPE_ARG_RECV, $this->compileOperand($param->result, $block, false), - $paramIdx + $paramIdx, + $defaultConst ); } @@ -655,8 +659,24 @@ protected function compileOperand(Operand $operand, Block $block, bool $isRead): return null; } elseif ($operand instanceof Operand\Literal) { assert($isRead === true); - $return = new Variable(Variable::mapFromType($operand->type)); - switch (Variable::mapFromType($operand->type)) { + $mappedType = null !== $operand->type + ? Variable::mapFromType($operand->type) + : Variable::TYPE_UNDEFINED; + if ($mappedType === Variable::TYPE_UNDEFINED) { + if (is_int($operand->value)) { + $mappedType = Variable::TYPE_INTEGER; + } elseif (is_float($operand->value)) { + $mappedType = Variable::TYPE_FLOAT; + } elseif (is_string($operand->value)) { + $mappedType = Variable::TYPE_STRING; + } elseif (is_bool($operand->value)) { + $mappedType = Variable::TYPE_BOOLEAN; + } elseif (null === $operand->value) { + $mappedType = Variable::TYPE_NULL; + } + } + $return = new Variable($mappedType); + switch ($mappedType) { case Variable::TYPE_STRING: $return->string($operand->value); break; @@ -669,8 +689,10 @@ protected function compileOperand(Operand $operand, Block $block, bool $isRead): case Variable::TYPE_BOOLEAN: $return->bool($operand->value); break; + case Variable::TYPE_NULL: + break; default: - throw new \LogicException("Unknown Literal Operand Type: " . $operand->type); + throw new \LogicException('Unknown Literal Operand Type: ' . ($operand->type ?? 'untyped')); } return $block->registerConstant($operand, $return); } elseif ($operand instanceof Operand\Temporary) { diff --git a/lib/Frame.php b/lib/Frame.php index 13c7b0bd..215513ee 100755 --- a/lib/Frame.php +++ b/lib/Frame.php @@ -26,6 +26,9 @@ class Frame { public ?Variable $returnVar = null; public ?Handler $handler = null; + /** When true, finishing this frame resumes the caller instead of ending execution. */ + public bool $ephemeral = false; + public function __construct(?Handler $handler, ?Block $block, ?Frame $parent, Variable ...$scope) { $this->handler = $handler; $this->block = $block; diff --git a/lib/JIT.php b/lib/JIT.php index 878ef839..c2b2ab78 100644 --- a/lib/JIT.php +++ b/lib/JIT.php @@ -145,7 +145,8 @@ private function compileBlock(Block $block, ?string $funcName = null): PHPLLVM\V if ($isVarArgs) { $this->context->functionProxies[$lcname] = new JIT\Call\Vararg($func, $funcName, count($args)); } else { - $this->context->functionProxies[$lcname] = new JIT\Call\Native($func, $funcName, $args); + $defaultArgs = $this->collectParamDefaults($block); + $this->context->functionProxies[$lcname] = new JIT\Call\Native($func, $funcName, $args, $defaultArgs); } } @@ -727,4 +728,42 @@ private function jitTypeFromLlvmValue(PHPLLVM\Value $value): int } } + /** + * @return array + */ + private function collectParamDefaults(Block $block): array { + $defaults = []; + foreach ($block->opCodes as $op) { + if ($op->type !== OpCode::TYPE_ARG_RECV || null === $op->arg3) { + continue; + } + if (!isset($block->constants[$op->arg3])) { + continue; + } + $defaults[$op->arg2] = $this->jitVariableFromVmConstant($block->constants[$op->arg3]); + } + return $defaults; + } + + private function jitVariableFromVmConstant(VM\Variable $vm): Variable { + switch ($vm->type) { + case VM\Variable::TYPE_INTEGER: + return Variable::fromConstantInt($this->context, $vm->toInt()); + case VM\Variable::TYPE_STRING: + $lit = new Operand\Literal($vm->toString()); + $lit->type = Type::string(); + return Variable::fromLiteral($this->context, $lit); + case VM\Variable::TYPE_FLOAT: + $lit = new Operand\Literal($vm->toFloat()); + $lit->type = Type::float(); + return Variable::fromLiteral($this->context, $lit); + case VM\Variable::TYPE_BOOLEAN: + $lit = new Operand\Literal($vm->toBool()); + $lit->type = Type::bool(); + return Variable::fromLiteral($this->context, $lit); + default: + throw new \LogicException('Unsupported default parameter type for JIT'); + } + } + } diff --git a/lib/JIT.pre b/lib/JIT.pre index aff76d38..00f43c6b 100755 --- a/lib/JIT.pre +++ b/lib/JIT.pre @@ -142,7 +142,8 @@ class JIT { if ($isVarArgs) { $this->context->functionProxies[$lcname] = new JIT\Call\Vararg($func, $funcName, count($args)); } else { - $this->context->functionProxies[$lcname] = new JIT\Call\Native($func, $funcName, $args); + $defaultArgs = $this->collectParamDefaults($block); + $this->context->functionProxies[$lcname] = new JIT\Call\Native($func, $funcName, $args, $defaultArgs); } } @@ -609,4 +610,42 @@ class JIT { } } + /** + * @return array + */ + private function collectParamDefaults(Block $block): array { + $defaults = []; + foreach ($block->opCodes as $op) { + if ($op->type !== OpCode::TYPE_ARG_RECV || null === $op->arg3) { + continue; + } + if (!isset($block->constants[$op->arg3])) { + continue; + } + $defaults[$op->arg2] = $this->jitVariableFromVmConstant($block->constants[$op->arg3]); + } + return $defaults; + } + + private function jitVariableFromVmConstant(VM\Variable $vm): Variable { + switch ($vm->type) { + case VM\Variable::TYPE_INTEGER: + return Variable::fromConstantInt($this->context, $vm->toInt()); + case VM\Variable::TYPE_STRING: + $lit = new Operand\Literal($vm->toString()); + $lit->type = Type::string(); + return Variable::fromLiteral($this->context, $lit); + case VM\Variable::TYPE_FLOAT: + $lit = new Operand\Literal($vm->toFloat()); + $lit->type = Type::float(); + return Variable::fromLiteral($this->context, $lit); + case VM\Variable::TYPE_BOOLEAN: + $lit = new Operand\Literal($vm->toBool()); + $lit->type = Type::bool(); + return Variable::fromLiteral($this->context, $lit); + default: + throw new \LogicException('Unsupported default parameter type for JIT'); + } + } + } diff --git a/lib/JIT/Call/Native.php b/lib/JIT/Call/Native.php index ddc62c62..834d32cc 100644 --- a/lib/JIT/Call/Native.php +++ b/lib/JIT/Call/Native.php @@ -24,15 +24,27 @@ class Native implements Call { public string $name; public array $argTypes; - public function __construct(Value $function, string $name, array $argTypes) { + /** @var array compile-time defaults for optional parameters */ + public array $defaultArgs = []; + + public function __construct(Value $function, string $name, array $argTypes, array $defaultArgs = []) { $this->function = $function; $this->name = $name; $this->argTypes = $argTypes; + $this->defaultArgs = $defaultArgs; } public function call(Context $context, Variable ... $args): Value { $argValues = []; - foreach ($args as $index => $arg) { + $total = count($this->argTypes); + for ($index = 0; $index < $total; $index++) { + if (isset($args[$index])) { + $arg = $args[$index]; + } elseif (isset($this->defaultArgs[$index])) { + $arg = $this->defaultArgs[$index]; + } else { + throw new \LogicException("Missing required argument {$index} for {$this->name}()"); + } $argValues[] = $this->compileArg($context, $arg, $index); } return $context->builder->call( diff --git a/lib/JIT/Call/Native.pre b/lib/JIT/Call/Native.pre index 1e64102d..a051c9e3 100755 --- a/lib/JIT/Call/Native.pre +++ b/lib/JIT/Call/Native.pre @@ -21,15 +21,27 @@ class Native implements Call { public string $name; public array $argTypes; - public function __construct(Value $function, string $name, array $argTypes) { + /** @var array compile-time defaults for optional parameters */ + public array $defaultArgs = []; + + public function __construct(Value $function, string $name, array $argTypes, array $defaultArgs = []) { $this->function = $function; $this->name = $name; $this->argTypes = $argTypes; + $this->defaultArgs = $defaultArgs; } public function call(Context $context, Variable ... $args): Value { $argValues = []; - foreach ($args as $index => $arg) { + $total = count($this->argTypes); + for ($index = 0; $index < $total; $index++) { + if (isset($args[$index])) { + $arg = $args[$index]; + } elseif (isset($this->defaultArgs[$index])) { + $arg = $this->defaultArgs[$index]; + } else { + throw new \LogicException("Missing required argument {$index} for {$this->name}()"); + } $argValues[] = $this->compileArg($context, $arg, $index); } return $context->builder->call( diff --git a/lib/VM.php b/lib/VM.php index d358a3a3..76d65c35 100755 --- a/lib/VM.php +++ b/lib/VM.php @@ -240,8 +240,15 @@ public function run(Block $block): int { case OpCode::TYPE_ARG_RECV: // Todo: do type checks and transformations $arg1 = $frame->scope[$op->arg1]; - $arg1->copyFrom($frame->calledArgs[$op->arg2]); - break; + if (array_key_exists($op->arg2, $frame->calledArgs)) { + $arg1->copyFrom($frame->calledArgs[$op->arg2]); + break; + } + if (null !== $op->arg3 && isset($frame->block->constants[$op->arg3])) { + $arg1->copyFrom($frame->block->constants[$op->arg3]); + break; + } + throw new \LogicException('Missing required argument ' . $op->arg2); case OpCode::TYPE_DECLARE_CLASS: $name = $frame->scope[$op->arg1]->toString(); $lcname = strtolower($name); @@ -331,6 +338,9 @@ public function run(Block $block): int { throw new \LogicException("VM OpCode Not Implemented: " . $op->getType()); } } + if ($frame->ephemeral) { + goto nextframe; + } return self::SUCCESS; } diff --git a/patches/php-cfg-mixed-reserved.patch b/patches/php-cfg-mixed-reserved.patch new file mode 100644 index 00000000..f3b9c74f --- /dev/null +++ b/patches/php-cfg-mixed-reserved.patch @@ -0,0 +1,32 @@ +--- a/lib/PHPCfg/Op/Type/Mixed.php ++++ b/lib/PHPCfg/Op/Type/Mixed_.php +@@ -13,5 +13,5 @@ namespace PHPCfg\Op\Type; + + use PHPCfg\Op\Type; + +-class Mixed extends Type ++class Mixed_ extends Type + { + } +--- a/lib/PHPCfg/Parser.php ++++ b/lib/PHPCfg/Parser.php +@@ -173,7 +173,7 @@ class Parser + return new Op\Type\Literal($type); + } + +- return new Op\Type\Mixed(); ++ return new Op\Type\Mixed_(); + } + + private function processType(?Node $type): Op\Type +--- a/lib/PHPCfg/Printer.php ++++ b/lib/PHPCfg/Printer.php +@@ -246,7 +246,7 @@ class Printer + if ($type instanceof Op\Type\Literal) { + return $type->name; + } +- if ($type instanceof Op\Type\Mixed) { ++ if ($type instanceof Op\Type\Mixed_) { + return 'mixed'; + } + if ($type instanceof Op\Type\Nullable) { diff --git a/patches/php-types-mixed-reserved.patch b/patches/php-types-mixed-reserved.patch new file mode 100644 index 00000000..f8a6e5c0 --- /dev/null +++ b/patches/php-types-mixed-reserved.patch @@ -0,0 +1,57 @@ +--- a/lib/PHPTypes/Type.php ++++ b/lib/PHPTypes/Type.php +@@ -293,6 +293,9 @@ class Type + if ($decl instanceof CfgType\Literal) { + return self::fromDecl($decl->name); + } ++ if ($decl instanceof CfgType\Mixed_) { ++ return self::mixed(); ++ } + + throw new \LogicException('Unsupported declaration type: '.get_class($decl)); + } +--- a/lib/PHPTypes/TypeReconstructor.php ++++ b/lib/PHPTypes/TypeReconstructor.php +@@ -393,9 +393,14 @@ class TypeReconstructor + protected function resolveOp_Expr_Param(Operand $var, Op\Expr\Param $op, SplObjectStorage $resolved) + { + $type = $this->resolveOpType($op->declaredType); +- if ($op->defaultVar) { +- if ($op->defaultBlock->children[0]->getType() === 'Expr_ConstFetch' && strtolower($op->defaultBlock->children[0]->name->value) === 'null') { ++ if ($op->defaultVar && $op->defaultBlock && isset($op->defaultBlock->children[0])) { ++ $defaultOp = $op->defaultBlock->children[0]; ++ if ( ++ method_exists($defaultOp, 'getType') ++ && $defaultOp->getType() === 'Expr_ConstFetch' ++ && strtolower($defaultOp->name->value) === 'null' ++ ) { + $type = (new Type(Type::TYPE_UNION, [$type, Type::null()]))->simplify(); + } + } +@@ -541,7 +546,7 @@ class TypeReconstructor + foreach ($this->state->classes as $class) { + foreach ($class->stmts->children as $stmt) { + if ($stmt instanceof Op\Stmt\Property) { +- if ($stmt->declaredType instanceof Op\Type\Mixed) { ++ if ($stmt->declaredType instanceof Op\Type\Mixed_) { + $stmt->type = Type::extractTypeFromComment('var', $stmt->getAttribute('doccomment')); + } else { + $stmt->type = $this->resolveOpType($stmt->declaredType); +@@ -677,7 +682,7 @@ class TypeReconstructor + foreach ($class->stmts->children as $property) { + if ($property instanceof Op\Stmt\Property) { + $property->type = $this->resolveOpType($property->declaredType); +- if ($property->declaredType instanceof Op\Type\Mixed && $property->type) { ++ if ($property->declaredType instanceof Op\Type\Mixed_ && $property->type) { + $property->declaredType = new Op\Type\Literal($property->type->toDecl()); + } + } +@@ -694,7 +699,7 @@ class TypeReconstructor + + private function resolveOpType(Op\Type $type): Type + { +- if ($type instanceof Op\Type\Mixed) { ++ if ($type instanceof Op\Type\Mixed_) { + return Type::mixed(); + } + if ($type instanceof Op\Type\Nullable) { diff --git a/script/apply-patches.sh b/script/apply-patches.sh index 41c5add8..47a985f0 100755 --- a/script/apply-patches.sh +++ b/script/apply-patches.sh @@ -36,10 +36,12 @@ if [[ -d "$ROOT/vendor/ircmaxell/php-types" ]]; then apply_patch "$PATCH_DIR/php-types-binaryop-spaceship.patch" apply_patch "$PATCH_DIR/php-types-str-bool-fns.patch" apply_patch "$PATCH_DIR/php-types-dollars-brace.patch" + apply_patch "$PATCH_DIR/php-types-mixed-reserved.patch" fi if [[ -d "$ROOT/vendor/ircmaxell/php-cfg" ]]; then apply_patch "$PATCH_DIR/php-cfg-dollars-brace.patch" + apply_patch "$PATCH_DIR/php-cfg-mixed-reserved.patch" fi if [[ -d "$ROOT/vendor/pre/plugin" ]]; then diff --git a/test/compliance/cases/language/default_params.phpt b/test/compliance/cases/language/default_params.phpt new file mode 100644 index 00000000..a9cd06bc --- /dev/null +++ b/test/compliance/cases/language/default_params.phpt @@ -0,0 +1,14 @@ +--TEST-- +Default parameter values and optional arguments +--FILE-- +