Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/codegen/lower_inst/builtins/math.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions src/codegen/lower_inst/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/codegen/lower_inst/objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/codegen/lower_inst/static_properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/codegen_support/hash_crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/codegen_support/runtime/arrays/value_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/codegen_support/runtime/spl/doubly_linked_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/codegen_support/runtime/spl/fixed_array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/codegen_support/runtime/system/json_throw_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
53 changes: 53 additions & 0 deletions src/codegen_support/sentinels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand All @@ -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<String>) {
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:?}"
);
}
}
Loading