From 56b2c01e7f90b71b349b56b9e03f03d01225f30f Mon Sep 17 00:00:00 2001 From: Guillaume Loulier Date: Tue, 30 Jun 2026 15:38:20 +0200 Subject: [PATCH] fix: array/hash strict equality and var_dump nested recursion #424: array/hash strict equality (===) was unsupported by the EIR backend, causing a compile error. New runtime helpers __rt_array_strict_eq and __rt_hash_strict_eq compare indexed arrays and hashes element-by-element following insertion-order chains. Pointer-identity short-circuit handles aliases and cycles. String elements compare via __rt_str_eq; Mixed elements via __rt_mixed_strict_eq. Loose equality (==) deferred. #388: var_dump printed NULL for nested arrays/hashes instead of recursing. New __rt_var_dump_value recursive renderer dispatches on runtime tags: arrays/hashes recurse with 2-space-per-level indent, Mixed cells unbox and redispatch. Depth cap at 128 prevents stack overflow on cyclic arrays. Rodata literals updated to remove baked-in 2-space prefix; indentation is now driven by __rt_var_dump_spaces. Objects (tag 6) remain NULL (documented limitation). --- docs/php/system-and-io.md | 2 +- examples/nested-arrays/main.php | 4 + src/codegen/runtime/arrays/array_strict_eq.rs | 323 ++++++ src/codegen/runtime/arrays/hash_strict_eq.rs | 396 +++++++ src/codegen/runtime/arrays/mod.rs | 6 + src/codegen/runtime/data/fixed.rs | 37 +- src/codegen/runtime/emitters.rs | 5 + src/codegen/runtime/io/mod.rs | 2 +- src/codegen/runtime/io/var_dump_walk.rs | 1007 ++++++++++++++--- src/codegen_ir/lower_inst/builtins/debug.rs | 67 +- src/codegen_ir/lower_inst/comparisons.rs | 75 ++ tests/codegen/io/printing.rs | 35 +- tests/codegen/regressions.rs | 2 + tests/codegen/regressions/array_equality.rs | 127 +++ tests/codegen/regressions/builtins_misc.rs | 8 +- .../types/iterable/builtins_and_casts.rs | 4 +- tests/codegen/types/iterable/foreach.rs | 12 +- tests/ir_backend_smoke_test.rs | 2 +- 18 files changed, 1951 insertions(+), 163 deletions(-) create mode 100644 src/codegen/runtime/arrays/array_strict_eq.rs create mode 100644 src/codegen/runtime/arrays/hash_strict_eq.rs create mode 100644 tests/codegen/regressions/array_equality.rs diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index c8c6cb1e04..440f6f8e87 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -346,7 +346,7 @@ wrappers are documented in [Streams](streams.md). | Function | Signature | Description | |---|---|---| -| `var_dump()` | `var_dump($value): void` | Output type and value. Homogeneous indexed arrays of `int`, `string`, `bool`, or `float` and associative arrays (hashes) print full per-element bodies (`[N]=>\n int(V)\n`, `["key"]=>\n string(…)\n`, etc.). Nested arrays/objects inside a Mixed-element array or hash print `NULL` (recursive nesting into those layouts is still pending). | +| `var_dump()` | `var_dump($value): void` | Output type and value. Homogeneous indexed arrays of `int`, `string`, `bool`, or `float` print full per-element bodies (`[N]=>\n int(V)\n`, `["key"]=>\n string(…)\n`, etc.). Associative arrays (hashes), `Mixed`-element arrays, and arbitrarily nested arrays recurse fully through the runtime value renderer, matching PHP's `array(N) {\n [key]=>\n TYPE(VAL)\n}\n` layout with 2-space indentation per level. Objects print `NULL` (full object dumps are not yet supported). | | `print_r()` | `print_r($value): void` | Human-readable output. Indexed arrays, associative arrays, and arbitrarily nested arrays print PHP's recursive `Array\n(\n [key] => value\n)\n` layout (unquoted keys, 4 spaces of indentation per level, `1`/empty for bool `true`/`false`, empty for `null`). The 2-argument return form (`print_r($v, true)`) is not yet supported. | | `var_export()` | `var_export($value, $return = false): ?string` | Parsable representation. Renders scalars (`'…'`-quoted strings with `\\`/`\'` escaping, `true`/`false`, `NULL`, integers, and floats — an integer-valued float gains a `.0`) and arbitrarily nested arrays in PHP's `array (\n key => value,\n)` layout (2 spaces of indentation per level, integer keys bare and string keys quoted, nested arrays on their own line). With `$return = true` the rendering is returned instead of printed. Objects are not yet rendered (PHP emits `\Class::__set_state(...)`). Floats use PHP's `serialize_precision = -1` semantics — the shortest decimal that round-trips back to the same `double` (so `1/3` renders as `0.3333333333333333`, not the 14-digit `(string)` form), with the same scientific layout as PHP (`1.0E+17`, `1.0E-6`), an integer-valued float gaining `.0` (`1.0`, `100.0`), and `-0.0`, `INF`, `-INF`, `NAN` preserved. This is independent of the default `precision` used by `echo`/`(string)`. | diff --git a/examples/nested-arrays/main.php b/examples/nested-arrays/main.php index 5e596af54a..19b9f19df7 100644 --- a/examples/nested-arrays/main.php +++ b/examples/nested-arrays/main.php @@ -22,3 +22,7 @@ echo "\nAfter adding a row:\n"; echo "Rows: " . count($matrix) . "\n"; echo "New row: " . $matrix[3][0] . " " . $matrix[3][1] . " " . $matrix[3][2] . "\n"; + +// var_dump recurses into nested arrays, matching PHP's layout +echo "\nvar_dump of the matrix:\n"; +var_dump($matrix); diff --git a/src/codegen/runtime/arrays/array_strict_eq.rs b/src/codegen/runtime/arrays/array_strict_eq.rs new file mode 100644 index 0000000000..952924fe29 --- /dev/null +++ b/src/codegen/runtime/arrays/array_strict_eq.rs @@ -0,0 +1,323 @@ +//! Purpose: +//! Emits the `__rt_array_strict_eq` runtime helper for indexed-array strict +//! equality (`===`) comparisons. Operates on the packed indexed-array layout +//! `[length:8][capacity:8][elem_size:8][elements...]` produced by +//! `__rt_array_new` / `__rt_array_push_*` / `__rt_array_set_*`. +//! +//! Called from: +//! - `crate::codegen::runtime::emitters::emit_runtime()` via `crate::codegen::runtime::arrays`. +//! +//! Key details: +//! - Compares two indexed arrays for PHP strict equality: identical length, +//! identical `elem_size`, and element-wise value equality. Element value +//! comparison dispatches on `elem_size`: 8-byte slots compare the full word +//! (int/bool/float bit pattern); 16-byte slots hold a string `(ptr, len)` +//! pair and compare by value through `__rt_str_eq`. +//! - A pointer-identity short-circuit handles aliases and cycles (`left == right`). +//! - The 8-byte float bit comparison treats `NaN == NaN` as true, which differs +//! from PHP's `NaN === NaN === false`. This is a documented first-cut limitation; +//! the runtime does not currently distinguish float slots by a separate tag. +//! Mixed-valued indexed arrays (boxed `Mixed` slots, `elem_size == 8`) compare +//! the boxed cell pointers through `__rt_mixed_strict_eq` so heterogeneous +//! element types compare by runtime tag and payload. + +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; + +/// Emits the indexed-array strict equality helper for the current target. +/// +/// Dispatches to the x86_64 Linux variant when targeting that architecture; +/// otherwise emits the portable ARM64 implementation. +/// +/// # Inputs (ARM64) +/// - `x0`: left indexed-array pointer +/// - `x1`: right indexed-array pointer +/// +/// # Output +/// - `x0` (ARM64) / `rax` (x86_64): `1` when strictly equal, `0` otherwise. +pub fn emit_array_strict_eq(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_array_strict_eq_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: array_strict_eq ---"); + emitter.label_global("__rt_array_strict_eq"); + + // -- pointer-identity short-circuit (aliases and cycles) -- + emitter.instruction("cmp x0, x1"); // compare the left and right array pointers for identity + emitter.instruction("b.eq __rt_array_strict_eq_true"); // identical pointers are strictly equal + + // -- set up stack frame and save inputs -- + // [sp,#0] = left array pointer + // [sp,#8] = right array pointer + // [sp,#16] = loop index + // [sp,#24] = length (shared once verified) + // [sp,#32] = elem_size (shared once verified) + // [sp,#40] = saved x29 + // [sp,#48] = saved x30 + emitter.instruction("sub sp, sp, #64"); // reserve the helper frame for inputs, loop state, and saved registers + emitter.instruction("stp x29, x30, [sp, #40]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #40"); // establish the helper frame pointer + emitter.instruction("stp x0, x1, [sp, #0]"); // save the left and right array pointers + + // -- compare lengths -- + emitter.instruction("ldr x2, [x0]"); // load the left indexed-array length + emitter.instruction("ldr x3, [x1]"); // load the right indexed-array length + emitter.instruction("cmp x2, x3"); // compare left and right lengths + emitter.instruction("b.ne __rt_array_strict_eq_false_restore"); // different lengths are never strictly equal + + // -- compare elem_sizes -- + emitter.instruction("ldr x4, [x0, #16]"); // load the left element size + emitter.instruction("ldr x5, [x1, #16]"); // load the right element size + emitter.instruction("cmp x4, x5"); // compare left and right element sizes + emitter.instruction("b.ne __rt_array_strict_eq_false_restore"); // different element sizes are never strictly equal + + // -- length zero short-circuits to true -- + emitter.instruction("cbz x2, __rt_array_strict_eq_true_restore"); // empty arrays with matching element size are strictly equal + + // -- save loop state and dispatch on element size -- + emitter.instruction("str x2, [sp, #24]"); // save the shared length for the element loop + emitter.instruction("str x4, [sp, #32]"); // save the shared element size for the element loop + emitter.instruction("str xzr, [sp, #16]"); // initialize the loop index to zero + + // -- dispatch on element size: 8-byte scalar/Mixed slots vs 16-byte string slots -- + emitter.instruction("cmp x4, #16"); // are the slots 16-byte string pairs? + emitter.instruction("b.eq __rt_array_strict_eq_str_loop"); // route 16-byte string slots through the string-equality loop + + // -- 8-byte element loop: int/bool/float bit pattern or boxed Mixed pointer -- + emitter.label("__rt_array_strict_eq_word_loop"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the left indexed-array pointer + emitter.instruction("ldr x1, [sp, #8]"); // reload the right indexed-array pointer + emitter.instruction("ldr x6, [sp, #16]"); // reload the current loop index + emitter.instruction("ldr x7, [sp, #32]"); // reload the element size (8 for word slots) + + // -- detect boxed Mixed slots (value_type tag 7) and delegate to __rt_mixed_strict_eq -- + emitter.instruction("ldr x8, [x0, #-8]"); // load the left packed indexed-array metadata + emitter.instruction("lsr x8, x8, #8"); // shift the value_type byte into the low byte + emitter.instruction("and x8, x8, #0xff"); // isolate the value_type tag + emitter.instruction("cmp x8, #7"); // value_type 7 marks boxed Mixed slots + emitter.instruction("b.ne __rt_array_strict_eq_word_cmp"); // non-Mixed slots compare the raw word directly + + // -- boxed Mixed slot comparison via __rt_mixed_strict_eq -- + emitter.instruction("add x9, x0, #24"); // compute the left data region base + emitter.instruction("ldr x0, [x9, x6, lsl #3]"); // load the left boxed Mixed pointer + emitter.instruction("add x9, x1, #24"); // compute the right data region base + emitter.instruction("ldr x1, [x9, x6, lsl #3]"); // load the right boxed Mixed pointer + emitter.instruction("stp x29, x30, [sp, #40]"); // re-save frame registers around the nested helper call + emitter.instruction("bl __rt_mixed_strict_eq"); // compare the boxed Mixed cells by tag and payload + emitter.instruction("ldp x29, x30, [sp, #40]"); // restore frame registers after the nested helper call + emitter.instruction("cbz x0, __rt_array_strict_eq_false_restore"); // a mismatched Mixed cell makes the arrays unequal + emitter.instruction("b __rt_array_strict_eq_word_next"); // advance to the next word slot + + // -- raw word comparison for int/bool/float slots -- + emitter.label("__rt_array_strict_eq_word_cmp"); + emitter.instruction("add x9, x0, #24"); // compute the left data region base + emitter.instruction("ldr x10, [x9, x6, lsl #3]"); // load the left element word + emitter.instruction("add x9, x1, #24"); // compute the right data region base + emitter.instruction("ldr x11, [x9, x6, lsl #3]"); // load the right element word + emitter.instruction("cmp x10, x11"); // compare the two element words + emitter.instruction("b.ne __rt_array_strict_eq_false_restore"); // a mismatched word makes the arrays unequal + + emitter.label("__rt_array_strict_eq_word_next"); + emitter.instruction("ldr x6, [sp, #16]"); // reload the loop index + emitter.instruction("add x6, x6, #1"); // advance to the next element + emitter.instruction("str x6, [sp, #16]"); // store the updated loop index + emitter.instruction("ldr x7, [sp, #24]"); // reload the shared length + emitter.instruction("cmp x6, x7"); // have all elements been compared? + emitter.instruction("b.lo __rt_array_strict_eq_word_loop"); // continue while the index remains below the length + emitter.instruction("b __rt_array_strict_eq_true_restore"); // all words matched + + // -- 16-byte string element loop: compare (ptr, len) pairs by value via __rt_str_eq -- + emitter.label("__rt_array_strict_eq_str_loop"); + emitter.instruction("ldr x6, [sp, #16]"); // reload the current loop index + emitter.instruction("ldr x0, [sp, #0]"); // reload the left indexed-array pointer + emitter.instruction("ldr x1, [sp, #8]"); // reload the right indexed-array pointer + + // -- compute left element address: base + 24 + index * 16 -- + emitter.instruction("add x9, x0, #24"); // compute the left data region base + emitter.instruction("add x9, x9, x6, lsl #4"); // offset to the left slot address + emitter.instruction("ldr x1, [x9]"); // load the left string pointer + emitter.instruction("ldr x2, [x9, #8]"); // load the left string length + + // -- compute right element address: base + 24 + index * 16 -- + emitter.instruction("ldr x0, [sp, #8]"); // reload the right indexed-array pointer into a temporary + emitter.instruction("add x9, x0, #24"); // compute the right data region base + emitter.instruction("add x9, x9, x6, lsl #4"); // offset to the right slot address + emitter.instruction("ldr x3, [x9]"); // load the right string pointer + emitter.instruction("ldr x4, [x9, #8]"); // load the right string length + + // -- call __rt_str_eq(ptr_a, len_a, ptr_b, len_b) -- + emitter.instruction("stp x29, x30, [sp, #40]"); // re-save frame registers around the nested helper call + emitter.instruction("bl __rt_str_eq"); // compare the two string payloads byte-by-byte + emitter.instruction("ldp x29, x30, [sp, #40]"); // restore frame registers after the nested helper call + emitter.instruction("cbz x0, __rt_array_strict_eq_false_restore"); // a mismatched string makes the arrays unequal + + // -- advance the string loop -- + emitter.instruction("ldr x6, [sp, #16]"); // reload the loop index + emitter.instruction("add x6, x6, #1"); // advance to the next string slot + emitter.instruction("str x6, [sp, #16]"); // store the updated loop index + emitter.instruction("ldr x7, [sp, #24]"); // reload the shared length + emitter.instruction("cmp x6, x7"); // have all string elements been compared? + emitter.instruction("b.lo __rt_array_strict_eq_str_loop"); // continue while the index remains below the length + emitter.instruction("b __rt_array_strict_eq_true_restore"); // all strings matched + + // -- result paths -- + emitter.label("__rt_array_strict_eq_true_restore"); + emitter.instruction("mov x0, #1"); // materialize the strict-equality true result + emitter.instruction("b __rt_array_strict_eq_epilogue"); // skip the false path + + emitter.label("__rt_array_strict_eq_false_restore"); + emitter.instruction("mov x0, #0"); // materialize the strict-equality false result + + emitter.label("__rt_array_strict_eq_epilogue"); + emitter.instruction("ldp x29, x30, [sp, #40]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // release the helper frame + emitter.instruction("ret"); // return the strict-equality boolean in x0 + + // -- no-frame fast paths (identity and early length checks jump here directly) -- + emitter.label("__rt_array_strict_eq_true"); + emitter.instruction("mov x0, #1"); // materialize true for identical pointers + emitter.instruction("ret"); // return true without allocating a frame +} + +/// Emits the x86_64 Linux variant of the indexed-array strict equality helper. +/// +/// Mirrors the ARM64 algorithm using the System V AMD64 ABI: `rdi` for the left +/// array pointer and `rsi` for the right, with the boolean result in `rax`. +fn emit_array_strict_eq_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: array_strict_eq ---"); + emitter.label_global("__rt_array_strict_eq"); + + // -- pointer-identity short-circuit (aliases and cycles) -- + emitter.instruction("cmp rdi, rsi"); // compare the left and right array pointers for identity + emitter.instruction("je __rt_array_strict_eq_true"); // identical pointers are strictly equal + + // -- set up stack frame and save inputs -- + // [rbp - 8] = left array pointer + // [rbp - 16] = right array pointer + // [rbp - 24] = loop index + // [rbp - 32] = length (shared once verified) + // [rbp - 40] = elem_size (shared once verified) + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame base + emitter.instruction("sub rsp, 48"); // reserve spill slots for inputs and loop state + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the left indexed-array pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the right indexed-array pointer + + // -- compare lengths -- + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the left indexed-array length + emitter.instruction("mov r11, QWORD PTR [rsi]"); // load the right indexed-array length + emitter.instruction("cmp r10, r11"); // compare left and right lengths + emitter.instruction("jne __rt_array_strict_eq_false_restore"); // different lengths are never strictly equal + + // -- compare elem_sizes -- + emitter.instruction("mov r10, QWORD PTR [rdi + 16]"); // load the left element size + emitter.instruction("mov r11, QWORD PTR [rsi + 16]"); // load the right element size + emitter.instruction("cmp r10, r11"); // compare left and right element sizes + emitter.instruction("jne __rt_array_strict_eq_false_restore"); // different element sizes are never strictly equal + + // -- length zero short-circuits to true -- + emitter.instruction("test r10, r10"); // is the shared length zero? + emitter.instruction("jz __rt_array_strict_eq_true_restore"); // empty arrays with matching element size are strictly equal + + // -- save loop state and dispatch on element size -- + emitter.instruction("mov rax, QWORD PTR [rdi]"); // reload the shared left indexed-array length + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // save the shared length for the element loop + emitter.instruction("mov QWORD PTR [rbp - 40], r10"); // save the shared element size for the element loop + emitter.instruction("mov QWORD PTR [rbp - 24], 0"); // initialize the loop index to zero + + // -- dispatch on element size: 8-byte scalar/Mixed slots vs 16-byte string slots -- + emitter.instruction("cmp r10, 16"); // are the slots 16-byte string pairs? + emitter.instruction("je __rt_array_strict_eq_str_loop"); // route 16-byte string slots through the string-equality loop + + // -- 8-byte element loop: int/bool/float bit pattern or boxed Mixed pointer -- + emitter.label("__rt_array_strict_eq_word_loop"); + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the left indexed-array pointer + emitter.instruction("mov r11, QWORD PTR [rbp - 16]"); // reload the right indexed-array pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // reload the current loop index + emitter.instruction("mov rcx, QWORD PTR [rbp - 40]"); // reload the element size (8 for word slots) + + // -- detect boxed Mixed slots (value_type tag 7) and delegate to __rt_mixed_strict_eq -- + emitter.instruction("mov r8, QWORD PTR [r10 - 8]"); // load the left packed indexed-array metadata + emitter.instruction("shr r8, 8"); // shift the value_type byte into the low byte + emitter.instruction("and r8, 0xff"); // isolate the value_type tag + emitter.instruction("cmp r8, 7"); // value_type 7 marks boxed Mixed slots + emitter.instruction("jne __rt_array_strict_eq_word_cmp"); // non-Mixed slots compare the raw word directly + + // -- boxed Mixed slot comparison via __rt_mixed_strict_eq -- + emitter.instruction("mov rdi, QWORD PTR [r10 + 24 + rax * 8]"); // load the left boxed Mixed pointer + emitter.instruction("mov rsi, QWORD PTR [r11 + 24 + rax * 8]"); // load the right boxed Mixed pointer + emitter.instruction("call __rt_mixed_strict_eq"); // compare the boxed Mixed cells by tag and payload + emitter.instruction("test rax, rax"); // check the strict-equality helper result + emitter.instruction("jz __rt_array_strict_eq_false_restore"); // a mismatched Mixed cell makes the arrays unequal + emitter.instruction("jmp __rt_array_strict_eq_word_next"); // advance to the next word slot + + // -- raw word comparison for int/bool/float slots -- + emitter.label("__rt_array_strict_eq_word_cmp"); + emitter.instruction("mov r8, QWORD PTR [r10 + 24 + rax * 8]"); // load the left element word + emitter.instruction("mov r9, QWORD PTR [r11 + 24 + rax * 8]"); // load the right element word + emitter.instruction("cmp r8, r9"); // compare the two element words + emitter.instruction("jne __rt_array_strict_eq_false_restore"); // a mismatched word makes the arrays unequal + + emitter.label("__rt_array_strict_eq_word_next"); + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // reload the loop index + emitter.instruction("add rax, 1"); // advance to the next element + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // store the updated loop index + emitter.instruction("mov rcx, QWORD PTR [rbp - 32]"); // reload the shared length + emitter.instruction("cmp rax, rcx"); // have all elements been compared? + emitter.instruction("jb __rt_array_strict_eq_word_loop"); // continue while the index remains below the length + emitter.instruction("jmp __rt_array_strict_eq_true_restore"); // all words matched + + // -- 16-byte string element loop: compare (ptr, len) pairs by value via __rt_str_eq -- + emitter.label("__rt_array_strict_eq_str_loop"); + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the left indexed-array pointer + emitter.instruction("mov r11, QWORD PTR [rbp - 16]"); // reload the right indexed-array pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // reload the current loop index + + // -- load left string (ptr, len): address = base + 24 + index * 16 -- + emitter.instruction("lea r8, [r10 + rax * 2]"); // scale the index by 16 bytes (rax * 2 * 8) + emitter.instruction("lea r8, [r8 + 24]"); // offset past the fixed header to the left slot + emitter.instruction("mov rdi, QWORD PTR [r8]"); // load the left string pointer into the first str_eq argument + emitter.instruction("mov rsi, QWORD PTR [r8 + 8]"); // load the left string length into the second str_eq argument + + // -- load right string (ptr, len): address = base + 24 + index * 16 -- + emitter.instruction("lea r8, [r11 + rax * 2]"); // scale the index by 16 bytes (rax * 2 * 8) + emitter.instruction("lea r8, [r8 + 24]"); // offset past the fixed header to the right slot + emitter.instruction("mov rdx, QWORD PTR [r8]"); // load the right string pointer into the third str_eq argument + emitter.instruction("mov rcx, QWORD PTR [r8 + 8]"); // load the right string length into the fourth str_eq argument + + // -- call __rt_str_eq(ptr_a, len_a, ptr_b, len_b) -- + emitter.instruction("call __rt_str_eq"); // compare the two string payloads byte-by-byte + emitter.instruction("test rax, rax"); // check the string-equality helper result + emitter.instruction("jz __rt_array_strict_eq_false_restore"); // a mismatched string makes the arrays unequal + + // -- advance the string loop -- + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // reload the loop index + emitter.instruction("add rax, 1"); // advance to the next string slot + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // store the updated loop index + emitter.instruction("mov rcx, QWORD PTR [rbp - 32]"); // reload the shared length + emitter.instruction("cmp rax, rcx"); // have all string elements been compared? + emitter.instruction("jb __rt_array_strict_eq_str_loop"); // continue while the index remains below the length + emitter.instruction("jmp __rt_array_strict_eq_true_restore"); // all strings matched + + // -- result paths -- + emitter.label("__rt_array_strict_eq_true_restore"); + emitter.instruction("mov rax, 1"); // materialize the strict-equality true result + emitter.instruction("jmp __rt_array_strict_eq_epilogue"); // skip the false path + + emitter.label("__rt_array_strict_eq_false_restore"); + emitter.instruction("xor rax, rax"); // materialize the strict-equality false result + + emitter.label("__rt_array_strict_eq_epilogue"); + emitter.instruction("add rsp, 48"); // release the helper spill slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the strict-equality boolean in rax + + // -- no-frame fast path for identical pointers -- + emitter.label("__rt_array_strict_eq_true"); + emitter.instruction("mov rax, 1"); // materialize true for identical pointers + emitter.instruction("ret"); // return true without allocating a frame +} \ No newline at end of file diff --git a/src/codegen/runtime/arrays/hash_strict_eq.rs b/src/codegen/runtime/arrays/hash_strict_eq.rs new file mode 100644 index 0000000000..9c5dc305b3 --- /dev/null +++ b/src/codegen/runtime/arrays/hash_strict_eq.rs @@ -0,0 +1,396 @@ +//! Purpose: +//! Emits the `__rt_hash_strict_eq` runtime helper for associative-array (hash) +//! strict equality (`===`) comparisons. Operates on the hash table layout +//! `[count:8][capacity:8][value_type:8][head:8][tail:8][entries...]` produced +//! by `__rt_hash_new` / `__rt_hash_set`, where each 64-byte entry is +//! `[occupied:8][key_ptr:8][key_len:8][value_lo:8][value_hi:8][value_tag:8][prev:8][next:8]`. +//! +//! Called from: +//! - `crate::codegen::runtime::emitters::emit_runtime()` via `crate::codegen::runtime::arrays`. +//! +//! Key details: +//! - Compares two hashes for PHP strict equality: identical count, identical +//! keys in the same insertion order, and identical value types and values. +//! - Both hashes are walked in parallel through their `head`/`next` +//! insertion-order chains. The right hash is NOT indexed by a sequential +//! position counter, because hash slots are placed by hashing, not by +//! insertion order; only the `head`/`next` chain preserves insertion order. +//! - A pointer-identity short-circuit handles aliases and cycles. +//! - Value comparison dispatches on the runtime value tag: scalar tags compare +//! the low/high payload words; string tags compare by value through +//! `__rt_str_eq`; boxed Mixed tags delegate to `__rt_mixed_strict_eq`. + +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; + +/// Emits the hash strict equality helper for the current target. +/// +/// Dispatches to the x86_64 Linux variant when targeting that architecture; +/// otherwise emits the portable ARM64 implementation. +/// +/// # Inputs (ARM64) +/// - `x0`: left hash table pointer +/// - `x1`: right hash table pointer +/// +/// # Output +/// - `x0` (ARM64) / `rax` (x86_64): `1` when strictly equal, `0` otherwise. +pub fn emit_hash_strict_eq(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_hash_strict_eq_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: hash_strict_eq ---"); + emitter.label_global("__rt_hash_strict_eq"); + + // -- pointer-identity short-circuit (aliases and cycles) -- + emitter.instruction("cmp x0, x1"); // compare the left and right hash pointers for identity + emitter.instruction("b.eq __rt_hash_strict_eq_true_fast"); // identical pointers are strictly equal + + // -- set up stack frame and save inputs -- + // [sp,#0] = left hash pointer + // [sp,#8] = right hash pointer + // [sp,#16] = current left slot index (insertion-order walk of left hash) + // [sp,#24] = current right slot index (insertion-order walk of right hash) + // [sp,#32] = saved x29 + // [sp,#40] = saved x30 + // [sp,#48] = left value_lo + // [sp,#56] = left value_hi + // [sp,#64] = left value_tag + // [sp,#72] = right value_lo + // [sp,#80] = right value_hi + // [sp,#88] = right value_tag + emitter.instruction("sub sp, sp, #96"); // reserve the helper frame for inputs, slots, values, and saved registers + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #32"); // establish the helper frame pointer + emitter.instruction("stp x0, x1, [sp, #0]"); // save the left and right hash pointers + + // -- compare counts -- + emitter.instruction("ldr x2, [x0]"); // load the left hash count + emitter.instruction("ldr x3, [x1]"); // load the right hash count + emitter.instruction("cmp x2, x3"); // compare left and right counts + emitter.instruction("b.ne __rt_hash_strict_eq_false_restore"); // different counts are never strictly equal + + // -- count zero short-circuits to true -- + emitter.instruction("cbz x2, __rt_hash_strict_eq_true_restore"); // empty hashes with matching counts are strictly equal + + // -- begin insertion-order walk from both hash heads -- + emitter.instruction("ldr x4, [x0, #24]"); // load the left hash head slot index + emitter.instruction("ldr x5, [x1, #24]"); // load the right hash head slot index + emitter.instruction("stp x4, x5, [sp, #16]"); // save the current left and right slot indices + + emitter.label("__rt_hash_strict_eq_slot"); + emitter.instruction("ldr x4, [sp, #16]"); // reload the current left slot index + emitter.instruction("ldr x5, [sp, #24]"); // reload the current right slot index + + // -- both chains ended together means a full match -- + emitter.instruction("cmp x4, #-1"); // has the left insertion-order chain ended? + emitter.instruction("b.ne __rt_hash_strict_eq_left_alive"); // left chain still has entries + emitter.instruction("cmp x5, #-1"); // has the right insertion-order chain ended? + emitter.instruction("b.eq __rt_hash_strict_eq_true_restore"); // both chains ended together: hashes are strictly equal + emitter.instruction("b __rt_hash_strict_eq_false_restore"); // left ended before right: insertion order differs + + emitter.label("__rt_hash_strict_eq_left_alive"); + emitter.instruction("cmp x5, #-1"); // has the right insertion-order chain ended early? + emitter.instruction("b.eq __rt_hash_strict_eq_false_restore"); // right ended before left: insertion order differs + + // -- compute the left entry address: left_base + 40 + left_slot * 64 -- + emitter.instruction("ldr x0, [sp, #0]"); // reload the left hash pointer + emitter.instruction("mov x6, #64"); // x6 = hash entry size in bytes + emitter.instruction("mul x7, x4, x6"); // x7 = left slot index * 64 + emitter.instruction("add x7, x0, x7"); // x7 = left hash base + slot offset + emitter.instruction("add x7, x7, #40"); // x7 = left entry address (skip header) + + // -- read the left key, value, and next link -- + emitter.instruction("ldr x8, [x7, #8]"); // x8 = left key_ptr + emitter.instruction("ldr x9, [x7, #16]"); // x9 = left key_len + emitter.instruction("ldr x10, [x7, #24]"); // x10 = left value_lo + emitter.instruction("ldr x11, [x7, #32]"); // x11 = left value_hi + emitter.instruction("ldr x12, [x7, #40]"); // x12 = left value_tag + emitter.instruction("ldr x13, [x7, #56]"); // x13 = left next slot index + + // -- compute the right entry address: right_base + 40 + right_slot * 64 -- + emitter.instruction("ldr x1, [sp, #8]"); // reload the right hash pointer + emitter.instruction("mov x6, #64"); // x6 = hash entry size in bytes + emitter.instruction("mul x14, x5, x6"); // x14 = right slot index * 64 + emitter.instruction("add x14, x1, x14"); // x14 = right hash base + slot offset + emitter.instruction("add x14, x14, #40"); // x14 = right entry address (skip header) + + // -- read the right key, value, and next link -- + emitter.instruction("ldr x15, [x14, #8]"); // x15 = right key_ptr + emitter.instruction("ldr x16, [x14, #16]"); // x16 = right key_len + emitter.instruction("ldr x17, [x14, #24]"); // x17 = right value_lo + emitter.instruction("ldr x19, [x14, #32]"); // x19 = right value_hi + emitter.instruction("ldr x20, [x14, #40]"); // x20 = right value_tag + emitter.instruction("ldr x21, [x14, #56]"); // x21 = right next slot index + + // -- save next links and value payloads across the key-eq call -- + emitter.instruction("stp x13, x21, [sp, #16]"); // store left and right next slot indices into the slot-index slots + emitter.instruction("stp x10, x11, [sp, #48]"); // save left value_lo and value_hi across the nested call + emitter.instruction("str x12, [sp, #64]"); // save left value_tag across the nested call + emitter.instruction("stp x17, x19, [sp, #72]"); // save right value_lo and value_hi across the nested call + emitter.instruction("str x20, [sp, #88]"); // save right value_tag across the nested call + emitter.instruction("stp x29, x30, [sp, #32]"); // preserve frame registers around the nested helper call + + // -- compare the keys via __rt_hash_key_eq(left_lo, left_hi, right_lo, right_hi) -- + emitter.instruction("mov x1, x8"); // left key_ptr into the first key-eq argument + emitter.instruction("mov x2, x9"); // left key_len into the second key-eq argument + emitter.instruction("mov x3, x15"); // right key_ptr into the third key-eq argument + emitter.instruction("mov x4, x16"); // right key_len into the fourth key-eq argument + emitter.instruction("bl __rt_hash_key_eq"); // compare the two keys for equality + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame registers after the nested helper call + emitter.instruction("cbz x0, __rt_hash_strict_eq_false_restore"); // a mismatched key makes the hashes unequal + + // -- reload saved value payloads and tags -- + emitter.instruction("ldr x7, [sp, #48]"); // x7 = left value_lo + emitter.instruction("ldr x8, [sp, #56]"); // x8 = left value_hi + emitter.instruction("ldr x9, [sp, #64]"); // x9 = left value_tag + emitter.instruction("ldr x10, [sp, #72]"); // x10 = right value_lo + emitter.instruction("ldr x11, [sp, #80]"); // x11 = right value_hi + emitter.instruction("ldr x12, [sp, #88]"); // x12 = right value_tag + + // -- value tags must match -- + emitter.instruction("cmp x9, x12"); // compare left and right value tags + emitter.instruction("b.ne __rt_hash_strict_eq_false_restore"); // different value tags are never strictly equal + + // -- dispatch on the shared value tag -- + emitter.instruction("cmp x9, #1"); // value tag 1 marks string payloads + emitter.instruction("b.eq __rt_hash_strict_eq_value_str"); // route string values through __rt_str_eq + emitter.instruction("cmp x9, #7"); // value tag 7 marks boxed Mixed payloads + emitter.instruction("b.eq __rt_hash_strict_eq_value_mixed"); // route Mixed values through __rt_mixed_strict_eq + + // -- scalar value comparison: low and high payload words -- + emitter.instruction("cmp x7, x10"); // compare the low payload words + emitter.instruction("b.ne __rt_hash_strict_eq_false_restore"); // mismatched low words are not equal + emitter.instruction("cmp x8, x11"); // compare the high payload words + emitter.instruction("b.ne __rt_hash_strict_eq_false_restore"); // mismatched high words are not equal + emitter.instruction("b __rt_hash_strict_eq_advance"); // scalar values matched + + // -- string value comparison via __rt_str_eq(ptr_a, len_a, ptr_b, len_b) -- + emitter.label("__rt_hash_strict_eq_value_str"); + emitter.instruction("mov x1, x7"); // left string pointer into the first str_eq argument + emitter.instruction("mov x2, x8"); // left string length into the second str_eq argument + emitter.instruction("mov x3, x10"); // right string pointer into the third str_eq argument + emitter.instruction("mov x4, x11"); // right string length into the fourth str_eq argument + emitter.instruction("stp x29, x30, [sp, #32]"); // preserve frame registers around the nested helper call + emitter.instruction("bl __rt_str_eq"); // compare the two string payloads byte-by-byte + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame registers after the nested helper call + emitter.instruction("cbz x0, __rt_hash_strict_eq_false_restore"); // a mismatched string makes the hashes unequal + emitter.instruction("b __rt_hash_strict_eq_advance"); // string values matched + + // -- boxed Mixed value comparison via __rt_mixed_strict_eq -- + emitter.label("__rt_hash_strict_eq_value_mixed"); + emitter.instruction("mov x0, x7"); // left boxed Mixed pointer into the first mixed-eq argument + emitter.instruction("mov x1, x10"); // right boxed Mixed pointer into the second mixed-eq argument + emitter.instruction("stp x29, x30, [sp, #32]"); // preserve frame registers around the nested helper call + emitter.instruction("bl __rt_mixed_strict_eq"); // compare the boxed Mixed cells by tag and payload + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame registers after the nested helper call + emitter.instruction("cbz x0, __rt_hash_strict_eq_false_restore"); // a mismatched Mixed cell makes the hashes unequal + emitter.instruction("b __rt_hash_strict_eq_advance"); // Mixed values matched + + // -- advance both chains to the next insertion-order slot -- + emitter.label("__rt_hash_strict_eq_advance"); + emitter.instruction("ldr x4, [sp, #16]"); // reload the left next slot index + emitter.instruction("ldr x5, [sp, #24]"); // reload the right next slot index + emitter.instruction("stp x4, x5, [sp, #16]"); // store the updated left and right slot indices + emitter.instruction("b __rt_hash_strict_eq_slot"); // continue the parallel walk + + // -- result paths -- + emitter.label("__rt_hash_strict_eq_true_restore"); + emitter.instruction("mov x0, #1"); // materialize the strict-equality true result + emitter.instruction("b __rt_hash_strict_eq_epilogue"); // skip the false path + + emitter.label("__rt_hash_strict_eq_false_restore"); + emitter.instruction("mov x0, #0"); // materialize the strict-equality false result + + emitter.label("__rt_hash_strict_eq_epilogue"); + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #96"); // release the helper frame + emitter.instruction("ret"); // return the strict-equality boolean in x0 + + // -- no-frame fast path for identical pointers -- + emitter.label("__rt_hash_strict_eq_true_fast"); + emitter.instruction("mov x0, #1"); // materialize true for identical pointers + emitter.instruction("ret"); // return true without allocating a frame +} + +/// Emits the x86_64 Linux variant of the hash strict equality helper. +/// +/// Mirrors the ARM64 algorithm using the System V AMD64 ABI: `rdi` for the left +/// hash pointer and `rsi` for the right, with the boolean result in `rax`. +/// Both hashes are walked in parallel through their `head`/`next` chains so +/// insertion order is compared correctly regardless of slot placement. +fn emit_hash_strict_eq_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: hash_strict_eq ---"); + emitter.label_global("__rt_hash_strict_eq"); + + // -- pointer-identity short-circuit (aliases and cycles) -- + emitter.instruction("cmp rdi, rsi"); // compare the left and right hash pointers for identity + emitter.instruction("je __rt_hash_strict_eq_true_fast"); // identical pointers are strictly equal + + // -- set up stack frame and save inputs -- + // [rbp - 8] = left hash pointer + // [rbp - 16] = right hash pointer + // [rbp - 24] = current left slot index + // [rbp - 32] = current right slot index + // [rbp - 40] = left next slot index (saved across key-eq call) + // [rbp - 48] = right next slot index (saved across key-eq call) + // [rbp - 56] = left value_lo + // [rbp - 64] = left value_hi + // [rbp - 72] = left value_tag + // [rbp - 80] = right value_lo + // [rbp - 88] = right value_hi + // [rbp - 96] = right value_tag + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame base + emitter.instruction("sub rsp, 96"); // reserve spill slots for inputs, slots, and value payloads + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the left hash pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the right hash pointer + + // -- compare counts -- + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the left hash count + emitter.instruction("mov r11, QWORD PTR [rsi]"); // load the right hash count + emitter.instruction("cmp r10, r11"); // compare left and right counts + emitter.instruction("jne __rt_hash_strict_eq_false_restore"); // different counts are never strictly equal + + // -- count zero short-circuits to true -- + emitter.instruction("test r10, r10"); // is the shared count zero? + emitter.instruction("jz __rt_hash_strict_eq_true_restore"); // empty hashes with matching counts are strictly equal + + // -- begin insertion-order walk from both hash heads -- + emitter.instruction("mov r10, QWORD PTR [rdi + 24]"); // load the left hash head slot index + emitter.instruction("mov r11, QWORD PTR [rsi + 24]"); // load the right hash head slot index + emitter.instruction("mov QWORD PTR [rbp - 24], r10"); // save the current left slot index + emitter.instruction("mov QWORD PTR [rbp - 32], r11"); // save the current right slot index + + emitter.label("__rt_hash_strict_eq_slot"); + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the current left slot index + emitter.instruction("mov r11, QWORD PTR [rbp - 32]"); // reload the current right slot index + + // -- both chains ended together means a full match -- + emitter.instruction("cmp r10, -1"); // has the left insertion-order chain ended? + emitter.instruction("jne __rt_hash_strict_eq_left_alive"); // left chain still has entries + emitter.instruction("cmp r11, -1"); // has the right insertion-order chain ended? + emitter.instruction("je __rt_hash_strict_eq_true_restore"); // both chains ended together: hashes are strictly equal + emitter.instruction("jmp __rt_hash_strict_eq_false_restore"); // left ended before right: insertion order differs + + emitter.label("__rt_hash_strict_eq_left_alive"); + emitter.instruction("cmp r11, -1"); // has the right insertion-order chain ended early? + emitter.instruction("je __rt_hash_strict_eq_false_restore"); // right ended before left: insertion order differs + + // -- compute the left entry address: left_base + 40 + left_slot * 64 -- + emitter.instruction("mov rax, r10"); // copy the left slot index before scaling it into a byte offset + emitter.instruction("shl rax, 6"); // convert the left slot index into a 64-byte hash-entry offset + emitter.instruction("mov rcx, QWORD PTR [rbp - 8]"); // reload the left hash pointer + emitter.instruction("lea r8, [rcx + rax + 40]"); // r8 = left entry address (skip header) + + // -- read the left key, value, and next link -- + emitter.instruction("mov rdi, QWORD PTR [r8 + 8]"); // rdi = left key_ptr (first key-eq argument) + emitter.instruction("mov rsi, QWORD PTR [r8 + 16]"); // rsi = left key_len (second key-eq argument) + emitter.instruction("mov r9, QWORD PTR [r8 + 24]"); // r9 = left value_lo + emitter.instruction("mov r10, QWORD PTR [r8 + 32]"); // r10 = left value_hi + emitter.instruction("mov r11, QWORD PTR [r8 + 40]"); // r11 = left value_tag + emitter.instruction("mov rcx, QWORD PTR [r8 + 56]"); // rcx = left next slot index + + // -- compute the right entry address: right_base + 40 + right_slot * 64 -- + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the current right slot index + emitter.instruction("shl rax, 6"); // convert the right slot index into a 64-byte hash-entry offset + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload the right hash pointer + emitter.instruction("lea r8, [rdx + rax + 40]"); // r8 = right entry address (skip header) + + // -- read the right key, value, and next link -- + emitter.instruction("mov rdx, QWORD PTR [r8 + 8]"); // rdx = right key_ptr (third key-eq argument) + emitter.instruction("mov rcx, QWORD PTR [r8 + 16]"); // rcx = right key_len (fourth key-eq argument) + emitter.instruction("mov r12, QWORD PTR [r8 + 24]"); // r12 = right value_lo + emitter.instruction("mov r13, QWORD PTR [r8 + 32]"); // r13 = right value_hi + emitter.instruction("mov r14, QWORD PTR [r8 + 40]"); // r14 = right value_tag + emitter.instruction("mov r15, QWORD PTR [r8 + 56]"); // r15 = right next slot index + + // -- save next links and value payloads across the key-eq call -- + emitter.instruction("mov QWORD PTR [rbp - 40], rcx"); // save left next slot index (rcx holds left next from above) + emitter.instruction("mov QWORD PTR [rbp - 48], r15"); // save right next slot index + emitter.instruction("mov QWORD PTR [rbp - 56], r9"); // save left value_lo + emitter.instruction("mov QWORD PTR [rbp - 64], r10"); // save left value_hi + emitter.instruction("mov QWORD PTR [rbp - 72], r11"); // save left value_tag + emitter.instruction("mov QWORD PTR [rbp - 80], r12"); // save right value_lo + emitter.instruction("mov QWORD PTR [rbp - 88], r13"); // save right value_hi + emitter.instruction("mov QWORD PTR [rbp - 96], r14"); // save right value_tag + + // -- compare the keys via __rt_hash_key_eq(left_lo, left_hi, right_lo, right_hi) -- + emitter.instruction("call __rt_hash_key_eq"); // compare the two keys for equality + emitter.instruction("test rax, rax"); // check the key-equality helper result + emitter.instruction("jz __rt_hash_strict_eq_false_restore"); // a mismatched key makes the hashes unequal + + // -- reload saved value payloads and tags -- + emitter.instruction("mov r8, QWORD PTR [rbp - 56]"); // r8 = left value_lo + emitter.instruction("mov rcx, QWORD PTR [rbp - 64]"); // rcx = left value_hi + emitter.instruction("mov r9, QWORD PTR [rbp - 72]"); // r9 = left value_tag + emitter.instruction("mov r12, QWORD PTR [rbp - 80]"); // r12 = right value_lo + emitter.instruction("mov r13, QWORD PTR [rbp - 88]"); // r13 = right value_hi + emitter.instruction("mov r14, QWORD PTR [rbp - 96]"); // r14 = right value_tag + + // -- value tags must match -- + emitter.instruction("cmp r9, r14"); // compare left and right value tags + emitter.instruction("jne __rt_hash_strict_eq_false_restore"); // different value tags are never strictly equal + + // -- dispatch on the shared value tag -- + emitter.instruction("cmp r9, 1"); // value tag 1 marks string payloads + emitter.instruction("je __rt_hash_strict_eq_value_str"); // route string values through __rt_str_eq + emitter.instruction("cmp r9, 7"); // value tag 7 marks boxed Mixed payloads + emitter.instruction("je __rt_hash_strict_eq_value_mixed"); // route Mixed values through __rt_mixed_strict_eq + + // -- scalar value comparison: low and high payload words -- + emitter.instruction("cmp r8, r12"); // compare the low payload words + emitter.instruction("jne __rt_hash_strict_eq_false_restore"); // mismatched low words are not equal + emitter.instruction("cmp rcx, r13"); // compare the high payload words + emitter.instruction("jne __rt_hash_strict_eq_false_restore"); // mismatched high words are not equal + emitter.instruction("jmp __rt_hash_strict_eq_advance"); // scalar values matched + + // -- string value comparison via __rt_str_eq(ptr_a, len_a, ptr_b, len_b) -- + emitter.label("__rt_hash_strict_eq_value_str"); + emitter.instruction("mov rdi, r8"); // left string pointer into the first str_eq argument + emitter.instruction("mov rsi, rcx"); // left string length into the second str_eq argument + emitter.instruction("mov rdx, r12"); // right string pointer into the third str_eq argument + emitter.instruction("mov rcx, r13"); // right string length into the fourth str_eq argument + emitter.instruction("call __rt_str_eq"); // compare the two string payloads byte-by-byte + emitter.instruction("test rax, rax"); // check the string-equality helper result + emitter.instruction("jz __rt_hash_strict_eq_false_restore"); // a mismatched string makes the hashes unequal + emitter.instruction("jmp __rt_hash_strict_eq_advance"); // string values matched + + // -- boxed Mixed value comparison via __rt_mixed_strict_eq -- + emitter.label("__rt_hash_strict_eq_value_mixed"); + emitter.instruction("mov rdi, r8"); // left boxed Mixed pointer into the first mixed-eq argument + emitter.instruction("mov rsi, r12"); // right boxed Mixed pointer into the second mixed-eq argument + emitter.instruction("call __rt_mixed_strict_eq"); // compare the boxed Mixed cells by tag and payload + emitter.instruction("test rax, rax"); // check the mixed-equality helper result + emitter.instruction("jz __rt_hash_strict_eq_false_restore"); // a mismatched Mixed cell makes the hashes unequal + emitter.instruction("jmp __rt_hash_strict_eq_advance"); // Mixed values matched + + // -- advance both chains to the next insertion-order slot -- + emitter.label("__rt_hash_strict_eq_advance"); + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the left next slot index + emitter.instruction("mov r11, QWORD PTR [rbp - 48]"); // reload the right next slot index + emitter.instruction("mov QWORD PTR [rbp - 24], r10"); // store the updated left slot index + emitter.instruction("mov QWORD PTR [rbp - 32], r11"); // store the updated right slot index + emitter.instruction("jmp __rt_hash_strict_eq_slot"); // continue the parallel walk + + // -- result paths -- + emitter.label("__rt_hash_strict_eq_true_restore"); + emitter.instruction("mov rax, 1"); // materialize the strict-equality true result + emitter.instruction("jmp __rt_hash_strict_eq_epilogue"); // skip the false path + + emitter.label("__rt_hash_strict_eq_false_restore"); + emitter.instruction("xor rax, rax"); // materialize the strict-equality false result + + emitter.label("__rt_hash_strict_eq_epilogue"); + emitter.instruction("add rsp, 96"); // release the helper spill slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the strict-equality boolean in rax + + // -- no-frame fast path for identical pointers -- + emitter.label("__rt_hash_strict_eq_true_fast"); + emitter.instruction("mov rax, 1"); // materialize true for identical pointers + emitter.instruction("ret"); // return true without allocating a frame +} \ No newline at end of file diff --git a/src/codegen/runtime/arrays/mod.rs b/src/codegen/runtime/arrays/mod.rs index 7a64909015..c71eb1613d 100644 --- a/src/codegen/runtime/arrays/mod.rs +++ b/src/codegen/runtime/arrays/mod.rs @@ -62,6 +62,7 @@ mod array_set_mixed; mod array_set_mixed_key; mod array_set_refcounted; mod array_set_str; +mod array_strict_eq; mod array_rand; mod random_u32; mod random_uniform; @@ -114,6 +115,7 @@ mod hash_insert_owned; mod hash_iter; mod hash_new; mod hash_set; +mod hash_strict_eq; mod hash_to_mixed; mod hash_union; mod hash_unset; @@ -259,6 +261,8 @@ pub use array_set_refcounted::emit_array_set_refcounted; /// Emit refcounted indexed-array set helper. pub use array_set_str::emit_array_set_str; /// Emit string indexed-array set helper. +pub use array_strict_eq::emit_array_strict_eq; +/// Emit indexed-array strict-equality comparison helper. pub use array_rand::emit_array_rand; /// Emit random array element helper. pub use random_u32::emit_random_u32; @@ -355,6 +359,8 @@ pub use hash_new::emit_hash_new; /// Emit new hash helper. pub use hash_set::emit_hash_set; /// Emit hash set helper. +pub use hash_strict_eq::emit_hash_strict_eq; +/// Emit hash strict-equality comparison helper. pub use hash_to_mixed::emit_hash_to_mixed; /// Emit hash-to-Mixed conversion helper. pub use hash_union::emit_hash_union; diff --git a/src/codegen/runtime/data/fixed.rs b/src/codegen/runtime/data/fixed.rs index 5f47e3e754..67081cafe7 100644 --- a/src/codegen/runtime/data/fixed.rs +++ b/src/codegen/runtime/data/fixed.rs @@ -694,25 +694,34 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin // __rt_hash_get. v1 limitation: only one active context at a time — // a fresh stream_context_create overwrites the slot. out.push_str(".comm _stream_context_options, 8, 3\n"); - // var_dump body literals (rodata): per-element prefix/suffix bytes - // used by the array walkers __rt_var_dump_array_int / _str. - out.push_str(".globl _vd_indent_open\n_vd_indent_open:\n .ascii \" [\"\n"); + // var_dump body literals (rodata): per-element prefix/suffix bytes used by + // the array walkers and the recursive `__rt_var_dump_value` renderer. The + // prefixes carry no leading indent; indentation is written separately by + // `__rt_var_dump_spaces` so nested arrays indent correctly. + out.push_str(".globl _vd_indent_open\n_vd_indent_open:\n .ascii \"[\"\n"); out.push_str(".globl _vd_close_arrow\n_vd_close_arrow:\n .ascii \"]=>\\n\"\n"); - out.push_str(".globl _vd_int_prefix\n_vd_int_prefix:\n .ascii \" int(\"\n"); + out.push_str(".globl _vd_int_prefix\n_vd_int_prefix:\n .ascii \"int(\"\n"); out.push_str(".globl _vd_close_paren\n_vd_close_paren:\n .ascii \")\\n\"\n"); - out.push_str(".globl _vd_str_prefix\n_vd_str_prefix:\n .ascii \" string(\"\n"); + out.push_str(".globl _vd_str_prefix\n_vd_str_prefix:\n .ascii \"string(\"\n"); out.push_str(".globl _vd_close_paren_space\n_vd_close_paren_space:\n .ascii \") \\\"\"\n"); out.push_str(".globl _vd_close_quote\n_vd_close_quote:\n .ascii \"\\\"\\n\"\n"); - // var_dump bool-array literals — preformatted lines (12 / 13 bytes) so - // the bool walker is a single dispatch + write. - out.push_str(".globl _vd_bool_true_line\n_vd_bool_true_line:\n .ascii \" bool(true)\\n\"\n"); - out.push_str(".globl _vd_bool_false_line\n_vd_bool_false_line:\n .ascii \" bool(false)\\n\"\n"); - out.push_str(".globl _vd_float_prefix\n_vd_float_prefix:\n .ascii \" float(\"\n"); - out.push_str(".globl _vd_null_line\n_vd_null_line:\n .ascii \" NULL\\n\"\n"); - // var_dump hash (associative array) string-key delimiters: ` ["` before the - // key bytes and `"]=>\n` after, matching PHP's ` ["key"]=>` line format. - out.push_str(".globl _vd_str_key_open\n_vd_str_key_open:\n .ascii \" [\\\"\"\n"); + // var_dump bool-array literals — preformatted lines so the bool walker is a + // single dispatch + write (no leading indent; spaces are written separately). + out.push_str(".globl _vd_bool_true_line\n_vd_bool_true_line:\n .ascii \"bool(true)\\n\"\n"); + out.push_str(".globl _vd_bool_false_line\n_vd_bool_false_line:\n .ascii \"bool(false)\\n\"\n"); + out.push_str(".globl _vd_float_prefix\n_vd_float_prefix:\n .ascii \"float(\"\n"); + out.push_str(".globl _vd_null_line\n_vd_null_line:\n .ascii \"NULL\\n\"\n"); + // var_dump hash (associative array) string-key delimiters: `["` before the + // key bytes and `"]=>\n` after, matching PHP's `["key"]=>` line format. + out.push_str(".globl _vd_str_key_open\n_vd_str_key_open:\n .ascii \"[\\\"\"\n"); out.push_str(".globl _vd_str_key_close\n_vd_str_key_close:\n .ascii \"\\\"]=>\\n\"\n"); + // 64-space pad used by `__rt_var_dump_spaces` (written in <=64-byte chunks). + out.push_str(".globl _vd_spaces\n_vd_spaces:\n .ascii \" \"\n"); + // var_dump recursive array-header/footer literals: `array(` before the + // count, `) {\n` after it, and `}\n` for the closing brace. + out.push_str(".globl _vd_array_open\n_vd_array_open:\n .ascii \"array(\"\n"); + out.push_str(".globl _vd_array_close_brace\n_vd_array_close_brace:\n .ascii \") {\\n\"\n"); + out.push_str(".globl _vd_close_brace_nl\n_vd_close_brace_nl:\n .ascii \"}\\n\"\n"); // print_r body literals (rodata): PHP's `Array\n(\n` header, `)\n` footer, // `[`/`] => ` key delimiters (unquoted keys, unlike var_dump), a lone // newline, the `1` rendered for boolean true, and a 64-space pad used by diff --git a/src/codegen/runtime/emitters.rs b/src/codegen/runtime/emitters.rs index 246bee71b2..0fa334ae3c 100644 --- a/src/codegen/runtime/emitters.rs +++ b/src/codegen/runtime/emitters.rs @@ -310,6 +310,8 @@ pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) { arrays::emit_mixed_is_empty(emitter); arrays::emit_mixed_numeric_binops(emitter); arrays::emit_mixed_strict_eq(emitter); + arrays::emit_array_strict_eq(emitter); + arrays::emit_hash_strict_eq(emitter); arrays::emit_mixed_unbox(emitter); arrays::emit_mixed_write_stdout(emitter); arrays::emit_object_free_deep(emitter); @@ -463,6 +465,9 @@ pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) { io::emit_var_dump_emit_bool_line(emitter); io::emit_var_dump_emit_float_line(emitter); io::emit_var_dump_emit_null_line(emitter); + io::emit_var_dump_spaces(emitter); + io::emit_var_dump_value(emitter); + io::emit_var_dump_indexed(emitter); io::emit_print_r_spaces(emitter); io::emit_print_r_open(emitter); io::emit_print_r_close(emitter); diff --git a/src/codegen/runtime/io/mod.rs b/src/codegen/runtime/io/mod.rs index 8e97ea5a09..7079ea1865 100644 --- a/src/codegen/runtime/io/mod.rs +++ b/src/codegen/runtime/io/mod.rs @@ -257,5 +257,5 @@ pub(crate) use var_dump_walk::{ emit_var_dump_array_mixed, emit_var_dump_array_str, emit_var_dump_emit_bool_line, emit_var_dump_emit_float_line, emit_var_dump_emit_indexed_key, emit_var_dump_emit_int_line, emit_var_dump_emit_null_line, emit_var_dump_emit_string_key, emit_var_dump_emit_string_line, - emit_var_dump_hash, + emit_var_dump_hash, emit_var_dump_indexed, emit_var_dump_spaces, emit_var_dump_value, }; diff --git a/src/codegen/runtime/io/var_dump_walk.rs b/src/codegen/runtime/io/var_dump_walk.rs index 56e56556b8..99c436bc83 100644 --- a/src/codegen/runtime/io/var_dump_walk.rs +++ b/src/codegen/runtime/io/var_dump_walk.rs @@ -1,15 +1,16 @@ //! Purpose: -//! Emits the `__rt_var_dump_array_int` / `__rt_var_dump_array_str` runtime -//! walkers that iterate a homogeneous indexed array and emit one -//! `[N]=>\n TYPE(VAL)\n` block per element (PHP `var_dump` body format, -//! 2-space indentation). The opening `array(N) {\n` and closing `}\n` -//! are emitted by the builtin caller around these walks. +//! Emits the `__rt_var_dump_*` runtime walkers that render PHP `var_dump` +//! output for indexed arrays, associative arrays (hashes), and the recursive +//! single-value renderer `__rt_var_dump_value`, matching PHP's +//! `array(N) {\n [key]=>\n TYPE(VAL)\n}\n` layout with 2-space-per-level +//! indentation. //! //! Called from: //! - `crate::codegen::runtime::emitters::emit_runtime()` via //! `crate::codegen::runtime::io`. -//! - The `var_dump` builtin emitter when the value's static type is -//! `Array(Int)` or `Array(Str)`. +//! - The `var_dump` builtin emitter (`codegen_ir::lower_inst::builtins::debug`) +//! when the value's static type is a homogeneous array, a hash, a boxed +//! Mixed cell, or a union (all routed through `__rt_var_dump_value`). //! //! Key details: //! - Array layout reused from the existing JSON encoders: 24-byte header @@ -18,10 +19,16 @@ //! - String elements use the elephc string-result ABI: 16-byte slots //! storing (ptr, len) — so element[N] for an indexed string array lives //! at offsets `24 + N*16` (ptr) and `32 + N*16` (len). -//! - Associative arrays (hashes) are handled by `__rt_var_dump_hash`, which -//! iterates entries via `__rt_hash_iter_next` and formats string/integer keys -//! plus scalar (and boxed-Mixed scalar) values. Nested arrays/objects inside a -//! hash fall back to `NULL`, matching the indexed Mixed walker's limitation. +//! - The rodata prefixes (`_vd_int_prefix`, `_vd_str_key_open`, …) carry no +//! leading indent; indentation is written separately by +//! `__rt_var_dump_spaces` so nested arrays indent correctly. +//! - `__rt_var_dump_value` is the recursive entry point modeled on +//! `__rt_print_r_value`: tags 4/5 recurse into `__rt_var_dump_indexed` / +//! `__rt_var_dump_hash` with `entry_indent = indent + 2`, tag 7 unboxes a +//! Mixed cell and redispatches, tag 6 (object) stays `NULL` (documented +//! limitation). A depth cap (`indent > 128`) stops cyclic arrays. +//! - Homogeneous typed-array walkers (`__rt_var_dump_array_int`, …) remain as +//! a fast path for arrays that cannot contain nested containers. use crate::codegen::{emit::Emitter, platform::Arch}; use crate::codegen::abi; @@ -39,10 +46,10 @@ pub fn emit_var_dump_array_int(emitter: &mut Emitter) { emitter.comment("--- runtime: var_dump_array_int ---"); emitter.label_global("__rt_var_dump_array_int"); - // Frame (32 bytes): [0..8] array ptr, [8..16] element index, - // [16..24] saved x29, [24..32] saved x30. - emitter.instruction("sub sp, sp, #32"); // helper frame - emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address + // Frame (48 bytes): [0]=array ptr, [8]=index, [16]=value scratch, + // [32]=x29, [40]=x30. + emitter.instruction("sub sp, sp, #48"); // helper frame + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address emitter.instruction("mov x29, sp"); // establish the helper frame pointer emitter.instruction("str x0, [sp, #0]"); // save the array pointer emitter.instruction("str xzr, [sp, #8]"); // index = 0 @@ -55,14 +62,20 @@ pub fn emit_var_dump_array_int(emitter: &mut Emitter) { emitter.instruction("b.ge __rt_vd_arr_int_done"); // walk complete // -- emit ` [N]=>\n` -- - emitter.instruction("bl __rt_var_dump_emit_indexed_key"); // emits " [N]=>\n" for x11=index + emitter.instruction("mov x0, #2"); // top-level entry indent = 2 spaces + emitter.instruction("bl __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("bl __rt_var_dump_emit_indexed_key"); // emits "[N]=>\n" for x11=index // -- emit ` int(VAL)\n` -- emitter.instruction("ldr x9, [sp, #0]"); // reload array pointer emitter.instruction("ldr x11, [sp, #8]"); // reload index emitter.instruction("add x12, x11, #3"); // skip the 24-byte (3 quads) header emitter.instruction("ldr x0, [x9, x12, lsl #3]"); // load element[index] - emitter.instruction("bl __rt_var_dump_emit_int_line"); // emits " int(VAL)\n" for x0=value + emitter.instruction("str x0, [sp, #16]"); // preserve the value across the spaces call + emitter.instruction("mov x0, #2"); // top-level entry indent = 2 spaces + emitter.instruction("bl __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("ldr x0, [sp, #16]"); // restore the integer value + emitter.instruction("bl __rt_var_dump_emit_int_line"); // emits "int(VAL)\n" for x0=value emitter.instruction("ldr x11, [sp, #8]"); // reload index emitter.instruction("add x11, x11, #1"); // advance index @@ -70,8 +83,8 @@ pub fn emit_var_dump_array_int(emitter: &mut Emitter) { emitter.instruction("b __rt_vd_arr_int_loop"); // continue scanning emitter.label("__rt_vd_arr_int_done"); - 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("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the helper frame emitter.instruction("ret"); // return to the var_dump builtin caller } @@ -98,6 +111,8 @@ fn emit_var_dump_array_int_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jge __rt_vd_arr_int_done_x86"); // walk complete // -- emit ` [N]=>\n` (helper expects index in rdi) -- + emitter.instruction("mov edi, 2"); // top-level entry indent = 2 spaces + emitter.instruction("call __rt_var_dump_spaces"); // pad the entry indent emitter.instruction("mov rdi, r11"); // prepare SysV call argument emitter.instruction("call __rt_var_dump_emit_indexed_key"); // call runtime helper @@ -106,7 +121,10 @@ fn emit_var_dump_array_int_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov r11, QWORD PTR [rbp - 16]"); // reload index emitter.instruction("mov r12, r11"); // move runtime value between registers emitter.instruction("add r12, 3"); // skip 3-quad header - emitter.instruction("mov rdi, QWORD PTR [r9 + r12 * 8]"); // load element[index] into the emit helper's first arg + emitter.instruction("mov r13, QWORD PTR [r9 + r12 * 8]"); // load element[index] into the emit helper's first arg + emitter.instruction("mov edi, 2"); // top-level entry indent = 2 spaces + emitter.instruction("call __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("mov rdi, r13"); // restore the integer value emitter.instruction("call __rt_var_dump_emit_int_line"); // call runtime helper emitter.instruction("mov r11, QWORD PTR [rbp - 16]"); // reload index @@ -133,9 +151,9 @@ pub fn emit_var_dump_array_str(emitter: &mut Emitter) { emitter.comment("--- runtime: var_dump_array_str ---"); emitter.label_global("__rt_var_dump_array_str"); - // Frame: same layout as the int walker. - emitter.instruction("sub sp, sp, #32"); // allocate runtime stack frame - emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address + // Frame (48 bytes): [0]arr [8]index [16]ptr [24]len [32]x29 [40]x30. + emitter.instruction("sub sp, sp, #48"); // allocate runtime stack frame + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address emitter.instruction("mov x29, sp"); // establish runtime frame pointer emitter.instruction("str x0, [sp, #0]"); // store runtime value emitter.instruction("str xzr, [sp, #8]"); // store runtime value @@ -148,7 +166,9 @@ pub fn emit_var_dump_array_str(emitter: &mut Emitter) { emitter.instruction("b.ge __rt_vd_arr_str_done"); // walk complete // -- emit ` [N]=>\n` -- - emitter.instruction("bl __rt_var_dump_emit_indexed_key"); // emits " [N]=>\n" for x11=index + emitter.instruction("mov x0, #2"); // top-level entry indent = 2 spaces + emitter.instruction("bl __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("bl __rt_var_dump_emit_indexed_key"); // emits "[N]=>\n" for x11=index // -- emit ` string(LEN) "VAL"\n` -- // String elements are 16-byte slots: ptr at offset 24+16*N, len at 32+16*N. @@ -159,7 +179,12 @@ pub fn emit_var_dump_array_str(emitter: &mut Emitter) { emitter.instruction("add x13, x9, x12"); // element address emitter.instruction("ldr x1, [x13]"); // load element string ptr emitter.instruction("ldr x2, [x13, #8]"); // load element string len - emitter.instruction("bl __rt_var_dump_emit_string_line"); // emits ` string(LEN) "VAL"\n` + emitter.instruction("stp x1, x2, [sp, #16]"); // save ptr/len across the spaces call + emitter.instruction("mov x0, #2"); // top-level entry indent = 2 spaces + emitter.instruction("bl __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("ldr x1, [sp, #16]"); // reload string ptr + emitter.instruction("ldr x2, [sp, #24]"); // reload string len + emitter.instruction("bl __rt_var_dump_emit_string_line"); // emits `string(LEN) "VAL"\n` emitter.instruction("ldr x11, [sp, #8]"); // reload index emitter.instruction("add x11, x11, #1"); // advance index @@ -167,8 +192,8 @@ pub fn emit_var_dump_array_str(emitter: &mut Emitter) { emitter.instruction("b __rt_vd_arr_str_loop"); // continue scanning emitter.label("__rt_vd_arr_str_done"); - emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #32"); // release runtime stack frame + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release runtime stack frame emitter.instruction("ret"); // return to caller } @@ -180,7 +205,7 @@ fn emit_var_dump_array_str_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("push rbp"); // save caller frame pointer emitter.instruction("mov rbp, rsp"); // establish runtime frame pointer - emitter.instruction("sub rsp, 16"); // allocate runtime stack frame + emitter.instruction("sub rsp, 32"); // allocate runtime stack frame emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the array pointer emitter.instruction("mov QWORD PTR [rbp - 16], 0"); // index = 0 @@ -191,6 +216,8 @@ fn emit_var_dump_array_str_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("cmp r11, r10"); // processed every element? emitter.instruction("jge __rt_vd_arr_str_done_x86"); // walk complete + emitter.instruction("mov edi, 2"); // top-level entry indent = 2 spaces + emitter.instruction("call __rt_var_dump_spaces"); // pad the entry indent emitter.instruction("mov rdi, r11"); // prepare SysV call argument emitter.instruction("call __rt_var_dump_emit_indexed_key"); // call runtime helper @@ -200,8 +227,14 @@ fn emit_var_dump_array_str_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("shl r12, 4"); // index * 16 emitter.instruction("add r12, 24"); // element base offset emitter.instruction("add r12, r9"); // element address - emitter.instruction("mov rdi, QWORD PTR [r12]"); // string ptr → emit helper's first arg - emitter.instruction("mov rsi, QWORD PTR [r12 + 8]"); // string len → emit helper's second arg + emitter.instruction("mov rax, QWORD PTR [r12]"); // string ptr + emitter.instruction("mov rcx, QWORD PTR [r12 + 8]"); // string len + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save string ptr across the spaces call + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save string len across the spaces call + emitter.instruction("mov edi, 2"); // top-level entry indent = 2 spaces + emitter.instruction("call __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload string ptr + emitter.instruction("mov rsi, QWORD PTR [rbp - 32]"); // reload string len emitter.instruction("call __rt_var_dump_emit_string_line"); // call runtime helper emitter.instruction("mov r11, QWORD PTR [rbp - 16]"); // move runtime value between registers @@ -210,12 +243,13 @@ fn emit_var_dump_array_str_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jmp __rt_vd_arr_str_loop_x86"); // continue at target label emitter.label("__rt_vd_arr_str_done_x86"); - emitter.instruction("add rsp, 16"); // release runtime stack frame + emitter.instruction("add rsp, 32"); // release runtime stack frame emitter.instruction("pop rbp"); // restore caller frame pointer emitter.instruction("ret"); // return to caller } -/// `__rt_var_dump_emit_indexed_key`: emit ` [N]=>\n` for a numeric index. +/// `__rt_var_dump_emit_indexed_key`: emit `[N]=>\n` for a numeric index. +/// The caller writes the entry indent via `__rt_var_dump_spaces` first. /// Input: AArch64 x11 / x86_64 rdi = index value. pub fn emit_var_dump_emit_indexed_key(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { @@ -231,9 +265,9 @@ pub fn emit_var_dump_emit_indexed_key(emitter: &mut Emitter) { emitter.instruction("stp x29, x30, [sp, #0]"); // save frame pointer and return address emitter.instruction("mov x29, sp"); // establish runtime frame pointer - // Emit " [" + // Emit "[" crate::codegen::abi::emit_symbol_address(emitter, "x1", "_vd_indent_open"); - emitter.instruction("mov x2, #3"); // len(" [") = 3 + emitter.instruction("mov x2, #1"); // len("[") = 1 emitter.instruction("mov x0, #1"); // fd=stdout emitter.syscall(4); @@ -265,9 +299,9 @@ fn emit_var_dump_emit_indexed_key_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("sub rsp, 16"); // allocate runtime stack frame emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the index - // Emit " [" + // Emit "[" abi::emit_symbol_address(emitter, "rsi", "_vd_indent_open"); // load runtime data address - emitter.instruction("mov edx, 3"); // prepare SysV call argument + emitter.instruction("mov edx, 1"); // len("[") = 1 emitter.instruction("mov edi, 1"); // prepare SysV call argument emitter.instruction("mov eax, 1"); // prepare runtime result value emitter.instruction("syscall"); // invoke kernel service @@ -292,7 +326,8 @@ fn emit_var_dump_emit_indexed_key_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("ret"); // return to caller } -/// `__rt_var_dump_emit_int_line`: emit ` int(VAL)\n` for a single int. +/// `__rt_var_dump_emit_int_line`: emit `int(VAL)\n` for a single int. The +/// caller writes the entry indent via `__rt_var_dump_spaces` first. /// Input: AArch64 x0 / x86_64 rdi = value. pub fn emit_var_dump_emit_int_line(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { @@ -308,9 +343,9 @@ pub fn emit_var_dump_emit_int_line(emitter: &mut Emitter) { emitter.instruction("stp x29, x30, [sp, #0]"); // save frame pointer and return address emitter.instruction("mov x29, sp"); // establish runtime frame pointer - // Emit " int(" + // Emit "int(" crate::codegen::abi::emit_symbol_address(emitter, "x1", "_vd_int_prefix"); - emitter.instruction("mov x2, #6"); // len(" int(") = 6 + emitter.instruction("mov x2, #4"); // len("int(") = 4 emitter.instruction("mov x9, x0"); // preserve value emitter.instruction("mov x0, #1"); // prepare AArch64 call argument emitter.syscall(4); @@ -344,7 +379,7 @@ fn emit_var_dump_emit_int_line_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save value abi::emit_symbol_address(emitter, "rsi", "_vd_int_prefix"); // load runtime data address - emitter.instruction("mov edx, 6"); // prepare SysV call argument + emitter.instruction("mov edx, 4"); // len("int(") = 4 emitter.instruction("mov edi, 1"); // prepare SysV call argument emitter.instruction("mov eax, 1"); // prepare runtime result value emitter.instruction("syscall"); // invoke kernel service @@ -367,8 +402,9 @@ fn emit_var_dump_emit_int_line_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("ret"); // return to caller } -/// `__rt_var_dump_emit_string_line`: emit ` string(LEN) "VAL"\n` for a -/// string. Input: AArch64 x1=ptr x2=len / x86_64 rdi=ptr rsi=len. +/// `__rt_var_dump_emit_string_line`: emit `string(LEN) "VAL"\n` for a +/// string. The caller writes the entry indent via `__rt_var_dump_spaces` first. +/// Input: AArch64 x1=ptr x2=len / x86_64 rdi=ptr rsi=len. pub fn emit_var_dump_emit_string_line(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_var_dump_emit_string_line_linux_x86_64(emitter); @@ -384,9 +420,9 @@ pub fn emit_var_dump_emit_string_line(emitter: &mut Emitter) { emitter.instruction("mov x29, sp"); // establish runtime frame pointer emitter.instruction("stp x1, x2, [sp, #0]"); // save ptr/len - // Emit " string(" + // Emit "string(" crate::codegen::abi::emit_symbol_address(emitter, "x1", "_vd_str_prefix"); - emitter.instruction("mov x2, #9"); // len(" string(") = 9 + emitter.instruction("mov x2, #7"); // len("string(") = 7 emitter.instruction("mov x0, #1"); // prepare AArch64 call argument emitter.syscall(4); @@ -396,7 +432,7 @@ pub fn emit_var_dump_emit_string_line(emitter: &mut Emitter) { emitter.instruction("mov x0, #1"); // prepare AArch64 call argument emitter.syscall(4); - // Emit ") " + // Emit ") \"" crate::codegen::abi::emit_symbol_address(emitter, "x1", "_vd_close_paren_space"); emitter.instruction("mov x2, #3"); // len(") \"") = 3 — includes the opening quote emitter.instruction("mov x0, #1"); // prepare AArch64 call argument @@ -432,7 +468,7 @@ fn emit_var_dump_emit_string_line_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save len abi::emit_symbol_address(emitter, "rsi", "_vd_str_prefix"); // load runtime data address - emitter.instruction("mov edx, 9"); // prepare SysV call argument + emitter.instruction("mov edx, 7"); // len("string(") = 7 emitter.instruction("mov edi, 1"); // prepare SysV call argument emitter.instruction("mov eax, 1"); // prepare runtime result value emitter.instruction("syscall"); // invoke kernel service @@ -467,8 +503,9 @@ fn emit_var_dump_emit_string_line_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("ret"); // return to caller } -/// `__rt_var_dump_emit_bool_line`: emit ` bool(true)\n` or -/// ` bool(false)\n` for a single bool. Input: AArch64 x0 / x86_64 rdi = +/// `__rt_var_dump_emit_bool_line`: emit `bool(true)\n` or +/// `bool(false)\n` for a single bool. The caller writes the entry indent via +/// `__rt_var_dump_spaces` first. Input: AArch64 x0 / x86_64 rdi = /// value (0 = false, non-zero = true). pub fn emit_var_dump_emit_bool_line(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { @@ -484,11 +521,11 @@ pub fn emit_var_dump_emit_bool_line(emitter: &mut Emitter) { let done_label = "__rt_vd_bool_done"; emitter.instruction(&format!("cbz x0, {}", false_label)); // value == 0 → false line crate::codegen::abi::emit_symbol_address(emitter, "x1", "_vd_bool_true_line"); - emitter.instruction("mov x2, #13"); // len(" bool(true)\n") = 13 + emitter.instruction("mov x2, #11"); // len("bool(true)\n") = 11 emitter.instruction(&format!("b {}", done_label)); // continue at target label emitter.label(false_label); crate::codegen::abi::emit_symbol_address(emitter, "x1", "_vd_bool_false_line"); - emitter.instruction("mov x2, #14"); // len(" bool(false)\n") = 14 + emitter.instruction("mov x2, #12"); // len("bool(false)\n") = 12 emitter.label(done_label); emitter.instruction("mov x0, #1"); // fd = stdout emitter.syscall(4); @@ -506,11 +543,11 @@ fn emit_var_dump_emit_bool_line_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("test rdi, rdi"); // check whether the runtime value is zero emitter.instruction(&format!("jz {}", false_label)); // branch when the checked value is zero or equal abi::emit_symbol_address(emitter, "rsi", "_vd_bool_true_line"); // load runtime data address - emitter.instruction("mov edx, 13"); // prepare SysV call argument + emitter.instruction("mov edx, 11"); // len("bool(true)\n") = 11 emitter.instruction(&format!("jmp {}", done_label)); // continue at target label emitter.label(false_label); abi::emit_symbol_address(emitter, "rsi", "_vd_bool_false_line"); // load runtime data address - emitter.instruction("mov edx, 14"); // prepare SysV call argument + emitter.instruction("mov edx, 12"); // len("bool(false)\n") = 12 emitter.label(done_label); emitter.instruction("mov edi, 1"); // fd = stdout emitter.instruction("mov eax, 1"); // sys_write @@ -531,8 +568,8 @@ pub fn emit_var_dump_array_bool(emitter: &mut Emitter) { emitter.comment("--- runtime: var_dump_array_bool ---"); emitter.label_global("__rt_var_dump_array_bool"); - emitter.instruction("sub sp, sp, #32"); // allocate runtime stack frame - emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address + emitter.instruction("sub sp, sp, #48"); // allocate runtime stack frame + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address emitter.instruction("mov x29, sp"); // establish runtime frame pointer emitter.instruction("str x0, [sp, #0]"); // store runtime value emitter.instruction("str xzr, [sp, #8]"); // store runtime value @@ -544,12 +581,18 @@ pub fn emit_var_dump_array_bool(emitter: &mut Emitter) { emitter.instruction("cmp x11, x10"); // compare runtime values for the next branch emitter.instruction("b.ge __rt_vd_arr_bool_done"); // branch when comparison is at least target + emitter.instruction("mov x0, #2"); // top-level entry indent = 2 spaces + emitter.instruction("bl __rt_var_dump_spaces"); // pad the entry indent emitter.instruction("bl __rt_var_dump_emit_indexed_key"); // call runtime helper emitter.instruction("ldr x9, [sp, #0]"); // load runtime value emitter.instruction("ldr x11, [sp, #8]"); // load runtime value emitter.instruction("add x12, x11, #3"); // skip 3-quad header - emitter.instruction("ldr x0, [x9, x12, lsl #3]"); // load element[index] (0 or 1) + emitter.instruction("ldr x13, [x9, x12, lsl #3]"); // load element[index] (0 or 1) + emitter.instruction("str x13, [sp, #16]"); // save the bool value across the spaces call + emitter.instruction("mov x0, #2"); // top-level entry indent = 2 spaces + emitter.instruction("bl __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("ldr x0, [sp, #16]"); // reload the bool value emitter.instruction("bl __rt_var_dump_emit_bool_line"); // call runtime helper emitter.instruction("ldr x11, [sp, #8]"); // load runtime value @@ -558,13 +601,14 @@ pub fn emit_var_dump_array_bool(emitter: &mut Emitter) { emitter.instruction("b __rt_vd_arr_bool_loop"); // continue at target label emitter.label("__rt_vd_arr_bool_done"); - emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #32"); // release runtime stack frame + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release runtime stack frame emitter.instruction("ret"); // return to caller } -/// `__rt_var_dump_emit_float_line`: emit ` float(VAL)\n` for a single -/// f64. Input: AArch64 d0 / x86_64 xmm0 = value. +/// `__rt_var_dump_emit_float_line`: emit `float(VAL)\n` for a single +/// f64. The caller writes the entry indent via `__rt_var_dump_spaces` first. +/// Input: AArch64 d0 / x86_64 xmm0 = value. pub fn emit_var_dump_emit_float_line(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_var_dump_emit_float_line_linux_x86_64(emitter); @@ -579,9 +623,9 @@ pub fn emit_var_dump_emit_float_line(emitter: &mut Emitter) { emitter.instruction("stp x29, x30, [sp, #0]"); // save frame pointer and return address emitter.instruction("mov x29, sp"); // establish runtime frame pointer - // Emit " float(" + // Emit "float(" crate::codegen::abi::emit_symbol_address(emitter, "x1", "_vd_float_prefix"); - emitter.instruction("mov x2, #8"); // len(" float(") = 8 + emitter.instruction("mov x2, #6"); // len("float(") = 6 emitter.instruction("mov x0, #1"); // prepare AArch64 call argument emitter.syscall(4); @@ -613,7 +657,7 @@ fn emit_var_dump_emit_float_line_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("movsd QWORD PTR [rbp - 8], xmm0"); // preserve xmm0 across the prefix syscall abi::emit_symbol_address(emitter, "rsi", "_vd_float_prefix"); // load runtime data address - emitter.instruction("mov edx, 8"); // prepare SysV call argument + emitter.instruction("mov edx, 6"); // len("float(") = 6 emitter.instruction("mov edi, 1"); // prepare SysV call argument emitter.instruction("mov eax, 1"); // prepare runtime result value emitter.instruction("syscall"); // invoke kernel service @@ -648,8 +692,8 @@ pub fn emit_var_dump_array_float(emitter: &mut Emitter) { emitter.comment("--- runtime: var_dump_array_float ---"); emitter.label_global("__rt_var_dump_array_float"); - emitter.instruction("sub sp, sp, #32"); // allocate runtime stack frame - emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address + emitter.instruction("sub sp, sp, #48"); // allocate runtime stack frame + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address emitter.instruction("mov x29, sp"); // establish runtime frame pointer emitter.instruction("str x0, [sp, #0]"); // store runtime value emitter.instruction("str xzr, [sp, #8]"); // store runtime value @@ -661,12 +705,18 @@ pub fn emit_var_dump_array_float(emitter: &mut Emitter) { emitter.instruction("cmp x11, x10"); // compare runtime values for the next branch emitter.instruction("b.ge __rt_vd_arr_float_done"); // branch when comparison is at least target + emitter.instruction("mov x0, #2"); // top-level entry indent = 2 spaces + emitter.instruction("bl __rt_var_dump_spaces"); // pad the entry indent emitter.instruction("bl __rt_var_dump_emit_indexed_key"); // call runtime helper emitter.instruction("ldr x9, [sp, #0]"); // load runtime value emitter.instruction("ldr x11, [sp, #8]"); // load runtime value emitter.instruction("add x12, x11, #3"); // skip 3-quad header emitter.instruction("ldr d0, [x9, x12, lsl #3]"); // load f64 element[index] + emitter.instruction("str d0, [sp, #16]"); // save the float across the spaces call + emitter.instruction("mov x0, #2"); // top-level entry indent = 2 spaces + emitter.instruction("bl __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("ldr d0, [sp, #16]"); // reload the float emitter.instruction("bl __rt_var_dump_emit_float_line"); // call runtime helper emitter.instruction("ldr x11, [sp, #8]"); // load runtime value @@ -675,8 +725,8 @@ pub fn emit_var_dump_array_float(emitter: &mut Emitter) { emitter.instruction("b __rt_vd_arr_float_loop"); // continue at target label emitter.label("__rt_vd_arr_float_done"); - emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #32"); // release runtime stack frame + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release runtime stack frame emitter.instruction("ret"); // return to caller } @@ -688,7 +738,7 @@ fn emit_var_dump_array_float_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("push rbp"); // save caller frame pointer emitter.instruction("mov rbp, rsp"); // establish runtime frame pointer - emitter.instruction("sub rsp, 16"); // allocate runtime stack frame + emitter.instruction("sub rsp, 24"); // allocate runtime stack frame emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // store runtime value emitter.instruction("mov QWORD PTR [rbp - 16], 0"); // store runtime value @@ -699,6 +749,8 @@ fn emit_var_dump_array_float_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("cmp r11, r10"); // compare runtime values for the next branch emitter.instruction("jge __rt_vd_arr_float_done_x86"); // branch when comparison is at least target + emitter.instruction("mov edi, 2"); // top-level entry indent = 2 spaces + emitter.instruction("call __rt_var_dump_spaces"); // pad the entry indent emitter.instruction("mov rdi, r11"); // prepare SysV call argument emitter.instruction("call __rt_var_dump_emit_indexed_key"); // call runtime helper @@ -707,6 +759,10 @@ fn emit_var_dump_array_float_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov r12, r11"); // move runtime value between registers emitter.instruction("add r12, 3"); // advance runtime pointer or counter emitter.instruction("movsd xmm0, QWORD PTR [r9 + r12 * 8]"); // load f64 element[index] into xmm0 + emitter.instruction("movsd QWORD PTR [rbp - 24], xmm0"); // save the float across the spaces call + emitter.instruction("mov edi, 2"); // top-level entry indent = 2 spaces + emitter.instruction("call __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("movsd xmm0, QWORD PTR [rbp - 24]"); // reload the float emitter.instruction("call __rt_var_dump_emit_float_line"); // call runtime helper emitter.instruction("mov r11, QWORD PTR [rbp - 16]"); // move runtime value between registers @@ -715,19 +771,20 @@ fn emit_var_dump_array_float_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jmp __rt_vd_arr_float_loop_x86"); // continue at target label emitter.label("__rt_vd_arr_float_done_x86"); - emitter.instruction("add rsp, 16"); // release runtime stack frame + emitter.instruction("add rsp, 24"); // release runtime stack frame emitter.instruction("pop rbp"); // restore caller frame pointer emitter.instruction("ret"); // return to caller } -/// `__rt_var_dump_emit_null_line`: emit ` NULL\n` for a null payload. +/// `__rt_var_dump_emit_null_line`: emit `NULL\n` for a null payload. The +/// caller writes the entry indent via `__rt_var_dump_spaces` first. pub fn emit_var_dump_emit_null_line(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emitter.blank(); emitter.comment("--- runtime: var_dump_emit_null_line ---"); emitter.label_global("__rt_var_dump_emit_null_line"); abi::emit_symbol_address(emitter, "rsi", "_vd_null_line"); // load runtime data address - emitter.instruction("mov edx, 7"); // len(" NULL\n") = 7 + emitter.instruction("mov edx, 5"); // len("NULL\n") = 5 emitter.instruction("mov edi, 1"); // prepare SysV call argument emitter.instruction("mov eax, 1"); // prepare runtime result value emitter.instruction("syscall"); // invoke kernel service @@ -738,7 +795,7 @@ pub fn emit_var_dump_emit_null_line(emitter: &mut Emitter) { emitter.comment("--- runtime: var_dump_emit_null_line ---"); emitter.label_global("__rt_var_dump_emit_null_line"); crate::codegen::abi::emit_symbol_address(emitter, "x1", "_vd_null_line"); - emitter.instruction("mov x2, #7"); // len(" NULL\n") = 7 + emitter.instruction("mov x2, #5"); // len("NULL\n") = 5 emitter.instruction("mov x0, #1"); // fd = stdout emitter.syscall(4); emitter.instruction("ret"); // return to caller @@ -746,9 +803,9 @@ pub fn emit_var_dump_emit_null_line(emitter: &mut Emitter) { /// `__rt_var_dump_array_mixed`: walk an indexed array of Mixed cell /// pointers and dispatch on each cell's runtime tag. Supports int, -/// string, float, bool payloads; unknown tags (nested arrays/objects) -/// fall back to NULL — full recursive nesting needs the var_dump entry -/// point to drive the walker, not the walker itself. +/// string, float, bool payloads; nested arrays/objects fall back to NULL +/// (the recursive `__rt_var_dump_value` handles full nesting and is the +/// preferred entry point for `Array(Mixed)`). pub fn emit_var_dump_array_mixed(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { emit_var_dump_array_mixed_linux_x86_64(emitter); @@ -771,8 +828,9 @@ pub fn emit_var_dump_array_mixed(emitter: &mut Emitter) { emitter.instruction("cmp x9, #7"); // Mixed? emitter.instruction("b.ne __rt_vd_arr_mixed_skip"); // not Mixed → leave the body empty - emitter.instruction("sub sp, sp, #32"); // allocate runtime stack frame - emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address + // Frame (48 bytes): [0]arr [8]index [16]cell_ptr [32]x29 [40]x30. + emitter.instruction("sub sp, sp, #48"); // allocate runtime stack frame + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address emitter.instruction("mov x29, sp"); // establish runtime frame pointer emitter.instruction("str x0, [sp, #0]"); // array ptr emitter.instruction("str xzr, [sp, #8]"); // index = 0 @@ -784,12 +842,15 @@ pub fn emit_var_dump_array_mixed(emitter: &mut Emitter) { emitter.instruction("cmp x11, x10"); // compare runtime values for the next branch emitter.instruction("b.ge __rt_vd_arr_mixed_done"); // branch when comparison is at least target + emitter.instruction("mov x0, #2"); // top-level entry indent = 2 spaces + emitter.instruction("bl __rt_var_dump_spaces"); // pad the entry indent emitter.instruction("bl __rt_var_dump_emit_indexed_key"); // call runtime helper emitter.instruction("ldr x9, [sp, #0]"); // load runtime value emitter.instruction("ldr x11, [sp, #8]"); // load runtime value emitter.instruction("add x12, x11, #3"); // skip 3-quad header emitter.instruction("ldr x13, [x9, x12, lsl #3]"); // Mixed cell pointer + emitter.instruction("str x13, [sp, #16]"); // save the Mixed cell pointer emitter.instruction("ldr x14, [x13]"); // runtime value tag at cell[0] emitter.instruction("cmp x14, #0"); // tag 0 = int emitter.instruction("b.eq __rt_vd_arr_mixed_int"); // branch when the checked value is zero or equal @@ -799,27 +860,50 @@ pub fn emit_var_dump_array_mixed(emitter: &mut Emitter) { emitter.instruction("b.eq __rt_vd_arr_mixed_flt"); // branch when the checked value is zero or equal emitter.instruction("cmp x14, #3"); // tag 3 = bool emitter.instruction("b.eq __rt_vd_arr_mixed_bool"); // branch when the checked value is zero or equal + emitter.instruction("mov x0, #2"); // top-level entry indent = 2 spaces + emitter.instruction("bl __rt_var_dump_spaces"); // pad the entry indent emitter.instruction("bl __rt_var_dump_emit_null_line"); // unsupported tag → NULL fallback emitter.instruction("b __rt_vd_arr_mixed_next"); // continue at target label emitter.label("__rt_vd_arr_mixed_int"); - emitter.instruction("ldr x0, [x13, #8]"); // load runtime value + emitter.instruction("ldr x0, [sp, #16]"); // reload the Mixed cell pointer + emitter.instruction("ldr x9, [x0, #8]"); // load the int payload + emitter.instruction("str x9, [sp, #16]"); // save the int payload across the spaces call + emitter.instruction("mov x0, #2"); // top-level entry indent = 2 spaces + emitter.instruction("bl __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("ldr x0, [sp, #16]"); // reload the int payload emitter.instruction("bl __rt_var_dump_emit_int_line"); // call runtime helper emitter.instruction("b __rt_vd_arr_mixed_next"); // continue at target label emitter.label("__rt_vd_arr_mixed_str"); - emitter.instruction("ldr x1, [x13, #8]"); // string ptr → x1 per elephc string ABI - emitter.instruction("ldr x2, [x13, #16]"); // string len → x2 + emitter.instruction("ldr x0, [sp, #16]"); // reload the Mixed cell pointer + emitter.instruction("ldr x1, [x0, #8]"); // string ptr + emitter.instruction("ldr x2, [x0, #16]"); // string len + emitter.instruction("stp x1, x2, [sp, #16]"); // save ptr/len across the spaces call + emitter.instruction("mov x0, #2"); // top-level entry indent = 2 spaces + emitter.instruction("bl __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("ldr x1, [sp, #16]"); // reload string ptr + emitter.instruction("ldr x2, [sp, #24]"); // reload string len emitter.instruction("bl __rt_var_dump_emit_string_line"); // call runtime helper emitter.instruction("b __rt_vd_arr_mixed_next"); // continue at target label emitter.label("__rt_vd_arr_mixed_flt"); - emitter.instruction("ldr d0, [x13, #8]"); // load runtime value + emitter.instruction("ldr x0, [sp, #16]"); // reload the Mixed cell pointer + emitter.instruction("ldr d0, [x0, #8]"); // load the float payload + emitter.instruction("str d0, [sp, #16]"); // save the float across the spaces call + emitter.instruction("mov x0, #2"); // top-level entry indent = 2 spaces + emitter.instruction("bl __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("ldr d0, [sp, #16]"); // reload the float emitter.instruction("bl __rt_var_dump_emit_float_line"); // call runtime helper emitter.instruction("b __rt_vd_arr_mixed_next"); // continue at target label emitter.label("__rt_vd_arr_mixed_bool"); - emitter.instruction("ldr x0, [x13, #8]"); // load runtime value + emitter.instruction("ldr x0, [sp, #16]"); // reload the Mixed cell pointer + emitter.instruction("ldr x9, [x0, #8]"); // load the bool payload + emitter.instruction("str x9, [sp, #16]"); // save the bool payload across the spaces call + emitter.instruction("mov x0, #2"); // top-level entry indent = 2 spaces + emitter.instruction("bl __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("ldr x0, [sp, #16]"); // reload the bool payload emitter.instruction("bl __rt_var_dump_emit_bool_line"); // call runtime helper emitter.label("__rt_vd_arr_mixed_next"); @@ -829,8 +913,8 @@ pub fn emit_var_dump_array_mixed(emitter: &mut Emitter) { emitter.instruction("b __rt_vd_arr_mixed_loop"); // continue at target label emitter.label("__rt_vd_arr_mixed_done"); - emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #32"); // release runtime stack frame + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release runtime stack frame emitter.label("__rt_vd_arr_mixed_skip"); // wrong stamp → return without any body emitter.instruction("ret"); // return to caller } @@ -851,7 +935,7 @@ fn emit_var_dump_array_mixed_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("push rbp"); // save caller frame pointer emitter.instruction("mov rbp, rsp"); // establish runtime frame pointer - emitter.instruction("sub rsp, 16"); // allocate runtime stack frame + emitter.instruction("sub rsp, 32"); // allocate runtime stack frame emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // store runtime value emitter.instruction("mov QWORD PTR [rbp - 16], 0"); // store runtime value @@ -862,6 +946,8 @@ fn emit_var_dump_array_mixed_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("cmp r11, r10"); // compare runtime values for the next branch emitter.instruction("jge __rt_vd_arr_mixed_done_x86"); // branch when comparison is at least target + emitter.instruction("mov edi, 2"); // top-level entry indent = 2 spaces + emitter.instruction("call __rt_var_dump_spaces"); // pad the entry indent emitter.instruction("mov rdi, r11"); // prepare SysV call argument emitter.instruction("call __rt_var_dump_emit_indexed_key"); // call runtime helper @@ -869,8 +955,9 @@ fn emit_var_dump_array_mixed_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov r11, QWORD PTR [rbp - 16]"); // move runtime value between registers emitter.instruction("mov r12, r11"); // move runtime value between registers emitter.instruction("add r12, 3"); // advance runtime pointer or counter - emitter.instruction("mov r13, QWORD PTR [r9 + r12 * 8]"); // move runtime value between registers - emitter.instruction("mov r14, QWORD PTR [r13]"); // move runtime value between registers + emitter.instruction("mov r13, QWORD PTR [r9 + r12 * 8]"); // Mixed cell pointer + emitter.instruction("mov QWORD PTR [rbp - 24], r13"); // save the Mixed cell pointer + emitter.instruction("mov r14, QWORD PTR [r13]"); // runtime value tag at cell[0] emitter.instruction("cmp r14, 0"); // compare runtime values for the next branch emitter.instruction("je __rt_vd_arr_mixed_int_x86"); // branch when the checked value is zero or equal @@ -880,27 +967,52 @@ fn emit_var_dump_array_mixed_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("je __rt_vd_arr_mixed_flt_x86"); // branch when the checked value is zero or equal emitter.instruction("cmp r14, 3"); // compare runtime values for the next branch emitter.instruction("je __rt_vd_arr_mixed_bool_x86"); // branch when the checked value is zero or equal + emitter.instruction("mov edi, 2"); // top-level entry indent = 2 spaces + emitter.instruction("call __rt_var_dump_spaces"); // pad the entry indent emitter.instruction("call __rt_var_dump_emit_null_line"); // call runtime helper emitter.instruction("jmp __rt_vd_arr_mixed_next_x86"); // continue at target label emitter.label("__rt_vd_arr_mixed_int_x86"); - emitter.instruction("mov rdi, QWORD PTR [r13 + 8]"); // prepare SysV call argument + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // reload the Mixed cell pointer + emitter.instruction("mov rax, QWORD PTR [rax + 8]"); // load the int payload + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // save the int payload across the spaces call + emitter.instruction("mov edi, 2"); // top-level entry indent = 2 spaces + emitter.instruction("call __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the int payload emitter.instruction("call __rt_var_dump_emit_int_line"); // call runtime helper emitter.instruction("jmp __rt_vd_arr_mixed_next_x86"); // continue at target label emitter.label("__rt_vd_arr_mixed_str_x86"); - emitter.instruction("mov rdi, QWORD PTR [r13 + 8]"); // prepare SysV call argument - emitter.instruction("mov rsi, QWORD PTR [r13 + 16]"); // prepare SysV call argument + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // reload the Mixed cell pointer + emitter.instruction("mov rax, QWORD PTR [rax + 8]"); // load the string ptr + emitter.instruction("mov rcx, QWORD PTR [rbp - 24]"); // reload the Mixed cell pointer + emitter.instruction("mov rcx, QWORD PTR [rcx + 16]"); // load the string len + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // save the string ptr + emitter.instruction("mov QWORD PTR [rbp - 24], rcx"); // save the string len + emitter.instruction("mov edi, 2"); // top-level entry indent = 2 spaces + emitter.instruction("call __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the string ptr + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // reload the string len emitter.instruction("call __rt_var_dump_emit_string_line"); // call runtime helper emitter.instruction("jmp __rt_vd_arr_mixed_next_x86"); // continue at target label emitter.label("__rt_vd_arr_mixed_flt_x86"); - emitter.instruction("movsd xmm0, QWORD PTR [r13 + 8]"); // load the mixed float payload into the SysV float argument register + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // reload the Mixed cell pointer + emitter.instruction("movsd xmm0, QWORD PTR [rax + 8]"); // load the mixed float payload + emitter.instruction("movsd QWORD PTR [rbp - 32], xmm0"); // save the float across the spaces call + emitter.instruction("mov edi, 2"); // top-level entry indent = 2 spaces + emitter.instruction("call __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("movsd xmm0, QWORD PTR [rbp - 32]"); // reload the float emitter.instruction("call __rt_var_dump_emit_float_line"); // call runtime helper emitter.instruction("jmp __rt_vd_arr_mixed_next_x86"); // continue at target label emitter.label("__rt_vd_arr_mixed_bool_x86"); - emitter.instruction("mov rdi, QWORD PTR [r13 + 8]"); // prepare SysV call argument + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // reload the Mixed cell pointer + emitter.instruction("mov rax, QWORD PTR [rax + 8]"); // load the bool payload + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // save the bool payload across the spaces call + emitter.instruction("mov edi, 2"); // top-level entry indent = 2 spaces + emitter.instruction("call __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the bool payload emitter.instruction("call __rt_var_dump_emit_bool_line"); // call runtime helper emitter.label("__rt_vd_arr_mixed_next_x86"); @@ -910,7 +1022,7 @@ fn emit_var_dump_array_mixed_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jmp __rt_vd_arr_mixed_loop_x86"); // continue at target label emitter.label("__rt_vd_arr_mixed_done_x86"); - emitter.instruction("add rsp, 16"); // release runtime stack frame + emitter.instruction("add rsp, 32"); // release runtime stack frame emitter.instruction("pop rbp"); // restore caller frame pointer emitter.label("__rt_vd_arr_mixed_skip_x86"); // wrong stamp → return without any body emitter.instruction("ret"); // return to caller @@ -924,7 +1036,7 @@ fn emit_var_dump_array_bool_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("push rbp"); // save caller frame pointer emitter.instruction("mov rbp, rsp"); // establish runtime frame pointer - emitter.instruction("sub rsp, 16"); // allocate runtime stack frame + emitter.instruction("sub rsp, 24"); // allocate runtime stack frame emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // store runtime value emitter.instruction("mov QWORD PTR [rbp - 16], 0"); // store runtime value @@ -935,6 +1047,8 @@ fn emit_var_dump_array_bool_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("cmp r11, r10"); // compare runtime values for the next branch emitter.instruction("jge __rt_vd_arr_bool_done_x86"); // branch when comparison is at least target + emitter.instruction("mov edi, 2"); // top-level entry indent = 2 spaces + emitter.instruction("call __rt_var_dump_spaces"); // pad the entry indent emitter.instruction("mov rdi, r11"); // prepare SysV call argument emitter.instruction("call __rt_var_dump_emit_indexed_key"); // call runtime helper @@ -942,7 +1056,11 @@ fn emit_var_dump_array_bool_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov r11, QWORD PTR [rbp - 16]"); // move runtime value between registers emitter.instruction("mov r12, r11"); // move runtime value between registers emitter.instruction("add r12, 3"); // advance runtime pointer or counter - emitter.instruction("mov rdi, QWORD PTR [r9 + r12 * 8]"); // prepare SysV call argument + emitter.instruction("mov rax, QWORD PTR [r9 + r12 * 8]"); // load element[index] (0 or 1) + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the bool value across the spaces call + emitter.instruction("mov edi, 2"); // top-level entry indent = 2 spaces + emitter.instruction("call __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload the bool value emitter.instruction("call __rt_var_dump_emit_bool_line"); // call runtime helper emitter.instruction("mov r11, QWORD PTR [rbp - 16]"); // move runtime value between registers @@ -951,12 +1069,13 @@ fn emit_var_dump_array_bool_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jmp __rt_vd_arr_bool_loop_x86"); // continue at target label emitter.label("__rt_vd_arr_bool_done_x86"); - emitter.instruction("add rsp, 16"); // release runtime stack frame + emitter.instruction("add rsp, 24"); // release runtime stack frame emitter.instruction("pop rbp"); // restore caller frame pointer emitter.instruction("ret"); // return to caller } -/// `__rt_var_dump_emit_string_key`: emit ` ["KEY"]=>\n` for a string hash key. +/// `__rt_var_dump_emit_string_key`: emit `["KEY"]=>\n` for a string hash key. +/// The caller writes the entry indent via `__rt_var_dump_spaces` first. /// Input: AArch64 x1=ptr x2=len / x86_64 rdi=ptr rsi=len. pub fn emit_var_dump_emit_string_key(emitter: &mut Emitter) { if emitter.target.arch == Arch::X86_64 { @@ -973,9 +1092,9 @@ pub fn emit_var_dump_emit_string_key(emitter: &mut Emitter) { emitter.instruction("mov x29, sp"); // establish runtime frame pointer emitter.instruction("stp x1, x2, [sp, #0]"); // save key ptr/len across the writes - // Emit ` ["` + // Emit `["` crate::codegen::abi::emit_symbol_address(emitter, "x1", "_vd_str_key_open"); - emitter.instruction("mov x2, #4"); // len(" [\"") = 4 + emitter.instruction("mov x2, #2"); // len("[\"") = 2 emitter.instruction("mov x0, #1"); // fd = stdout emitter.syscall(4); @@ -1009,7 +1128,7 @@ fn emit_var_dump_emit_string_key_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save key len abi::emit_symbol_address(emitter, "rsi", "_vd_str_key_open"); // load runtime data address - emitter.instruction("mov edx, 4"); // len(" [\"") = 4 + emitter.instruction("mov edx, 2"); // len("[\"") = 2 emitter.instruction("mov edi, 1"); // fd = stdout emitter.instruction("mov eax, 1"); // sys_write emitter.instruction("syscall"); // invoke kernel service @@ -1048,13 +1167,15 @@ pub fn emit_var_dump_hash(emitter: &mut Emitter) { emitter.comment("--- runtime: var_dump_hash ---"); emitter.label_global("__rt_var_dump_hash"); - // Frame (96 bytes): [0]=hash ptr, [8]=cursor, [16]=count, [24]=items, + // Frame (112 bytes): [0]=hash ptr, [8]=cursor, [16]=count, [24]=items, // [32]=key_ptr, [40]=key_len, [48]=val_lo, [56]=val_hi, [64]=val_tag, - // [80]=x29, [88]=x30. - emitter.instruction("sub sp, sp, #96"); // allocate the hash-walk frame - emitter.instruction("stp x29, x30, [sp, #80]"); // save frame pointer and return address - emitter.instruction("add x29, sp, #80"); // establish runtime frame pointer + // [72]=scratch0, [80]=scratch1, [88]=entry_indent, [96]=x29, [104]=x30. + emitter.instruction("sub sp, sp, #112"); // allocate the hash-walk frame + emitter.instruction("stp x29, x30, [sp, #96]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #96"); // establish runtime frame pointer emitter.instruction("str x0, [sp, #0]"); // save the hash table pointer + emitter.instruction("add x9, x1, #2"); // entry indent = indent + 2 + emitter.instruction("str x9, [sp, #88]"); // save the entry indent emitter.instruction("bl __rt_hash_count"); // x0 = number of entries emitter.instruction("str x0, [sp, #16]"); // save the entry count emitter.instruction("str xzr, [sp, #8]"); // iterator cursor = 0 @@ -1077,16 +1198,18 @@ pub fn emit_var_dump_hash(emitter: &mut Emitter) { emitter.instruction("str x5, [sp, #64]"); // save value runtime tag // -- emit the key prefix -- + emitter.instruction("ldr x0, [sp, #88]"); // entry indent → spaces helper + emitter.instruction("bl __rt_var_dump_spaces"); // pad the entry indent emitter.instruction("ldr x2, [sp, #40]"); // reload key len emitter.instruction("cmn x2, #1"); // integer key? (len == -1) emitter.instruction("b.eq __rt_vd_hash_int_key"); // format integer keys as [N] emitter.instruction("ldr x1, [sp, #32]"); // reload key ptr emitter.instruction("ldr x2, [sp, #40]"); // reload key len - emitter.instruction("bl __rt_var_dump_emit_string_key"); // emit ` ["KEY"]=>\n` + emitter.instruction("bl __rt_var_dump_emit_string_key"); // emit `["KEY"]=>\n` emitter.instruction("b __rt_vd_hash_after_key"); // continue to the value line emitter.label("__rt_vd_hash_int_key"); emitter.instruction("ldr x11, [sp, #32]"); // integer key payload → indexed-key helper's x11 input - emitter.instruction("bl __rt_var_dump_emit_indexed_key"); // emit ` [N]=>\n` + emitter.instruction("bl __rt_var_dump_emit_indexed_key"); // emit `[N]=>\n` emitter.label("__rt_vd_hash_after_key"); // -- dispatch the value on its runtime tag; unbox boxed Mixed cells first -- @@ -1109,28 +1232,59 @@ pub fn emit_var_dump_hash(emitter: &mut Emitter) { emitter.instruction("b.eq __rt_vd_hash_v_flt"); // format float values emitter.instruction("cmp x12, #3"); // tag 3 = bool emitter.instruction("b.eq __rt_vd_hash_v_bool"); // format bool values - emitter.instruction("bl __rt_var_dump_emit_null_line"); // tags 4/5/6/8 (nested/object/null) → NULL line + emitter.instruction("cmp x12, #4"); // tag 4 = indexed array + emitter.instruction("b.eq __rt_vd_hash_v_arr"); // recurse into the value renderer + emitter.instruction("cmp x12, #5"); // tag 5 = hash + emitter.instruction("b.eq __rt_vd_hash_v_arr"); // recurse into the value renderer + emitter.instruction("ldr x0, [sp, #88]"); // entry indent → spaces helper + emitter.instruction("bl __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("bl __rt_var_dump_emit_null_line"); // tags 6/8 (object/null) → NULL line emitter.instruction("b __rt_vd_hash_next"); // advance to the next entry emitter.label("__rt_vd_hash_v_int"); - emitter.instruction("ldr x0, [sp, #48]"); // load the integer payload - emitter.instruction("bl __rt_var_dump_emit_int_line"); // emit ` int(VAL)\n` + emitter.instruction("ldr x9, [sp, #48]"); // load the integer payload + emitter.instruction("str x9, [sp, #72]"); // save it across the spaces call + emitter.instruction("ldr x0, [sp, #88]"); // entry indent → spaces helper + emitter.instruction("bl __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("ldr x0, [sp, #72]"); // reload the integer payload + emitter.instruction("bl __rt_var_dump_emit_int_line"); // emit `int(VAL)\n` emitter.instruction("b __rt_vd_hash_next"); // advance to the next entry emitter.label("__rt_vd_hash_v_str"); emitter.instruction("ldr x1, [sp, #48]"); // load the string pointer emitter.instruction("ldr x2, [sp, #56]"); // load the string length - emitter.instruction("bl __rt_var_dump_emit_string_line"); // emit ` string(LEN) "VAL"\n` + emitter.instruction("stp x1, x2, [sp, #72]"); // save ptr/len across the spaces call + emitter.instruction("ldr x0, [sp, #88]"); // entry indent → spaces helper + emitter.instruction("bl __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("ldr x1, [sp, #72]"); // reload the string pointer + emitter.instruction("ldr x2, [sp, #80]"); // reload the string length + emitter.instruction("bl __rt_var_dump_emit_string_line"); // emit `string(LEN) "VAL"\n` emitter.instruction("b __rt_vd_hash_next"); // advance to the next entry emitter.label("__rt_vd_hash_v_flt"); emitter.instruction("ldr d0, [sp, #48]"); // load the float bit pattern - emitter.instruction("bl __rt_var_dump_emit_float_line"); // emit ` float(VAL)\n` + emitter.instruction("str d0, [sp, #72]"); // save the float across the spaces call + emitter.instruction("ldr x0, [sp, #88]"); // entry indent → spaces helper + emitter.instruction("bl __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("ldr d0, [sp, #72]"); // reload the float + emitter.instruction("bl __rt_var_dump_emit_float_line"); // emit `float(VAL)\n` emitter.instruction("b __rt_vd_hash_next"); // advance to the next entry emitter.label("__rt_vd_hash_v_bool"); - emitter.instruction("ldr x0, [sp, #48]"); // load the bool payload (0 or 1) - emitter.instruction("bl __rt_var_dump_emit_bool_line"); // emit ` bool(true|false)\n` + emitter.instruction("ldr x9, [sp, #48]"); // load the bool payload (0 or 1) + emitter.instruction("str x9, [sp, #72]"); // save the bool payload across the spaces call + emitter.instruction("ldr x0, [sp, #88]"); // entry indent → spaces helper + emitter.instruction("bl __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("ldr x0, [sp, #72]"); // reload the bool payload + emitter.instruction("bl __rt_var_dump_emit_bool_line"); // emit `bool(true|false)\n` + emitter.instruction("b __rt_vd_hash_next"); // advance to the next entry + + emitter.label("__rt_vd_hash_v_arr"); + emitter.instruction("ldr x0, [sp, #64]"); // reload the value tag + emitter.instruction("ldr x1, [sp, #48]"); // reload the value low (array/hash pointer) + emitter.instruction("mov x2, #0"); // high word unused for containers + emitter.instruction("ldr x3, [sp, #88]"); // entry indent for the nested container + emitter.instruction("bl __rt_var_dump_value"); // recurse into the value renderer emitter.label("__rt_vd_hash_next"); emitter.instruction("ldr x9, [sp, #24]"); // reload items emitted @@ -1139,8 +1293,8 @@ pub fn emit_var_dump_hash(emitter: &mut Emitter) { emitter.instruction("b __rt_vd_hash_loop"); // continue with the next entry emitter.label("__rt_vd_hash_done"); - emitter.instruction("ldp x29, x30, [sp, #80]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #96"); // release the hash-walk frame + emitter.instruction("ldp x29, x30, [sp, #96]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #112"); // release the hash-walk frame emitter.instruction("ret"); // return to the var_dump caller } @@ -1151,11 +1305,15 @@ fn emit_var_dump_hash_linux_x86_64(emitter: &mut Emitter) { emitter.label_global("__rt_var_dump_hash"); // rbp-relative frame: [-8]=hash ptr, [-16]=cursor, [-24]=count, [-32]=items, - // [-40]=key_ptr, [-48]=key_len, [-56]=val_lo, [-64]=val_hi, [-72]=val_tag. + // [-40]=key_ptr, [-48]=key_len, [-56]=val_lo, [-64]=val_hi, [-72]=val_tag, + // [-80]=scratch0, [-88]=scratch1, [-96]=entry_indent. emitter.instruction("push rbp"); // save caller frame pointer emitter.instruction("mov rbp, rsp"); // establish runtime frame pointer - emitter.instruction("sub rsp, 96"); // allocate the hash-walk frame + emitter.instruction("sub rsp, 128"); // allocate the hash-walk frame emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the hash table pointer + emitter.instruction("mov rax, rsi"); // copy the indent + emitter.instruction("add rax, 2"); // entry indent = indent + 2 + emitter.instruction("mov QWORD PTR [rbp - 96], rax"); // save the entry indent emitter.instruction("call __rt_hash_count"); // rax = number of entries (hash ptr already in rdi) emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the entry count emitter.instruction("mov QWORD PTR [rbp - 16], 0"); // iterator cursor = 0 @@ -1177,16 +1335,18 @@ fn emit_var_dump_hash_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [rbp - 64], r8"); // save value high payload word emitter.instruction("mov QWORD PTR [rbp - 72], r9"); // save value runtime tag + emitter.instruction("mov rdi, QWORD PTR [rbp - 96]"); // entry indent → spaces helper + emitter.instruction("call __rt_var_dump_spaces"); // pad the entry indent emitter.instruction("mov rdx, QWORD PTR [rbp - 48]"); // reload key len emitter.instruction("cmp rdx, -1"); // integer key? emitter.instruction("je __rt_vd_hash_int_key_x86"); // format integer keys as [N] emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // reload key ptr → string-key helper's rdi emitter.instruction("mov rsi, QWORD PTR [rbp - 48]"); // reload key len → string-key helper's rsi - emitter.instruction("call __rt_var_dump_emit_string_key"); // emit ` ["KEY"]=>\n` + emitter.instruction("call __rt_var_dump_emit_string_key"); // emit `["KEY"]=>\n` emitter.instruction("jmp __rt_vd_hash_after_key_x86"); // continue to the value line emitter.label("__rt_vd_hash_int_key_x86"); emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // integer key payload → indexed-key helper's rdi - emitter.instruction("call __rt_var_dump_emit_indexed_key"); // emit ` [N]=>\n` + emitter.instruction("call __rt_var_dump_emit_indexed_key"); // emit `[N]=>\n` emitter.label("__rt_vd_hash_after_key_x86"); emitter.instruction("mov r10, QWORD PTR [rbp - 72]"); // reload value tag @@ -1208,28 +1368,60 @@ fn emit_var_dump_hash_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("je __rt_vd_hash_v_flt_x86"); // format float values emitter.instruction("cmp r10, 3"); // tag 3 = bool emitter.instruction("je __rt_vd_hash_v_bool_x86"); // format bool values - emitter.instruction("call __rt_var_dump_emit_null_line"); // tags 4/5/6/8 (nested/object/null) → NULL line + emitter.instruction("cmp r10, 4"); // tag 4 = indexed array + emitter.instruction("je __rt_vd_hash_v_arr_x86"); // recurse into the value renderer + emitter.instruction("cmp r10, 5"); // tag 5 = hash + emitter.instruction("je __rt_vd_hash_v_arr_x86"); // recurse into the value renderer + emitter.instruction("mov rdi, QWORD PTR [rbp - 96]"); // entry indent → spaces helper + emitter.instruction("call __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("call __rt_var_dump_emit_null_line"); // tags 6/8 (object/null) → NULL line emitter.instruction("jmp __rt_vd_hash_next_x86"); // advance to the next entry emitter.label("__rt_vd_hash_v_int_x86"); - emitter.instruction("mov rdi, QWORD PTR [rbp - 56]"); // load the integer payload - emitter.instruction("call __rt_var_dump_emit_int_line"); // emit ` int(VAL)\n` + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // load the integer payload + emitter.instruction("mov QWORD PTR [rbp - 80], rax"); // save it across the spaces call + emitter.instruction("mov rdi, QWORD PTR [rbp - 96]"); // entry indent → spaces helper + emitter.instruction("call __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("mov rdi, QWORD PTR [rbp - 80]"); // reload the integer payload + emitter.instruction("call __rt_var_dump_emit_int_line"); // emit `int(VAL)\n` emitter.instruction("jmp __rt_vd_hash_next_x86"); // advance to the next entry emitter.label("__rt_vd_hash_v_str_x86"); - emitter.instruction("mov rdi, QWORD PTR [rbp - 56]"); // load the string pointer - emitter.instruction("mov rsi, QWORD PTR [rbp - 64]"); // load the string length - emitter.instruction("call __rt_var_dump_emit_string_line"); // emit ` string(LEN) "VAL"\n` + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // load the string pointer + emitter.instruction("mov rcx, QWORD PTR [rbp - 64]"); // load the string length + emitter.instruction("mov QWORD PTR [rbp - 80], rax"); // save the string ptr + emitter.instruction("mov QWORD PTR [rbp - 88], rcx"); // save the string len + emitter.instruction("mov rdi, QWORD PTR [rbp - 96]"); // entry indent → spaces helper + emitter.instruction("call __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("mov rdi, QWORD PTR [rbp - 80]"); // reload the string pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 88]"); // reload the string length + emitter.instruction("call __rt_var_dump_emit_string_line"); // emit `string(LEN) "VAL"\n` emitter.instruction("jmp __rt_vd_hash_next_x86"); // advance to the next entry emitter.label("__rt_vd_hash_v_flt_x86"); emitter.instruction("movsd xmm0, QWORD PTR [rbp - 56]"); // load the float bit pattern - emitter.instruction("call __rt_var_dump_emit_float_line"); // emit ` float(VAL)\n` + emitter.instruction("movsd QWORD PTR [rbp - 80], xmm0"); // save the float across the spaces call + emitter.instruction("mov rdi, QWORD PTR [rbp - 96]"); // entry indent → spaces helper + emitter.instruction("call __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("movsd xmm0, QWORD PTR [rbp - 80]"); // reload the float + emitter.instruction("call __rt_var_dump_emit_float_line"); // emit `float(VAL)\n` emitter.instruction("jmp __rt_vd_hash_next_x86"); // advance to the next entry emitter.label("__rt_vd_hash_v_bool_x86"); - emitter.instruction("mov rdi, QWORD PTR [rbp - 56]"); // load the bool payload (0 or 1) - emitter.instruction("call __rt_var_dump_emit_bool_line"); // emit ` bool(true|false)\n` + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // load the bool payload (0 or 1) + emitter.instruction("mov QWORD PTR [rbp - 80], rax"); // save the bool payload across the spaces call + emitter.instruction("mov rdi, QWORD PTR [rbp - 96]"); // entry indent → spaces helper + emitter.instruction("call __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("mov rdi, QWORD PTR [rbp - 80]"); // reload the bool payload + emitter.instruction("call __rt_var_dump_emit_bool_line"); // emit `bool(true|false)\n` + emitter.instruction("jmp __rt_vd_hash_next_x86"); // advance to the next entry + + emitter.label("__rt_vd_hash_v_arr_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 72]"); // reload the value tag + emitter.instruction("mov rsi, QWORD PTR [rbp - 56]"); // reload the value low (array/hash pointer) + emitter.instruction("mov edx, 0"); // high word unused for containers + emitter.instruction("mov rcx, QWORD PTR [rbp - 96]"); // entry indent for the nested container + emitter.instruction("call __rt_var_dump_value"); // recurse into the value renderer emitter.label("__rt_vd_hash_next_x86"); emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload items emitted @@ -1238,7 +1430,564 @@ fn emit_var_dump_hash_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("jmp __rt_vd_hash_loop_x86"); // continue with the next entry emitter.label("__rt_vd_hash_done_x86"); - emitter.instruction("add rsp, 96"); // release the hash-walk frame + emitter.instruction("add rsp, 128"); // release the hash-walk frame emitter.instruction("pop rbp"); // restore caller frame pointer emitter.instruction("ret"); // return to the var_dump caller } + +/// `__rt_var_dump_spaces`: write `n` ASCII spaces to stdout in <=64-byte chunks. +/// Input: AArch64 x0 / x86_64 rdi = space count. +pub fn emit_var_dump_spaces(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_var_dump_spaces_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: var_dump_spaces ---"); + emitter.label_global("__rt_var_dump_spaces"); + + emitter.instruction("sub sp, sp, #32"); // allocate the helper frame + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address + emitter.instruction("mov x29, sp"); // establish the helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // remaining space count + + emitter.label("__rt_vd_spaces_loop"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the remaining count + emitter.instruction("cmp x0, #0"); // any spaces left to write? + emitter.instruction("b.le __rt_vd_spaces_done"); // none → finish + emitter.instruction("mov x9, #64"); // the pad buffer is 64 bytes wide + emitter.instruction("cmp x0, x9"); // remaining vs the chunk cap + emitter.instruction("csel x2, x0, x9, lt"); // chunk len = min(remaining, 64) + emitter.instruction("sub x0, x0, x2"); // remaining -= chunk + emitter.instruction("str x0, [sp, #0]"); // save the decremented count + abi::emit_symbol_address(emitter, "x1", "_vd_spaces"); // buffer = the 64-space pad + emitter.instruction("mov x0, #1"); // fd = stdout + emitter.syscall(4); // write the space chunk + emitter.instruction("b __rt_vd_spaces_loop"); // continue padding + + emitter.label("__rt_vd_spaces_done"); + 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 to caller +} + +/// Emits the Linux x86_64 helper that writes `n` spaces in <=64-byte chunks. +fn emit_var_dump_spaces_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: var_dump_spaces ---"); + emitter.label_global("__rt_var_dump_spaces"); + + emitter.instruction("push rbp"); // save caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish the helper frame pointer + emitter.instruction("sub rsp, 16"); // allocate the helper frame + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // remaining space count + + emitter.label("__rt_vd_spaces_loop_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the remaining count + emitter.instruction("cmp rax, 0"); // any spaces left to write? + emitter.instruction("jle __rt_vd_spaces_done_x86"); // none → finish + emitter.instruction("mov rdx, 64"); // the pad buffer is 64 bytes wide + emitter.instruction("cmp rax, 64"); // remaining vs the chunk cap + emitter.instruction("cmovl rdx, rax"); // chunk len = min(remaining, 64) + emitter.instruction("sub rax, rdx"); // remaining -= chunk + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the decremented count + abi::emit_symbol_address(emitter, "rsi", "_vd_spaces"); // buffer = the 64-space pad + emitter.instruction("mov edi, 1"); // fd = stdout + emitter.instruction("mov eax, 1"); // sys_write + emitter.instruction("syscall"); // write the space chunk + emitter.instruction("jmp __rt_vd_spaces_loop_x86"); // continue padding + + emitter.label("__rt_vd_spaces_done_x86"); + emitter.instruction("add rsp, 16"); // release the helper frame + emitter.instruction("pop rbp"); // restore caller frame pointer + emitter.instruction("ret"); // return to caller +} + +/// `__rt_var_dump_value`: render one PHP value with PHP `var_dump` formatting +/// and `indent`-space indentation. Tags 4/5 recurse into +/// `__rt_var_dump_indexed` / `__rt_var_dump_hash` with `entry_indent = indent +/// + 2`, tag 7 unboxes a Mixed cell and redispatches, tag 6 (object) and tag +/// 8 (null) emit `NULL` (object is a documented limitation). A depth cap +/// (`indent > 128`) stops cyclic arrays from overflowing the stack. +/// Input: AArch64 x0=tag x1=lo x2=hi x3=indent / +/// x86_64 rdi=tag rsi=lo rdx=hi rcx=indent. +pub fn emit_var_dump_value(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_var_dump_value_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: var_dump_value ---"); + emitter.label_global("__rt_var_dump_value"); + + // Frame (48 bytes): [0]=lo [8]=hi [16]=indent [32]=x29 [40]=x30. + emitter.instruction("sub sp, sp, #48"); // allocate the value frame + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address + emitter.instruction("mov x29, sp"); // establish the value frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the value low word + emitter.instruction("str x2, [sp, #8]"); // save the value high word + emitter.instruction("str x3, [sp, #16]"); // save the indent + + // -- depth cap: indent > 128 → render NULL to avoid stack overflow on cycles -- + emitter.instruction("cmp x3, #128"); // depth beyond the cap? + emitter.instruction("b.hi __rt_vd_val_null"); // too deep → render NULL + + emitter.instruction("cmp x0, #7"); // boxed Mixed cell? + emitter.instruction("b.eq __rt_vd_val_mixed"); // unbox then redispatch + emitter.instruction("cmp x0, #0"); // tag 0 = int + emitter.instruction("b.eq __rt_vd_val_int"); // render the integer + emitter.instruction("cmp x0, #1"); // tag 1 = string + emitter.instruction("b.eq __rt_vd_val_str"); // render the string + emitter.instruction("cmp x0, #2"); // tag 2 = float + emitter.instruction("b.eq __rt_vd_val_flt"); // render the float + emitter.instruction("cmp x0, #3"); // tag 3 = bool + emitter.instruction("b.eq __rt_vd_val_bool"); // render the bool + emitter.instruction("cmp x0, #4"); // tag 4 = indexed array + emitter.instruction("b.eq __rt_vd_val_arr"); // recurse into the indexed walker + emitter.instruction("cmp x0, #5"); // tag 5 = hash + emitter.instruction("b.eq __rt_vd_val_hash"); // recurse into the hash walker + emitter.instruction("b __rt_vd_val_null"); // tag 6 object / 8 null → NULL line + + emitter.label("__rt_vd_val_int"); + emitter.instruction("ldr x0, [sp, #16]"); // indent → spaces helper + emitter.instruction("bl __rt_var_dump_spaces"); // pad the indent + emitter.instruction("ldr x0, [sp, #0]"); // reload the integer payload + emitter.instruction("bl __rt_var_dump_emit_int_line"); // emit `int(VAL)\n` + emitter.instruction("b __rt_vd_val_done"); // value rendered + + emitter.label("__rt_vd_val_str"); + emitter.instruction("ldr x0, [sp, #16]"); // indent → spaces helper + emitter.instruction("bl __rt_var_dump_spaces"); // pad the indent + emitter.instruction("ldr x1, [sp, #0]"); // reload the string ptr + emitter.instruction("ldr x2, [sp, #8]"); // reload the string len + emitter.instruction("bl __rt_var_dump_emit_string_line"); // emit `string(LEN) "VAL"\n` + emitter.instruction("b __rt_vd_val_done"); // value rendered + + emitter.label("__rt_vd_val_flt"); + emitter.instruction("ldr x0, [sp, #16]"); // indent → spaces helper + emitter.instruction("bl __rt_var_dump_spaces"); // pad the indent + emitter.instruction("ldr d0, [sp, #0]"); // reload the float bit pattern + emitter.instruction("bl __rt_var_dump_emit_float_line"); // emit `float(VAL)\n` + emitter.instruction("b __rt_vd_val_done"); // value rendered + + emitter.label("__rt_vd_val_bool"); + emitter.instruction("ldr x0, [sp, #16]"); // indent → spaces helper + emitter.instruction("bl __rt_var_dump_spaces"); // pad the indent + emitter.instruction("ldr x0, [sp, #0]"); // reload the bool payload + emitter.instruction("bl __rt_var_dump_emit_bool_line"); // emit `bool(true|false)\n` + emitter.instruction("b __rt_vd_val_done"); // value rendered + + emitter.label("__rt_vd_val_arr"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the indexed-array pointer + emitter.instruction("ldr x1, [sp, #16]"); // reload the indent → entry base + emitter.instruction("bl __rt_var_dump_indexed"); // recurse into the indexed walker (writes its own header indent) + emitter.instruction("b __rt_vd_val_done"); // value rendered + + emitter.label("__rt_vd_val_hash"); + // -- emit `array(N) {\n` header -- + emitter.instruction("ldr x0, [sp, #16]"); // indent → spaces helper + emitter.instruction("bl __rt_var_dump_spaces"); // pad the indent for the array header + abi::emit_symbol_address(emitter, "x1", "_vd_array_open"); // load the `array(` literal + emitter.instruction("mov x2, #6"); // len("array(") = 6 + emitter.instruction("mov x0, #1"); // fd = stdout + emitter.syscall(4); // write `array(` + emitter.instruction("ldr x0, [sp, #0]"); // reload the hash pointer + emitter.instruction("bl __rt_hash_count"); // x0 = number of entries + emitter.instruction("bl __rt_itoa"); // x1=digits ptr, x2=digits len + emitter.instruction("mov x0, #1"); // fd = stdout + emitter.syscall(4); // write the count digits + abi::emit_symbol_address(emitter, "x1", "_vd_array_close_brace"); // load the `) {\n` literal + emitter.instruction("mov x2, #4"); // len(") {\n") = 4 + emitter.instruction("mov x0, #1"); // fd = stdout + emitter.syscall(4); // write `) {\n` + // -- emit the hash entries (body) -- + emitter.instruction("ldr x0, [sp, #0]"); // reload the hash pointer + emitter.instruction("ldr x1, [sp, #16]"); // reload the indent → entry base + emitter.instruction("bl __rt_var_dump_hash"); // walk the hash entries + // -- emit `}\n` footer -- + emitter.instruction("ldr x0, [sp, #16]"); // indent → spaces helper + emitter.instruction("bl __rt_var_dump_spaces"); // pad the indent for the closing brace + abi::emit_symbol_address(emitter, "x1", "_vd_close_brace_nl"); // load the `}\n` literal + emitter.instruction("mov x2, #2"); // len("}\n") = 2 + emitter.instruction("mov x0, #1"); // fd = stdout + emitter.syscall(4); // write `}\n` + emitter.instruction("b __rt_vd_val_done"); // value rendered + + emitter.label("__rt_vd_val_mixed"); + emitter.instruction("ldr x0, [sp, #0]"); // boxed Mixed cell pointer + emitter.instruction("bl __rt_mixed_unbox"); // x0=inner tag, x1=lo, x2=hi + emitter.instruction("ldr x3, [sp, #16]"); // reload the indent + emitter.instruction("bl __rt_var_dump_value"); // redispatch the unboxed value + emitter.instruction("b __rt_vd_val_done"); // value rendered + + emitter.label("__rt_vd_val_null"); + emitter.instruction("ldr x0, [sp, #16]"); // indent → spaces helper + emitter.instruction("bl __rt_var_dump_spaces"); // pad the indent + emitter.instruction("bl __rt_var_dump_emit_null_line"); // emit `NULL\n` + + emitter.label("__rt_vd_val_done"); + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the value frame + emitter.instruction("ret"); // return to caller +} + +/// Emits the Linux x86_64 single-value renderer for var_dump. +fn emit_var_dump_value_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: var_dump_value ---"); + emitter.label_global("__rt_var_dump_value"); + + emitter.instruction("push rbp"); // save caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish the value frame pointer + emitter.instruction("sub rsp, 48"); // allocate the value frame + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the value low word + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the value high word + emitter.instruction("mov QWORD PTR [rbp - 24], rcx"); // save the indent + emitter.instruction("mov rax, rdi"); // tag → dispatch register + + // -- depth cap: indent > 128 → render NULL to avoid stack overflow on cycles -- + emitter.instruction("cmp rcx, 128"); // depth beyond the cap? + emitter.instruction("ja __rt_vd_val_null_x86"); // too deep → render NULL + + emitter.instruction("cmp rax, 7"); // boxed Mixed cell? + emitter.instruction("je __rt_vd_val_mixed_x86"); // unbox then redispatch + emitter.instruction("cmp rax, 0"); // tag 0 = int + emitter.instruction("je __rt_vd_val_int_x86"); // render the integer + emitter.instruction("cmp rax, 1"); // tag 1 = string + emitter.instruction("je __rt_vd_val_str_x86"); // render the string + emitter.instruction("cmp rax, 2"); // tag 2 = float + emitter.instruction("je __rt_vd_val_flt_x86"); // render the float + emitter.instruction("cmp rax, 3"); // tag 3 = bool + emitter.instruction("je __rt_vd_val_bool_x86"); // render the bool + emitter.instruction("cmp rax, 4"); // tag 4 = indexed array + emitter.instruction("je __rt_vd_val_arr_x86"); // recurse into the indexed walker + emitter.instruction("cmp rax, 5"); // tag 5 = hash + emitter.instruction("je __rt_vd_val_hash_x86"); // recurse into the hash walker + emitter.instruction("jmp __rt_vd_val_null_x86"); // tag 6 object / 8 null → NULL line + + emitter.label("__rt_vd_val_int_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // indent → spaces helper + emitter.instruction("call __rt_var_dump_spaces"); // pad the indent + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the integer payload + emitter.instruction("call __rt_var_dump_emit_int_line"); // emit `int(VAL)\n` + emitter.instruction("jmp __rt_vd_val_done_x86"); // value rendered + + emitter.label("__rt_vd_val_str_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // indent → spaces helper + emitter.instruction("call __rt_var_dump_spaces"); // pad the indent + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the string ptr + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload the string len + emitter.instruction("call __rt_var_dump_emit_string_line"); // emit `string(LEN) "VAL"\n` + emitter.instruction("jmp __rt_vd_val_done_x86"); // value rendered + + emitter.label("__rt_vd_val_flt_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // indent → spaces helper + emitter.instruction("call __rt_var_dump_spaces"); // pad the indent + emitter.instruction("movsd xmm0, QWORD PTR [rbp - 8]"); // reload the float bit pattern + emitter.instruction("call __rt_var_dump_emit_float_line"); // emit `float(VAL)\n` + emitter.instruction("jmp __rt_vd_val_done_x86"); // value rendered + + emitter.label("__rt_vd_val_bool_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // indent → spaces helper + emitter.instruction("call __rt_var_dump_spaces"); // pad the indent + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the bool payload + emitter.instruction("call __rt_var_dump_emit_bool_line"); // emit `bool(true|false)\n` + emitter.instruction("jmp __rt_vd_val_done_x86"); // value rendered + + emitter.label("__rt_vd_val_arr_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the indexed-array pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // reload the indent → entry base + emitter.instruction("call __rt_var_dump_indexed"); // recurse into the indexed walker (writes its own header indent) + emitter.instruction("jmp __rt_vd_val_done_x86"); // value rendered + + emitter.label("__rt_vd_val_hash_x86"); + // -- emit `array(N) {\n` header -- + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // indent → spaces helper + emitter.instruction("call __rt_var_dump_spaces"); // pad the indent for the array header + abi::emit_symbol_address(emitter, "rsi", "_vd_array_open"); // load the `array(` literal + emitter.instruction("mov edx, 6"); // len("array(") = 6 + emitter.instruction("mov edi, 1"); // fd = stdout + emitter.instruction("mov eax, 1"); // sys_write + emitter.instruction("syscall"); // write `array(` + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the hash pointer + emitter.instruction("call __rt_hash_count"); // rax = number of entries + emitter.instruction("call __rt_itoa"); // rax=digits ptr, rdx=digits len + emitter.instruction("mov rsi, rax"); // digits ptr → write buffer + emitter.instruction("mov edi, 1"); // fd = stdout + emitter.instruction("mov eax, 1"); // sys_write + emitter.instruction("syscall"); // write the count digits + abi::emit_symbol_address(emitter, "rsi", "_vd_array_close_brace"); // load the `) {\n` literal + emitter.instruction("mov edx, 4"); // len(") {\n") = 4 + emitter.instruction("mov edi, 1"); // fd = stdout + emitter.instruction("mov eax, 1"); // sys_write + emitter.instruction("syscall"); // write `) {\n` + // -- emit the hash entries (body) -- + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the hash pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // reload the indent → entry base + emitter.instruction("call __rt_var_dump_hash"); // walk the hash entries + // -- emit `}\n` footer -- + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // indent → spaces helper + emitter.instruction("call __rt_var_dump_spaces"); // pad the indent for the closing brace + abi::emit_symbol_address(emitter, "rsi", "_vd_close_brace_nl"); // load the `}\n` literal + emitter.instruction("mov edx, 2"); // len("}\n") = 2 + emitter.instruction("mov edi, 1"); // fd = stdout + emitter.instruction("mov eax, 1"); // sys_write + emitter.instruction("syscall"); // write `}\n` + emitter.instruction("jmp __rt_vd_val_done_x86"); // value rendered + + emitter.label("__rt_vd_val_mixed_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // boxed Mixed cell pointer → RAX + emitter.instruction("call __rt_mixed_unbox"); // rax=inner tag, rdi=lo, rdx=hi + emitter.instruction("mov rsi, rdi"); // unboxed lo → value low argument + emitter.instruction("mov rdi, rax"); // unboxed tag → value tag argument + emitter.instruction("mov rcx, QWORD PTR [rbp - 24]"); // reload the indent + emitter.instruction("call __rt_var_dump_value"); // redispatch the unboxed value + emitter.instruction("jmp __rt_vd_val_done_x86"); // value rendered + + emitter.label("__rt_vd_val_null_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // indent → spaces helper + emitter.instruction("call __rt_var_dump_spaces"); // pad the indent + emitter.instruction("call __rt_var_dump_emit_null_line"); // emit `NULL\n` + + emitter.label("__rt_vd_val_done_x86"); + emitter.instruction("add rsp, 48"); // release the value frame + emitter.instruction("pop rbp"); // restore caller frame pointer + emitter.instruction("ret"); // return to caller +} + +/// `__rt_var_dump_indexed`: render an indexed array body `array(N) {\n ... +/// }\n` for the recursive value renderer, emitting the `array(N) {` header and +/// closing `}` at `indent` and each entry's key/value at `indent + 2`. The +/// array self-dispatches each element on its value_type stamp and recurses +/// through `__rt_var_dump_value` so nested containers render fully. +/// Input: AArch64 x0=arr x1=indent / x86_64 rdi=arr rsi=indent. +pub fn emit_var_dump_indexed(emitter: &mut Emitter) { + if emitter.target.arch == Arch::X86_64 { + emit_var_dump_indexed_linux_x86_64(emitter); + return; + } + + emitter.blank(); + emitter.comment("--- runtime: var_dump_indexed ---"); + emitter.label_global("__rt_var_dump_indexed"); + + // Frame (64 bytes): [0]arr [8]indent [16]entry_indent [24]count + // [32]index [40]stamp [48]x29 [56]x30. + emitter.instruction("sub sp, sp, #64"); // allocate the indexed-walk frame + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address + emitter.instruction("add x29, sp, #48"); // establish the walk frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the array pointer + emitter.instruction("str x1, [sp, #8]"); // save the indent + emitter.instruction("add x9, x1, #2"); // entry indent = indent + 2 + emitter.instruction("str x9, [sp, #16]"); // save the entry indent + emitter.instruction("ldr x10, [x0]"); // load the element count from the header + emitter.instruction("str x10, [sp, #24]"); // save the element count + emitter.instruction("str xzr, [sp, #32]"); // index = 0 + emitter.instruction("ldr x11, [x0, #-8]"); // load the packed array kind word + emitter.instruction("lsr x11, x11, #8"); // shift the value_type stamp into the low byte + emitter.instruction("and x11, x11, #0x0f"); // isolate the value_type field (low nibble), dropping the COW bit + emitter.instruction("str x11, [sp, #40]"); // save the element value_type stamp + + // -- emit `array(N) {\n` -- + emitter.instruction("ldr x0, [sp, #8]"); // indent → spaces helper + emitter.instruction("bl __rt_var_dump_spaces"); // pad the indent for the header + abi::emit_symbol_address(emitter, "x1", "_vd_array_open"); // load the `array(` literal + emitter.instruction("mov x2, #6"); // len("array(") = 6 + emitter.instruction("mov x0, #1"); // fd = stdout + emitter.syscall(4); // write `array(` + emitter.instruction("ldr x0, [sp, #24]"); // reload the element count + emitter.instruction("bl __rt_itoa"); // x1=digits ptr, x2=digits len + emitter.instruction("mov x0, #1"); // fd = stdout + emitter.syscall(4); // write the count digits + abi::emit_symbol_address(emitter, "x1", "_vd_array_close_brace"); // load the `) {\n` literal + emitter.instruction("mov x2, #4"); // len(") {\n") = 4 + emitter.instruction("mov x0, #1"); // fd = stdout + emitter.syscall(4); // write `) {\n` + + emitter.label("__rt_vd_idx_loop"); + emitter.instruction("ldr x9, [sp, #32]"); // reload the current index + emitter.instruction("ldr x10, [sp, #24]"); // reload the element count + emitter.instruction("cmp x9, x10"); // processed every element? + emitter.instruction("b.ge __rt_vd_idx_done"); // walk complete + + // -- emit `[i]=>\n` -- + emitter.instruction("ldr x0, [sp, #16]"); // entry indent → spaces helper + emitter.instruction("bl __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("ldr x11, [sp, #32]"); // reload the current index → key helper's x11 + emitter.instruction("bl __rt_var_dump_emit_indexed_key"); // write `[i]=>\n` + + // -- render the element via __rt_var_dump_value -- + emitter.instruction("ldr x12, [sp, #40]"); // reload the element stamp + emitter.instruction("ldr x13, [sp, #0]"); // reload the array pointer + emitter.instruction("ldr x14, [sp, #32]"); // reload the current index + emitter.instruction("cmp x12, #1"); // string elements use a 16-byte stride + emitter.instruction("b.eq __rt_vd_idx_str"); // handle string elements + emitter.instruction("cmp x12, #7"); // mixed elements are boxed cells + emitter.instruction("b.eq __rt_vd_idx_mixed"); // handle mixed cells + + // 8-byte-stride elements: int(0) / float(2) / bool(3) / array(4) / hash(5) / object(6). + emitter.instruction("add x15, x14, #3"); // skip the 24-byte (3-quad) header + emitter.instruction("ldr x1, [x13, x15, lsl #3]"); // load the raw element word → value low + emitter.instruction("mov x0, x12"); // tag = the array stamp + emitter.instruction("mov x2, #0"); // high word unused for 8-byte elements + emitter.instruction("ldr x3, [sp, #16]"); // entry indent → value renderer indent + emitter.instruction("bl __rt_var_dump_value"); // render the element + emitter.instruction("b __rt_vd_idx_next"); // advance to the next element + + emitter.label("__rt_vd_idx_str"); + emitter.instruction("lsl x15, x14, #4"); // index * 16 + emitter.instruction("add x15, x15, #24"); // element base offset = 24 + index*16 + emitter.instruction("add x15, x13, x15"); // element address + emitter.instruction("ldr x1, [x15]"); // string ptr → value low + emitter.instruction("ldr x2, [x15, #8]"); // string len → value high + emitter.instruction("mov x0, #1"); // tag = string + emitter.instruction("ldr x3, [sp, #16]"); // entry indent → value renderer indent + emitter.instruction("bl __rt_var_dump_value"); // render the element + emitter.instruction("b __rt_vd_idx_next"); // advance to the next element + + emitter.label("__rt_vd_idx_mixed"); + emitter.instruction("add x15, x14, #3"); // skip the 24-byte (3-quad) header + emitter.instruction("ldr x15, [x13, x15, lsl #3]"); // load the Mixed cell pointer + emitter.instruction("ldr x0, [x15]"); // cell tag → value tag + emitter.instruction("ldr x1, [x15, #8]"); // cell low word → value low + emitter.instruction("ldr x2, [x15, #16]"); // cell high word → value high + emitter.instruction("ldr x3, [sp, #16]"); // entry indent → value renderer indent + emitter.instruction("bl __rt_var_dump_value"); // render the element + + emitter.label("__rt_vd_idx_next"); + emitter.instruction("ldr x9, [sp, #32]"); // reload the index + emitter.instruction("add x9, x9, #1"); // advance the index + emitter.instruction("str x9, [sp, #32]"); // save the updated index + emitter.instruction("b __rt_vd_idx_loop"); // continue scanning + + emitter.label("__rt_vd_idx_done"); + // -- emit `}\n` -- + emitter.instruction("ldr x0, [sp, #8]"); // indent → spaces helper + emitter.instruction("bl __rt_var_dump_spaces"); // pad the indent for the closing brace + abi::emit_symbol_address(emitter, "x1", "_vd_close_brace_nl"); // load the `}\n` literal + emitter.instruction("mov x2, #2"); // len("}\n") = 2 + emitter.instruction("mov x0, #1"); // fd = stdout + emitter.syscall(4); // write `}\n` + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // release the indexed-walk frame + emitter.instruction("ret"); // return to caller +} + +/// Emits the Linux x86_64 indexed-array recursive walker for var_dump. +fn emit_var_dump_indexed_linux_x86_64(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: var_dump_indexed ---"); + emitter.label_global("__rt_var_dump_indexed"); + + // rbp-relative frame: [-8]arr [-16]indent [-24]entry_indent [-32]count + // [-40]index [-48]stamp. + emitter.instruction("push rbp"); // save caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish the walk frame pointer + emitter.instruction("sub rsp, 64"); // allocate the indexed-walk frame + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the array pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the indent + emitter.instruction("mov rax, rsi"); // copy the indent + emitter.instruction("add rax, 2"); // entry indent = indent + 2 + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the entry indent + emitter.instruction("mov rax, QWORD PTR [rdi]"); // load the element count from the header + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // save the element count + emitter.instruction("mov QWORD PTR [rbp - 40], 0"); // index = 0 + emitter.instruction("mov rax, QWORD PTR [rdi - 8]"); // load the packed array kind word + emitter.instruction("shr rax, 8"); // shift the value_type stamp into the low byte + emitter.instruction("and rax, 0x0f"); // isolate the value_type field (low nibble), dropping the COW bit + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the element value_type stamp + + // -- emit `array(N) {\n` -- + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // indent → spaces helper + emitter.instruction("call __rt_var_dump_spaces"); // pad the indent for the header + abi::emit_symbol_address(emitter, "rsi", "_vd_array_open"); // load the `array(` literal + emitter.instruction("mov edx, 6"); // len("array(") = 6 + emitter.instruction("mov edi, 1"); // fd = stdout + emitter.instruction("mov eax, 1"); // sys_write + emitter.instruction("syscall"); // write `array(` + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the element count + emitter.instruction("call __rt_itoa"); // rax=digits ptr, rdx=digits len + emitter.instruction("mov rsi, rax"); // digits ptr → write buffer + emitter.instruction("mov edi, 1"); // fd = stdout + emitter.instruction("mov eax, 1"); // sys_write + emitter.instruction("syscall"); // write the count digits + abi::emit_symbol_address(emitter, "rsi", "_vd_array_close_brace"); // load the `) {\n` literal + emitter.instruction("mov edx, 4"); // len(") {\n") = 4 + emitter.instruction("mov edi, 1"); // fd = stdout + emitter.instruction("mov eax, 1"); // sys_write + emitter.instruction("syscall"); // write `) {\n` + + emitter.label("__rt_vd_idx_loop_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the current index + emitter.instruction("mov rcx, QWORD PTR [rbp - 32]"); // reload the element count + emitter.instruction("cmp rax, rcx"); // processed every element? + emitter.instruction("jge __rt_vd_idx_done_x86"); // walk complete + + // -- emit `[i]=>\n` -- + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // entry indent → spaces helper + emitter.instruction("call __rt_var_dump_spaces"); // pad the entry indent + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // reload the current index → key helper's rdi + emitter.instruction("call __rt_var_dump_emit_indexed_key"); // write `[i]=>\n` + + // -- render the element via __rt_var_dump_value -- + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the element stamp + emitter.instruction("mov r9, QWORD PTR [rbp - 8]"); // reload the array pointer + emitter.instruction("mov r11, QWORD PTR [rbp - 40]"); // reload the current index + emitter.instruction("cmp r10, 1"); // string elements use a 16-byte stride + emitter.instruction("je __rt_vd_idx_str_x86"); // handle string elements + emitter.instruction("cmp r10, 7"); // mixed elements are boxed cells + emitter.instruction("je __rt_vd_idx_mixed_x86"); // handle mixed cells + + // 8-byte-stride elements: int(0) / float(2) / bool(3) / array(4) / hash(5) / object(6). + emitter.instruction("mov rax, r11"); // copy the index + emitter.instruction("add rax, 3"); // skip the 24-byte (3-quad) header + emitter.instruction("mov rsi, QWORD PTR [r9 + rax * 8]"); // load the raw element word → value low + emitter.instruction("mov rdi, r10"); // tag = the array stamp + emitter.instruction("mov rdx, 0"); // high word unused for 8-byte elements + emitter.instruction("mov rcx, QWORD PTR [rbp - 24]"); // entry indent → value renderer indent + emitter.instruction("call __rt_var_dump_value"); // render the element + emitter.instruction("jmp __rt_vd_idx_next_x86"); // advance to the next element + + emitter.label("__rt_vd_idx_str_x86"); + emitter.instruction("mov rax, r11"); // copy the index + emitter.instruction("shl rax, 4"); // index * 16 + emitter.instruction("add rax, 24"); // element base offset = 24 + index*16 + emitter.instruction("add rax, r9"); // element address + emitter.instruction("mov rsi, QWORD PTR [rax]"); // string ptr → value low + emitter.instruction("mov rdx, QWORD PTR [rax + 8]"); // string len → value high + emitter.instruction("mov rdi, 1"); // tag = string + emitter.instruction("mov rcx, QWORD PTR [rbp - 24]"); // entry indent → value renderer indent + emitter.instruction("call __rt_var_dump_value"); // render the element + emitter.instruction("jmp __rt_vd_idx_next_x86"); // advance to the next element + + emitter.label("__rt_vd_idx_mixed_x86"); + emitter.instruction("mov rax, r11"); // copy the index + emitter.instruction("add rax, 3"); // skip the 24-byte (3-quad) header + emitter.instruction("mov rax, QWORD PTR [r9 + rax * 8]"); // load the Mixed cell pointer + emitter.instruction("mov rdi, QWORD PTR [rax]"); // cell tag → value tag + emitter.instruction("mov rsi, QWORD PTR [rax + 8]"); // cell low word → value low + emitter.instruction("mov rdx, QWORD PTR [rax + 16]"); // cell high word → value high + emitter.instruction("mov rcx, QWORD PTR [rbp - 24]"); // entry indent → value renderer indent + emitter.instruction("call __rt_var_dump_value"); // render the element + + emitter.label("__rt_vd_idx_next_x86"); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the index + emitter.instruction("add rax, 1"); // advance the index + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the updated index + emitter.instruction("jmp __rt_vd_idx_loop_x86"); // continue scanning + + emitter.label("__rt_vd_idx_done_x86"); + // -- emit `}\n` -- + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // indent → spaces helper + emitter.instruction("call __rt_var_dump_spaces"); // pad the indent for the closing brace + abi::emit_symbol_address(emitter, "rsi", "_vd_close_brace_nl"); // load the `}\n` literal + emitter.instruction("mov edx, 2"); // len("}\n") = 2 + emitter.instruction("mov edi, 1"); // fd = stdout + emitter.instruction("mov eax, 1"); // sys_write + emitter.instruction("syscall"); // write `}\n` + emitter.instruction("add rsp, 64"); // release the indexed-walk frame + emitter.instruction("pop rbp"); // restore caller frame pointer + emitter.instruction("ret"); // return to caller +} diff --git a/src/codegen_ir/lower_inst/builtins/debug.rs b/src/codegen_ir/lower_inst/builtins/debug.rs index 00b48b6ed8..c1f5d10859 100644 --- a/src/codegen_ir/lower_inst/builtins/debug.rs +++ b/src/codegen_ir/lower_inst/builtins/debug.rs @@ -366,7 +366,17 @@ fn emit_var_dump_null(ctx: &mut FunctionContext<'_>) { } /// Emits `var_dump` output for an array/hash payload in the integer result register. +/// Homogeneous typed arrays use a per-type fast-path walker; `Array(Mixed)`, +/// `AssocArray`, and heterogeneous containers route through the recursive +/// `__rt_var_dump_value` renderer so nested arrays print fully. fn emit_var_dump_array(ctx: &mut FunctionContext<'_>, ty: &PhpType) -> Result<()> { + // Recursive containers (Mixed elements, hashes, unions) go through the + // single-value renderer, which emits the `array(N) {\n ... }\n` body itself. + if var_dump_uses_recursive_renderer(ty) { + emit_var_dump_recursive_array(ctx, ty)?; + return Ok(()); + } + let result_reg = abi::int_result_reg(ctx.emitter); abi::emit_push_reg(ctx.emitter, result_reg); emit_write_literal(ctx, b"array("); @@ -394,12 +404,55 @@ fn emit_var_dump_array(ctx: &mut FunctionContext<'_>, ty: &PhpType) -> Result<() Ok(()) } -/// Returns the runtime var_dump walker for an array/hash element layout. -/// -/// Homogeneous indexed arrays use a per-element-type walker; `Array(Mixed)` uses the -/// boxed-cell walker; associative arrays (hashes) use `__rt_var_dump_hash`, which iterates -/// entries and formats string/integer keys plus scalar values (nested containers fall back -/// to `NULL`, matching the indexed Mixed walker). +/// Routes a heterogeneous container through the recursive `__rt_var_dump_value` +/// renderer with tag and indent 0, so nested arrays/hashes print fully. The +/// renderer emits the `array(N) {\n ... }\n` body itself. +fn emit_var_dump_recursive_array(ctx: &mut FunctionContext<'_>, ty: &PhpType) -> Result<()> { + let tag = var_dump_recursive_tag(ty); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x1, x0"); // container pointer → value low argument + ctx.emitter.instruction(&format!("mov x0, #{}", tag)); // runtime tag for the value renderer + ctx.emitter.instruction("mov x2, #0"); // high word unused for containers + ctx.emitter.instruction("mov x3, #0"); // top-level indent = 0 + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rsi, rax"); // container pointer → value low argument + ctx.emitter.instruction(&format!("mov edi, {}", tag)); // runtime tag for the value renderer + ctx.emitter.instruction("mov edx, 0"); // high word unused for containers + ctx.emitter.instruction("mov ecx, 0"); // top-level indent = 0 + } + } + abi::emit_call_label(ctx.emitter, "__rt_var_dump_value"); + Ok(()) +} + +/// Returns the runtime tag to feed `__rt_var_dump_value` for a recursive container type. +fn var_dump_recursive_tag(ty: &PhpType) -> u64 { + match ty { + PhpType::AssocArray { .. } => 5, + PhpType::Array(_) => 4, + _ => 7, + } +} + +/// Returns true when the container type must route through the recursive +/// `__rt_var_dump_value` renderer (because it may hold nested arrays/hashes +/// that the flat per-type walkers cannot format). +fn var_dump_uses_recursive_renderer(ty: &PhpType) -> bool { + match ty { + PhpType::AssocArray { .. } => true, + PhpType::Array(elem_ty) => matches!( + elem_ty.as_ref(), + PhpType::Mixed | PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Union(_) + ), + _ => false, + } +} + +/// Returns the runtime var_dump walker for a homogeneous typed array. +/// `Array(Mixed)` and `AssocArray` are handled by the recursive renderer +/// (`emit_var_dump_recursive_array`) instead, so they return `None` here. fn var_dump_array_walker(ty: &PhpType) -> Option<&'static str> { match ty { PhpType::Array(elem_ty) => match elem_ty.as_ref() { @@ -407,10 +460,8 @@ fn var_dump_array_walker(ty: &PhpType) -> Option<&'static str> { PhpType::Str => Some("__rt_var_dump_array_str"), PhpType::Bool => Some("__rt_var_dump_array_bool"), PhpType::Float => Some("__rt_var_dump_array_float"), - PhpType::Mixed => Some("__rt_var_dump_array_mixed"), _ => None, }, - PhpType::AssocArray { .. } => Some("__rt_var_dump_hash"), _ => None, } } diff --git a/src/codegen_ir/lower_inst/comparisons.rs b/src/codegen_ir/lower_inst/comparisons.rs index 2b5bf23261..a1f7221fc1 100644 --- a/src/codegen_ir/lower_inst/comparisons.rs +++ b/src/codegen_ir/lower_inst/comparisons.rs @@ -88,6 +88,14 @@ pub(super) fn lower_strict_eq( emit_bool_literal(ctx, !is_equal); return store_if_result(ctx, inst); } + if matches!(lhs_ty, PhpType::Array(_)) { + emit_array_strict_eq_call(ctx, lhs, rhs, is_equal)?; + return store_if_result(ctx, inst); + } + if matches!(lhs_ty, PhpType::AssocArray { .. }) { + emit_hash_strict_eq_call(ctx, lhs, rhs, is_equal)?; + return store_if_result(ctx, inst); + } match lhs_ty { PhpType::Int | PhpType::Bool | PhpType::Void | PhpType::Never => { emit_intish_compare(ctx, lhs, rhs, is_equal, false)?; @@ -872,3 +880,70 @@ fn equality_cond(is_equal: bool, arch: Arch) -> &'static str { (false, Arch::X86_64) => "ne", } } + +/// Loads both indexed-array operands and calls `__rt_array_strict_eq`, then inverts the +/// result for `!==`. Array operands materialize as single heap pointers in the integer +/// result register, so loading into the call argument registers is straightforward. +fn emit_array_strict_eq_call( + ctx: &mut FunctionContext<'_>, + lhs: ValueId, + rhs: ValueId, + is_equal: bool, +) -> Result<()> { + let lhs_reg = abi::secondary_scratch_reg(ctx.emitter); + let rhs_reg = abi::tertiary_scratch_reg(ctx.emitter); + ctx.load_value_to_reg(lhs, lhs_reg)?; + ctx.load_value_to_reg(rhs, rhs_reg)?; + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("mov x0, {}", lhs_reg)); // move the left indexed-array pointer into the first helper argument + ctx.emitter.instruction(&format!("mov x1, {}", rhs_reg)); // move the right indexed-array pointer into the second helper argument + abi::emit_call_label(ctx.emitter, "__rt_array_strict_eq"); + if !is_equal { + ctx.emitter.instruction("eor x0, x0, #1"); // invert the array strict-equality result for !== + } + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!("mov rdi, {}", lhs_reg)); // move the left indexed-array pointer into the first helper argument + ctx.emitter.instruction(&format!("mov rsi, {}", rhs_reg)); // move the right indexed-array pointer into the second helper argument + abi::emit_call_label(ctx.emitter, "__rt_array_strict_eq"); + if !is_equal { + ctx.emitter.instruction("xor rax, 1"); // invert the array strict-equality result for !== + } + } + } + Ok(()) +} + +/// Loads both associative-array (hash) operands and calls `__rt_hash_strict_eq`, then +/// inverts the result for `!==`. Hash operands materialize as single heap pointers. +fn emit_hash_strict_eq_call( + ctx: &mut FunctionContext<'_>, + lhs: ValueId, + rhs: ValueId, + is_equal: bool, +) -> Result<()> { + let lhs_reg = abi::secondary_scratch_reg(ctx.emitter); + let rhs_reg = abi::tertiary_scratch_reg(ctx.emitter); + ctx.load_value_to_reg(lhs, lhs_reg)?; + ctx.load_value_to_reg(rhs, rhs_reg)?; + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("mov x0, {}", lhs_reg)); // move the left hash pointer into the first helper argument + ctx.emitter.instruction(&format!("mov x1, {}", rhs_reg)); // move the right hash pointer into the second helper argument + abi::emit_call_label(ctx.emitter, "__rt_hash_strict_eq"); + if !is_equal { + ctx.emitter.instruction("eor x0, x0, #1"); // invert the hash strict-equality result for !== + } + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!("mov rdi, {}", lhs_reg)); // move the left hash pointer into the first helper argument + ctx.emitter.instruction(&format!("mov rsi, {}", rhs_reg)); // move the right hash pointer into the second helper argument + abi::emit_call_label(ctx.emitter, "__rt_hash_strict_eq"); + if !is_equal { + ctx.emitter.instruction("xor rax, 1"); // invert the hash strict-equality result for !== + } + } + } + Ok(()) +} diff --git a/tests/codegen/io/printing.rs b/tests/codegen/io/printing.rs index 458c769c8e..d8404f797b 100644 --- a/tests/codegen/io/printing.rs +++ b/tests/codegen/io/printing.rs @@ -126,7 +126,7 @@ var_dump($map["o"]); ); assert_eq!( out, - "int(42)\nstring(5) \"hello\"\nbool(true)\nNULL\narray(2) {\n}\nobject(Box)\n" + "int(42)\nstring(5) \"hello\"\nbool(true)\nNULL\narray(2) {\n [0]=>\n int(1)\n [1]=>\n int(2)\n}\nobject(Box)\n" ); } @@ -282,6 +282,39 @@ var_dump([1, "x", 2.5]); ); } +/// Regression for issue #388: `var_dump` recurses into nested indexed arrays +/// instead of printing `NULL`. Output matches PHP's 2-space-per-level layout. +#[test] +fn test_var_dump_nested_indexed_array() { + let out = compile_and_run(r#"\n int(1)\n [1]=>\n array(2) {\n [0]=>\n int(2)\n [1]=>\n int(3)\n }\n}\n" + ); +} + +/// Regression for issue #388: `var_dump` recurses into nested hashes (assoc +/// arrays) inside a hash, with correct indentation and key formatting. +#[test] +fn test_var_dump_nested_hash_value() { + let out = compile_and_run(r#" [1, 2]]);"#); + assert_eq!( + out, + "array(1) {\n [\"a\"]=>\n array(2) {\n [0]=>\n int(1)\n [1]=>\n int(2)\n }\n}\n" + ); +} + +/// Regression for issue #388: `var_dump` recurses into deeply nested arrays +/// (three levels), verifying the indent accumulates by 2 spaces per level. +#[test] +fn test_var_dump_deeply_nested_array() { + let out = compile_and_run(r#"\n array(1) {\n [0]=>\n array(1) {\n [0]=>\n int(1)\n }\n }\n}\n" + ); +} + /// `var_export` renders scalars the way PHP does: bare integers, `'…'`-quoted strings with /// `\\`/`\'` escaping, `true`/`false`, `NULL`, and an integer-valued float gaining a `.0`. #[test] diff --git a/tests/codegen/regressions.rs b/tests/codegen/regressions.rs index 1dafe5e473..888b4f3923 100644 --- a/tests/codegen/regressions.rs +++ b/tests/codegen/regressions.rs @@ -33,3 +33,5 @@ mod mixed_method_dispatch; mod switch_and_float_params; #[path = "regressions/return_this_ownership.rs"] mod return_this_ownership; +#[path = "regressions/array_equality.rs"] +mod array_equality; diff --git a/tests/codegen/regressions/array_equality.rs b/tests/codegen/regressions/array_equality.rs new file mode 100644 index 0000000000..4bdfef9011 --- /dev/null +++ b/tests/codegen/regressions/array_equality.rs @@ -0,0 +1,127 @@ +//! Purpose: +//! Regression tests for issue #424: array/hash strict equality (===) was +//! unsupported by the EIR backend, causing a compile error for any array +//! or associative-array operand to `===` / `!==`. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Strict equality compares same key/value pairs in the same order with +//! identical types (no juggling). Loose equality (`==`) is deferred. + +use crate::support::compile_and_run; + +#[test] +fn test_indexed_array_strict_eq_true() { + let out = compile_and_run(r#" 1, "b" => 2] === ["a" => 1, "b" => 2]); +"#); + assert_eq!(out, "bool(true)\n"); +} + +#[test] +fn test_hash_strict_eq_false_different_order() { + let out = compile_and_run(r#" 1, "b" => 2] === ["b" => 2, "a" => 1]); +"#); + assert_eq!(out, "bool(false)\n"); +} + +#[test] +fn test_hash_strict_eq_string_values_true() { + let out = compile_and_run(r#" "hello"] === ["x" => "hello"]); +"#); + assert_eq!(out, "bool(true)\n"); +} + +#[test] +fn test_hash_strict_eq_string_values_false() { + let out = compile_and_run(r#" "hello"] === ["x" => "world"]); +"#); + assert_eq!(out, "bool(false)\n"); +} + +#[test] +fn test_hash_strict_not_eq() { + let out = compile_and_run(r#" 1] !== ["a" => 2]); +"#); + assert_eq!(out, "bool(true)\n"); +} + +#[test] +fn test_empty_hash_strict_eq() { + let out = compile_and_run(r#" 1, "b" => 2] === ["a" => 1, "b" => 2, "c" => 3]); +"#); + assert_eq!(out, "bool(false)\n"); +} \ No newline at end of file diff --git a/tests/codegen/regressions/builtins_misc.rs b/tests/codegen/regressions/builtins_misc.rs index 53911f4a2a..b481caf8e0 100644 --- a/tests/codegen/regressions/builtins_misc.rs +++ b/tests/codegen/regressions/builtins_misc.rs @@ -61,15 +61,15 @@ fn test_var_dump_hash_heterogeneous_values() { ); } -/// Regression: a nested array value inside a hash falls back to `NULL` (the -/// same limitation as the indexed Mixed walker) instead of crashing or -/// emitting garbage. The surrounding scalar entries still format correctly. +/// Regression: a nested array value inside a hash now recurses fully (issue #388) +/// instead of falling back to `NULL`. The surrounding scalar entries still format +/// correctly, and the nested array body indents with 2 spaces per level. #[test] fn test_var_dump_hash_nested_value_falls_back_to_null() { let out = compile_and_run(r#" 5, "inner" => [1, 2], "y" => 7]);"#); assert_eq!( out, - "array(3) {\n [\"x\"]=>\n int(5)\n [\"inner\"]=>\n NULL\n [\"y\"]=>\n int(7)\n}\n" + "array(3) {\n [\"x\"]=>\n int(5)\n [\"inner\"]=>\n array(2) {\n [0]=>\n int(1)\n [1]=>\n int(2)\n }\n [\"y\"]=>\n int(7)\n}\n" ); } diff --git a/tests/codegen/types/iterable/builtins_and_casts.rs b/tests/codegen/types/iterable/builtins_and_casts.rs index 0bcc387b48..66b8614e28 100644 --- a/tests/codegen/types/iterable/builtins_and_casts.rs +++ b/tests/codegen/types/iterable/builtins_and_casts.rs @@ -215,7 +215,9 @@ fn test_iterable_boxes_to_mixed_with_concrete_array_tag() { var_dump(box([10, 20])); ", ); - assert_eq!(out, "y|array|array(2) {\n}\n"); + // The boxed Mixed value is the indexed array [10, 20]; var_dump now recurses + // after unboxing instead of printing the empty `array(2) {}` shell. + assert_eq!(out, "y|array|array(2) {\n [0]=>\n int(10)\n [1]=>\n int(20)\n}\n"); } /// Verifies `empty()` on an `iterable` uses the underlying array length: empty array is "empty", diff --git a/tests/codegen/types/iterable/foreach.rs b/tests/codegen/types/iterable/foreach.rs index 034c6282fe..89fb893e1b 100644 --- a/tests/codegen/types/iterable/foreach.rs +++ b/tests/codegen/types/iterable/foreach.rs @@ -527,7 +527,9 @@ fn test_iterable_value_in_indexed_array_stays_boxed() { show([id([1, 2])]); ", ); - assert_eq!(out, "array:array(2) {\n}\n"); + // The boxed Mixed value is the indexed array [1, 2]; var_dump now recurses + // after unboxing instead of printing the empty `array(2) {}` shell. + assert_eq!(out, "array:array(2) {\n [0]=>\n int(1)\n [1]=>\n int(2)\n}\n"); } /// Verifies that a `mixed` iterable containing an inner associative array preserves @@ -547,7 +549,9 @@ fn test_iterable_value_in_assoc_array_stays_boxed() { } ", ); - assert_eq!(out, "array:array(2) {\n}\n"); + // The boxed Mixed value is the indexed array [1, 2]; var_dump now recurses + // after unboxing instead of printing the empty `array(2) {}` shell. + assert_eq!(out, "array:array(2) {\n [0]=>\n int(1)\n [1]=>\n int(2)\n}\n"); } /// Verifies an `iterable` value stored in a mixed associative array remains a boxed Mixed value @@ -566,7 +570,9 @@ fn test_iterable_value_in_mixed_assoc_array_direct_read_stays_boxed() { var_dump($value); ", ); - assert_eq!(out, "array:array(2) {\n}\n"); + // The boxed Mixed value is the indexed array [1, 2]; var_dump now recurses + // after unboxing instead of printing the empty `array(2) {}` shell. + assert_eq!(out, "array:array(2) {\n [0]=>\n int(1)\n [1]=>\n int(2)\n}\n"); } /// Verifies an inner `iterable` array appended to a plain array stays boxed and `is_iterable()` is diff --git a/tests/ir_backend_smoke_test.rs b/tests/ir_backend_smoke_test.rs index 563f18cac0..e24044ab33 100644 --- a/tests/ir_backend_smoke_test.rs +++ b/tests/ir_backend_smoke_test.rs @@ -1689,7 +1689,7 @@ echo "]"; "#; assert_eq!( compile_and_run_ir_backend("mixed_assoc_array_slots", source), - "int(42)\nstring(5) \"hello\"\nbool(true)\nNULL\narray(2) {\n}\n[hello|]" + "int(42)\nstring(5) \"hello\"\nbool(true)\nNULL\narray(2) {\n [0]=>\n int(1)\n [1]=>\n int(2)\n}\n[hello|]" ); }