diff --git a/src/codegen/runtime/data/fixed.rs b/src/codegen/runtime/data/fixed.rs index 5f47e3e754..958ea060c0 100644 --- a/src/codegen/runtime/data/fixed.rs +++ b/src/codegen/runtime/data/fixed.rs @@ -242,6 +242,10 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin out.push_str(".globl _diag_define_already_defined_msg\n_diag_define_already_defined_msg:\n .ascii \"Warning: define(): Constant already defined\\n\"\n"); out.push_str(".globl _diag_undefined_array_key_prefix\n_diag_undefined_array_key_prefix:\n .ascii \"Warning: Undefined array key \"\n"); out.push_str(".globl _diag_undefined_array_key_suffix\n_diag_undefined_array_key_suffix:\n .ascii \"\\n\"\n"); + out.push_str(".globl _diag_string_offset_prefix\n_diag_string_offset_prefix:\n .ascii \"Warning: Uninitialized string offset \"\n"); + out.push_str(".globl _diag_string_offset_nl\n_diag_string_offset_nl:\n .ascii \"\\n\"\n"); + out.push_str(".globl _diag_float_key_prefix\n_diag_float_key_prefix:\n .ascii \"Deprecated: Implicit conversion from float \"\n"); + out.push_str(".globl _diag_float_key_suffix\n_diag_float_key_suffix:\n .ascii \" to int loses precision\\n\"\n"); out.push_str(".globl _fiber_msg_already_started\n_fiber_msg_already_started:\n .ascii \"Cannot start a fiber that has already been started\"\n"); out.push_str(".globl _fiber_msg_not_suspended\n_fiber_msg_not_suspended:\n .ascii \"Cannot resume a fiber that is not suspended\"\n"); out.push_str(".globl _fiber_msg_throw_not_suspended\n_fiber_msg_throw_not_suspended:\n .ascii \"Cannot resume a fiber that is not suspended\"\n"); diff --git a/src/codegen/runtime/diagnostics/float_to_int_key.rs b/src/codegen/runtime/diagnostics/float_to_int_key.rs new file mode 100644 index 0000000000..c39a8e5c53 --- /dev/null +++ b/src/codegen/runtime/diagnostics/float_to_int_key.rs @@ -0,0 +1,108 @@ +//! Purpose: +//! Emits the `__rt_warn_float_to_int_key` runtime helper for float-to-int array key conversion. +//! Formats the PHP "Implicit conversion from float … to int loses precision" deprecation. +//! +//! Called from: +//! - `crate::codegen::runtime::diagnostics::emit_float_to_int_key_deprecation()`. +//! +//! Key details: +//! - The helper is deprecation-only: callers still truncate the float to perform the lookup. +//! - `__rt_ftoa` uses `_concat_buf`, so `_concat_off` is restored before returning. + +use crate::codegen::abi; +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; + +const FLOAT_KEY_PREFIX_LEN: usize = "Deprecated: Implicit conversion from float ".len(); +const FLOAT_KEY_SUFFIX_LEN: usize = " to int loses precision\n".len(); + +/// Emits `__rt_warn_float_to_int_key` for the active target. +/// +/// # ABI +/// - ARM64: input float in `d0`. +/// - x86_64 Linux: input float in `xmm0`. +/// +/// # Behavior +/// Writes `Deprecated: Implicit conversion from float to int loses precision\n` +/// to stderr (via `__rt_diag_warning`) when `@` suppression is inactive, then returns. +pub fn emit_float_to_int_key_deprecation(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_float_to_int_key_deprecation_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: float_to_int_key_deprecation ---"); + emitter.label_global("__rt_warn_float_to_int_key"); + + // -- set up stack frame -- + emitter.instruction("sub sp, sp, #64"); // reserve saved float, concat cursor, and frame linkage + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #48"); // establish a stable runtime deprecation frame + emitter.instruction("str d0, [sp, #0]"); // save the float key across deprecation fragments + abi::emit_symbol_address(emitter, "x9", "_concat_off"); + emitter.instruction("ldr x10, [x9]"); // snapshot concat scratch state before formatting the float + emitter.instruction("str x10, [sp, #8]"); // preserve the concat cursor across ftoa + + // -- emit prefix -- + abi::emit_symbol_address(emitter, "x1", "_diag_float_key_prefix"); + emitter.instruction(&format!("mov x2, #{}", FLOAT_KEY_PREFIX_LEN)); // pass the float-key deprecation prefix length + abi::emit_call_label(emitter, "__rt_diag_warning"); // emit or suppress the float-key deprecation prefix + + // -- emit formatted float -- + emitter.instruction("ldr d0, [sp, #0]"); // reload the float key for decimal formatting + abi::emit_call_label(emitter, "__rt_ftoa"); // format the float key into concat scratch + abi::emit_call_label(emitter, "__rt_diag_warning"); // emit or suppress the formatted float-key value + emitter.instruction("ldr x10, [sp, #8]"); // reload the pre-warning concat cursor + abi::emit_symbol_address(emitter, "x9", "_concat_off"); + emitter.instruction("str x10, [x9]"); // restore concat scratch state for surrounding expressions + + // -- emit suffix -- + abi::emit_symbol_address(emitter, "x1", "_diag_float_key_suffix"); + emitter.instruction(&format!("mov x2, #{}", FLOAT_KEY_SUFFIX_LEN)); // pass the float-key deprecation suffix length + abi::emit_call_label(emitter, "__rt_diag_warning"); // emit or suppress the float-key deprecation suffix + + // -- restore stack frame -- + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // release the runtime deprecation frame + emitter.instruction("ret"); // return to the hash-key caller +} + +/// Emits the x86_64 implementation of `__rt_warn_float_to_int_key`. +fn emit_float_to_int_key_deprecation_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: float_to_int_key_deprecation ---"); + emitter.label_global("__rt_warn_float_to_int_key"); + + // -- set up stack frame -- + emitter.instruction("push rbp"); // save the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable runtime deprecation frame + emitter.instruction("sub rsp, 32"); // reserve saved float and concat cursor while keeping calls aligned + emitter.instruction("movsd QWORD PTR [rbp - 8], xmm0"); // save the float key across deprecation fragments + abi::emit_load_symbol_to_reg(emitter, "r10", "_concat_off", 0); // snapshot concat scratch state before formatting the float + emitter.instruction("mov QWORD PTR [rbp - 16], r10"); // preserve the concat cursor across ftoa + + // -- emit prefix -- + abi::emit_symbol_address(emitter, "rdi", "_diag_float_key_prefix"); + emitter.instruction(&format!("mov esi, {}", FLOAT_KEY_PREFIX_LEN)); // pass the float-key deprecation prefix length + abi::emit_call_label(emitter, "__rt_diag_warning"); // emit or suppress the float-key deprecation prefix + + // -- emit formatted float -- + emitter.instruction("movsd xmm0, QWORD PTR [rbp - 8]"); // reload the float key for decimal formatting + abi::emit_call_label(emitter, "__rt_ftoa"); // format the float key into concat scratch + emitter.instruction("mov rdi, rax"); // pass the formatted float-key pointer to the deprecation helper + emitter.instruction("mov rsi, rdx"); // pass the formatted float-key length to the deprecation helper + abi::emit_call_label(emitter, "__rt_diag_warning"); // emit or suppress the formatted float-key value + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the pre-warning concat cursor + abi::emit_store_reg_to_symbol(emitter, "r10", "_concat_off", 0); // restore concat scratch state for surrounding expressions + + // -- emit suffix -- + abi::emit_symbol_address(emitter, "rdi", "_diag_float_key_suffix"); + emitter.instruction(&format!("mov esi, {}", FLOAT_KEY_SUFFIX_LEN)); // pass the float-key deprecation suffix length + abi::emit_call_label(emitter, "__rt_diag_warning"); // emit or suppress the float-key deprecation suffix + + // -- restore stack frame -- + emitter.instruction("mov rsp, rbp"); // release the runtime deprecation frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return to the hash-key caller +} \ No newline at end of file diff --git a/src/codegen/runtime/diagnostics.rs b/src/codegen/runtime/diagnostics/mod.rs similarity index 88% rename from src/codegen/runtime/diagnostics.rs rename to src/codegen/runtime/diagnostics/mod.rs index 480a3557b6..11a133eeeb 100644 --- a/src/codegen/runtime/diagnostics.rs +++ b/src/codegen/runtime/diagnostics/mod.rs @@ -8,6 +8,9 @@ //! Key details: //! - Suppression depth lives in _rt_diag_suppression and warning output must follow each target syscall ABI. +mod float_to_int_key; +mod string_offset; + use crate::codegen::emit::Emitter; use crate::codegen::platform::Arch; use crate::codegen::abi; @@ -106,3 +109,22 @@ fn emit_diagnostics_linux_x86_64(emitter: &mut Emitter) { emitter.label("__rt_diag_warning_done_linux_x86_64"); emitter.instruction("ret"); // return after either writing or suppressing the warning } + +/// Emits the `__rt_warn_string_offset` runtime helper for the active target. +/// +/// Dispatches to the x86_64 variant when targeting Linux x86_64; otherwise +/// emits the ARM64 helper. The helper formats a PHP "Uninitialized string +/// offset" warning carrying the runtime offset value, honoring `@` suppression. +pub(crate) fn emit_string_offset_warning(emitter: &mut Emitter) { + string_offset::emit_string_offset_warning(emitter); +} + +/// Emits the `__rt_warn_float_to_int_key` runtime helper for the active target. +/// +/// Dispatches to the x86_64 variant when targeting Linux x86_64; otherwise +/// emits the ARM64 helper. The helper formats a PHP "Implicit conversion from +/// float … to int loses precision" deprecation carrying the runtime float +/// value, honoring `@` suppression. +pub(crate) fn emit_float_to_int_key_deprecation(emitter: &mut Emitter) { + float_to_int_key::emit_float_to_int_key_deprecation(emitter); +} diff --git a/src/codegen/runtime/diagnostics/string_offset.rs b/src/codegen/runtime/diagnostics/string_offset.rs new file mode 100644 index 0000000000..b6fb57e203 --- /dev/null +++ b/src/codegen/runtime/diagnostics/string_offset.rs @@ -0,0 +1,108 @@ +//! Purpose: +//! Emits the `__rt_warn_string_offset` runtime helper for out-of-bounds string offsets. +//! Formats the PHP "Uninitialized string offset N" warning carrying the runtime offset value. +//! +//! Called from: +//! - `crate::codegen::runtime::diagnostics::emit_string_offset_warning()`. +//! +//! Key details: +//! - The helper is warning-only: callers still materialize their own empty-string fallback. +//! - `__rt_itoa` uses `_concat_buf`, so `_concat_off` is restored before returning. + +use crate::codegen::abi; +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; + +const STRING_OFFSET_PREFIX_LEN: usize = "Warning: Uninitialized string offset ".len(); +const STRING_OFFSET_NL_LEN: usize = "\n".len(); + +/// Emits `__rt_warn_string_offset` for the active target. +/// +/// # ABI +/// - ARM64: input offset in `x0`. +/// - x86_64 Linux: input offset in `rax`. +/// +/// # Behavior +/// Writes `Warning: Uninitialized string offset \n` to stderr (via +/// `__rt_diag_warning`) when `@` suppression is inactive, then returns. +pub fn emit_string_offset_warning(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_string_offset_warning_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: string_offset_warning ---"); + emitter.label_global("__rt_warn_string_offset"); + + // -- set up stack frame -- + emitter.instruction("sub sp, sp, #48"); // reserve saved offset, concat cursor, and frame linkage + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #32"); // establish a stable runtime warning frame + emitter.instruction("str x0, [sp, #0]"); // save the out-of-bounds offset across warning fragments + abi::emit_symbol_address(emitter, "x9", "_concat_off"); + emitter.instruction("ldr x10, [x9]"); // snapshot concat scratch state before formatting the offset + emitter.instruction("str x10, [sp, #8]"); // preserve the concat cursor across itoa + + // -- emit prefix -- + abi::emit_symbol_address(emitter, "x1", "_diag_string_offset_prefix"); + emitter.instruction(&format!("mov x2, #{}", STRING_OFFSET_PREFIX_LEN)); // pass the string-offset warning prefix length + abi::emit_call_label(emitter, "__rt_diag_warning"); // emit or suppress the string-offset warning prefix + + // -- emit formatted offset -- + emitter.instruction("ldr x0, [sp, #0]"); // reload the out-of-bounds offset for decimal formatting + abi::emit_call_label(emitter, "__rt_itoa"); // format the offset into concat scratch + abi::emit_call_label(emitter, "__rt_diag_warning"); // emit or suppress the formatted offset value + emitter.instruction("ldr x10, [sp, #8]"); // reload the pre-warning concat cursor + abi::emit_symbol_address(emitter, "x9", "_concat_off"); + emitter.instruction("str x10, [x9]"); // restore concat scratch state for surrounding expressions + + // -- emit newline suffix -- + abi::emit_symbol_address(emitter, "x1", "_diag_string_offset_nl"); + emitter.instruction(&format!("mov x2, #{}", STRING_OFFSET_NL_LEN)); // pass the string-offset warning newline length + abi::emit_call_label(emitter, "__rt_diag_warning"); // emit or suppress the string-offset warning newline + + // -- restore stack frame -- + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the runtime warning frame + emitter.instruction("ret"); // return to the string-index caller +} + +/// Emits the x86_64 implementation of `__rt_warn_string_offset`. +fn emit_string_offset_warning_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: string_offset_warning ---"); + emitter.label_global("__rt_warn_string_offset"); + + // -- set up stack frame -- + emitter.instruction("push rbp"); // save the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable runtime warning frame + emitter.instruction("sub rsp, 32"); // reserve saved offset and concat cursor while keeping calls aligned + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the out-of-bounds offset across warning fragments + abi::emit_load_symbol_to_reg(emitter, "r10", "_concat_off", 0); // snapshot concat scratch state before formatting the offset + emitter.instruction("mov QWORD PTR [rbp - 16], r10"); // preserve the concat cursor across itoa + + // -- emit prefix -- + abi::emit_symbol_address(emitter, "rdi", "_diag_string_offset_prefix"); + emitter.instruction(&format!("mov esi, {}", STRING_OFFSET_PREFIX_LEN)); // pass the string-offset warning prefix length + abi::emit_call_label(emitter, "__rt_diag_warning"); // emit or suppress the string-offset warning prefix + + // -- emit formatted offset -- + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the out-of-bounds offset for decimal formatting + abi::emit_call_label(emitter, "__rt_itoa"); // format the offset into concat scratch + emitter.instruction("mov rdi, rax"); // pass the formatted offset pointer to the warning helper + emitter.instruction("mov rsi, rdx"); // pass the formatted offset length to the warning helper + abi::emit_call_label(emitter, "__rt_diag_warning"); // emit or suppress the formatted offset value + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the pre-warning concat cursor + abi::emit_store_reg_to_symbol(emitter, "r10", "_concat_off", 0); // restore concat scratch state for surrounding expressions + + // -- emit newline suffix -- + abi::emit_symbol_address(emitter, "rdi", "_diag_string_offset_nl"); + emitter.instruction(&format!("mov esi, {}", STRING_OFFSET_NL_LEN)); // pass the string-offset warning newline length + abi::emit_call_label(emitter, "__rt_diag_warning"); // emit or suppress the string-offset warning newline + + // -- restore stack frame -- + emitter.instruction("mov rsp, rbp"); // release the runtime warning frame + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return to the string-index caller +} \ No newline at end of file diff --git a/src/codegen/runtime/emitters.rs b/src/codegen/runtime/emitters.rs index 246bee71b2..32c51759bd 100644 --- a/src/codegen/runtime/emitters.rs +++ b/src/codegen/runtime/emitters.rs @@ -33,6 +33,8 @@ use crate::codegen::RuntimeFeatures; /// are available when branches are assembled. pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) { diagnostics::emit_diagnostics(emitter); + diagnostics::emit_string_offset_warning(emitter); + diagnostics::emit_float_to_int_key_deprecation(emitter); // String runtime functions strings::emit_itoa(emitter); diff --git a/src/codegen_ir/lower_inst/hashes.rs b/src/codegen_ir/lower_inst/hashes.rs index 4cf9ea4b95..8344b7525a 100644 --- a/src/codegen_ir/lower_inst/hashes.rs +++ b/src/codegen_ir/lower_inst/hashes.rs @@ -430,6 +430,7 @@ pub(super) fn materialize_hash_key_aarch64(ctx: &mut FunctionContext<'_>, key: V } PhpType::Float => { ctx.load_value_to_reg(key, "d0")?; + emit_float_key_deprecation_check_aarch64(ctx); ctx.emitter.instruction("fcvtzs x1, d0"); // PHP casts float array keys to integer keys abi::emit_load_int_immediate(ctx.emitter, "x2", -1); Ok(()) @@ -460,6 +461,7 @@ pub(super) fn materialize_hash_key_x86_64(ctx: &mut FunctionContext<'_>, key: Va } PhpType::Float => { ctx.load_value_to_reg(key, "xmm0")?; + emit_float_key_deprecation_check_x86_64(ctx); ctx.emitter.instruction("cvttsd2si rsi, xmm0"); // PHP casts float array keys to integer keys abi::emit_load_int_immediate(ctx.emitter, "rdx", -1); Ok(()) @@ -474,6 +476,46 @@ pub(super) fn materialize_hash_key_x86_64(ctx: &mut FunctionContext<'_>, key: Va } } +/// Emits the fractional float check and deprecation warning for AArch64 float keys. +/// +/// Converts the float in `d0` to int and back, comparing for equality; if they +/// differ (fractional part lost), calls `__rt_warn_float_to_int_key` with the +/// original float. `d0` is saved/restored across the call so the caller can +/// still truncate it. +fn emit_float_key_deprecation_check_aarch64(ctx: &mut FunctionContext<'_>) { + let skip = ctx.next_label("float_key_no_deprecation"); + // d0 holds the float key; save it across the truncation test and warning call. + ctx.emitter.instruction("str d0, [sp, #-16]!"); // spill the float key so it survives the warning call + ctx.emitter.instruction("fcvtzs x9, d0"); // truncate the float key to an integer for the whole-number check + ctx.emitter.instruction("scvtf d1, x9"); // convert the truncated integer back to a float for comparison + ctx.emitter.instruction("fcmp d0, d1"); // compare the original float with the round-tripped value + ctx.emitter.instruction(&format!("b.eq {}", skip)); // skip the deprecation when the float is a whole number + abi::emit_call_label(ctx.emitter, "__rt_warn_float_to_int_key"); // emit the PHP deprecation for the fractional float key + ctx.emitter.label(&skip); + ctx.emitter.instruction("ldr d0, [sp], #16"); // restore the float key for the caller's truncation +} + +/// Emits the fractional float check and deprecation warning for x86_64 float keys. +/// +/// Converts the float in `xmm0` to int and back, comparing for equality; if they +/// differ (fractional part lost), calls `__rt_warn_float_to_int_key` with the +/// original float. `xmm0` is saved/restored across the call so the caller can +/// still truncate it. +fn emit_float_key_deprecation_check_x86_64(ctx: &mut FunctionContext<'_>) { + let skip = ctx.next_label("float_key_no_deprecation"); + // xmm0 holds the float key; save it across the truncation test and warning call. + ctx.emitter.instruction("sub rsp, 16"); // reserve aligned scratch space for the float key + ctx.emitter.instruction("movsd QWORD PTR [rsp], xmm0"); // spill the float key so it survives the warning call + ctx.emitter.instruction("cvttsd2si rax, xmm0"); // truncate the float key to an integer for the whole-number check + ctx.emitter.instruction("cvtsi2sd xmm1, rax"); // convert the truncated integer back to a float for comparison + ctx.emitter.instruction("ucomisd xmm0, xmm1"); // compare the original float with the round-tripped value + ctx.emitter.instruction(&format!("je {}", skip)); // skip the deprecation when the float is a whole number + abi::emit_call_label(ctx.emitter, "__rt_warn_float_to_int_key"); // emit the PHP deprecation for the fractional float key + ctx.emitter.label(&skip); + ctx.emitter.instruction("movsd xmm0, QWORD PTR [rsp]"); // restore the float key for the caller's truncation + ctx.emitter.instruction("add rsp, 16"); // release the scratch space +} + /// Materializes a boxed Mixed key as the AArch64 hash key pair `x1`/`x2`. fn materialize_mixed_hash_key_aarch64( ctx: &mut FunctionContext<'_>, diff --git a/src/codegen_ir/lower_inst/strings.rs b/src/codegen_ir/lower_inst/strings.rs index e6c3e4bd34..5afbfdf48f 100644 --- a/src/codegen_ir/lower_inst/strings.rs +++ b/src/codegen_ir/lower_inst/strings.rs @@ -172,6 +172,7 @@ pub(super) fn lower_str_char_at(ctx: &mut FunctionContext<'_>, inst: &Instructio Arch::AArch64 => { ctx.load_string_value_to_regs(string, "x1", "x2")?; require_integer_like(ctx.load_value_to_reg(index, "x0")?, inst)?; + ctx.emitter.instruction("mov x9, x0"); // save the original offset so the oob warning reports the user-visible value ctx.emitter.instruction("cmp x0, #0"); // check whether the requested string offset is negative ctx.emitter.instruction(&format!("b.ge {}", non_negative)); // keep non-negative string offsets unchanged ctx.emitter.instruction("add x0, x2, x0"); // convert negative string offsets to length plus offset @@ -184,12 +185,16 @@ pub(super) fn lower_str_char_at(ctx: &mut FunctionContext<'_>, inst: &Instructio ctx.emitter.instruction("mov x2, #1"); // in-bounds string indexing returns one byte ctx.emitter.instruction(&format!("b {}", end)); // skip the out-of-bounds empty-string result ctx.emitter.label(&oob); + ctx.emitter.instruction("mov x0, x9"); // restore the original offset for the uninitialized-string-offset warning + abi::emit_call_label(ctx.emitter, "__rt_warn_string_offset"); // emit the PHP warning for the out-of-bounds string offset + ctx.emitter.instruction("mov x1, #0"); // materialize a null pointer for the empty string result ctx.emitter.instruction("mov x2, #0"); // out-of-bounds string indexing returns an empty string ctx.emitter.label(&end); } Arch::X86_64 => { ctx.load_string_value_to_regs(string, "r8", "r9")?; require_integer_like(ctx.load_value_to_reg(index, "rax")?, inst)?; + ctx.emitter.instruction("mov r10, rax"); // save the original offset so the oob warning reports the user-visible value ctx.emitter.instruction("cmp rax, 0"); // check whether the requested string offset is negative ctx.emitter.instruction(&format!("jge {}", non_negative)); // keep non-negative string offsets unchanged ctx.emitter.instruction("add rax, r9"); // convert negative string offsets to length plus offset @@ -203,6 +208,9 @@ pub(super) fn lower_str_char_at(ctx: &mut FunctionContext<'_>, inst: &Instructio ctx.emitter.instruction("mov rdx, 1"); // in-bounds string indexing returns one byte ctx.emitter.instruction(&format!("jmp {}", end)); // skip the out-of-bounds empty-string result ctx.emitter.label(&oob); + ctx.emitter.instruction("mov rax, r10"); // restore the original offset for the uninitialized-string-offset warning + abi::emit_call_label(ctx.emitter, "__rt_warn_string_offset"); // emit the PHP warning for the out-of-bounds string offset + ctx.emitter.instruction("xor r8, r8"); // materialize a null pointer for the empty string result ctx.emitter.instruction("mov rax, r8"); // preserve a valid source pointer for the empty string result ctx.emitter.instruction("mov rdx, 0"); // out-of-bounds string indexing returns an empty string ctx.emitter.label(&end); diff --git a/src/ir_lower/context.rs b/src/ir_lower/context.rs index a3f200e30f..3773ed116d 100644 --- a/src/ir_lower/context.rs +++ b/src/ir_lower/context.rs @@ -128,6 +128,13 @@ pub(crate) struct LoweringContext<'m, 'f> { /// so a `return $obj->prop` yields the property's ref-cell pointer instead of a value copy. pub by_ref_return: bool, pub in_main: bool, + /// `true` when the body being lowered is a generator coroutine (contains + /// `yield`, or declares a `Generator` return type). `Generator::throw()` + /// injects exceptions via `__rt_fiber_throw` + `__rt_throw_current`, which + /// longjmps to the nearest `try_push_handler` site. A try/finally with no + /// catch would otherwise skip its finally body under that longjmp path, so + /// the try-lowering synthesizes a catch-all handler for generator bodies. + pub is_generator_body: bool, pub all_global_var_names: HashSet, owner_name: String, closures: Vec, @@ -190,6 +197,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { return_php_type, by_ref_return: false, in_main, + is_generator_body: false, all_global_var_names, owner_name, closures: Vec::new(), diff --git a/src/ir_lower/function.rs b/src/ir_lower/function.rs index 3d2e4d4f41..2e4c4a3377 100644 --- a/src/ir_lower/function.rs +++ b/src/ir_lower/function.rs @@ -571,6 +571,7 @@ fn lower_body_into_function( ) -> Vec { let owner_name = function.name.clone(); let function_by_ref_return = function.flags.by_ref_return; + let is_generator_body = function.flags.is_generator; let by_ref_params = function .params .iter() @@ -602,6 +603,7 @@ fn lower_body_into_function( all_global_var_names, ); ctx.by_ref_return = function_by_ref_return; + ctx.is_generator_body = is_generator_body; for (index, (name, php_type)) in params.iter().enumerate() { ctx.declare_local(name, php_type.clone()); ctx.mark_local_initialized(name); diff --git a/src/ir_lower/stmt/mod.rs b/src/ir_lower/stmt/mod.rs index b447656678..5e640a5147 100644 --- a/src/ir_lower/stmt/mod.rs +++ b/src/ir_lower/stmt/mod.rs @@ -1515,6 +1515,10 @@ fn lower_try_finally_without_catches( try_body: &[Stmt], finally_body: &[Stmt], ) { + if ctx.is_generator_body { + lower_try_finally_generator(ctx, try_body, finally_body); + return; + } let depth = push_finally_frame(ctx, finally_body, true, None); lower_block(ctx, try_body); pop_finally_frame_if_active(ctx, depth); @@ -1523,6 +1527,63 @@ fn lower_try_finally_without_catches( } } +/// Lowers `try`/`finally` (no catch) inside a generator coroutine body. +/// +/// `Generator::throw()` parks a Throwable and resumes the generator's `yield` +/// via `__rt_fiber_suspend`, which re-raises through `__rt_throw_current` — a +/// longjmp that unwinds to the nearest `try_push_handler` site. A plain +/// try/finally without catch never pushes a handler, so the longjmp would skip +/// the finally body and unwind straight to the caller's catch (issue #355). +/// +/// The fix mirrors the #329 catch-in-generator path: push a handler around the +/// try body, and on longjmp into that handler run the finally once then re-raise +/// the captured exception. Normal exits still run the finally via the static +/// finally-frame duplication, exactly like the non-generator lowering above. +fn lower_try_finally_generator( + ctx: &mut LoweringContext<'_, '_>, + try_body: &[Stmt], + finally_body: &[Stmt], +) { + let span = Span::dummy(); + let handler_block = ctx.builder.create_named_block("try.gen_finally_handler", Vec::new()); + let after_block = ctx.builder.create_named_block("try.after", Vec::new()); + let handler_token = handler_block.as_raw() as i64; + + ctx.clear_static_callable_locals(); + ctx.emit_void( + Op::TryPushHandler, + Vec::new(), + Some(Immediate::I64(handler_token)), + Op::TryPushHandler.default_effects(), + Some(span), + ); + // The finally frame is marked run-on-throw=false so a re-throw from inside + // the synthetic handler cannot re-enter the same finally body (that would + // double-run it). The handler emits its own copy of the finally body. + let depth = push_finally_frame(ctx, finally_body, false, Some((handler_token, span))); + lower_block(ctx, try_body); + pop_finally_frame_if_active(ctx, depth); + if !ctx.builder.insertion_block_is_terminated() { + emit_try_pop_handler(ctx, handler_token, span); + lower_block(ctx, finally_body); + branch_to(ctx, after_block); + } + + // Longjmp landing pad: `__rt_throw_current` resumes here with the active + // exception bound to `_exc_value`. Run the finally body exactly once, then + // re-raise so the exception keeps unwinding to the caller's catch. + ctx.builder.position_at_end(handler_block); + emit_try_pop_handler(ctx, handler_token, span); + let exception = lower_current_exception(ctx, span); + lower_block(ctx, finally_body); + if !ctx.builder.insertion_block_is_terminated() { + ctx.builder.terminate(Terminator::Throw { value: exception.value }); + } + + ctx.builder.position_at_end(after_block); + ctx.clear_static_callable_locals(); +} + /// Lowers a `try`/`catch`/`finally` statement while preserving catch-before-finally order. fn lower_try_catch_finally( ctx: &mut LoweringContext<'_, '_>, diff --git a/tests/codegen/arrays/float_key_deprecation.rs b/tests/codegen/arrays/float_key_deprecation.rs new file mode 100644 index 0000000000..25256869ed --- /dev/null +++ b/tests/codegen/arrays/float_key_deprecation.rs @@ -0,0 +1,38 @@ +//! Purpose: +//! Integration tests for PHP deprecation warnings on float array keys. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Asserts both stdout and stderr for fractional, whole-number, and suppressed float keys. + +use crate::support::*; + +/// Verifies that a fractional float array key emits the implicit-conversion deprecation. +#[test] +fn test_float_array_key_emits_deprecation() { + let out = compile_and_run_capture(r#" 'x'][1.9];"#); + assert_eq!(out.stdout, "x"); + assert!( + out.stderr.contains("Implicit conversion from float 1.9 to int loses precision"), + "stderr was: {}", + out.stderr + ); +} + +/// Verifies that a whole-number float array key does not emit the deprecation. +#[test] +fn test_float_array_key_whole_number_no_deprecation() { + let out = compile_and_run_capture(r#" 'x'][2.0];"#); + assert_eq!(out.stdout, ""); + assert_eq!(out.stderr, "", "expected no stderr, got: {}", out.stderr); +} + +/// Verifies that the `@` suppression operator silences the float-key deprecation. +#[test] +fn test_float_array_key_suppressed_by_at() { + let out = compile_and_run_capture(r#" 'x'][1.9]; echo $x;"#); + assert_eq!(out.stdout, "x"); + assert_eq!(out.stderr, ""); +} \ No newline at end of file diff --git a/tests/codegen/arrays/mod.rs b/tests/codegen/arrays/mod.rs index 3fcf6dff96..bd62639ab1 100644 --- a/tests/codegen/arrays/mod.rs +++ b/tests/codegen/arrays/mod.rs @@ -15,3 +15,4 @@ mod callbacks; mod foreach_key_write; mod list_and_keys; mod assoc_set_ops; +mod float_key_deprecation; diff --git a/tests/codegen/generators/send_throw.rs b/tests/codegen/generators/send_throw.rs index de022e4cb4..1217606603 100644 --- a/tests/codegen/generators/send_throw.rs +++ b/tests/codegen/generators/send_throw.rs @@ -258,3 +258,137 @@ echo $g->getReturn(); ); assert_eq!(out, "10|10|null|12"); } + +/// Regression for issue #355: `Generator::throw()` resumes the generator and +/// re-raises via `__rt_throw_current`, a longjmp to the nearest `try_push_handler` +/// site. A `try`/`finally` with no catch never pushes a handler, so the longjmp +/// used to skip the finally body and unwind straight to the caller's catch. +/// PHP 8.4 prints `F:x`. +#[test] +fn test_generator_throw_runs_finally_without_catch() { + let out = compile_and_run( + r#"rewind(); +try { + $g->throw(new Exception('x')); +} catch (Exception $e) { + echo ':' . $e->getMessage(); +} +"#, + ); + assert_eq!(out, "F:x"); +} + +/// Issue #355: nested `try`/`finally` (no catch) in a generator must run both +/// finally bodies on `Generator::throw()`, inner before outer, then re-raise. +/// PHP 8.4 prints `IO:x`. +#[test] +fn test_generator_throw_runs_nested_finally_without_catch() { + let out = compile_and_run( + r#"rewind(); +try { + $g->throw(new Exception('x')); +} catch (Exception $e) { + echo ':' . $e->getMessage(); +} +"#, + ); + assert_eq!(out, "IO:x"); +} + +/// Issue #355: a `return` inside a `finally` suppresses the in-flight exception +/// and becomes the generator's `getReturn()` value, mirroring PHP semantics. +/// PHP 8.4 prints `F` then `int(99)`. +#[test] +fn test_generator_throw_finally_return_suppresses_exception() { + let out = compile_and_run( + r#"rewind(); +try { + $g->throw(new Exception('x')); +} catch (Exception $e) { + echo ':caught'; +} +echo '|'; +var_dump($g->getReturn()); +"#, + ); + assert_eq!(out, "F|int(99)\n"); +} + +/// Issue #355: a `throw` inside a `finally` replaces the original exception and +/// propagates to the caller's catch, mirroring PHP semantics. +/// PHP 8.4 prints `F:replaced`. +#[test] +fn test_generator_throw_finally_throw_replaces_exception() { + let out = compile_and_run( + r#"rewind(); +try { + $g->throw(new Exception('x')); +} catch (Exception $e) { + echo ':' . $e->getMessage(); +} +"#, + ); + assert_eq!(out, "F:replaced"); +} + +/// Issue #355 regression guard: a non-generator `try`/`finally` without catch +/// must keep its existing behavior (finally runs once on normal fall-through; no +/// handler is installed because `__rt_throw_current` is never injected here). +#[test] +fn test_nongenerator_try_finally_without_catch_unchanged() { + let out = compile_and_run( + r#"