Summary
The whole-function JIT can compile functions with untyped integer arguments, but their parameter CV slots are initialized to zero instead of receiving the runtime arguments.
This produces silently incorrect results.
Tested at commit 67ef61e68048a2a508cae3bb0834cfe69863fe46 on Linux x86_64 with Rust 1.96.0.
Current JIT build blocker
At this commit, cargo build --release --features jit first fails because jit_loop_hook() uses ctx without receiving it.
For runtime testing, I applied only the mechanical wiring fix:
-pub(crate) fn jit_loop_hook(&mut self, idx: usize, header: usize, end: usize)
+pub(crate) fn jit_loop_hook(&mut self, ctx: &mut RunCtx, idx: usize, header: usize, end: usize)
and:
-vm.jit_loop_hook(idx, cur, end);
+vm.jit_loop_hook(ctx, idx, cur, end);
No JIT logic was otherwise changed.
Reproducer
<?php
function add1($x) {
return $x + 1;
}
function double($x) {
return $x + $x;
}
function combine($a, $b) {
return add1($a) + double($b);
}
$sum = 0;
for ($i = 0; $i < 5_000_000; $i++) {
$sum += combine($i, $i + 1);
}
echo $sum, PHP_EOL;
Build and run:
cargo build --release --features jit
./target/release/rphp reproducer.php
./target/release/rphp --jit reproducer.php
Result
Without JIT:
With JIT:
Expected:
Suspected root cause
The compiler elides RECV for required untyped by-value parameters because the interpreter receives arguments directly in the callee CV slots.
However, the whole-function JIT uses a different convention:
is_int_only() accepts the resulting op array as JIT-compatible.
translate() initializes every JIT slot to zero.
- Runtime arguments are loaded into CV slots only while translating an
Opcode::Recv.
- Because
RECV was elided, the generated kernel never reads the corresponding values from args_ptr.
As a result, leaf functions behave as if every untyped parameter were zero:
add1($x) => add1(0) => 1
double($x) => double(0) => 0
Each combine() call therefore returns 1, explaining the observed total of exactly 5000000.
The existing parameter JIT test uses a typed parameter:
function kern(int $n): int
That form retains RECV, so it does not exercise the compiler/JIT mismatch for untyped parameters.
Possible fixes
A conservative short-term fix would be to reject whole-function JIT compilation when a parameter has no corresponding RECV.
A more complete fix would initialize parameter CV slots from args_ptr using the op-array parameter/CV metadata, independently of whether the interpreter bytecode retained RECV.
It would also be useful to add an integration test that goes through the normal compiler and VM call path with an untyped function receiving runtime integer arguments.
Summary
The whole-function JIT can compile functions with untyped integer arguments, but their parameter CV slots are initialized to zero instead of receiving the runtime arguments.
This produces silently incorrect results.
Tested at commit
67ef61e68048a2a508cae3bb0834cfe69863fe46on Linux x86_64 with Rust 1.96.0.Current JIT build blocker
At this commit,
cargo build --release --features jitfirst fails becausejit_loop_hook()usesctxwithout receiving it.For runtime testing, I applied only the mechanical wiring fix:
and:
No JIT logic was otherwise changed.
Reproducer
Build and run:
Result
Without JIT:
With JIT:
Expected:
Suspected root cause
The compiler elides
RECVfor required untyped by-value parameters because the interpreter receives arguments directly in the callee CV slots.However, the whole-function JIT uses a different convention:
is_int_only()accepts the resulting op array as JIT-compatible.translate()initializes every JIT slot to zero.Opcode::Recv.RECVwas elided, the generated kernel never reads the corresponding values fromargs_ptr.As a result, leaf functions behave as if every untyped parameter were zero:
Each
combine()call therefore returns1, explaining the observed total of exactly5000000.The existing parameter JIT test uses a typed parameter:
That form retains
RECV, so it does not exercise the compiler/JIT mismatch for untyped parameters.Possible fixes
A conservative short-term fix would be to reject whole-function JIT compilation when a parameter has no corresponding
RECV.A more complete fix would initialize parameter CV slots from
args_ptrusing the op-array parameter/CV metadata, independently of whether the interpreter bytecode retainedRECV.It would also be useful to add an integration test that goes through the normal compiler and VM call path with an untyped function receiving runtime integer arguments.