diff --git a/CHANGELOG.md b/CHANGELOG.md index b09eb3465a..0ce439e587 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to elephc, a PHP-to-native compiler written in Rust. Releases are listed newest first. ## [Unreleased] +- Fixed a transposed x86_64 heap magic stamped by several throwable emitters (issue #482): objects created through the runtime-raised `ValueError`/`TypeError`/`LogicException`/`Error` paths, JSON throw errors, and some SPL/hash/static-property paths carried a header magic the refcount helpers do not recognize, silently opting them out of reference counting on linux-x86_64. Every kind-word stamp now goes through one shared, tested helper (`x86_64_heap_kind_word`), and a repository lint test keeps the hand-typed transposed literal from coming back. - Removed the legacy direct AST → ASM backend completely. EIR is now the only codegen implementation path. - Int-backed enum `from()` / `tryFrom()` now accept a dynamically-typed (`mixed`) argument (issue #449): a `foreach` value over a heterogeneous array, an untyped parameter, etc. are coerced on their runtime type before the enum lookup — integer/numeric-string resolve (or throw `ValueError`), float truncates, bool/null coerce, and array/object/resource/closure throw `TypeError` naming the given type. Previously any `mixed` argument was rejected at compile time. Target-aware on every supported backend. diff --git a/src/codegen/lower_inst/builtins/math.rs b/src/codegen/lower_inst/builtins/math.rs index 2e6c4f8d9a..3078d7e9f6 100644 --- a/src/codegen/lower_inst/builtins/math.rs +++ b/src/codegen/lower_inst/builtins/math.rs @@ -652,7 +652,7 @@ fn emit_throw_value_error_x86_64( ctx.emitter.instruction("sub rsp, 16"); // keep the nested heap allocation call 16-byte aligned ctx.emitter.instruction("mov rax, 32"); // request Throwable payload storage for the clamp ValueError ctx.emitter.instruction("call __rt_heap_alloc"); // allocate the ValueError object payload - ctx.emitter.instruction("mov r10, 0x4548504c00000006"); // materialize the x86_64 object heap-kind header + ctx.emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(6))); // stamp the canonical x86_64 heap-kind word (magic + kind 6 throwable) ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the allocation header as a runtime object ctx.emitter.instruction("mov r10, QWORD PTR [rip + _spl_value_error_class_id]"); // load ValueError's runtime class id for this program ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store the ValueError class id in the Throwable header diff --git a/src/codegen/lower_inst/enums.rs b/src/codegen/lower_inst/enums.rs index 1ad4740788..7d8dbe3989 100644 --- a/src/codegen/lower_inst/enums.rs +++ b/src/codegen/lower_inst/enums.rs @@ -437,7 +437,7 @@ fn emit_throw_value_error_from_string_result_aarch64(ctx: &mut FunctionContext<' fn emit_throw_value_error_from_string_result_x86_64(ctx: &mut FunctionContext<'_>) { abi::emit_load_int_immediate(ctx.emitter, "rax", 32); abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); - ctx.emitter.instruction("mov r10, 0x4548504c00000006"); // x86_64 heap-kind word: HE LP magic + kind 6 object + ctx.emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(6))); // stamp the canonical x86_64 heap-kind word (magic + kind 6 throwable) ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object abi::emit_load_symbol_to_reg(ctx.emitter, "r10", "_spl_value_error_class_id", 0); ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store ValueError class id at object header @@ -681,7 +681,7 @@ fn emit_throw_type_error_from_string_result(ctx: &mut FunctionContext<'_>) { Arch::X86_64 => { abi::emit_load_int_immediate(ctx.emitter, "rax", 32); abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); - ctx.emitter.instruction("mov r10, 0x4548504c00000006"); // x86_64 heap-kind word: HELP magic + kind 6 object + ctx.emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(6))); // stamp the canonical x86_64 heap-kind word (magic + kind 6 throwable) ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object abi::emit_load_symbol_to_reg(ctx.emitter, "r10", "_spl_type_error_class_id", 0); ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store TypeError class id at object header @@ -726,7 +726,7 @@ fn emit_throw_enum_from_type_error_x86_64( ) { abi::emit_load_int_immediate(ctx.emitter, "rax", 32); abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); - ctx.emitter.instruction("mov r10, 0x4548504c00000006"); // x86_64 heap-kind word: HELP magic + kind 6 object + ctx.emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(6))); // stamp the canonical x86_64 heap-kind word (magic + kind 6 throwable) ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object abi::emit_load_symbol_to_reg(ctx.emitter, "r10", "_spl_type_error_class_id", 0); ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store TypeError class id at object header diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index 49cf6fff96..17b2e293b8 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -708,7 +708,7 @@ fn emit_throw_iterator_iterator_downcast_logic_exception(ctx: &mut FunctionConte ctx.emitter.instruction("sub rsp, 16"); // keep the nested heap allocation call aligned ctx.emitter.instruction("mov rax, 32"); // request Throwable payload storage abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); - ctx.emitter.instruction("mov r10, 0x4548504c00000006"); // materialize the x86_64 object heap kind word + ctx.emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(6))); // stamp the canonical x86_64 heap-kind word (magic + kind 6 throwable) ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object ctx.emitter.instruction("mov r10, QWORD PTR [rip + _spl_logic_exception_class_id]"); // load LogicException's runtime class id ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store the class id at object header @@ -5034,7 +5034,7 @@ fn emit_uninitialized_typed_property_fatal( ctx.emitter.instruction("sub rsp, 16"); // keep the nested heap allocation call 16-byte aligned ctx.emitter.instruction("mov rax, 32"); // request Throwable payload storage ctx.emitter.instruction("call __rt_heap_alloc"); // allocate the Error object payload - ctx.emitter.instruction("mov r10, 0x4548504c00000006"); // x86_64 heap-kind word: HE LP magic + kind 6 object + ctx.emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(6))); // stamp the canonical x86_64 heap-kind word (magic + kind 6 throwable) ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object abi::emit_load_symbol_to_reg(ctx.emitter, "r10", "_spl_error_class_id", 0); // load Error's runtime class id for this program ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store class id at the object header diff --git a/src/codegen/lower_inst/static_properties.rs b/src/codegen/lower_inst/static_properties.rs index f62a893a1e..70de1c5670 100644 --- a/src/codegen/lower_inst/static_properties.rs +++ b/src/codegen/lower_inst/static_properties.rs @@ -752,7 +752,7 @@ fn emit_uninitialized_static_property_fatal( ctx.emitter.instruction("sub rsp, 16"); // keep the nested heap allocation call 16-byte aligned ctx.emitter.instruction("mov rax, 32"); // request Throwable payload storage ctx.emitter.instruction("call __rt_heap_alloc"); // allocate the Error object payload - ctx.emitter.instruction("mov r10, 0x4548504c00000006"); // x86_64 heap-kind word: HE LP magic + kind 6 object + ctx.emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(6))); // stamp the canonical x86_64 heap-kind word (magic + kind 6 throwable) ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object abi::emit_load_symbol_to_reg(ctx.emitter, "r10", "_spl_error_class_id", 0); // load Error's runtime class id for this program ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store class id at the object header diff --git a/src/codegen_support/hash_crypto.rs b/src/codegen_support/hash_crypto.rs index 0c0f2b5004..93d2ba7c93 100644 --- a/src/codegen_support/hash_crypto.rs +++ b/src/codegen_support/hash_crypto.rs @@ -112,7 +112,7 @@ fn emit_throw_value_error_x86_64(emitter: &mut Emitter, message_symbol: &str, me emitter.instruction("sub rsp, 16"); // keep the nested heap allocation call 16-byte aligned emitter.instruction("mov rax, 32"); // request Throwable payload storage emitter.instruction("call __rt_heap_alloc"); // allocate the ValueError object payload - emitter.instruction("mov r10, 0x4548504c00000006"); // x86_64 heap-kind word: HE LP magic + kind 6 object + emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(6))); // stamp the canonical x86_64 heap-kind word (magic + kind 6 throwable) emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object abi::emit_load_symbol_to_reg(emitter, "r10", "_spl_value_error_class_id", 0); // load ValueError's runtime class id for this program emitter.instruction("mov QWORD PTR [rax], r10"); // store class id at the object header diff --git a/src/codegen_support/runtime/arrays/value_error.rs b/src/codegen_support/runtime/arrays/value_error.rs index 30b40ea716..0bcd50c271 100644 --- a/src/codegen_support/runtime/arrays/value_error.rs +++ b/src/codegen_support/runtime/arrays/value_error.rs @@ -53,7 +53,7 @@ pub(super) fn emit_throw_value_error_x86_64( emitter.instruction("sub rsp, 16"); // keep the nested heap allocation call 16-byte aligned emitter.instruction("mov rax, 32"); // request Throwable payload storage emitter.instruction("call __rt_heap_alloc"); // allocate the ValueError object payload - emitter.instruction("mov r10, 0x4548504c00000006"); // x86_64 heap-kind word: HE LP magic + kind 6 object + emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(6))); // stamp the canonical x86_64 heap-kind word (magic + kind 6 throwable) emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object abi::emit_load_symbol_to_reg(emitter, "r10", "_spl_value_error_class_id", 0); // load ValueError's runtime class id for this program emitter.instruction("mov QWORD PTR [rax], r10"); // store class id at the object header diff --git a/src/codegen_support/runtime/spl/doubly_linked_list.rs b/src/codegen_support/runtime/spl/doubly_linked_list.rs index 56ef9777d8..b6bcf5585f 100644 --- a/src/codegen_support/runtime/spl/doubly_linked_list.rs +++ b/src/codegen_support/runtime/spl/doubly_linked_list.rs @@ -2525,7 +2525,7 @@ fn emit_throw_exception_x86_64( emitter.instruction("sub rsp, 16"); // keep the nested heap allocation call 16-byte aligned emitter.instruction("mov rax, 32"); // request Throwable payload storage emitter.instruction("call __rt_heap_alloc"); // allocate the exception object payload - emitter.instruction("mov r10, 0x4548504c00000006"); // x86_64 heap-kind word: HE LP magic + kind 6 object + emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(6))); // stamp the canonical x86_64 heap-kind word (magic + kind 6 throwable) emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object abi::emit_load_symbol_to_reg(emitter, "r10", class_id_symbol, 0); // load the exception class id for this program emitter.instruction("mov QWORD PTR [rax], r10"); // store class id at object header diff --git a/src/codegen_support/runtime/spl/fixed_array.rs b/src/codegen_support/runtime/spl/fixed_array.rs index c3db05f1ff..8caff31105 100644 --- a/src/codegen_support/runtime/spl/fixed_array.rs +++ b/src/codegen_support/runtime/spl/fixed_array.rs @@ -1447,7 +1447,7 @@ fn emit_throw_exception_x86_64( emitter.instruction("sub rsp, 16"); // keep the nested heap allocation call 16-byte aligned emitter.instruction("mov rax, 32"); // request Throwable payload storage emitter.instruction("call __rt_heap_alloc"); // allocate the exception object payload - emitter.instruction("mov r10, 0x4548504c00000006"); // x86_64 heap-kind word: HE LP magic + kind 6 object + emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(6))); // stamp the canonical x86_64 heap-kind word (magic + kind 6 throwable) emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object abi::emit_load_symbol_to_reg(emitter, "r10", class_id_symbol, 0); // load the exception class id for this program emitter.instruction("mov QWORD PTR [rax], r10"); // store class id at object header diff --git a/src/codegen_support/runtime/system/json_throw_error.rs b/src/codegen_support/runtime/system/json_throw_error.rs index 3cc87e97a5..1e470e0c38 100644 --- a/src/codegen_support/runtime/system/json_throw_error.rs +++ b/src/codegen_support/runtime/system/json_throw_error.rs @@ -161,7 +161,7 @@ fn emit_json_throw_error_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rax, 32"); // size = class_id (8) + message ptr/len (16) + code (8) emitter.instruction("call __rt_heap_alloc"); // allocate the JsonException payload (rax = payload ptr) - emitter.instruction("mov r10, 0x4548504c00000006"); // x86_64 heap-kind word: HE LP magic + kind 6 (object) + emitter.instruction(&format!("mov r10, 0x{:x}", crate::codegen_support::sentinels::x86_64_heap_kind_word(6))); // stamp the canonical x86_64 heap-kind word (magic + kind 6 throwable) emitter.instruction("mov QWORD PTR [rax - 8], r10"); // tag the allocation as an object in the uniform header abi::emit_load_symbol_to_reg(emitter, "r10", "_json_exception_class_id", 0); // load JsonException's runtime class id (-1 when absent) diff --git a/src/codegen_support/sentinels.rs b/src/codegen_support/sentinels.rs index 309e7247c9..0682eaf511 100644 --- a/src/codegen_support/sentinels.rs +++ b/src/codegen_support/sentinels.rs @@ -147,6 +147,21 @@ pub(crate) fn emit_tagged_scalar_to_int_null_as_zero(emitter: &mut Emitter) { } } + +/// High 32 bits of the x86_64 uniform heap-header kind word (`"ELPH"` in ASCII bytes +/// `0x45 0x4C 0x50 0x48`). Every x86_64 allocation is stamped with this marker; the +/// refcount and free helpers ignore pointers whose header does not carry it, so an +/// emitter that stamps a different value silently opts its objects out of refcounting. +pub(crate) const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; + +/// Builds the full x86_64 heap-header kind word for `kind`: the heap magic in the high +/// word and the uniform heap-kind tag in the low byte. Emitters must use this instead of +/// a hand-typed literal — a transposed magic assembles fine but makes every refcount +/// operation on the stamped object a silent no-op (issue #482). +pub(crate) fn x86_64_heap_kind_word(kind: u8) -> u64 { + (X86_64_HEAP_MAGIC_HI32 << 32) | kind as u64 +} + #[cfg(test)] mod tests { use super::*; @@ -167,4 +182,42 @@ mod tests { ); assert_eq!(UNINITIALIZED_TYPED_PROPERTY_SENTINEL, i64::MAX - 2); } + + /// Verifies the x86_64 heap kind word carries the canonical magic ("ELPH") in the + /// high word and the kind tag in the low byte — guarding against the transposed + /// literal that no-ops refcounting (issue #482). + #[test] + fn test_x86_64_heap_kind_word_layout() { + assert_eq!(x86_64_heap_kind_word(6), 0x454C_5048_0000_0006); + assert_eq!(x86_64_heap_kind_word(4), 0x454C_5048_0000_0004); + assert_ne!(x86_64_heap_kind_word(6) >> 32, 0x4548_504C, "transposed magic"); + } + + /// Repo lint: no emitter may hand-type the transposed heap magic again — every + /// stamped kind word must come from `x86_64_heap_kind_word` (issue #482). + #[test] + fn test_no_transposed_heap_magic_in_source() { + fn scan(dir: &std::path::Path, hits: &mut Vec) { + for entry in std::fs::read_dir(dir).expect("readable src dir") { + let path = entry.expect("dir entry").path(); + if path.is_dir() { + scan(&path, hits); + } else if path.extension().is_some_and(|e| e == "rs") { + let body = std::fs::read_to_string(&path).expect("readable source"); + // Needle built at runtime so this test file never matches itself. + let needle = ["0x4548", "504c"].concat(); + if body.to_lowercase().contains(&needle) { + hits.push(path.display().to_string()); + } + } + } + } + let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut hits = Vec::new(); + scan(&src, &mut hits); + assert!( + hits.is_empty(), + "transposed x86_64 heap magic found in: {hits:?}" + ); + } }