diff --git a/src/codegen/runtime/arrays/array_get_mixed_key.rs b/src/codegen/runtime/arrays/array_get_mixed_key.rs new file mode 100644 index 0000000000..7874d9fabc --- /dev/null +++ b/src/codegen/runtime/arrays/array_get_mixed_key.rs @@ -0,0 +1,288 @@ +//! Purpose: +//! Emits the `__rt_array_get_mixed_key` runtime helper for reads from a +//! statically `Array(Mixed)` indexed local whose key is a boxed `Mixed` cell +//! (notably PHP `foreach` loop keys, which are always `Mixed` in EIR). +//! +//! Called from: +//! - `crate::codegen::runtime::emitters::emit_runtime()` via `crate::codegen::runtime::arrays`. +//! +//! Key details: +//! - The key tag is only known at runtime, so the helper dispatches on the +//! destination runtime kind first. An already-promoted hash (kind 3, produced +//! by an earlier string-key write in the same foreach rebuild) reads through +//! `__rt_hash_get`; a hit whose slot tag is 7 (boxed Mixed pointer) is retained +//! before returning, and any other slot tag is re-boxed via +//! `__rt_mixed_from_value`. On a miss the helper returns boxed `Mixed(null)`. +//! - An indexed destination (kind 1 or 2) unboxes the key: a string key returns +//! boxed `Mixed(null)` (undefined index, matching PHP quiet access), while an +//! integer key is bounds-checked and the element is boxed via +//! `__rt_mixed_from_value` (or retained when the slot already holds a boxed +//! Mixed pointer, tag 7). +//! - The helper does not release or mutate the incoming array pointer; it is a +//! pure read returning an owned boxed `Mixed` cell (borrowed slots are +//! retained first), mirroring `__rt_mixed_array_get` ownership. + +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; + +/// Emits the boxed-Mixed-key indexed/hash array get helper for the current target. +pub fn emit_array_get_mixed_key(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_array_get_mixed_key_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: array_get_mixed_key ---"); + emitter.label_global("__rt_array_get_mixed_key"); + + // Stack: + // [sp, #0] = array/hash pointer + // [sp, #8] = boxed Mixed key cell + // [sp, #16] = saved x29 + // [sp, #24] = saved x30 + emitter.instruction("sub sp, sp, #32"); // reserve frame: 2 inputs + saved fp/lr (16-byte aligned) + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #16"); // establish a helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the incoming array/hash pointer + emitter.instruction("str x1, [sp, #8]"); // save the boxed Mixed key cell + + // -- dispatch on the destination runtime kind -- + emitter.instruction("ldr x0, [sp, #0]"); // reload the destination pointer + emitter.instruction("bl __rt_heap_kind"); // load the destination heap kind byte into x0 + emitter.instruction("cmp x0, #3"); // kind 3 marks already-promoted associative hash storage + emitter.instruction("b.eq __rt_array_get_mixed_key_hash_path"); // route already-hash destinations through the hash reader + + // -- indexed destination: dispatch on the key tag -- + emitter.instruction("ldr x0, [sp, #8]"); // reload the boxed Mixed key cell + emitter.instruction("bl __rt_mixed_unbox"); // peel the key cell to tag in x0 and payload in x1/x2 + emitter.instruction("cmp x0, #1"); // string mixed keys are undefined on indexed storage + emitter.instruction("b.eq __rt_array_get_mixed_key_missing"); // return boxed Mixed(null) for a string key on indexed storage + emitter.instruction("cmp x0, #2"); // float mixed keys are cast to integer keys like PHP + emitter.instruction("b.ne __rt_array_get_mixed_key_int_ready"); // integer/bool keys are already valid indexed indexes + emitter.instruction("fmov d0, x1"); // load the float key payload into the FP register + emitter.instruction("fcvtzs x1, d0"); // cast the float key to an integer index like PHP + emitter.label("__rt_array_get_mixed_key_int_ready"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the indexed-array pointer + emitter.instruction("ldr x9, [x0]"); // load the current logical length of the indexed array + emitter.instruction("cmp x1, #0"); // negative int keys are undefined on indexed storage + emitter.instruction("b.lt __rt_array_get_mixed_key_missing"); // return boxed Mixed(null) for a negative indexed-array key + emitter.instruction("cmp x1, x9"); // an index past the end is undefined + emitter.instruction("b.ge __rt_array_get_mixed_key_missing"); // return boxed Mixed(null) for an out-of-bounds indexed-array key + emitter.instruction("ldr x13, [x0, #-8]"); // load packed indexed-array kind metadata + emitter.instruction("ubfx x13, x13, #8, #7"); // extract the runtime element value_type tag + emitter.instruction("add x10, x0, #24"); // skip the 24-byte array header to reach the contiguous payload + emitter.instruction("cmp x13, #7"); // are indexed slots already boxed Mixed pointers? + emitter.instruction("b.eq __rt_array_get_mixed_key_indexed_boxed"); // boxed slots must be retained before returning + emitter.instruction("cmp x13, #1"); // do indexed slots contain string pointer/length pairs? + emitter.instruction("b.eq __rt_array_get_mixed_key_indexed_string"); // string slots need a 16-byte load before boxing + emitter.instruction("cmp x13, #8"); // do indexed slots represent null payloads? + emitter.instruction("b.eq __rt_array_get_mixed_key_null"); // null slots have no payload to read + emitter.instruction("ldr x1, [x10, x1, lsl #3]"); // load scalar or pointer payload from the typed indexed slot + emitter.instruction("mov x2, #0"); // typed indexed slots use one payload word except strings + emitter.instruction("mov x0, x13"); // x0 = runtime value_type tag for the boxed result + emitter.instruction("bl __rt_mixed_from_value"); // box the typed indexed-array element into a Mixed cell + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the helper frame + emitter.instruction("ret"); // return the owned Mixed* in x0 + + emitter.label("__rt_array_get_mixed_key_indexed_boxed"); + emitter.instruction("ldr x0, [x10, x1, lsl #3]"); // load the boxed Mixed pointer from the indexed slot + emitter.instruction("cbz x0, __rt_array_get_mixed_key_null_ret"); // empty slot → Mixed(null) + emitter.instruction("bl __rt_incref"); // retain the stored Mixed cell so the caller owns the returned result + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the helper frame + emitter.instruction("ret"); // return the owned Mixed* in x0 + + emitter.label("__rt_array_get_mixed_key_indexed_string"); + emitter.instruction("lsl x1, x1, #4"); // convert the element index to a 16-byte string slot offset + emitter.instruction("add x10, x10, x1"); // x10 = address of the selected string slot + emitter.instruction("ldr x1, [x10]"); // load string pointer from the selected slot + emitter.instruction("ldr x2, [x10, #8]"); // load string length from the selected slot + emitter.instruction("mov x0, #1"); // x0 = string runtime value_type tag + emitter.instruction("bl __rt_mixed_from_value"); // box the string indexed-array element into a Mixed cell + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the helper frame + emitter.instruction("ret"); // return the owned Mixed* in x0 + + emitter.label("__rt_array_get_mixed_key_null"); + emitter.instruction("mov x0, #8"); // x0 = null runtime value_type tag + emitter.instruction("mov x1, #0"); // value_lo = 0 for null + emitter.instruction("mov x2, #0"); // value_hi = 0 for null + emitter.instruction("bl __rt_mixed_from_value"); // box the null value into a fresh Mixed cell + emitter.label("__rt_array_get_mixed_key_null_ret"); + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the helper frame + emitter.instruction("ret"); // return the owned Mixed* in x0 + + emitter.label("__rt_array_get_mixed_key_missing"); + emitter.instruction("mov x0, #8"); // x0 = null runtime value_type tag + emitter.instruction("mov x1, #0"); // value_lo = 0 for null + emitter.instruction("mov x2, #0"); // value_hi = 0 for null + emitter.instruction("bl __rt_mixed_from_value"); // box the null value into a fresh Mixed cell + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the helper frame + emitter.instruction("ret"); // return the owned Mixed* in x0 + + // -- already-hash destination: materialize the key and read directly -- + emitter.label("__rt_array_get_mixed_key_hash_path"); + emitter.instruction("ldr x0, [sp, #8]"); // reload the boxed Mixed key cell + emitter.instruction("bl __rt_mixed_unbox"); // peel the key cell to tag in x0 and payload in x1/x2 + emitter.instruction("cmp x0, #1"); // string mixed keys need normalization + emitter.instruction("b.eq __rt_array_get_mixed_key_hash_string"); // route string keys through the hash-key normalizer + emitter.instruction("cmp x0, #2"); // float mixed keys are cast to integer keys like PHP + emitter.instruction("b.ne __rt_array_get_mixed_key_hash_int"); // integer/bool keys become scalar integer hash keys + emitter.instruction("fmov d0, x1"); // load the float key payload into the FP register + emitter.instruction("fcvtzs x1, d0"); // cast the float key to an integer hash key like PHP + emitter.label("__rt_array_get_mixed_key_hash_int"); + emitter.instruction("mov x2, #-1"); // key_hi sentinel marks scalar integer hash keys + emitter.instruction("b __rt_array_get_mixed_key_hash_get"); // proceed to the hash read with an integer key + emitter.label("__rt_array_get_mixed_key_hash_string"); + emitter.instruction("bl __rt_hash_normalize_key"); // normalize the string key payload (x1/x2) into a hash key pair + emitter.label("__rt_array_get_mixed_key_hash_get"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the hash pointer as the hash_get target + emitter.instruction("bl __rt_hash_get"); // x0=found, x1=value_lo, x2=value_hi, x3=value_tag + emitter.instruction("cbz x0, __rt_array_get_mixed_key_missing"); // miss → boxed Mixed(null) + emitter.instruction("cmp x3, #7"); // is the hash entry already a boxed Mixed pointer? + emitter.instruction("b.ne __rt_array_get_mixed_key_hash_box"); // no → box (lo, hi, tag) into a fresh Mixed cell + emitter.instruction("mov x0, x1"); // yes → move the stored Mixed cell into the return register + emitter.instruction("bl __rt_incref"); // retain the stored Mixed cell so the caller owns the returned result + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the helper frame + emitter.instruction("ret"); // return the owned Mixed* in x0 + emitter.label("__rt_array_get_mixed_key_hash_box"); + emitter.instruction("mov x0, x3"); // x0 = value_tag (mixed_from_value first arg) + emitter.instruction("bl __rt_mixed_from_value"); // box the typed entry into a Mixed cell + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the helper frame + emitter.instruction("ret"); // return the owned Mixed* in x0 +} + +/// Emits the Linux x86_64 boxed-Mixed-key indexed/hash array get helper. +fn emit_array_get_mixed_key_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: array_get_mixed_key ---"); + emitter.label_global("__rt_array_get_mixed_key"); + + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame + emitter.instruction("sub rsp, 32"); // reserve 16-aligned slots for the 2 saved inputs + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the incoming array/hash pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the boxed Mixed key cell + + // -- dispatch on the destination runtime kind -- + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the destination pointer + emitter.instruction("call __rt_heap_kind"); // load the destination heap kind byte into rax + emitter.instruction("cmp rax, 3"); // kind 3 marks already-promoted associative hash storage + emitter.instruction("je __rt_array_get_mixed_key_hash_path"); // route already-hash destinations through the hash reader + + // -- indexed destination: dispatch on the key tag -- + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the boxed Mixed key cell + emitter.instruction("call __rt_mixed_unbox"); // peel the key cell to tag in rax and payload in rdi/rdx + emitter.instruction("cmp rax, 1"); // string mixed keys are undefined on indexed storage + emitter.instruction("je __rt_array_get_mixed_key_missing"); // return boxed Mixed(null) for a string key on indexed storage + emitter.instruction("cmp rax, 2"); // float mixed keys are cast to integer keys like PHP + emitter.instruction("jne __rt_array_get_mixed_key_int_ready"); // integer/bool keys are already valid indexed indexes + emitter.instruction("movq xmm0, rdi"); // load the float key payload into the FP register + emitter.instruction("cvttsd2si rdi, xmm0"); // cast the float key to an integer index like PHP + emitter.label("__rt_array_get_mixed_key_int_ready"); + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the indexed-array pointer + emitter.instruction("mov r9, QWORD PTR [rax]"); // load the current logical length of the indexed array + emitter.instruction("cmp rdi, 0"); // negative int keys are undefined on indexed storage + emitter.instruction("jl __rt_array_get_mixed_key_missing"); // return boxed Mixed(null) for a negative indexed-array key + emitter.instruction("cmp rdi, r9"); // an index past the end is undefined + emitter.instruction("jge __rt_array_get_mixed_key_missing"); // return boxed Mixed(null) for an out-of-bounds indexed-array key + emitter.instruction("mov r10, QWORD PTR [rax - 8]"); // load packed indexed-array kind metadata + emitter.instruction("shr r10, 8"); // shift the runtime element value_type tag into the low bits + emitter.instruction("and r10, 0x7f"); // remove the persistent COW flag from the extracted tag + emitter.instruction("lea r11, [rax + 24]"); // skip the 24-byte array header to reach the contiguous payload + emitter.instruction("cmp r10, 7"); // are indexed slots already boxed Mixed pointers? + emitter.instruction("je __rt_array_get_mixed_key_indexed_boxed"); // boxed slots must be retained before returning + emitter.instruction("cmp r10, 1"); // do indexed slots contain string pointer/length pairs? + emitter.instruction("je __rt_array_get_mixed_key_indexed_string"); // string slots need a 16-byte load before boxing + emitter.instruction("cmp r10, 8"); // do indexed slots represent null payloads? + emitter.instruction("je __rt_array_get_mixed_key_null"); // null slots have no payload to read + emitter.instruction("mov rax, r10"); // rax = runtime value_type tag for mixed_from_value + emitter.instruction("mov rdi, QWORD PTR [r11 + rdi * 8]"); // load scalar or pointer payload from the typed indexed slot + emitter.instruction("xor esi, esi"); // typed indexed slots use one payload word except strings + emitter.instruction("call __rt_mixed_from_value"); // box the typed indexed-array element into a Mixed cell + emitter.instruction("mov rsp, rbp"); // restore stack pointer + emitter.instruction("pop rbp"); // restore caller frame pointer + emitter.instruction("ret"); // return the owned Mixed* in rax + + emitter.label("__rt_array_get_mixed_key_indexed_boxed"); + emitter.instruction("mov rax, QWORD PTR [r11 + rdi * 8]"); // load the boxed Mixed pointer from the indexed slot + emitter.instruction("test rax, rax"); // empty slot → Mixed(null) + emitter.instruction("je __rt_array_get_mixed_key_null_ret"); // return boxed Mixed(null) for an empty indexed slot + emitter.instruction("call __rt_incref"); // retain the stored Mixed cell so the caller owns the returned result + emitter.instruction("mov rsp, rbp"); // restore stack pointer + emitter.instruction("pop rbp"); // restore caller frame pointer + emitter.instruction("ret"); // return the owned Mixed* in rax + + emitter.label("__rt_array_get_mixed_key_indexed_string"); + emitter.instruction("shl rdi, 4"); // convert the element index to a 16-byte string slot offset + emitter.instruction("add r11, rdi"); // r11 = address of the selected string slot + emitter.instruction("mov rax, 1"); // rax = string runtime value_type tag + emitter.instruction("mov rdi, QWORD PTR [r11]"); // load string pointer from the selected slot + emitter.instruction("mov rsi, QWORD PTR [r11 + 8]"); // load string length from the selected slot + emitter.instruction("call __rt_mixed_from_value"); // box the string indexed-array element into a Mixed cell + emitter.instruction("mov rsp, rbp"); // restore stack pointer + emitter.instruction("pop rbp"); // restore caller frame pointer + emitter.instruction("ret"); // return the owned Mixed* in rax + + emitter.label("__rt_array_get_mixed_key_null"); + emitter.instruction("mov rax, 8"); // rax = null runtime value_type tag + emitter.instruction("mov rdi, 0"); // value_lo = 0 for null + emitter.instruction("mov rsi, 0"); // value_hi = 0 for null + emitter.instruction("call __rt_mixed_from_value"); // box the null value into a fresh Mixed cell + emitter.label("__rt_array_get_mixed_key_null_ret"); + emitter.instruction("mov rsp, rbp"); // restore stack pointer + emitter.instruction("pop rbp"); // restore caller frame pointer + emitter.instruction("ret"); // return the owned Mixed* in rax + + emitter.label("__rt_array_get_mixed_key_missing"); + emitter.instruction("mov rax, 8"); // rax = null runtime value_type tag + emitter.instruction("mov rdi, 0"); // value_lo = 0 for null + emitter.instruction("mov rsi, 0"); // value_hi = 0 for null + emitter.instruction("call __rt_mixed_from_value"); // box the null value into a fresh Mixed cell + emitter.instruction("mov rsp, rbp"); // restore stack pointer + emitter.instruction("pop rbp"); // restore caller frame pointer + emitter.instruction("ret"); // return the owned Mixed* in rax + + // -- already-hash destination: materialize the key and read directly -- + emitter.label("__rt_array_get_mixed_key_hash_path"); + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the boxed Mixed key cell + emitter.instruction("call __rt_mixed_unbox"); // peel the key cell to tag in rax and payload in rdi/rdx + emitter.instruction("cmp rax, 1"); // string mixed keys need normalization + emitter.instruction("je __rt_array_get_mixed_key_hash_string"); // route string keys through the hash-key normalizer + emitter.instruction("cmp rax, 2"); // float mixed keys are cast to integer keys like PHP + emitter.instruction("jne __rt_array_get_mixed_key_hash_int"); // integer/bool keys become scalar integer hash keys + emitter.instruction("movq xmm0, rdi"); // load the float key payload into the FP register + emitter.instruction("cvttsd2si rdi, xmm0"); // cast the float key to an integer hash key like PHP + emitter.label("__rt_array_get_mixed_key_hash_int"); + emitter.instruction("mov rsi, rdi"); // publish the integer key payload as the hash key low word + emitter.instruction("mov rdx, -1"); // key_hi sentinel marks scalar integer hash keys + emitter.instruction("jmp __rt_array_get_mixed_key_hash_get"); // proceed to the hash read with an integer key + emitter.label("__rt_array_get_mixed_key_hash_string"); + emitter.instruction("mov rax, rdi"); // move the unboxed string pointer into the normalizer input + emitter.instruction("call __rt_hash_normalize_key"); // normalize the string key into a hash key pair in rax/rdx + emitter.instruction("mov rsi, rax"); // publish the normalized key low word as the hash key low word + emitter.label("__rt_array_get_mixed_key_hash_get"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the hash pointer as the hash_get target + emitter.instruction("call __rt_hash_get"); // rax=found, rdi=value_lo, rsi=value_hi, rcx=value_tag + emitter.instruction("test rax, rax"); // miss → boxed Mixed(null) + emitter.instruction("je __rt_array_get_mixed_key_missing"); // return boxed Mixed(null) after a hash miss + emitter.instruction("cmp rcx, 7"); // is the hash entry already a boxed Mixed pointer? + emitter.instruction("jne __rt_array_get_mixed_key_hash_box"); // no → box (lo, hi, tag) into a fresh Mixed cell + emitter.instruction("mov rax, rdi"); // yes → move the stored Mixed cell into the return register + emitter.instruction("call __rt_incref"); // retain the stored Mixed cell so the caller owns the returned result + emitter.instruction("mov rsp, rbp"); // restore stack pointer + emitter.instruction("pop rbp"); // restore caller frame pointer + emitter.instruction("ret"); // return the owned Mixed* in rax + emitter.label("__rt_array_get_mixed_key_hash_box"); + emitter.instruction("mov rax, rcx"); // rax = value_tag (mixed_from_value first arg) + emitter.instruction("call __rt_mixed_from_value"); // box the typed entry into a Mixed cell + emitter.instruction("mov rsp, rbp"); // restore stack pointer + emitter.instruction("pop rbp"); // restore caller frame pointer + emitter.instruction("ret"); // return the owned Mixed* in rax +} \ No newline at end of file diff --git a/src/codegen/runtime/arrays/mod.rs b/src/codegen/runtime/arrays/mod.rs index 7a64909015..aa21d9aa54 100644 --- a/src/codegen/runtime/arrays/mod.rs +++ b/src/codegen/runtime/arrays/mod.rs @@ -60,6 +60,7 @@ mod array_push_str; mod array_set_int; mod array_set_mixed; mod array_set_mixed_key; +mod array_get_mixed_key; mod array_set_refcounted; mod array_set_str; mod array_rand; @@ -254,6 +255,8 @@ pub use array_set_int::emit_array_set_int; pub use array_set_mixed::emit_array_set_mixed; /// Emit boxed-Mixed-key indexed/hash array set helper. pub use array_set_mixed_key::emit_array_set_mixed_key; +/// Emit boxed-Mixed-key indexed/hash array get helper. +pub use array_get_mixed_key::emit_array_get_mixed_key; /// Emit refcounted indexed-array set helper. pub use array_set_refcounted::emit_array_set_refcounted; /// Emit refcounted indexed-array set helper. diff --git a/src/codegen/runtime/emitters.rs b/src/codegen/runtime/emitters.rs index 246bee71b2..680fb036cb 100644 --- a/src/codegen/runtime/emitters.rs +++ b/src/codegen/runtime/emitters.rs @@ -190,6 +190,7 @@ pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) { arrays::emit_array_set_int(emitter); arrays::emit_array_set_mixed(emitter); arrays::emit_array_set_mixed_key(emitter); + arrays::emit_array_get_mixed_key(emitter); arrays::emit_array_set_refcounted(emitter); arrays::emit_array_set_str(emitter); arrays::emit_array_union(emitter); diff --git a/src/codegen_ir/lower_inst.rs b/src/codegen_ir/lower_inst.rs index 560c8c3daf..1c877114b8 100644 --- a/src/codegen_ir/lower_inst.rs +++ b/src/codegen_ir/lower_inst.rs @@ -137,6 +137,7 @@ pub(super) fn lower_instruction(ctx: &mut FunctionContext<'_>, inst_id: InstId) Op::ArrayIsset => builtins::lower_array_isset(ctx, &inst), Op::ArraySet => arrays::lower_array_set(ctx, &inst), Op::ArraySetMixedKey => arrays::lower_array_set_mixed_key(ctx, &inst), + Op::ArrayGetMixedKey => arrays::lower_array_get_mixed_key(ctx, &inst), Op::ArrayPush => arrays::lower_array_push(ctx, &inst), Op::MixedArrayAppend => arrays::lower_mixed_array_append(ctx, &inst), Op::ArrayUnion => arrays::lower_array_union(ctx, &inst), diff --git a/src/codegen_ir/lower_inst/arrays.rs b/src/codegen_ir/lower_inst/arrays.rs index 4727a940d9..d60aead1ed 100644 --- a/src/codegen_ir/lower_inst/arrays.rs +++ b/src/codegen_ir/lower_inst/arrays.rs @@ -323,6 +323,60 @@ fn lower_array_set_mixed_key_x86_64( Ok(()) } +/// Lowers a boxed-Mixed-key read from a statically `Array(Mixed)` indexed local. +/// +/// The key tag is only known at runtime (PHP `foreach` keys are always `Mixed` +/// in EIR), so the read goes through `__rt_array_get_mixed_key`, which dispatches +/// on the destination runtime kind: an already-promoted hash reads through +/// `__rt_hash_get`, while an indexed array bounds-checks integer keys and returns +/// `Mixed(null)` for string keys. The result is an owned boxed `Mixed` cell. +pub(super) fn lower_array_get_mixed_key( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + let array = expect_operand(inst, 0)?; + let key = expect_operand(inst, 1)?; + require_indexed_array(ctx.value_php_type(array)?.codegen_repr(), inst)?; + let key_ty = ctx.value_php_type(key)?.codegen_repr(); + if !matches!(key_ty, PhpType::Mixed | PhpType::Union(_)) { + return Err(CodegenIrError::unsupported(format!( + "array_get_mixed_key key PHP type {:?}", + key_ty + ))); + } + match ctx.emitter.target.arch { + Arch::AArch64 => lower_array_get_mixed_key_aarch64(ctx, array, key)?, + Arch::X86_64 => lower_array_get_mixed_key_x86_64(ctx, array, key)?, + } + store_if_result(ctx, inst) +} + +/// Loads the array and boxed Mixed key into argument registers on AArch64 and +/// calls `__rt_array_get_mixed_key`, leaving the owned Mixed result in x0. +fn lower_array_get_mixed_key_aarch64( + ctx: &mut FunctionContext<'_>, + array: ValueId, + key: ValueId, +) -> Result<()> { + ctx.load_value_to_reg(array, "x0")?; + ctx.load_value_to_reg(key, "x1")?; + abi::emit_call_label(ctx.emitter, "__rt_array_get_mixed_key"); + Ok(()) +} + +/// Loads the array and boxed Mixed key into argument registers on x86_64 and +/// calls `__rt_array_get_mixed_key`, leaving the owned Mixed result in rax. +fn lower_array_get_mixed_key_x86_64( + ctx: &mut FunctionContext<'_>, + array: ValueId, + key: ValueId, +) -> Result<()> { + ctx.load_value_to_reg(array, "rdi")?; + ctx.load_value_to_reg(key, "rsi")?; + abi::emit_call_label(ctx.emitter, "__rt_array_get_mixed_key"); + Ok(()) +} + /// Lowers an indexed-array append through the runtime helper for the value type. pub(super) fn lower_array_push(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { let array = expect_operand(inst, 0)?; diff --git a/src/image_prelude.rs b/src/image_prelude.rs index 1c5413c347..f45006be14 100644 --- a/src/image_prelude.rs +++ b/src/image_prelude.rs @@ -1064,12 +1064,9 @@ function image_type_to_mime_type(int $image_type): string { } } -// Known limitation: PHP returns `string|false`, returning `false` for an unknown -// type. elephc currently collapses a string|false function return to `string` -// (coercing the `false` to ""), and an explicit union return type hits an -// unsupported EIR path for the dead-code copy of this function. So an unknown -// type yields "" here rather than `false`. Revisit when scalar-union values are -// representable end-to-end (the union-value-runtime work). +// PHP returns `string|false` for `image_type_to_extension`: `false` for an +// unknown type. The inferred return type widens to Mixed so `false` preserves +// its bool tag through the boxed Mixed cell at runtime. function image_type_to_extension(int $image_type, bool $include_dot = true) { $ext = ""; switch ($image_type) { @@ -1176,10 +1173,8 @@ function exif_imagetype(string $filename) { return elephc_img_probe_type(); } -// PHP returns string|false (false for an unknown tag). elephc collapses a -// string|false return to string, so an unknown tag yields "" here rather than -// false — the same limitation documented on image_type_to_extension. Test with -// `=== ""` instead of `=== false`. +// PHP returns string|false (false for an unknown tag). The inferred return +// type widens to Mixed so `false` preserves its bool tag. function exif_tagname(int $index) { $_len = elephc_exif_tagname($index); if ($_len < 0) { diff --git a/src/ir/instr.rs b/src/ir/instr.rs index e24252281a..5f2b0235bb 100644 --- a/src/ir/instr.rs +++ b/src/ir/instr.rs @@ -263,6 +263,7 @@ pub enum Op { HashArrayUnion, ArrayToHash, ArraySetMixedKey, + ArrayGetMixedKey, ArrayKeyExists, OffsetExists, OffsetUnset, @@ -415,6 +416,7 @@ impl Op { | PtrWriteString => E::WRITES_HEAP | E::MAY_FATAL | E::REFCOUNT_OP, MixedArrayAppend => E::READS_HEAP | E::WRITES_HEAP | E::ALLOC_HEAP | E::MAY_FATAL | E::REFCOUNT_OP, ArraySetMixedKey => E::READS_HEAP | E::WRITES_HEAP | E::ALLOC_HEAP | E::MAY_FATAL | E::REFCOUNT_OP, + ArrayGetMixedKey => E::READS_HEAP | E::MAY_FATAL | E::REFCOUNT_OP, ArrayUnion | HashUnion | ArrayHashUnion | HashArrayUnion | ArrayToHash => { E::READS_HEAP | E::ALLOC_HEAP | E::REFCOUNT_OP } @@ -579,6 +581,7 @@ impl Op { HashArrayUnion => "hash_array_union", ArrayToHash => "array_to_hash", ArraySetMixedKey => "array_set_mixed_key", + ArrayGetMixedKey => "array_get_mixed_key", ArrayKeyExists => "array_key_exists", OffsetExists => "offset_exists", OffsetUnset => "offset_unset", diff --git a/src/ir/validator.rs b/src/ir/validator.rs index 2e43f9daf0..82c2966734 100644 --- a/src/ir/validator.rs +++ b/src/ir/validator.rs @@ -407,7 +407,7 @@ fn validate_opcode_rules(function: &Function, inst_id: InstId, inst: &Instructio ArrayHashUnion => check_array_hash_union(function, inst_id, inst), HashArrayUnion => check_hash_array_union(function, inst_id, inst), ArrayLen | ArrayGet | ArrayIsset | ArraySet | ArrayPush | ArrayEnsureUnique - | ArrayCloneShallow | ArrayToHash | ArraySetMixedKey => { + | ArrayCloneShallow | ArrayToHash | ArraySetMixedKey | ArrayGetMixedKey => { check_first_heap(function, inst_id, inst, IrHeapKind::Array, "Heap(Array)") } MixedArrayAppend => { diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 7d7b9fe08b..1195243873 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -6693,8 +6693,21 @@ fn lower_array_access_from_value( let mut index_value = lower_expr(ctx, index); let op = match array_value.ir_type { IrType::Heap(IrHeapKind::Array) => { - index_value = coerce_to_int_at_span(ctx, index_value, Some(index.span)); - Op::ArrayGet + // Array(Mixed) may have been promoted to a hash at runtime (e.g. + // foreach-rebuilt arrays), so the key tag is only known at runtime. + // Route through ArrayGetMixedKey instead of coercing the index to + // int, which would collapse a string key onto int 0. + let is_mixed_array = matches!( + ctx.builder.value_php_type(array_value.value).codegen_repr(), + PhpType::Array(boxed) if *boxed == PhpType::Mixed + ); + if is_mixed_array { + index_value = box_index_for_mixed_key(ctx, index_value, index); + Op::ArrayGetMixedKey + } else { + index_value = coerce_to_int_at_span(ctx, index_value, Some(index.span)); + Op::ArrayGet + } } IrType::Heap(IrHeapKind::Hash) => Op::HashGet, IrType::Heap(IrHeapKind::Buffer) => Op::BufferGet, @@ -6715,6 +6728,33 @@ fn lower_array_access_from_value( ) } +/// Boxes a lowered index into a Mixed cell for `Op::ArrayGetMixedKey`. +/// +/// A foreach loop key already arrives as a boxed Mixed/Union cell, so it is +/// passed through unchanged. Literal int/string keys (and any other typed +/// index) are boxed via `Op::MixedBox` so the runtime helper can dispatch on +/// the key tag exactly like `Op::ArraySetMixedKey`. +fn box_index_for_mixed_key( + ctx: &mut LoweringContext<'_, '_>, + index_value: LoweredValue, + index: &Expr, +) -> LoweredValue { + if matches!( + index_value.ir_type, + IrType::Heap(IrHeapKind::Mixed) | IrType::Heap(IrHeapKind::Union) + ) { + return index_value; + } + ctx.emit_value( + Op::MixedBox, + vec![index_value.value], + None, + PhpType::Mixed, + Op::MixedBox.default_effects(), + Some(index.span), + ) +} + /// Lowers nullable receiver indexing without evaluating the index on a null receiver. fn lower_nullable_array_access( ctx: &mut LoweringContext<'_, '_>, @@ -6766,6 +6806,7 @@ fn array_access_result_type( ) -> PhpType { match op { Op::StrCharAt => PhpType::Str, + Op::ArrayGetMixedKey => PhpType::Mixed, Op::ArrayGet => match ctx.builder.value_php_type(array).codegen_repr() { PhpType::Array(elem_ty) => { array_access_element_result_type(normalize_value_php_type(*elem_ty)) diff --git a/src/types/checker/functions/returns.rs b/src/types/checker/functions/returns.rs index c42d5a6f84..1922b5a2bb 100644 --- a/src/types/checker/functions/returns.rs +++ b/src/types/checker/functions/returns.rs @@ -420,15 +420,22 @@ impl Checker { /// Computes the wider of two PHP types for return-type merging: /// - If equal, returns a clone. - /// - Str + anything → Str; Float + anything → Float. + /// - Str/Float absorb compatible numeric scalars (Int/Float/numeric-string) but + /// do NOT absorb Bool, so `string|false` and `float|false` widen to Mixed + /// (boxed tagged cell) instead of collapsing `false` to `""`/`0.0`. /// - Void or Never resolves to the other type; otherwise → Mixed. pub(crate) fn wider_type(a: &PhpType, b: &PhpType) -> PhpType { match (a, b) { _ if a == b => a.clone(), - (PhpType::Str, _) | (_, PhpType::Str) => PhpType::Str, - (PhpType::Float, _) | (_, PhpType::Float) => PhpType::Float, (PhpType::Void, other) | (other, PhpType::Void) => other.clone(), (PhpType::Never, other) | (other, PhpType::Never) => other.clone(), + // Once we reach Mixed (e.g. Bool + scalar), stay Mixed: don't let + // Str/Float absorb it back, which would re-collapse `false`. + (PhpType::Mixed, _) | (_, PhpType::Mixed) => PhpType::Mixed, + // Bool + scalar must not be absorbed: `false` would lose its tag. + (PhpType::Bool, _) | (_, PhpType::Bool) => PhpType::Mixed, + (PhpType::Str, _) | (_, PhpType::Str) => PhpType::Str, + (PhpType::Float, _) | (_, PhpType::Float) => PhpType::Float, _ => PhpType::Mixed, } } diff --git a/src/types/checker/inference/expr/mod.rs b/src/types/checker/inference/expr/mod.rs index fb075de3ce..b50f46cb83 100644 --- a/src/types/checker/inference/expr/mod.rs +++ b/src/types/checker/inference/expr/mod.rs @@ -178,6 +178,11 @@ impl Checker { Ok(PhpType::Str) } PhpType::Array(elem_ty) => { + // Array(Mixed) may have been promoted to a hash at runtime + // (e.g. foreach-rebuilt arrays). Allow any PHP array key. + if **elem_ty == PhpType::Mixed { + return Ok(PhpType::Mixed); + } if normalized_idx_ty != PhpType::Int { return Err(CompileError::new( expr.span, @@ -215,12 +220,17 @@ impl Checker { } PhpType::Array(elem_ty) => { saw_indexable_member = true; - if normalized_idx_ty != PhpType::Int { + // Array(Mixed) may have been promoted to a hash + // at runtime; allow any PHP array key. + if **elem_ty == PhpType::Mixed { + result_members.push(PhpType::Mixed); + } else if normalized_idx_ty != PhpType::Int { first_index_error = first_index_error.or(Some("Array index must be integer")); continue; + } else { + result_members.push(*elem_ty.clone()); } - result_members.push(*elem_ty.clone()); } PhpType::AssocArray { value, .. } => { saw_indexable_member = true; diff --git a/tests/codegen/image/exif_iptc.rs b/tests/codegen/image/exif_iptc.rs index 6ee699f3ac..74eb9c153a 100644 --- a/tests/codegen/image/exif_iptc.rs +++ b/tests/codegen/image/exif_iptc.rs @@ -37,16 +37,16 @@ echo exif_tagname(0x010F), "|", exif_tagname(0x0112), "|", exif_tagname(0x8825), assert_eq!(out, "Make|Orientation|GPS_IFD_Pointer|ExposureTime"); } -/// An unknown tag yields "" (the documented `string|false` collapse), not false. +/// An unknown tag yields `false` (PHP's `string|false` return semantics), not "". #[test] -fn test_exif_tagname_unknown_is_empty() { +fn test_exif_tagname_unknown_is_false() { let out = compile_and_run( r#" $v) { $dst[$k] = $v; } + return $dst; +} +$r = rebuild(["name" => "Alice", "age" => 30]); +echo $r["name"], $r["age"]; +"#); + assert_eq!(out, "Alice30"); +} + +#[test] +fn test_foreach_mixed_int_key_direct_read() { + let out = compile_and_run(r#" $v) { $dst[$k] = $v; } + return $dst; +} +$r = rebuild([10, 20, 30]); +echo $r[0], $r[1], $r[2]; +"#); + assert_eq!(out, "102030"); +} + +#[test] +fn test_foreach_mixed_missing_key_returns_null() { + let out = compile_and_run(r#" $v) { $dst[$k] = $v; } + return $dst; +} +$r = rebuild(["a" => 1]); +var_dump($r["missing"]); +"#); + assert_eq!(out, "NULL\n"); +} \ No newline at end of file diff --git a/tests/codegen/regressions/scalar_union_returns.rs b/tests/codegen/regressions/scalar_union_returns.rs new file mode 100644 index 0000000000..23077acfc3 --- /dev/null +++ b/tests/codegen/regressions/scalar_union_returns.rs @@ -0,0 +1,68 @@ +//! Purpose: +//! Regression tests for issue #398: scalar-union builtin/user function returns +//! (e.g. `string|false`, `int|false`) collapsed `false` to `""`/`0` because +//! `wider_type` let `Str`/`Float` absorb `Bool`. The fix makes `Bool` widen to +//! `Mixed` (boxed tagged cell) so `false` preserves its tag through `return`. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Each test compiles inline PHP and asserts stdout matches PHP behavior. +//! - The return type infers to `Mixed`, not `Str`/`Int`, so `=== false` works. + +use crate::support::compile_and_run; + +/// User function returning `string|false` must preserve `false` as a bool-tagged +/// Mixed cell, not coerce it to `""`. +#[test] +fn test_user_function_string_or_false_return() { + let out = compile_and_run( + r#"