Skip to content
Open
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
600 changes: 600 additions & 0 deletions Zend/Optimizer/dfa_pass.c

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions Zend/Optimizer/zend_optimizer.c
Original file line number Diff line number Diff line change
Expand Up @@ -1306,7 +1306,7 @@ static void zend_optimize(zend_op_array *op_array,
}
}

static void zend_revert_pass_two(zend_op_array *op_array)
void zend_revert_pass_two(zend_op_array *op_array)
{
zend_op *opline;

Expand Down Expand Up @@ -1336,7 +1336,7 @@ static void zend_revert_pass_two(zend_op_array *op_array)
op_array->fn_flags &= ~ZEND_ACC_DONE_PASS_TWO;
}

static void zend_redo_pass_two(zend_op_array *op_array)
void zend_redo_pass_two(zend_op_array *op_array)
{
zend_op *opline, *end;
#if ZEND_USE_ABS_JMP_ADDR && !ZEND_USE_ABS_CONST_ADDR
Expand Down
2 changes: 2 additions & 0 deletions Zend/Optimizer/zend_optimizer.h
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ typedef void (*zend_optimizer_pass_t)(zend_script *, void *context);

BEGIN_EXTERN_C()
ZEND_API void zend_optimize_script(zend_script *script, zend_long optimization_level, zend_long debug_level);
ZEND_API uint32_t zend_aot_monomorphize_script(zend_script *script, zend_long opt_level);
ZEND_API uint32_t zend_aot_upgrade_dispatch_to_ucall(zend_script *script);
ZEND_API int zend_optimizer_register_pass(zend_optimizer_pass_t pass);
ZEND_API void zend_optimizer_unregister_pass(int idx);
zend_result zend_optimizer_startup(void);
Expand Down
2 changes: 2 additions & 0 deletions Zend/Optimizer/zend_optimizer_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ void zend_optimizer_pass3(zend_op_array *op_array, zend_optimizer_ctx *ctx);
void zend_optimize_func_calls(zend_op_array *op_array, zend_optimizer_ctx *ctx);
void zend_optimize_cfg(zend_op_array *op_array, zend_optimizer_ctx *ctx);
void zend_optimize_dfa(zend_op_array *op_array, zend_optimizer_ctx *ctx);
void zend_revert_pass_two(zend_op_array *op_array);
void zend_redo_pass_two(zend_op_array *op_array);
zend_result zend_dfa_analyze_op_array(zend_op_array *op_array, zend_optimizer_ctx *ctx, zend_ssa *ssa);
void zend_dfa_optimize_op_array(zend_op_array *op_array, zend_optimizer_ctx *ctx, zend_ssa *ssa, zend_call_info **call_map);
void zend_optimize_temporary_variables(zend_op_array *op_array, zend_optimizer_ctx *ctx);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
--TEST--
Reification: a non-turbofish call to an all-defaulted generic callee with non-literal args infers nothing and stays generic (must not build an empty type-arg box)
--FILE--
<?php
// Inferring zero type args must bail, not build a zero-length box (underflow -> crash).
function neighbors<TNode = mixed, TWeight = mixed>(mixed $graph, TNode $node): array {
return [$node];
}

function caller(mixed $g, mixed $n): array {
return neighbors($g, $n);
}

var_dump(caller('graph', 'x'));
var_dump(caller('graph', 42));
echo "ok\n";
?>
--EXPECT--
array(1) {
[0]=>
string(1) "x"
}
array(1) {
[0]=>
int(42)
}
ok
26 changes: 26 additions & 0 deletions Zend/tests/generics/reification/inference_literal_arg.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
--TEST--
Reification: a scalar literal argument monomorphizes a non-turbofish generic call at compile time (no opcache needed); the reified return type enforces the inferred T
--FILE--
<?php
declare(strict_types=1);

function retStr<T : int|float|string>(T $x): T { return "s"; }
function retInt<T : int|float|string>(T $x): T { return 7; }

function lit_int() { return retStr(42); }
function lit_float() { return retStr(3.14); }
function lit_string() { return retInt("hi"); }

foreach (['lit_int', 'lit_float', 'lit_string'] as $fn) {
try {
$fn();
echo "$fn: NO error (not monomorphized!)\n";
} catch (TypeError $e) {
echo "$fn: ", $e->getMessage(), "\n";
}
}
?>
--EXPECT--
lit_int: retStr(): Return value must be of type int, string returned
lit_float: retStr(): Return value must be of type float, string returned
lit_string: retInt(): Return value must be of type string, int returned
45 changes: 45 additions & 0 deletions Zend/tests/generics/reification/inference_optimizer_ssa.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
--TEST--
Reification: the opcache DFA optimizer monomorphizes non-turbofish generic calls from each argument's SSA-inferred type; reassignment binds the actual type, not the declared one, and unknown types stay generic
--EXTENSIONS--
opcache
--INI--
opcache.enable=1
opcache.enable_cli=1
opcache.optimization_level=-1
--FILE--
<?php
declare(strict_types=1);

function retStr<T : int|float|string>(T $x): T { return "s"; }
function retInt<T : int|float|string>(T $x): T { return 7; }

function from_int(int $i) { return retStr($i); }
function from_float(float $f) { return retStr($f); }
function local_literal() { $v = 1.5; return retStr($v); }

// Reassignment must bind T from the SSA type (string), not the declared int.
function reassigned(int $val) { $val = 'x'; return retInt($val); }

function from_unknown(array $a) { $v = $a[0]; return retInt($v); }

function check(string $name, callable $fn): void {
try {
$r = $fn();
echo "$name: ", var_export($r, true), "\n";
} catch (TypeError $e) {
echo "$name: TypeError\n";
}
}

check('from_int', fn() => from_int(1));
check('from_float', fn() => from_float(1.5));
check('local_literal', fn() => local_literal());
check('reassigned', fn() => reassigned(1));
check('from_unknown', fn() => from_unknown([42]));
?>
--EXPECT--
from_int: TypeError
from_float: TypeError
local_literal: TypeError
reassigned: TypeError
from_unknown: 7
28 changes: 28 additions & 0 deletions Zend/tests/generics/reification/preload_aot_call_lowering.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Bench;

function id<T>(T $x): T { return $x; }

function add<T : int|float>(T $a, T $b): T { return $a + $b; }

// No value params: dispatch must fire from the turbofish alone, nothing inferable from args.
function zero<T : int|float|string>(): string { return "z"; }

class Runner {
public static function ints(int $n): int {
$acc = 0;
for ($i = 0; $i < $n; $i++) {
$acc = add::<int>($acc, id::<int>($i));
}
return $acc;
}

public static function strs(): string {
return id::<string>("hello") . ":" . zero::<int>();
}

public static function badType(): void {
add::<int>(1, []);
}
}
36 changes: 36 additions & 0 deletions Zend/tests/generics/reification/preload_aot_call_lowering.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
--TEST--
Reification: AOT-lowered preloaded generic calls (INIT_FCALL/DO_UCALL + concrete RECV + by-value SEND) stay correct and type-safe
--INI--
opcache.enable=1
opcache.enable_cli=1
opcache.optimization_level=-1
opcache.preload={PWD}/preload_aot_call_lowering.inc
--EXTENSIONS--
opcache
--SKIPIF--
<?php
if (PHP_OS_FAMILY == 'Windows') die('skip Preloading is not supported on Windows');
?>
--FILE--
<?php
use Bench\Runner;

var_dump(Runner::ints(5));
var_dump(Runner::strs());

// Weak-mode coercion still applies on the synthesized RECV: "5" -> 5.
var_dump(Bench\add::<int>("5", 2));

try {
Runner::badType();
} catch (\TypeError $e) {
echo $e->getMessage(), "\n";
}
echo "done\n";
?>
--EXPECTF--
int(10)
string(7) "hello:z"
int(7)
Bench\add(): Argument #2 ($b) must be of type int, array given, called in %s on line %d
done
27 changes: 27 additions & 0 deletions Zend/tests/generics/reification/preload_class_monomorph_new.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

// preload_fix_trait_op_array must not re-sync the monomorph's generic-inheritance method clones as trait clones.

final class Vec<T> {
public function __construct(public array $items = []) {}
public function with(T $x): Vec<T> {
$n = $this->items;
$n[] = $x;
return new Vec::<T>($n);
}
public function sum(): int {
$s = 0;
foreach ($this->items as $v) { $s += $v; }
return $s;
}
public function size(): int { return count($this->items); }
}

class Driver {
public static function build(): string {
$v = new Vec::<int>([1, 2]);
$v = $v->with(3);
$v = $v->with(4);
return $v->sum() . ':' . $v->size() . ':' . get_class($v);
}
}
27 changes: 27 additions & 0 deletions Zend/tests/generics/reification/preload_class_monomorph_new.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
--TEST--
Reification: a compile-time class monomorph (`new C::<int>`) inside a preloaded method preloads cleanly and behaves correctly
--INI--
opcache.enable=1
opcache.enable_cli=1
opcache.optimization_level=-1
opcache.preload={PWD}/preload_class_monomorph_new.inc
--EXTENSIONS--
opcache
--SKIPIF--
<?php
if (PHP_OS_FAMILY == 'Windows') die('skip Preloading is not supported on Windows');
?>
--FILE--
<?php
// Regression: preload_fix_trait_op_array deref'd a missing xlat "original" -> SEGV at preload.
echo Driver::build(), "\n";

$s = new Vec::<string>(['a']);
$s = $s->with('b');
echo $s->size(), ':', get_class($s), "\n";
echo "done\n";
?>
--EXPECT--
10:4:Vec<int>
2:Vec<string>
done
18 changes: 18 additions & 0 deletions Zend/tests/generics/reification/preload_class_typed_param.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Zoo;

class Animal {}
class Dog extends Animal {}

// The persisted monomorph's arg_info must name `Zoo\Animal`, not `\Animal`.
function keep<T>(T $x): T { return $x; }

class Keeper {
public static function take(Animal $a): string {
return get_class(keep::<Animal>($a));
}
public static function bad(): void {
keep::<Animal>("not an animal");
}
}
35 changes: 35 additions & 0 deletions Zend/tests/generics/reification/preload_class_typed_param.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
--TEST--
Reification: an AOT-preloaded monomorph of a bare class-typed parameter accepts subclasses and rejects non-instances
--INI--
opcache.enable=1
opcache.enable_cli=1
opcache.optimization_level=-1
opcache.preload={PWD}/preload_class_typed_param.inc
--EXTENSIONS--
opcache
--SKIPIF--
<?php
if (PHP_OS_FAMILY == 'Windows') die('skip Preloading is not supported on Windows');
?>
--FILE--
<?php
use Zoo\Animal;
use Zoo\Dog;
use Zoo\Keeper;

// Regression: persisted monomorph checked global `\Animal` and rejected this valid `Zoo\Dog`.
echo Keeper::take(new Dog()), "\n";
echo Keeper::take(new Animal()), "\n";

try {
Keeper::bad();
} catch (\TypeError $e) {
echo $e->getMessage(), "\n";
}
echo "done\n";
?>
--EXPECTF--
Zoo\Dog
Zoo\Animal
Zoo\keep(): Argument #1 ($x) must be of type Zoo\Animal, string given, called in %s on line %d
done
10 changes: 10 additions & 0 deletions Zend/tests/generics/reification/preload_generic_call_ops.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

// destroy_op_array must not read the preloaded generic op's already-freed shared runtime cache (UAF).
function inner<T : int|float|string>(T $x): T {
return $x;
}

function outer<T : int|float|string>(T $x): T {
return inner($x);
}
25 changes: 25 additions & 0 deletions Zend/tests/generics/reification/preload_generic_call_ops.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
--TEST--
Reification: a preloaded generic function that contains a generic call op resolves correctly and shuts down cleanly (smoke test for the destroy_op_array runtime-cache cleanup; the underlying UAF is caught deterministically under valgrind/ASAN)
--INI--
opcache.enable=1
opcache.enable_cli=1
opcache.optimization_level=-1
opcache.preload={PWD}/preload_generic_call_ops.inc
--EXTENSIONS--
opcache
--SKIPIF--
<?php
if (PHP_OS_FAMILY == 'Windows') die('skip Preloading is not supported on Windows');
?>
--FILE--
<?php
var_dump(outer::<int>(5));
var_dump(outer(7));
var_dump(inner::<string>("ok"));
echo "done\n";
?>
--EXPECT--
int(5)
int(7)
string(2) "ok"
done
36 changes: 36 additions & 0 deletions Zend/tests/generics/reification/turbofish_namespace_class_arg.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
--TEST--
Reification: an unqualified class name in a turbofish type argument resolves against the current namespace
--FILE--
<?php

namespace Acme;

class Animal {}
class Dog extends Animal {}

function identity<T>(T $x): T { return $x; }

$dog = identity::<Animal>(new Dog());
echo get_class($dog), "\n";

$animal = identity::<Animal>(new Animal());
echo get_class($animal), "\n";

try {
identity::<Animal>("not an animal");
} catch (\TypeError $e) {
echo $e->getMessage(), "\n";
}

function wrap<T>(T $x): T { return $x; }
$d2 = wrap::<Animal>(new Dog());
echo get_class($d2), "\n";

echo "done\n";
?>
--EXPECTF--
Acme\Dog
Acme\Animal
Acme\identity(): Argument #1 ($x) must be of type Acme\Animal, string given, called in %s on line %d
Acme\Dog
done
5 changes: 5 additions & 0 deletions Zend/zend_closures.c
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,11 @@ static void zend_create_closure_ex(zval *res, zend_function *func, zend_class_en
if (closure->func.op_array.refcount) {
(*closure->func.op_array.refcount)++;
}
if ((closure->func.common.fn_flags2 & ZEND_ACC2_MONOMORPH_TYPE_ARGS)
&& closure->func.op_array.generic_types
&& closure->func.op_array.generic_types->monomorph_type_args) {
closure->func.op_array.generic_types->monomorph_type_args->refcount++;
}

/* For fake closures, we want to reuse the static variables of the original function. */
HashTable *ht = ZEND_MAP_PTR_GET(func->op_array.static_variables_ptr);
Expand Down
Loading