From 4b8cd21fc6e309062868b571e7697b7b0994fa4c Mon Sep 17 00:00:00 2001 From: Ralph Kuepper Date: Fri, 11 Sep 2026 15:00:50 +0200 Subject: [PATCH 1/9] fix(string): preserve UTF-16 slices and scalarize suffix parsing (cherry picked from commit 0c5348ce5694654d8aa6e4477900ebc9fffe67de) --- benchmarks/string_slice/.gitignore | 4 + benchmarks/string_slice/README.md | 79 +++++++ .../string_slice/baseline-artifacts.json | 5 + benchmarks/string_slice/baseline.json | 176 +++++++++++++++ benchmarks/string_slice/measure.py | 70 ++++++ .../string_slice/public-baseline-check.json | 6 + .../string_slice/string-slice-astral.ts | 10 + .../string-slice-parse-loop-ascii.ts | 93 ++++++++ .../string-slice-parse-loop-unicode.ts | 93 ++++++++ changelog.d/10061-string-slice.md | 9 + crates/perry-codegen/src/codegen/closure.rs | 2 + crates/perry-codegen/src/codegen/entry.rs | 4 + crates/perry-codegen/src/codegen/function.rs | 2 + crates/perry-codegen/src/codegen/method.rs | 4 + .../perry-codegen/src/collectors/hir_facts.rs | 7 + crates/perry-codegen/src/collectors/mod.rs | 1 + .../src/collectors/suffix_strings.rs | 211 ++++++++++++++++++ .../src/collectors/suffix_strings/tests.rs | 97 ++++++++ crates/perry-codegen/src/expr/dispatch.rs | 3 + crates/perry-codegen/src/expr/mod.rs | 5 + .../perry-codegen/src/expr/suffix_cursor.rs | 97 ++++++++ .../src/runtime_decls/strings.rs | 7 + crates/perry-codegen/src/stmt/mod.rs | 8 +- .../gc/tests/runtime_roots/string_slice.rs | 44 ++++ crates/perry-runtime/src/string/mod.rs | 4 + crates/perry-runtime/src/string/slice_ops.rs | 57 +---- .../perry-runtime/src/string/slice_range.rs | 78 +++++++ .../perry-runtime/src/string/slice_tests.rs | 127 +++++++++++ .../perry-runtime/src/string/suffix_cursor.rs | 116 ++++++++++ .../test_gap_gc_string_suffix_cursor.ts | 28 +++ .../test_gap_string_slice_utf16_suffix.ts | 59 +++++ test-parity/gc_repsel_corpus.txt | 2 + 32 files changed, 1454 insertions(+), 54 deletions(-) create mode 100644 benchmarks/string_slice/.gitignore create mode 100644 benchmarks/string_slice/README.md create mode 100644 benchmarks/string_slice/baseline-artifacts.json create mode 100644 benchmarks/string_slice/baseline.json create mode 100644 benchmarks/string_slice/measure.py create mode 100644 benchmarks/string_slice/public-baseline-check.json create mode 100644 benchmarks/string_slice/string-slice-astral.ts create mode 100644 benchmarks/string_slice/string-slice-parse-loop-ascii.ts create mode 100644 benchmarks/string_slice/string-slice-parse-loop-unicode.ts create mode 100644 changelog.d/10061-string-slice.md create mode 100644 crates/perry-codegen/src/collectors/suffix_strings.rs create mode 100644 crates/perry-codegen/src/collectors/suffix_strings/tests.rs create mode 100644 crates/perry-codegen/src/expr/suffix_cursor.rs create mode 100644 crates/perry-runtime/src/string/slice_range.rs create mode 100644 crates/perry-runtime/src/string/slice_tests.rs create mode 100644 crates/perry-runtime/src/string/suffix_cursor.rs create mode 100644 test-files/test_gap_gc_string_suffix_cursor.ts create mode 100644 test-files/test_gap_string_slice_utf16_suffix.ts diff --git a/benchmarks/string_slice/.gitignore b/benchmarks/string_slice/.gitignore new file mode 100644 index 0000000000..14c434901e --- /dev/null +++ b/benchmarks/string_slice/.gitignore @@ -0,0 +1,4 @@ +*.exe +string-slice-parse-loop-ascii +string-slice-parse-loop-unicode +__pycache__/ diff --git a/benchmarks/string_slice/README.md b/benchmarks/string_slice/README.md new file mode 100644 index 0000000000..518abff5ab --- /dev/null +++ b/benchmarks/string_slice/README.md @@ -0,0 +1,79 @@ +# String suffix parsing (#10061) + +The three TypeScript sources are copied unchanged from [issue #10061](https://github.com/PerryTS/perry/issues/10061). +`measure.py` runs each engine serially, with the issue's five input sizes and +60-second process timeout. The workload itself checks every warmup and measured +checksum, warms for at least 200 ms and five invocations, and reports the median +of seven samples of at least 20 ms each. Input setup remains outside the timer. + +## Implementation and limits + +Materialized `slice`, `substring`, and `substr` now share a bounded WTF-8 boundary +walker. A boundary between the two UTF-16 units of an astral scalar retains the +requested high or low surrogate, encoded as WTF-8. Result byte length, UTF-16 +length, and lone-surrogate flags agree. Complete byte ranges use the existing +rooted copy; split boundaries are assembled in Rust-owned memory before any +destination allocation can move or collect the source. + +The native compiler also keeps an eligible suffix local as its ordinary rooted +source plus three scalar offsets on the stack. `s = s.slice(k)` advances that +cursor, and `.length`/`charCodeAt(i)` read relative to it. The byte cursor can stop +between an astral scalar's surrogate halves. Consuming the whole input has linear +total decoding work and performs no substring allocations or suffix copies. + +Eligibility is conservative: a mutable local declaration in the function's outer +statement list, only discarded self-assignments from `slice` with an omitted or +nonnegative constant start, and only length and constant-index `charCodeAt` +consumers. Return values, aliases, captures, other writes, negative/dynamic slice +bounds, an explicit end, and other string consumers keep ordinary materialized +strings. A runtime string-tag guard preserves the existing property/method path +when a TypeScript string annotation actually holds a different kind of value. +This is compiler scalar replacement, not a new public string representation; +the flat string layout and FFI ABI are unchanged. Unselected loops can still +incur repeated suffix copying; general escaping substring views are separate +representation work. + +Memory policy: an eligible cursor retains its original source through the +ordinary local GC root until that root is released; its state contains no +interior pointers. It creates no shared backing-store chain, cache, or persistent +GC root. A small materialized slice owns its bytes and retains no source string. +Moving-GC coverage asserts that the source really relocates while a cursor sits +between surrogate halves, and that a separately retained slice remains valid. +The compiled stress fixture overwrites the original binding and allocates inside +the parse loop. + +The change leaves trim operations (#10054), general Unicode random indexing +(#10055), and HIR `for-of` iteration stride (#10062) independent. + +## Reproduction + +Base: `603b074ace01464bc66fc07cc8d532f26ccf5a0f` (pristine main), Perry +`0.5.1532`; Node `v26.5.1`; native Windows x64. Compiler and both matching static +archives were built together with: + +```powershell +$env:LLVM_SYS_221_PREFIX = 'C:\llvm' +$env:CARGO_PROFILE_RELEASE_CODEGEN_UNITS = '16' +cargo build --release --locked -j 6 -p perry -p perry-runtime-static -p perry-stdlib-static +$env:PATH = 'C:\llvm\bin;' + $env:PATH +$env:PERRY_RUNTIME_DIR = (Resolve-Path target/release).Path +python benchmarks/string_slice/measure.py --perry target/release/perry.exe --output benchmarks/string_slice/fixed.json +``` + +The release optimization level and thin LTO are unchanged; 16 codegen units are +used in both arms. `baseline-artifacts.json` records the pristine compiler and +archive hashes. Result files include source hashes and engine versions. Timing +runs are serialized with each other and with local builds; this is a shared +development host, so constant factors are diagnostic rather than a quiet-host +performance claim. Unicode timings are interpreted only after checksum parity. + +The pristine reduction reproduces the issue exactly: + +```text +Perry: 5:228,4:20013,3:55357,2:195,1:150, +Node: 5:228,4:20013,3:55357,2:56832,1:214, +``` + +The baseline ASCII exponent is 2.013 over completed sizes 100–10,000; 100,000 +times out. Unicode fails checksum stability at 100 and mismatches at 1,000 and +10,000, so its baseline speed is not classified. diff --git a/benchmarks/string_slice/baseline-artifacts.json b/benchmarks/string_slice/baseline-artifacts.json new file mode 100644 index 0000000000..6ade9768e9 --- /dev/null +++ b/benchmarks/string_slice/baseline-artifacts.json @@ -0,0 +1,5 @@ +{ + "target/release/perry.exe": "bd112c9c1394946cab99671d9494bdbd723092ed8d2594ba6b6bc78e354a79ce", + "target/release/perry_runtime.lib": "50a570b026314a0b60271749ef1328166d211f8aecf5f263e4029e52b8c39286", + "target/release/perry_stdlib.lib": "2e6dc08e44d008a6e49947faa14873e6dec67ba1557238a0566913e823f88de3" +} diff --git a/benchmarks/string_slice/baseline.json b/benchmarks/string_slice/baseline.json new file mode 100644 index 0000000000..ec5e9ca0df --- /dev/null +++ b/benchmarks/string_slice/baseline.json @@ -0,0 +1,176 @@ +{ + "revision": "603b074ace01464bc66fc07cc8d532f26ccf5a0f", + "node": "v26.5.1", + "workloads": { + "ascii": { + "sha256": "3895cd80f04d760f98c3447607347ff5ea0c8d900129a187ddfd6f55be100bfb", + "engines": { + "node": [ + { + "name": "string-slice-parse-loop-ascii", + "category": "strings", + "n": 100, + "ms_per_run": 0.006702244556114048, + "runs": 19849, + "checksum": 464151292 + }, + { + "name": "string-slice-parse-loop-ascii", + "category": "strings", + "n": 1000, + "ms_per_run": 0.08602875536480824, + "runs": 1647, + "checksum": 710929850 + }, + { + "name": "string-slice-parse-loop-ascii", + "category": "strings", + "n": 10000, + "ms_per_run": 0.8166440000000057, + "runs": 174, + "checksum": 535454277 + }, + { + "name": "string-slice-parse-loop-ascii", + "category": "strings", + "n": 100000, + "ms_per_run": 7.339999999999994, + "runs": 21, + "checksum": 35382078 + }, + { + "name": "string-slice-parse-loop-ascii", + "category": "strings", + "n": 1000000, + "ms_per_run": 73.84990000000005, + "runs": 7, + "checksum": 153135489 + } + ], + "perry": [ + { + "name": "string-slice-parse-loop-ascii", + "category": "strings", + "n": 100, + "ms_per_run": 0.015718067556952185, + "runs": 8805, + "checksum": 464151292, + "checksum_match": true + }, + { + "name": "string-slice-parse-loop-ascii", + "category": "strings", + "n": 1000, + "ms_per_run": 1.3900666666666666, + "runs": 105, + "checksum": 710929850, + "checksum_match": true + }, + { + "name": "string-slice-parse-loop-ascii", + "category": "strings", + "n": 10000, + "ms_per_run": 166.93410000000017, + "runs": 7, + "checksum": 535454277, + "checksum_match": true + }, + { + "n": 100000, + "status": "TIMEOUT" + } + ] + }, + "slopes": { + "node": 1.0015311702090146, + "perry": 2.0130729544915633 + } + }, + "unicode": { + "sha256": "ca1ea8b7ce414346c145e60062c2dbe2be6209f78e203df1567f9f86da9efedb", + "engines": { + "node": [ + { + "name": "string-slice-parse-loop-unicode", + "category": "strings", + "n": 100, + "ms_per_run": 0.010193883792049139, + "runs": 13930, + "checksum": 319467163 + }, + { + "name": "string-slice-parse-loop-unicode", + "category": "strings", + "n": 1000, + "ms_per_run": 0.10086180904522792, + "runs": 1374, + "checksum": 431622199 + }, + { + "name": "string-slice-parse-loop-unicode", + "category": "strings", + "n": 10000, + "ms_per_run": 1.028454999999991, + "runs": 140, + "checksum": 36132863 + }, + { + "name": "string-slice-parse-loop-unicode", + "category": "strings", + "n": 100000, + "ms_per_run": 8.99769999999999, + "runs": 21, + "checksum": 49951631 + }, + { + "name": "string-slice-parse-loop-unicode", + "category": "strings", + "n": 1000000, + "ms_per_run": 91.22919999999999, + "runs": 7, + "checksum": 481167302 + } + ], + "perry": [ + { + "n": 100, + "status": "ERROR", + "exit": 1, + "stdout": "", + "stderr": "Error: CORRECTNESS: unstable checksum during warmup\n at \n" + }, + { + "name": "string-slice-parse-loop-unicode", + "category": "strings", + "n": 1000, + "ms_per_run": 14.891149999999982, + "runs": 14, + "checksum": 399614474, + "checksum_match": false, + "status": "CHECKSUM_MISMATCH" + }, + { + "name": "string-slice-parse-loop-unicode", + "category": "strings", + "n": 10000, + "ms_per_run": 1736.2381999999998, + "runs": 7, + "checksum": 455826758, + "checksum_match": false, + "status": "CHECKSUM_MISMATCH" + }, + { + "n": 100000, + "status": "TIMEOUT" + } + ] + }, + "slopes": { + "node": 0.9853993131693931, + "perry": null + } + } + }, + "platform": "Windows-11-10.0.26200-SP0", + "processor": "AMD64 Family 25 Model 116 Stepping 1, AuthenticAMD" +} diff --git a/benchmarks/string_slice/measure.py b/benchmarks/string_slice/measure.py new file mode 100644 index 0000000000..a37c98f35c --- /dev/null +++ b/benchmarks/string_slice/measure.py @@ -0,0 +1,70 @@ +"""Run the unchanged #10061 workloads sequentially against Node and Perry.""" +import argparse +import hashlib +import json +import math +import os +import platform +from pathlib import Path +import statistics +import subprocess + + +def slope(rows): + pairs = [(math.log(r["n"]), math.log(r["ms_per_run"])) for r in rows + if "ms_per_run" in r and "status" not in r] + if len(pairs) < 2: + return None + mx = statistics.mean(x for x, _ in pairs) + my = statistics.mean(y for _, y in pairs) + return sum((x - mx) * (y - my) for x, y in pairs) / sum((x - mx) ** 2 for x, _ in pairs) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--perry", required=True) + parser.add_argument("--output", required=True) + parser.add_argument("--skip-compile", action="store_true") + args = parser.parse_args() + folder = Path(__file__).resolve().parent + exe_suffix = ".exe" if os.name == "nt" else "" + result = {"revision": subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip(), + "node": subprocess.check_output(["node", "--version"], text=True).strip(), + "platform": platform.platform(), "processor": platform.processor(), "workloads": {}} + for variant in ["ascii", "unicode"]: + name = "string-slice-parse-loop-" + variant + source = folder / (name + ".ts") + binary = folder / (name + exe_suffix) + if not args.skip_compile: + subprocess.run([args.perry, "compile", str(source), "--no-auto-optimize", "-o", str(binary)], check=True) + engines = {} + for n in [100, 1000, 10000, 100000, 1000000]: + for engine, command in [("node", ["node", str(source)]), ("perry", [str(binary)])]: + rows = engines.setdefault(engine, []) + if any(r.get("status") == "TIMEOUT" for r in rows): + continue + try: + run = subprocess.run(command + [str(n)], capture_output=True, text=True, timeout=60) + if run.returncode: + row = {"n": n, "status": "ERROR", "exit": run.returncode, + "stdout": run.stdout, "stderr": run.stderr} + else: + row = json.loads(run.stdout) + except subprocess.TimeoutExpired: + row = {"n": n, "status": "TIMEOUT"} + if engine == "perry" and "checksum" in row: + oracle = next((r for r in engines.get("node", []) if r["n"] == n), {}) + if "checksum" in oracle: + row["checksum_match"] = row["checksum"] == oracle["checksum"] + if not row["checksum_match"]: + row["status"] = "CHECKSUM_MISMATCH" + rows.append(row) + print(variant, engine, row, flush=True) + result["workloads"][variant] = {"sha256": hashlib.sha256(source.read_bytes()).hexdigest(), + "engines": engines, + "slopes": {e: slope(rs) for e, rs in engines.items()}} + Path(args.output).write_text(json.dumps(result, indent=2) + "\n", encoding="utf8") + + +if __name__ == "__main__": + main() diff --git a/benchmarks/string_slice/public-baseline-check.json b/benchmarks/string_slice/public-baseline-check.json new file mode 100644 index 0000000000..a5b73b0b77 --- /dev/null +++ b/benchmarks/string_slice/public-baseline-check.json @@ -0,0 +1,6 @@ +{ + "base": "603b074ace01464bc66fc07cc8d532f26ccf5a0f", + "benchmark_inputs_identical_to_base": true, + "source_fingerprint": "9507434be47f7bb383c30810bdc5d66d9be65290da529f38b7473860ea98f75e", + "harness_fingerprint": "513dba8ff9eaf931edc8a5fc01a0093a1156c31b0b6c9cc1218f710405e315e0" +} diff --git a/benchmarks/string_slice/string-slice-astral.ts b/benchmarks/string_slice/string-slice-astral.ts new file mode 100644 index 0000000000..b34a5da9f1 --- /dev/null +++ b/benchmarks/string_slice/string-slice-astral.ts @@ -0,0 +1,10 @@ +function inspect(input: string): string { + let s = input; + let out = ""; + for (let i = 0; i < 16 && s.length; i++) { + out += s.length + ":" + s.charCodeAt(0) + ","; + s = s.slice(1); + } + return out; +} +console.log(inspect("Γ€δΈ­πŸ˜€Γ–")); diff --git a/benchmarks/string_slice/string-slice-parse-loop-ascii.ts b/benchmarks/string_slice/string-slice-parse-loop-ascii.ts new file mode 100644 index 0000000000..c11cbd0851 --- /dev/null +++ b/benchmarks/string_slice/string-slice-parse-loop-ascii.ts @@ -0,0 +1,93 @@ +// @runtime {"name": "string-slice-parse-loop-ascii", "category": "strings", "verification": "checksum", "sources": [{"file": "crates/perry-runtime/src/string/slice_ops.rs", "function": "js_string_slice"}, {"file": "crates/perry-runtime/src/string/mod.rs", "function": "string_copy_range"}], "hypothesis": "Hypothesis: each slice allocates and copies the remaining suffix, so removing one code unit per iteration copies a quadratic total number of bytes.", "notes": "ascii variant. n counts repeated input tokens, not bytes; UTF-16 length and UTF-8 byte length differ for Unicode. String result hashes sample roughly 32 positions for long strings (at most 63 for short strings), plus length. Reads one UTF-16 unit then removes it, including individual emoji surrogate halves; no manual optimized parser. charCodeAt(0) inspects only the current leading code unit, so the checksum itself does not scan a growing prefix. For Unicode, an offset inside an emoji must retain its low surrogate; the source byte-offset helper instead rounds beyond the complete code point.", "asynchronous": false, "output_stderr": false, "fresh_input": false} +// Standalone file. Shared helpers/driver are inlined by common.py. + +let seed = 0x12345678; +function rnd(): number { + seed ^= seed << 13; seed ^= seed >>> 17; seed ^= seed << 5; + return (seed >>> 0) / 4294967296; +} +function numbers(n: number): number[] { + const a: number[] = []; + for (let i = 0; i < n; i++) a.push(Math.floor(rnd() * 1000000)); + return a; +} +function hashArray(a: number[]): number { + let h = a.length; + for (let i = 0; i < a.length; i++) h = (h * 31 + a[i]) % 1000000007; + return h; +} +// Bounded checksum work avoids making string slicing/indexing part of every +// string benchmark's asymptotic cost. The workload itself consumes its result. +function hashString(s: string): number { + let h = s.length; + const step = Math.max(1, Math.floor(s.length / 32)); + for (let i = 0; i < s.length; i += step) h = (h * 31 + s.charCodeAt(i)) % 1000000007; + return h; +} + +function setup(n: number): string { return "aBcD".repeat(n); } +function run(input: string): number { + let s = input; + let h = 0; + while (s.length) { + h = (h * 31 + s.charCodeAt(0)) % 1000000007; + s = s.slice(1); + } + return h; +} + +// Size is the final argument: both native Perry and Node expose it reliably. +const n = Number(process.argv[process.argv.length - 1]); +if (!(n > 0)) throw new Error("Expected a positive size argument"); +function benchmarkMain(): void { + seed = 0x12345678; + const preparedInput = setup(n); + let checksum = 0; + let seen = false; + let warmMs = 0; + let warmRuns = 0; + while (warmMs < 200 || warmRuns < 5) { + seed = 0x12345678; + const input = preparedInput; + const start = performance.now(); + const value = run(input); + const elapsed = performance.now() - start; + if (!(elapsed >= 0)) throw new Error("Invalid monotonic timer"); + warmMs += elapsed; + warmRuns++; + if (seen && value !== checksum) throw new Error("CORRECTNESS: unstable checksum during warmup"); + checksum = value; + seen = true; + } + const samples: number[] = []; + let runs = 0; + for (let sample = 0; sample < 7; sample++) { + let elapsed = 0; + let count = 0; + // Mutable workloads prepare fresh input BEFORE each timer; immutable + // workloads reuse setup. Neither preparation nor validation is measured. + while (elapsed < 20) { + seed = 0x12345678; + const input = preparedInput; + const start = performance.now(); + const value = run(input); + const duration = performance.now() - start; + if (!(duration >= 0)) throw new Error("Invalid monotonic timer"); + elapsed += duration; + count++; + if (value !== checksum) throw new Error("CORRECTNESS: unstable checksum during sampling"); + } + samples.push(elapsed / count); + runs += count; + } + // Do not depend on Array.sort to compute the median of a sort benchmark. + for (let i = 1; i < samples.length; i++) { + const v = samples[i]; + let j = i - 1; + while (j >= 0 && samples[j] > v) { samples[j + 1] = samples[j]; j--; } + samples[j + 1] = v; + } + console.log(JSON.stringify({name: "string-slice-parse-loop-ascii", category: "strings", n, + ms_per_run: samples[3], runs, checksum})); +} +benchmarkMain(); diff --git a/benchmarks/string_slice/string-slice-parse-loop-unicode.ts b/benchmarks/string_slice/string-slice-parse-loop-unicode.ts new file mode 100644 index 0000000000..eea49ef247 --- /dev/null +++ b/benchmarks/string_slice/string-slice-parse-loop-unicode.ts @@ -0,0 +1,93 @@ +// @runtime {"name": "string-slice-parse-loop-unicode", "category": "strings", "verification": "checksum", "sources": [{"file": "crates/perry-runtime/src/string/slice_ops.rs", "function": "js_string_slice"}, {"file": "crates/perry-runtime/src/string/mod.rs", "function": "string_copy_range"}, {"file": "crates/perry-runtime/src/string/mod.rs", "function": "utf16_offset_to_byte_offset"}], "hypothesis": "Hypothesis: utf16_offset_to_byte_offset advances over a whole astral character when slice starts between its surrogate halves; js_string_slice then copies the remaining bytes while stamping end-start as UTF-16 length, creating a payload/header mismatch. Repeated full-suffix copies also give quadratic work.", "notes": "unicode variant. n counts repeated input tokens, not bytes; UTF-16 length and UTF-8 byte length differ for Unicode. String result hashes sample roughly 32 positions for long strings (at most 63 for short strings), plus length. Reads one UTF-16 unit then removes it, including individual emoji surrogate halves; no manual optimized parser. charCodeAt(0) inspects only the current leading code unit, so the checksum itself does not scan a growing prefix. For Unicode, an offset inside an emoji must retain its low surrogate; the source byte-offset helper instead rounds beyond the complete code point.", "asynchronous": false, "output_stderr": false, "fresh_input": false} +// Standalone file. Shared helpers/driver are inlined by common.py. + +let seed = 0x12345678; +function rnd(): number { + seed ^= seed << 13; seed ^= seed >>> 17; seed ^= seed << 5; + return (seed >>> 0) / 4294967296; +} +function numbers(n: number): number[] { + const a: number[] = []; + for (let i = 0; i < n; i++) a.push(Math.floor(rnd() * 1000000)); + return a; +} +function hashArray(a: number[]): number { + let h = a.length; + for (let i = 0; i < a.length; i++) h = (h * 31 + a[i]) % 1000000007; + return h; +} +// Bounded checksum work avoids making string slicing/indexing part of every +// string benchmark's asymptotic cost. The workload itself consumes its result. +function hashString(s: string): number { + let h = s.length; + const step = Math.max(1, Math.floor(s.length / 32)); + for (let i = 0; i < s.length; i += step) h = (h * 31 + s.charCodeAt(i)) % 1000000007; + return h; +} + +function setup(n: number): string { return "Γ€δΈ­πŸ˜€Γ–".repeat(n); } +function run(input: string): number { + let s = input; + let h = 0; + while (s.length) { + h = (h * 31 + s.charCodeAt(0)) % 1000000007; + s = s.slice(1); + } + return h; +} + +// Size is the final argument: both native Perry and Node expose it reliably. +const n = Number(process.argv[process.argv.length - 1]); +if (!(n > 0)) throw new Error("Expected a positive size argument"); +function benchmarkMain(): void { + seed = 0x12345678; + const preparedInput = setup(n); + let checksum = 0; + let seen = false; + let warmMs = 0; + let warmRuns = 0; + while (warmMs < 200 || warmRuns < 5) { + seed = 0x12345678; + const input = preparedInput; + const start = performance.now(); + const value = run(input); + const elapsed = performance.now() - start; + if (!(elapsed >= 0)) throw new Error("Invalid monotonic timer"); + warmMs += elapsed; + warmRuns++; + if (seen && value !== checksum) throw new Error("CORRECTNESS: unstable checksum during warmup"); + checksum = value; + seen = true; + } + const samples: number[] = []; + let runs = 0; + for (let sample = 0; sample < 7; sample++) { + let elapsed = 0; + let count = 0; + // Mutable workloads prepare fresh input BEFORE each timer; immutable + // workloads reuse setup. Neither preparation nor validation is measured. + while (elapsed < 20) { + seed = 0x12345678; + const input = preparedInput; + const start = performance.now(); + const value = run(input); + const duration = performance.now() - start; + if (!(duration >= 0)) throw new Error("Invalid monotonic timer"); + elapsed += duration; + count++; + if (value !== checksum) throw new Error("CORRECTNESS: unstable checksum during sampling"); + } + samples.push(elapsed / count); + runs += count; + } + // Do not depend on Array.sort to compute the median of a sort benchmark. + for (let i = 1; i < samples.length; i++) { + const v = samples[i]; + let j = i - 1; + while (j >= 0 && samples[j] > v) { samples[j + 1] = samples[j]; j--; } + samples[j + 1] = v; + } + console.log(JSON.stringify({name: "string-slice-parse-loop-unicode", category: "strings", n, + ms_per_run: samples[3], runs, checksum})); +} +benchmarkMain(); diff --git a/changelog.d/10061-string-slice.md b/changelog.d/10061-string-slice.md new file mode 100644 index 0000000000..e880d8df55 --- /dev/null +++ b/changelog.d/10061-string-slice.md @@ -0,0 +1,9 @@ +Fix `String.prototype.slice`, `substring`, and `substr` at UTF-16 boundaries +inside astral characters, preserving either surrogate half and pre-existing +lone surrogates with consistent length and WTF-8 metadata. + +Avoid repeated full-suffix copying in eligible native parse loops by keeping +non-escaping suffix locals as rooted sources with scalar UTF-16/byte cursors. +Escaping strings retain their flat storage and independent lifetime. Includes +Unicode boundary, aliasing, moving-GC, and unchanged workload benchmark coverage +for #10061. diff --git a/crates/perry-codegen/src/codegen/closure.rs b/crates/perry-codegen/src/codegen/closure.rs index 1facefbe65..616317cef8 100644 --- a/crates/perry-codegen/src/codegen/closure.rs +++ b/crates/perry-codegen/src/codegen/closure.rs @@ -1266,6 +1266,8 @@ pub(super) fn compile_closure( .non_escaping_array_length_only_indices() .clone(), fusible_uppercase_locals: native_facts.fusible_uppercase_locals().clone(), + suffix_cursor_locals: native_facts.suffix_cursor_locals().clone(), + suffix_cursors: std::collections::HashMap::new(), non_escaping_object_literals: native_facts.non_escaping_object_literals().clone(), non_escaping_object_literal_used_fields: native_facts .non_escaping_object_literal_used_fields() diff --git a/crates/perry-codegen/src/codegen/entry.rs b/crates/perry-codegen/src/codegen/entry.rs index 5d2f5cc498..ed0ef5eccf 100644 --- a/crates/perry-codegen/src/codegen/entry.rs +++ b/crates/perry-codegen/src/codegen/entry.rs @@ -1040,6 +1040,8 @@ pub(super) fn compile_module_entry( .non_escaping_array_length_only_indices() .clone(), fusible_uppercase_locals: main_native_facts.fusible_uppercase_locals().clone(), + suffix_cursor_locals: main_native_facts.suffix_cursor_locals().clone(), + suffix_cursors: std::collections::HashMap::new(), non_escaping_object_literals: main_native_facts.non_escaping_object_literals().clone(), non_escaping_object_literal_used_fields: main_native_facts .non_escaping_object_literal_used_fields() @@ -1831,6 +1833,8 @@ pub(super) fn compile_module_entry( .non_escaping_array_length_only_indices() .clone(), fusible_uppercase_locals: init_native_facts.fusible_uppercase_locals().clone(), + suffix_cursor_locals: init_native_facts.suffix_cursor_locals().clone(), + suffix_cursors: std::collections::HashMap::new(), non_escaping_object_literals: init_native_facts.non_escaping_object_literals().clone(), non_escaping_object_literal_used_fields: init_native_facts .non_escaping_object_literal_used_fields() diff --git a/crates/perry-codegen/src/codegen/function.rs b/crates/perry-codegen/src/codegen/function.rs index 0289c4b085..4fda54f02e 100644 --- a/crates/perry-codegen/src/codegen/function.rs +++ b/crates/perry-codegen/src/codegen/function.rs @@ -1210,6 +1210,8 @@ pub(super) fn compile_function( .non_escaping_array_length_only_indices() .clone(), fusible_uppercase_locals: native_facts.fusible_uppercase_locals().clone(), + suffix_cursor_locals: native_facts.suffix_cursor_locals().clone(), + suffix_cursors: std::collections::HashMap::new(), non_escaping_object_literals: native_facts.non_escaping_object_literals().clone(), non_escaping_object_literal_used_fields: native_facts .non_escaping_object_literal_used_fields() diff --git a/crates/perry-codegen/src/codegen/method.rs b/crates/perry-codegen/src/codegen/method.rs index c562c1de88..c38453e1f3 100644 --- a/crates/perry-codegen/src/codegen/method.rs +++ b/crates/perry-codegen/src/codegen/method.rs @@ -648,6 +648,8 @@ pub(super) fn compile_method( .non_escaping_array_length_only_indices() .clone(), fusible_uppercase_locals: native_facts.fusible_uppercase_locals().clone(), + suffix_cursor_locals: native_facts.suffix_cursor_locals().clone(), + suffix_cursors: std::collections::HashMap::new(), non_escaping_object_literals: native_facts.non_escaping_object_literals().clone(), non_escaping_object_literal_used_fields: native_facts .non_escaping_object_literal_used_fields() @@ -1793,6 +1795,8 @@ pub(super) fn compile_static_method( .non_escaping_array_length_only_indices() .clone(), fusible_uppercase_locals: native_facts.fusible_uppercase_locals().clone(), + suffix_cursor_locals: native_facts.suffix_cursor_locals().clone(), + suffix_cursors: std::collections::HashMap::new(), non_escaping_object_literals: native_facts.non_escaping_object_literals().clone(), non_escaping_object_literal_used_fields: native_facts .non_escaping_object_literal_used_fields() diff --git a/crates/perry-codegen/src/collectors/hir_facts.rs b/crates/perry-codegen/src/collectors/hir_facts.rs index 5a65978bb1..48b4d6b2b0 100644 --- a/crates/perry-codegen/src/collectors/hir_facts.rs +++ b/crates/perry-codegen/src/collectors/hir_facts.rs @@ -165,6 +165,7 @@ pub(crate) struct EscapeFacts { pub non_escaping_array_used_indices: HashMap>, pub non_escaping_array_length_only_indices: HashMap>, pub fusible_uppercase_locals: HashSet, + pub suffix_cursor_locals: HashSet, pub non_escaping_object_literals: HashMap>, pub non_escaping_object_literal_used_fields: HashMap>, /// #9843, the fourth member of this family: `for (let {segment: O} of @@ -388,6 +389,10 @@ impl TypeFacts { &self.escape.non_escaping_array_length_only_indices } + pub(crate) fn suffix_cursor_locals(&self) -> &HashSet { + &self.escape.suffix_cursor_locals + } + pub(crate) fn fusible_uppercase_locals(&self) -> &HashSet { &self.escape.fusible_uppercase_locals } @@ -672,6 +677,7 @@ pub(crate) fn collect_type_facts( stmts, &non_escaping_arrays, ); + let suffix_cursor_locals = super::suffix_strings::collect(stmts, boxed_vars, module_globals); let fusible_uppercase_locals = super::uppercase_strings::collect_fusible_uppercase_locals( stmts, &non_escaping_arrays, @@ -786,6 +792,7 @@ pub(crate) fn collect_type_facts( non_escaping_array_used_indices, non_escaping_array_length_only_indices, fusible_uppercase_locals, + suffix_cursor_locals, non_escaping_object_literals, non_escaping_object_literal_used_fields, segment_for_of_sites, diff --git a/crates/perry-codegen/src/collectors/mod.rs b/crates/perry-codegen/src/collectors/mod.rs index f576335f32..f633c0df3c 100644 --- a/crates/perry-codegen/src/collectors/mod.rs +++ b/crates/perry-codegen/src/collectors/mod.rs @@ -56,6 +56,7 @@ pub mod segview; mod segview_tests; mod shadow_slots; pub(crate) mod spec_abi_sites; +pub(crate) mod suffix_strings; mod this_as_value; mod uppercase_strings; diff --git a/crates/perry-codegen/src/collectors/suffix_strings.rs b/crates/perry-codegen/src/collectors/suffix_strings.rs new file mode 100644 index 0000000000..99ea593320 --- /dev/null +++ b/crates/perry-codegen/src/collectors/suffix_strings.rs @@ -0,0 +1,211 @@ +//! Prove that a local suffix is observed only through length/character reads. +//! Remove admitted uses in a temporary HIR copy, then use the exhaustive HIR +//! reference collector to reject every remaining read, write, and capture. + +use perry_hir::{Expr, Stmt}; +use std::collections::{HashMap, HashSet}; + +#[cfg(test)] +mod tests; + +pub(crate) fn literal_index(expr: &Expr) -> Option { + match expr { + Expr::Integer(n) => i32::try_from(*n).ok(), + Expr::Number(n) + if n.is_finite() + && n.fract() == 0.0 + && *n >= i32::MIN as f64 + && *n <= i32::MAX as f64 => + { + Some(*n as i32) + } + _ => None, + } +} + +pub(crate) fn method(expr: &Expr, name: &str) -> Option<(u32, i32)> { + let Expr::Call { callee, args, .. } = expr else { + return None; + }; + let Expr::PropertyGet { + object, property, .. + } = callee.as_ref() + else { + return None; + }; + let Expr::LocalGet(id) = object.as_ref() else { + return None; + }; + if property != name { + return None; + } + let index = match args.as_slice() { + [] => 0, + [arg] => literal_index(arg)?, + _ => return None, + }; + Some((*id, index)) +} + +pub(crate) fn collect( + stmts: &[Stmt], + boxed: &HashSet, + globals: &HashMap, +) -> HashSet { + // Top-level declarations dominate the admitted loops. Nested declarations + // remain materialized; in particular, no cursor crosses a closure region. + let mut candidates: HashSet = stmts + .iter() + .filter_map(|stmt| match stmt { + Stmt::Let { + id, + init: Some(_), + mutable: true, + .. + } if !boxed.contains(id) && !globals.contains_key(id) => Some(*id), + _ => None, + }) + .collect(); + if candidates.is_empty() || !stmts.iter().any(|s| has_update(s, &candidates)) { + return HashSet::new(); + } + let mut masked = stmts.to_vec(); + let mut updates = HashSet::new(); + for stmt in &mut masked { + mask_stmt(stmt, &candidates, &mut updates); + } + let mut refs = Vec::new(); + let mut visited = HashSet::new(); + for stmt in &masked { + perry_hir::analysis::collect_local_refs_stmt(stmt, &mut refs, &mut visited); + } + for id in refs { + candidates.remove(&id); + } + candidates.retain(|id| updates.contains(id)); + candidates +} + +fn has_update(stmt: &Stmt, candidates: &HashSet) -> bool { + let update = |expr: &Expr| match expr { + Expr::LocalSet(id, value) => { + candidates.contains(id) + && method(value, "slice") + .is_some_and(|(receiver, count)| receiver == *id && count >= 0) + } + _ => false, + }; + match stmt { + Stmt::Expr(expr) => update(expr), + Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => { + body.iter().any(|s| has_update(s, candidates)) + } + Stmt::If { + then_branch, + else_branch, + .. + } => { + then_branch.iter().any(|s| has_update(s, candidates)) + || else_branch + .as_ref() + .is_some_and(|branch| branch.iter().any(|s| has_update(s, candidates))) + } + Stmt::For { + init, + update: step, + body, + .. + } => { + init.as_ref().is_some_and(|s| has_update(s, candidates)) + || step.as_ref().is_some_and(update) + || body.iter().any(|s| has_update(s, candidates)) + } + _ => false, + } +} + +fn mask_expr(expr: &mut Expr, allowed: &HashSet, updates: &mut HashSet, discarded: bool) { + if discarded { + if let Expr::LocalSet(id, value) = expr { + if let Some((receiver, count)) = method(value, "slice") { + if receiver == *id && count >= 0 && allowed.contains(id) { + updates.insert(*id); + *expr = Expr::Undefined; + return; + } + } + } + } + let admitted = match expr { + Expr::PropertyGet { + object, property, .. + } if property == "length" => { + matches!(object.as_ref(), Expr::LocalGet(id) if allowed.contains(id)) + } + _ => method(expr, "charCodeAt").is_some_and(|(id, _)| allowed.contains(&id)), + }; + if admitted { + *expr = Expr::Undefined; + return; + } + // Captures, closure defaults, and bodies must remain visible to the + // reference collector, even if their reads look locally fusible. + if matches!(expr, Expr::Closure { .. }) { + return; + } + perry_hir::walker::walk_expr_children_mut(expr, &mut |child| { + mask_expr(child, allowed, updates, false) + }); +} + +fn mask_stmt(stmt: &mut Stmt, allowed: &HashSet, updates: &mut HashSet) { + match stmt { + Stmt::Let { init: Some(e), .. } | Stmt::Return(Some(e)) | Stmt::Throw(e) => { + mask_expr(e, allowed, updates, false) + } + Stmt::Expr(e) => mask_expr(e, allowed, updates, true), + Stmt::If { + condition, + then_branch, + else_branch, + } => { + mask_expr(condition, allowed, updates, false); + for s in then_branch { + mask_stmt(s, allowed, updates); + } + if let Some(branch) = else_branch { + for s in branch { + mask_stmt(s, allowed, updates); + } + } + } + Stmt::While { condition, body } | Stmt::DoWhile { condition, body } => { + mask_expr(condition, allowed, updates, false); + for s in body { + mask_stmt(s, allowed, updates); + } + } + Stmt::For { + init, + condition, + update, + body, + } => { + if let Some(s) = init { + mask_stmt(s, allowed, updates); + } + if let Some(e) = condition { + mask_expr(e, allowed, updates, false); + } + if let Some(e) = update { + mask_expr(e, allowed, updates, true); + } + for s in body { + mask_stmt(s, allowed, updates); + } + } + // Unhandled control-flow shapes remain intact and conservatively + // reject any referenced candidate through the exhaustive collector. + _ => {} + } +} diff --git a/crates/perry-codegen/src/collectors/suffix_strings/tests.rs b/crates/perry-codegen/src/collectors/suffix_strings/tests.rs new file mode 100644 index 0000000000..824b82dffd --- /dev/null +++ b/crates/perry-codegen/src/collectors/suffix_strings/tests.rs @@ -0,0 +1,97 @@ +use super::*; +use perry_hir::types::Type; + +fn property(name: &str) -> Expr { + Expr::PropertyGet { + object: Box::new(Expr::LocalGet(1)), + property: name.into(), + byte_offset: 0, + } +} +fn call(name: &str, args: Vec) -> Expr { + Expr::Call { + callee: Box::new(property(name)), + args, + type_args: vec![], + byte_offset: 0, + } +} +fn assign(args: Vec) -> Expr { + Expr::LocalSet(1, Box::new(call("slice", args))) +} +fn body() -> Vec { + vec![ + Stmt::Let { + id: 1, + name: "s".into(), + ty: Type::String, + mutable: true, + init: Some(Expr::String("πŸ˜€abc".into())), + }, + Stmt::While { + condition: property("length"), + body: vec![ + Stmt::Expr(call("charCodeAt", vec![Expr::Integer(0)])), + Stmt::Expr(assign(vec![Expr::Integer(1)])), + ], + }, + ] +} +fn selected(stmts: &[Stmt]) -> bool { + collect(stmts, &HashSet::new(), &HashMap::new()).contains(&1) +} + +#[test] +fn accepts_scalar_consumption_with_utf16_strides() { + assert!(selected(&body())); + for count in [0, 2, 5, i32::MAX] { + let mut stmts = body(); + stmts.push(Stmt::Expr(assign(vec![Expr::Integer(count as i64)]))); + stmts.push(Stmt::Return(Some(property("length")))); + assert!(selected(&stmts)); + } +} + +#[test] +fn leaves_unrelated_and_immutable_locals_alone() { + assert!(!selected(&body()[..1])); + let mut stmts = body(); + let Stmt::Let { mutable, .. } = &mut stmts[0] else { + unreachable!(); + }; + *mutable = false; + assert!(!selected(&stmts)); +} + +#[test] +fn rejects_escapes_aliases_captures_and_unsupported_updates() { + let cases = [ + Stmt::Return(Some(Expr::LocalGet(1))), + Stmt::Let { + id: 2, + name: "alias".into(), + ty: Type::String, + mutable: false, + init: Some(Expr::LocalGet(1)), + }, + Stmt::Return(Some(assign(vec![Expr::Integer(1)]))), + Stmt::Expr(assign(vec![Expr::Integer(-1)])), + Stmt::Expr(assign(vec![Expr::Integer(1), Expr::Integer(2)])), + Stmt::Expr(assign(vec![Expr::LocalGet(3)])), + Stmt::Expr(call("charCodeAt", vec![Expr::LocalGet(3)])), + Stmt::Expr(call("toString", vec![])), + Stmt::Expr(Expr::LocalSet(1, Box::new(Expr::String("changed".into())))), + ]; + for extra in cases { + let mut stmts = body(); + stmts.push(extra.clone()); + assert!(!selected(&stmts), "must reject {extra:?}"); + } + assert!(collect(&body(), &HashSet::from([1]), &HashMap::new()).is_empty()); + assert!(collect( + &body(), + &HashSet::new(), + &HashMap::from([(1, "global".into())]) + ) + .is_empty()); +} diff --git a/crates/perry-codegen/src/expr/dispatch.rs b/crates/perry-codegen/src/expr/dispatch.rs index 89ada2b9ed..7fd8fd61c4 100644 --- a/crates/perry-codegen/src/expr/dispatch.rs +++ b/crates/perry-codegen/src/expr/dispatch.rs @@ -28,6 +28,9 @@ pub(crate) fn lower_expr(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // one. Handlers that care receive it as an argument, because they consult // it after lowering their operands β€” by which point the field is gone. let value_discarded = std::mem::take(&mut ctx.discard_this_expr); + if let Some(value) = super::suffix_cursor::try_lower(ctx, expr)? { + return Ok(value); + } if let Some(lowered) = lower_expr_value(ctx, expr)? { if ctx.discard_expr_value { return Ok(materialize_js_value_without_record(ctx, lowered)); diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs index 60b33c7168..bcc1eb6f32 100644 --- a/crates/perry-codegen/src/expr/mod.rs +++ b/crates/perry-codegen/src/expr/mod.rs @@ -1458,6 +1458,10 @@ pub(crate) struct FnCtx<'a> { pub non_escaping_array_length_only_indices: std::collections::HashMap>, pub fusible_uppercase_locals: std::collections::HashSet, + /// Locals proven to have only scalar suffix consumers. + pub suffix_cursor_locals: std::collections::HashSet, + /// Stack storage holds byte/UTF-16 offsets only, never a GC pointer. + pub suffix_cursors: std::collections::HashMap, /// Non-escaping object literals identified by escape analysis. Maps /// local_id β†’ field names (declaration order, deduplicated). Used by @@ -2785,6 +2789,7 @@ pub(crate) mod masked_window; mod null_default_numeric_add_tests; mod string_length; pub(crate) mod string_window; +pub(crate) mod suffix_cursor; mod ptr_numarray_access; mod ta_param_f64_read; diff --git a/crates/perry-codegen/src/expr/suffix_cursor.rs b/crates/perry-codegen/src/expr/suffix_cursor.rs new file mode 100644 index 0000000000..86e5aeb03a --- /dev/null +++ b/crates/perry-codegen/src/expr/suffix_cursor.rs @@ -0,0 +1,97 @@ +//! Virtual suffix lowering. Only offsets are stored outside ordinary locals; +//! the existing source-local root remains responsible for GC relocation. + +use crate::expr::{lower_expr, FnCtx}; +use crate::types::{DOUBLE, I1, I32, I64}; +use anyhow::Result; +use perry_hir::Expr; + +pub(crate) fn initialize(ctx: &mut FnCtx<'_>, id: u32) { + let slot = ctx.func.alloca_entry("[3 x i32]"); + ctx.block().store("[3 x i32]", "zeroinitializer", &slot); + ctx.suffix_cursors.insert(id, slot); +} + +pub(crate) fn try_lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result> { + let (id, operation, index) = match expr { + Expr::PropertyGet { + object, property, .. + } if property == "length" => { + let Expr::LocalGet(id) = object.as_ref() else { + return Ok(None); + }; + (*id, "length", 0) + } + Expr::LocalSet(id, value) => { + let Some((receiver, count)) = crate::collectors::suffix_strings::method(value, "slice") + else { + return Ok(None); + }; + if *id != receiver || count < 0 { + return Ok(None); + } + (*id, "advance", count) + } + _ => { + let Some((id, index)) = crate::collectors::suffix_strings::method(expr, "charCodeAt") + else { + return Ok(None); + }; + (id, "char_code_at", index) + } + }; + let Some(cursor_slot) = ctx.suffix_cursors.get(&id).cloned() else { + return Ok(None); + }; + let source = lower_expr(ctx, &Expr::LocalGet(id))?; + let bits = ctx.block().bitcast_double_to_i64(&source); + let tag = ctx.block().lshr(I64, &bits, "48"); + let heap = ctx + .block() + .icmp_eq(I64, &tag, crate::nanbox::STRING_TAG_TOP16_I64); + let short = ctx + .block() + .icmp_eq(I64, &tag, crate::nanbox::SHORT_STRING_TAG_TOP16_I64); + let is_string = ctx.block().or(I1, &heap, &short); + let fast = ctx.new_block("suffix.string"); + let slow = ctx.new_block("suffix.other"); + let merge = ctx.new_block("suffix.merge"); + let fast_label = ctx.block_label(fast); + let slow_label = ctx.block_label(slow); + let merge_label = ctx.block_label(merge); + ctx.block().cond_br(&is_string, &fast_label, &slow_label); + ctx.current_block = fast; + let cursor = ctx.block().ptrtoint(&cursor_slot, I64); + let index = index.to_string(); + let fast_value = if operation == "advance" { + ctx.block().call_void( + "js_string_suffix_advance", + &[(DOUBLE, &source), (I64, &cursor), (I32, &index)], + ); + source + } else { + let mut args = vec![(DOUBLE, source.as_str()), (I64, cursor.as_str())]; + if operation == "char_code_at" { + args.push((I32, &index)); + } + ctx.block() + .call(DOUBLE, &format!("js_string_suffix_{operation}"), &args) + }; + let fast_pred = ctx.block().label.clone(); + ctx.block().br(&merge_label); + + ctx.current_block = slow; + // Types are not runtime validation. Preserve the existing method/property + // behavior if a string-annotated local actually contains another value. + ctx.suffix_cursors.remove(&id); + let fallback = lower_expr(ctx, expr); + ctx.suffix_cursors.insert(id, cursor_slot); + let fallback = fallback?; + let slow_pred = ctx.block().label.clone(); + ctx.block().br(&merge_label); + ctx.current_block = merge; + Ok(Some(ctx.block().phi( + DOUBLE, + &[(&fast_value, &fast_pred), (&fallback, &slow_pred)], + ))) +} diff --git a/crates/perry-codegen/src/runtime_decls/strings.rs b/crates/perry-codegen/src/runtime_decls/strings.rs index 2e99cd4db1..e221ce098f 100644 --- a/crates/perry-codegen/src/runtime_decls/strings.rs +++ b/crates/perry-codegen/src/runtime_decls/strings.rs @@ -10,6 +10,13 @@ use super::*; /// and `and` with `POINTER_MASK` (0x0000_FFFF_FFFF_FFFF), then re-boxes the /// result with `js_nanbox_string`. pub fn declare_phase_b_strings(module: &mut LlModule) { + module.declare_function("js_string_suffix_length", DOUBLE, &[DOUBLE, I64]); + module.declare_function( + "js_string_suffix_advance", + crate::types::VOID, + &[DOUBLE, I64, I32], + ); + module.declare_function("js_string_suffix_char_code_at", DOUBLE, &[DOUBLE, I64, I32]); module.declare_function("js_string_concat", I64, &[I64, I64]); // SSO-aware concat: NaN-boxed f64 in, NaN-boxed f64 out. Avoids // the `js_get_string_pointer_unified`-driven SSO materialization diff --git a/crates/perry-codegen/src/stmt/mod.rs b/crates/perry-codegen/src/stmt/mod.rs index 95dbf94c9d..cc407d30c0 100644 --- a/crates/perry-codegen/src/stmt/mod.rs +++ b/crates/perry-codegen/src/stmt/mod.rs @@ -399,7 +399,13 @@ pub(crate) fn lower_stmt(ctx: &mut FnCtx<'_>, stmt: &Stmt) -> Result<()> { ty, mutable, .. - } => lower_let(ctx, *id, name, init.as_ref(), ty, *mutable), + } => { + lower_let(ctx, *id, name, init.as_ref(), ty, *mutable)?; + if ctx.suffix_cursor_locals.contains(id) { + crate::expr::suffix_cursor::initialize(ctx, *id); + } + Ok(()) + } Stmt::If { condition, diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/string_slice.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/string_slice.rs index fd182ba49d..dc668c301d 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/string_slice.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/string_slice.rs @@ -1,5 +1,49 @@ use super::*; +#[test] +fn suffix_cursor_offsets_survive_source_evacuation_and_split_slice_owns_its_bytes() { + use crate::string::suffix_cursor::*; + let _guard = CopyingNurseryTestGuard::new(0); + let _trigger = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + let _mode = + crate::arena::ProtectionModeGuard::set(crate::arena::FromSpaceProtection::PoisonOnly); + register_runtime_handle_root_scanner_for_tests(); + let scope = RuntimeHandleScope::new(); + let bytes = "Γ€δΈ­πŸ˜€Γ–".repeat(100); + let source = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); + assert!(crate::arena::pointer_in_nursery(source as usize)); + let root = scope.root_string_ptr(source); + let before = source as usize; + let boxed = |s| f64::from_bits(crate::value::JSValue::string_ptr(s).bits()); + let mut cursor = SuffixCursor::default(); + unsafe { + js_string_suffix_advance(boxed(source), &mut cursor, 3); + } + let kept = crate::string::js_string_slice(source, 3, 5); + let kept_root = scope.root_string_ptr(kept); + gc_collect_minor(); + let source = root.get_raw_mut_ptr::(); + assert_ne!( + source as usize, before, + "the test must actually move the source" + ); + unsafe { + assert_eq!(js_string_suffix_length(boxed(source), &cursor), 497.0); + assert_eq!( + js_string_suffix_char_code_at(boxed(source), &cursor, 0), + 56832.0 + ); + js_string_suffix_advance(boxed(source), &mut cursor, 1); + assert_eq!( + js_string_suffix_char_code_at(boxed(source), &cursor, 0), + 214.0 + ); + } + let kept = kept_root.get_raw_const_ptr::(); + assert_eq!(crate::string::js_string_char_code_at(kept, 0), 56832.0); + assert_eq!(crate::string::js_string_char_code_at(kept, 1), 214.0); +} + /// #5062: `String.prototype.slice` copies the selected range out of the source /// string AFTER allocating the destination, via a raw pointer derived from the /// source (`string_data(s) + offset`). If that destination allocation trips a diff --git a/crates/perry-runtime/src/string/mod.rs b/crates/perry-runtime/src/string/mod.rs index a718f0c8ef..b638bbbf8f 100644 --- a/crates/perry-runtime/src/string/mod.rs +++ b/crates/perry-runtime/src/string/mod.rs @@ -118,12 +118,16 @@ mod locale; mod pad; mod raw; mod slice_ops; +mod slice_range; mod split; +pub(crate) mod suffix_cursor; pub(crate) mod trim_cache; mod utf16_count; #[cfg(feature = "regex-engine")] pub(crate) use split::{spec_fancy_regex_split, spec_regex_split}; +#[cfg(test)] +mod slice_tests; #[cfg(test)] mod tests; #[cfg(test)] diff --git a/crates/perry-runtime/src/string/slice_ops.rs b/crates/perry-runtime/src/string/slice_ops.rs index 6a57ac602c..d233e56f80 100644 --- a/crates/perry-runtime/src/string/slice_ops.rs +++ b/crates/perry-runtime/src/string/slice_ops.rs @@ -2,7 +2,7 @@ use super::*; -/// Get a slice of a string (byte-based for now) +/// Get a slice of a string in UTF-16 code units /// Returns a new string from start to end (exclusive). /// start/end are in UTF-16 code unit indices (JS semantics). #[no_mangle] @@ -33,24 +33,7 @@ pub extern "C" fn js_string_slice( return js_string_from_bytes(ptr::null(), 0); } - // ASCII fast path: byte offsets == UTF-16 offsets, skip utf16_len scan. - // Copy GC-safely: the destination allocation can move/sweep `s` (#5062). - if is_ascii_string(s) { - let slice_len = (end - start) as u32; - return string_copy_range(s, start as usize, slice_len, slice_len, 0); - } - - // Convert UTF-16 offsets to byte offsets - let str_data = string_as_str(s); - let byte_start = utf16_offset_to_byte_offset(str_data, start as usize); - let byte_end = utf16_offset_to_byte_offset(str_data, end as usize); - string_copy_range( - s, - byte_start, - (byte_end - byte_start) as u32, - (end - start) as u32, - 0, - ) + super::slice_range::copy_utf16_range(s, start as u32, end as u32) } /// Get a substring (similar to slice but different behavior) @@ -82,23 +65,7 @@ pub extern "C" fn js_string_substring( return js_string_from_bytes(ptr::null(), 0); } - // ASCII fast path: skip utf16_len scan in allocator. - // Copy GC-safely: the destination allocation can move/sweep `s` (#5062). - if is_ascii_string(s) { - let slice_len = (end - start) as u32; - return string_copy_range(s, start as usize, slice_len, slice_len, 0); - } - - let str_data = string_as_str(s); - let byte_start = utf16_offset_to_byte_offset(str_data, start as usize); - let byte_end = utf16_offset_to_byte_offset(str_data, end as usize); - string_copy_range( - s, - byte_start, - (byte_end - byte_start) as u32, - (end - start) as u32, - 0, - ) + super::slice_range::copy_utf16_range(s, start as u32, end as u32) } /// Legacy `String.prototype.substr(start, length)` (ECMA-262 Annex B.2.3.1). @@ -159,23 +126,7 @@ pub extern "C" fn js_string_substr( let start = start as i32; let end = end as i32; - // ASCII fast path: byte offsets == UTF-16 offsets. - // Copy GC-safely: the destination allocation can move/sweep `s` (#5062). - if is_ascii_string(s) { - let slice_len = (end - start) as u32; - return string_copy_range(s, start as usize, slice_len, slice_len, 0); - } - - let str_data = string_as_str(s); - let byte_start = utf16_offset_to_byte_offset(str_data, start as usize); - let byte_end = utf16_offset_to_byte_offset(str_data, end as usize); - string_copy_range( - s, - byte_start, - (byte_end - byte_start) as u32, - (end - start) as u32, - 0, - ) + super::slice_range::copy_utf16_range(s, start as u32, end as u32) } // `#[used]` keepalive: `js_string_substr` is reached only from generated `.o`, diff --git a/crates/perry-runtime/src/string/slice_range.rs b/crates/perry-runtime/src/string/slice_range.rs new file mode 100644 index 0000000000..e4fbd12577 --- /dev/null +++ b/crates/perry-runtime/src/string/slice_range.rs @@ -0,0 +1,78 @@ +//! UTF-16 boundaries over the runtime's UTF-8/WTF-8 payloads. + +use super::*; + +/// A boundary can lie between the two code units of one four-byte scalar. +/// In that case `byte` still names the scalar's lead byte. +#[derive(Clone, Copy, Default)] +pub(super) struct Boundary { + pub byte: usize, + pub low: bool, +} + +pub(super) fn advance(bytes: &[u8], mut at: Boundary, mut units: usize) -> Boundary { + while units > 0 && at.byte < bytes.len() { + let (width, count, _) = wtf8_step(bytes, at.byte); + let count = count.saturating_sub(usize::from(at.low)); + if units < count { + at.low = true; + break; + } + units -= count; + at.byte = (at.byte + width).min(bytes.len()); + at.low = false; + } + at +} + +/// Copy a normalized nonempty UTF-16 range, preserving split surrogate halves. +/// Complete scalar ranges use the rooted copy helper. Split boundaries are +/// staged in Rust-owned bytes before the destination allocation can collect. +pub(super) fn copy_utf16_range(s: *const StringHeader, start: u32, end: u32) -> *mut StringHeader { + if is_ascii_string(s) { + return string_copy_range(s, start as usize, end - start, end - start, 0); + } + let bytes = unsafe { slice::from_raw_parts(string_data(s), (*s).byte_len as usize) }; + let first = advance(bytes, Boundary::default(), start as usize); + // A suffix's end is already known: do not scan the entire remaining string. + let last = if end == unsafe { (*s).utf16_len } { + Boundary { + byte: bytes.len(), + low: false, + } + } else { + advance(bytes, first, (end - start) as usize) + }; + if !first.low && !last.low { + let part = &bytes[first.byte..last.byte]; + let flags = if unsafe { (*s).flags } & STRING_FLAG_HAS_LONE_SURROGATES != 0 + && bytes_have_lone_surrogate(part) + { + STRING_FLAG_HAS_LONE_SURROGATES + } else { + 0 + }; + return string_copy_range(s, first.byte, part.len() as u32, end - start, flags); + } + + let mut out = Vec::with_capacity(last.byte - first.byte + 6); + let mut byte_start = first.byte; + if first.low { + let (width, _, cp) = wtf8_step(bytes, first.byte); + let low = 0xDC00 + (cp.wrapping_sub(0x10000) & 0x3FF) as u16; + char_ops::push_code_unit_wtf8(&mut out, low); + byte_start = (byte_start + width).min(bytes.len()); + } + out.extend_from_slice(&bytes[byte_start..last.byte]); + if last.low { + let (_, _, cp) = wtf8_step(bytes, last.byte); + let high = 0xD800 + ((cp.wrapping_sub(0x10000) >> 10) & 0x3FF) as u16; + char_ops::push_code_unit_wtf8(&mut out, high); + } + js_string_from_bytes_known_utf16( + out.as_ptr(), + out.len() as u32, + end - start, + STRING_FLAG_HAS_LONE_SURROGATES, + ) +} diff --git a/crates/perry-runtime/src/string/slice_tests.rs b/crates/perry-runtime/src/string/slice_tests.rs new file mode 100644 index 0000000000..24ec857593 --- /dev/null +++ b/crates/perry-runtime/src/string/slice_tests.rs @@ -0,0 +1,127 @@ +use super::suffix_cursor::*; +use super::*; +use crate::value::JSValue; + +fn units(s: *const StringHeader) -> Vec { + (0..unsafe { (*s).utf16_len }) + .map(|i| js_string_char_code_at(s, i as i32) as u16) + .collect() +} + +#[test] +fn slice_utf16_bounds_and_lone_surrogates() { + for bytes in [ + b"".as_slice(), + b"abc", + "Γ€δΈ­πŸ˜€Γ–".as_bytes(), + "πŸ˜€πŸ˜€".as_bytes(), + b"\xed\xa0\x80A\xed\xbf\xbf", + b"\xed\xa0\x80\xf0\x9f\x98\x80\xed\xbf\xbf", + ] { + let scope = crate::gc::RuntimeHandleScope::new(); + let source = js_string_from_wtf8_bytes(bytes.as_ptr(), bytes.len() as u32); + let root = scope.root_string_ptr(source); + let expected = units(source); + let len = expected.len() as i32; + for start in -len - 2..=len + 2 { + for end in -len - 2..=len + 2 { + let normalize = |n: i32| if n < 0 { (len + n).max(0) } else { n.min(len) }; + let a = normalize(start) as usize; + let b = normalize(end) as usize; + let result = js_string_slice(root.get_raw_const_ptr(), start, end); + assert_eq!( + units(result), + expected[a..b.max(a)], + "slice({start}, {end}) of {expected:?}" + ); + let payload = unsafe { + slice::from_raw_parts(string_data(result), (*result).byte_len as usize) + }; + assert_eq!( + unsafe { (*result).utf16_len }, + compute_utf16_len_wtf8(payload) + ); + assert_eq!( + unsafe { (*result).flags } & STRING_FLAG_HAS_LONE_SURROGATES != 0, + bytes_have_lone_surrogate(payload) + ); + let a = start.clamp(0, len) as usize; + let b = end.clamp(0, len) as usize; + let result = js_string_substring(root.get_raw_const_ptr(), start, end); + assert_eq!(units(result), expected[a.min(b)..a.max(b)]); + } + } + assert!( + units(js_string_slice( + root.get_raw_const_ptr(), + i32::MIN, + i32::MAX + )) == expected + ); + } +} + +#[test] +fn suffix_cursor_matches_code_units_and_clamps_reads_and_advances() { + for text in ["", "abc", "Γ€δΈ­πŸ˜€Γ–", "πŸ˜€πŸ˜€"] { + let source = js_string_from_str(text); + let boxed = f64::from_bits(JSValue::string_ptr(source).bits()); + let expected: Vec = text.encode_utf16().collect(); + for step in [0, 1, 2, 3, 20] { + let mut cursor = SuffixCursor::default(); + let mut consumed = 0; + for _ in 0..expected.len() + 2 { + unsafe { + assert_eq!( + js_string_suffix_length(boxed, &cursor), + (expected.len() - consumed) as f64 + ); + for index in -1..=expected.len() as i32 { + let got = js_string_suffix_char_code_at(boxed, &cursor, index); + if index < 0 || consumed + index as usize >= expected.len() { + assert!(got.is_nan()); + } else { + assert_eq!(got, expected[consumed + index as usize] as f64); + } + } + js_string_suffix_advance(boxed, &mut cursor, step); + } + consumed = (consumed + step as usize).min(expected.len()); + } + } + } + let short = f64::from_bits(JSValue::try_short_string(b"abc").unwrap().bits()); + let mut cursor = SuffixCursor::default(); + unsafe { + js_string_suffix_advance(short, &mut cursor, 1); + assert_eq!(js_string_suffix_length(short, &cursor), 2.0); + assert_eq!(js_string_suffix_char_code_at(short, &cursor, 0), 98.0); + } +} + +#[test] +fn suffix_cursor_skips_unpaired_continuation_bytes_like_char_code_at() { + let bytes = b"\x80A\x80\xf0\x9f\x98\x80\x80B"; + let source = js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); + let boxed = f64::from_bits(JSValue::string_ptr(source).bits()); + let expected = units(source); + assert_eq!(expected, [65, 55357, 56832, 66]); + let mut cursor = SuffixCursor::default(); + for consumed in 0..=expected.len() { + unsafe { + for index in 0..expected.len() - consumed { + assert_eq!( + js_string_suffix_char_code_at(boxed, &cursor, index as i32), + expected[consumed + index] as f64 + ); + } + assert!(js_string_suffix_char_code_at( + boxed, + &cursor, + (expected.len() - consumed) as i32 + ) + .is_nan()); + js_string_suffix_advance(boxed, &mut cursor, 1); + } + } +} diff --git a/crates/perry-runtime/src/string/suffix_cursor.rs b/crates/perry-runtime/src/string/suffix_cursor.rs new file mode 100644 index 0000000000..82ba1f02bc --- /dev/null +++ b/crates/perry-runtime/src/string/suffix_cursor.rs @@ -0,0 +1,116 @@ +//! Allocation-free operations on a compiler-proven, non-escaping suffix. +//! +//! The source remains a normal, rooted string. The stack cursor contains only +//! offsets, so moving GC cannot invalidate it. No substring or backing store +//! escapes this representation; ordinary string consumers still get flat data. + +use super::slice_range::{advance, Boundary}; +use super::*; + +/// Mirrors the code generator's zero-initialized `[3 x i32]` stack allocation. +#[repr(C)] +#[derive(Default)] +pub struct SuffixCursor { + byte: u32, + consumed: u32, + low: u32, +} + +fn length(source: f64) -> u32 { + let value = crate::value::JSValue::from_bits(source.to_bits()); + if value.is_short_string() { + value.short_string_len() as u32 + } else { + unsafe { (*value.as_string_ptr()).utf16_len } + } +} + +/// The compiler guards `source` as a string and supplies live stack storage. +#[no_mangle] +pub unsafe extern "C" fn js_string_suffix_length(source: f64, cursor: *const SuffixCursor) -> f64 { + length(source).saturating_sub((*cursor).consumed) as f64 +} + +/// Advance by a nonnegative, already-coerced UTF-16 count, clamped to the tail. +#[no_mangle] +pub unsafe extern "C" fn js_string_suffix_advance( + source: f64, + cursor: *mut SuffixCursor, + count: i32, +) { + let count = (count.max(0) as u32).min(length(source).saturating_sub((*cursor).consumed)); + let mut scratch = [0; crate::value::SHORT_STRING_MAX_LEN]; + let (data, len) = str_bytes_from_jsvalue(source, &mut scratch).unwrap(); + let bytes = if len == 0 { + &[] + } else { + slice::from_raw_parts(data, len as usize) + }; + let at = advance( + bytes, + Boundary { + byte: (*cursor).byte as usize, + low: (*cursor).low != 0, + }, + count as usize, + ); + (*cursor).byte = at.byte as u32; + (*cursor).low = u32::from(at.low); + (*cursor).consumed += count; +} + +/// Read a UTF-16 code unit relative to the current suffix without materializing it. +#[no_mangle] +pub unsafe extern "C" fn js_string_suffix_char_code_at( + source: f64, + cursor: *const SuffixCursor, + index: i32, +) -> f64 { + if index < 0 || index as u32 >= length(source).saturating_sub((*cursor).consumed) { + return f64::NAN; + } + let mut scratch = [0; crate::value::SHORT_STRING_MAX_LEN]; + let (data, len) = str_bytes_from_jsvalue(source, &mut scratch).unwrap(); + let bytes = slice::from_raw_parts(data, len as usize); + let mut at = advance( + bytes, + Boundary { + byte: (*cursor).byte as usize, + low: (*cursor).low != 0, + }, + index as usize, + ); + if at.byte >= bytes.len() { + return f64::NAN; + } + // Invalid FFI/binary payloads can contain stray continuation bytes, which + // the runtime's UTF-16 counter and charCodeAt skip rather than count. + while at.byte < bytes.len() && wtf8_step(bytes, at.byte).1 == 0 { + at.byte += 1; + } + if at.byte >= bytes.len() { + return f64::NAN; + } + let (_, units, cp) = wtf8_step(bytes, at.byte); + if units == 2 { + let v = cp.wrapping_sub(0x10000); + if at.low { + (0xDC00 + (v & 0x3FF)) as f64 + } else { + (0xD800 + ((v >> 10) & 0x3FF)) as f64 + } + } else { + cp as f64 + } +} + +#[cfg(feature = "keepalive-anchors")] +#[used] +static KEEP_LENGTH: unsafe extern "C" fn(f64, *const SuffixCursor) -> f64 = js_string_suffix_length; +#[cfg(feature = "keepalive-anchors")] +#[used] +static KEEP_ADVANCE: unsafe extern "C" fn(f64, *mut SuffixCursor, i32) = js_string_suffix_advance; +#[cfg(feature = "keepalive-anchors")] +#[used] +static KEEP_CHAR_CODE: unsafe extern "C" fn(f64, *const SuffixCursor, i32) -> f64 = + js_string_suffix_char_code_at; diff --git a/test-files/test_gap_gc_string_suffix_cursor.ts b/test-files/test_gap_gc_string_suffix_cursor.ts new file mode 100644 index 0000000000..8a8a1c2de2 --- /dev/null +++ b/test-files/test_gap_gc_string_suffix_cursor.ts @@ -0,0 +1,28 @@ +// #10061: the source remains rooted after its original binding is overwritten. +// parity-env: PERRY_GC_SCHEDULE_SEED=10061 PERRY_GC_SCHEDULE_RATE=1 PERRY_GC_SCHEDULE_ALLOC_KB=0 PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1 PERRY_GC_PROTECT_FROMSPACE=1 +function parse(): number { + let source = "Γ€δΈ­πŸ˜€Γ–".repeat(20); + let s = source; + source = "gone"; + let hash = 0; + while (s.length) { + const garbage = [s.length, hash, { value: hash }]; + hash = (hash * 31 + s.charCodeAt(0) + garbage[0]) % 1000000007; + s = s.slice(1); + } + return hash; +} +for (let i = 0; i < 5; i++) console.log(parse()); + +// A retained substring owns its bytes after both the source and other suffixes die. +function keep(): string { + let source = "Γ€δΈ­πŸ˜€Γ–".repeat(30); + const kept = source.slice(3, 6); + source = "released"; + for (let i = 0; i < 50; i++) { + const garbage = ("noise" + i).repeat(100); + if (garbage.length < 100) throw new Error("invalid allocation"); + } + return kept; +} +console.log(JSON.stringify(keep())); diff --git a/test-files/test_gap_string_slice_utf16_suffix.ts b/test-files/test_gap_string_slice_utf16_suffix.ts new file mode 100644 index 0000000000..16b56cc4d5 --- /dev/null +++ b/test-files/test_gap_string_slice_utf16_suffix.ts @@ -0,0 +1,59 @@ +// #10061: materialized slices and scalar suffix consumers must agree with Node. +function inspect(input: string): string { + let s = input; + let out = ""; + for (let i = 0; i < 16 && s.length; i++) { + out += s.length + ":" + s.charCodeAt(0) + ","; + s = s.slice(1); + } + return out; +} +console.log(inspect("Γ€δΈ­πŸ˜€Γ–")); +console.log(inspect("\ud800AπŸ˜€\udfff")); +console.log(inspect("")); + +const input = "Γ€δΈ­πŸ˜€Γ–\ud800A\udfff"; +for (let start = -11; start <= 11; start++) { + for (let end = -11; end <= 11; end++) { + const part = input.slice(start, end); + console.log(start, end, part.length, JSON.stringify(part), part.isWellFormed()); + } +} +console.log("πŸ˜€".substring(0, 1).charCodeAt(0), "πŸ˜€".substr(1, 1).charCodeAt(0)); + +// Escapes and aliases require ordinary flat strings. +let current = "Γ€δΈ­πŸ˜€Γ–"; +const retained: string[] = []; +while (current.length) { + retained.push(current); + current = current.slice(1); +} +for (let i = 0; i < retained.length; i++) console.log(JSON.stringify(retained[i])); +let captured = "πŸ˜€Γ–"; +const read = () => captured; +captured = captured.slice(1); +console.log(JSON.stringify(read())); +let builder = ""; +builder += "πŸ˜€"; +let alias = builder; +builder += "X"; +console.log(inspect(alias), builder); + +// Different strides, out-of-range reads, and repeated slicing after exhaustion. +function stride(input: string): string { + let s = input; + let result = ""; + for (let i = 0; i < 8; i++) { + result += s.length + ":" + s.charCodeAt(0) + ":" + s.charCodeAt(1) + ","; + s = s.slice(2); + } + return result; +} +console.log(stride("Γ€δΈ­πŸ˜€Γ–πŸ˜€")); +// A string annotation must not erase a non-string receiver's own methods. +const custom: any = { + length: 3, + charCodeAt: (i: number) => 70 + i, + slice: (_: number) => "xy", +}; +console.log(stride(custom)); diff --git a/test-parity/gc_repsel_corpus.txt b/test-parity/gc_repsel_corpus.txt index 80310c49f9..1e14f29a38 100644 --- a/test-parity/gc_repsel_corpus.txt +++ b/test-parity/gc_repsel_corpus.txt @@ -859,3 +859,5 @@ test_gap_gc_coalesce_local_root # for-in output/receiver custody across Proxy callbacks (#4644) test_gap_gc_for_in_proxy_callback_roots + +test_gap_gc_string_suffix_cursor From d612b25528066afe0ae31f2d56176ebbf210e99d Mon Sep 17 00:00:00 2001 From: Ralph Kuepper Date: Fri, 11 Sep 2026 15:19:52 +0200 Subject: [PATCH 2/9] docs: record string suffix benchmark and GC validation (cherry picked from commit 8d66646ffe4f7eb8f0b9d7b9f2573ee7af6d4bd8) --- benchmarks/string_slice/README.md | 73 +++++++ benchmarks/string_slice/fixed-artifacts.json | 8 + benchmarks/string_slice/fixed.json | 204 +++++++++++++++++++ benchmarks/string_slice/validation.json | 89 ++++++++ 4 files changed, 374 insertions(+) create mode 100644 benchmarks/string_slice/fixed-artifacts.json create mode 100644 benchmarks/string_slice/fixed.json create mode 100644 benchmarks/string_slice/validation.json diff --git a/benchmarks/string_slice/README.md b/benchmarks/string_slice/README.md index 518abff5ab..85f7fd7937 100644 --- a/benchmarks/string_slice/README.md +++ b/benchmarks/string_slice/README.md @@ -77,3 +77,76 @@ Node: 5:228,4:20013,3:55357,2:56832,1:214, The baseline ASCII exponent is 2.013 over completed sizes 100–10,000; 100,000 times out. Unicode fails checksum stability at 100 and mismatches at 1,000 and 10,000, so its baseline speed is not classified. + +## Final measurements + +CPU: AMD Ryzen 5 7640HS, 12 logical processors. LLVM 22.1.8. +Compiler/archive source revision: `0c5348ce5694654d8aa6e4477900ebc9fffe67de`. +`fixed-artifacts.json` records the matching compiler and archive SHA-256 hashes. +All ten original workload/size pairs complete with stable checksums matching Node. + +### ASCII + +| n | Base Perry ms / status | Fixed Perry ms | Fixed Node ms | Perry / Node | Checksum | +|---:|---:|---:|---:|---:|---:| +| 100 | 0.015718 | 0.005188 | 0.006745 | 0.77x | 464151292 | +| 1,000 | 1.390067 | 0.052812 | 0.080744 | 0.65x | 710929850 | +| 10,000 | 166.934100 | 0.526479 | 0.804260 | 0.65x | 535454277 | +| 100,000 | TIMEOUT | 5.251350 | 7.428233 | 0.71x | 35382078 | +| 1,000,000 | NOT RUN | 52.088600 | 71.593200 | 0.73x | 153135489 | + +Fixed log-log slopes over all five sizes: Perry **1.000**, Node **1.002**. + +### UNICODE + +| n | Base Perry ms / status | Fixed Perry ms | Fixed Node ms | Perry / Node | Checksum | +|---:|---:|---:|---:|---:|---:| +| 100 | ERROR | 0.006948 | 0.010043 | 0.69x | 319467163 | +| 1,000 | 14.891150 (wrong checksum) | 0.070113 | 0.100875 | 0.70x | 431622199 | +| 10,000 | 1736.238200 (wrong checksum) | 0.699286 | 1.004910 | 0.70x | 36132863 | +| 100,000 | TIMEOUT | 7.232333 | 8.955500 | 0.81x | 49951631 | +| 1,000,000 | NOT RUN | 69.962600 | 89.631600 | 0.78x | 481167302 | + +Fixed log-log slopes over all five sizes: Perry **1.002**, Node **0.985**. + +## Validation and host limitations + +- Final native reduction exactly matches Node: `5:228,4:20013,3:55357,2:56832,1:214,`. +- Final compiled boundary/aliasing fixture matches Node, including empty and + negative bounds, both surrogate halves, lone surrogates, retained aliases, + captured locals, stride-two reads, and a non-string runtime receiver. +- Final forced-GC fixture matches Node with 1,061 copying minors, + 168 moved objects and 1,055 loop polls; evacuation verification + and from-space protection are enabled. Two runtime slice GC tests pass, + including an assertion that the source address actually changes. +- Runtime suite: 3,439 passed, one failed, four ignored (`--test-threads=1`). + All three new slice/cursor unit tests pass. The failing unchanged + `emergency_full_trace_is_excluded_from_ordinary_pause_stats` assertion expects + allocator trimming to be unsupported on this Windows host; it fails in + isolation too. +- Compiler unit suite (candidate build): 1,460 passed, three failed, one ignored. All three new + eligibility-analysis tests pass. The unchanged failures are a frameless-entry + assembly assertion and two native-emission byte-equality assertions; each + also fails in isolation on Windows. See `validation.json` for exact names. +- The broader 68-case string sweep on the initial candidate build has 47 passes, + one parity mismatch, 19 compile failures, and one skip. Every compile failure + reports the existing Windows RS4GC/WinEH restriction (#7354). The mismatch is + `test_gap_tolocalestring_locale_options_9414`: Node's default locale is German + on this host, while Perry formats default-locale rows as English. All three + focused native fixtures were recompiled and rechecked on the final build. +- Test registration, Node-version consistency, GC root-holder/store/address + inventories, local-binding proof audit, architecture checks, and public + baseline harness tests pass. The Rust file-size gate passes. Recursive + `rustfmt --check --edition 2021` on both changed crate roots passes; the + workspace-wide `cargo fmt` invocation exceeds Windows' command-line limit. +- The quick pre-tag gate's published-benchmark freshness check remains red. + Its fingerprinted inputs are identical to pristine base (recorded in + `public-baseline-check.json`); this PR does not regenerate that unrelated + published artifact. These host/gate limitations are reported, not counted + as passing checks. Linux/macOS and full workspace checks were not run locally. + +Re-run the affected suites with `cargo test --release --locked --lib -p +perry-runtime -- --test-threads=1` and `cargo test --release --locked --lib -p +perry-codegen`. Use the same build environment as above. The corpus sweep was +`bash run_parity_tests.sh --filter string` with `PERRY_SKIP_BUILD=1`, `PERRY_BIN` +and `PERRY_RUNTIME_DIR` pointing to the matching build. No version files change. diff --git a/benchmarks/string_slice/fixed-artifacts.json b/benchmarks/string_slice/fixed-artifacts.json new file mode 100644 index 0000000000..c3f1cec780 --- /dev/null +++ b/benchmarks/string_slice/fixed-artifacts.json @@ -0,0 +1,8 @@ +{ + "source_revision": "0c5348ce5694654d8aa6e4477900ebc9fffe67de", + "sha256": { + "target/release/perry.exe": "75c6318c1938254cd0466d03116069e47432e5ba69d90788e59d95b2682bc7ca", + "target/release/perry_runtime.lib": "20ae523b5c57eeb25960c09719d4147c6fc53084e0f6fb290708472a6866f219", + "target/release/perry_stdlib.lib": "675573b11328430592a8859dc4842832be55cb3d119c3bf85c1cfaa13795bff0" + } +} diff --git a/benchmarks/string_slice/fixed.json b/benchmarks/string_slice/fixed.json new file mode 100644 index 0000000000..21c9cb61e9 --- /dev/null +++ b/benchmarks/string_slice/fixed.json @@ -0,0 +1,204 @@ +{ + "revision": "0c5348ce5694654d8aa6e4477900ebc9fffe67de", + "node": "v26.5.1", + "platform": "Windows-11-10.0.26200-SP0", + "processor": "AMD64 Family 25 Model 116 Stepping 1, AuthenticAMD", + "workloads": { + "ascii": { + "sha256": "3895cd80f04d760f98c3447607347ff5ea0c8d900129a187ddfd6f55be100bfb", + "engines": { + "node": [ + { + "name": "string-slice-parse-loop-ascii", + "category": "strings", + "n": 100, + "ms_per_run": 0.0067449426837486726, + "runs": 19904, + "checksum": 464151292 + }, + { + "name": "string-slice-parse-loop-ascii", + "category": "strings", + "n": 1000, + "ms_per_run": 0.08074435483871113, + "runs": 1718, + "checksum": 710929850 + }, + { + "name": "string-slice-parse-loop-ascii", + "category": "strings", + "n": 10000, + "ms_per_run": 0.8042599999999948, + "runs": 175, + "checksum": 535454277 + }, + { + "name": "string-slice-parse-loop-ascii", + "category": "strings", + "n": 100000, + "ms_per_run": 7.428233333333348, + "runs": 21, + "checksum": 35382078 + }, + { + "name": "string-slice-parse-loop-ascii", + "category": "strings", + "n": 1000000, + "ms_per_run": 71.59320000000002, + "runs": 7, + "checksum": 153135489 + } + ], + "perry": [ + { + "name": "string-slice-parse-loop-ascii", + "category": "strings", + "n": 100, + "ms_per_run": 0.005188482490272402, + "runs": 26905, + "checksum": 464151292, + "checksum_match": true + }, + { + "name": "string-slice-parse-loop-ascii", + "category": "strings", + "n": 1000, + "ms_per_run": 0.05281160949868106, + "runs": 2656, + "checksum": 710929850, + "checksum_match": true + }, + { + "name": "string-slice-parse-loop-ascii", + "category": "strings", + "n": 10000, + "ms_per_run": 0.5264789473684213, + "runs": 269, + "checksum": 535454277, + "checksum_match": true + }, + { + "name": "string-slice-parse-loop-ascii", + "category": "strings", + "n": 100000, + "ms_per_run": 5.251350000000002, + "runs": 28, + "checksum": 35382078, + "checksum_match": true + }, + { + "name": "string-slice-parse-loop-ascii", + "category": "strings", + "n": 1000000, + "ms_per_run": 52.08860000000004, + "runs": 7, + "checksum": 153135489, + "checksum_match": true + } + ] + }, + "slopes": { + "node": 1.001556039034457, + "perry": 1.0000946220402622 + } + }, + "unicode": { + "sha256": "ca1ea8b7ce414346c145e60062c2dbe2be6209f78e203df1567f9f86da9efedb", + "engines": { + "node": [ + { + "name": "string-slice-parse-loop-unicode", + "category": "strings", + "n": 100, + "ms_per_run": 0.01004327309236978, + "runs": 14125, + "checksum": 319467163 + }, + { + "name": "string-slice-parse-loop-unicode", + "category": "strings", + "n": 1000, + "ms_per_run": 0.10087537688442307, + "runs": 1435, + "checksum": 431622199 + }, + { + "name": "string-slice-parse-loop-unicode", + "category": "strings", + "n": 10000, + "ms_per_run": 1.004910000000001, + "runs": 142, + "checksum": 36132863 + }, + { + "name": "string-slice-parse-loop-unicode", + "category": "strings", + "n": 100000, + "ms_per_run": 8.955499999999992, + "runs": 21, + "checksum": 49951631 + }, + { + "name": "string-slice-parse-loop-unicode", + "category": "strings", + "n": 1000000, + "ms_per_run": 89.63160000000005, + "runs": 7, + "checksum": 481167302 + } + ], + "perry": [ + { + "name": "string-slice-parse-loop-unicode", + "category": "strings", + "n": 100, + "ms_per_run": 0.006948454324418352, + "runs": 20095, + "checksum": 319467163, + "checksum_match": true + }, + { + "name": "string-slice-parse-loop-unicode", + "category": "strings", + "n": 1000, + "ms_per_run": 0.07011328671328723, + "runs": 1994, + "checksum": 431622199, + "checksum_match": true + }, + { + "name": "string-slice-parse-loop-unicode", + "category": "strings", + "n": 10000, + "ms_per_run": 0.699286206896551, + "runs": 202, + "checksum": 36132863, + "checksum_match": true + }, + { + "name": "string-slice-parse-loop-unicode", + "category": "strings", + "n": 100000, + "ms_per_run": 7.232333333333344, + "runs": 21, + "checksum": 49951631, + "checksum_match": true + }, + { + "name": "string-slice-parse-loop-unicode", + "category": "strings", + "n": 1000000, + "ms_per_run": 69.96260000000007, + "runs": 7, + "checksum": 481167302, + "checksum_match": true + } + ] + }, + "slopes": { + "node": 0.9849476421934262, + "perry": 1.0019433575448282 + } + } + } +} diff --git a/benchmarks/string_slice/validation.json b/benchmarks/string_slice/validation.json new file mode 100644 index 0000000000..183dba195a --- /dev/null +++ b/benchmarks/string_slice/validation.json @@ -0,0 +1,89 @@ +{ + "source_revision": "0c5348ce5694654d8aa6e4477900ebc9fffe67de", + "fixtures": [ + { + "source": "benchmarks/string_slice/string-slice-astral.ts", + "node_exit": 0, + "perry_exit": 0, + "stdout_match": true, + "stdout_sha256": "f03c3c3ddab4baed9e5c2b65edbce7bda6779c5bd63f027379320f4c2a427b04" + }, + { + "source": "test-files/test_gap_string_slice_utf16_suffix.ts", + "node_exit": 0, + "perry_exit": 0, + "stdout_match": true, + "stdout_sha256": "45fda4b878798c05d5fb83427a820a5a13c79de2d9c09df5b9779f83dc7900e4" + }, + { + "source": "test-files/test_gap_gc_string_suffix_cursor.ts", + "node_exit": 0, + "perry_exit": 0, + "stdout_match": true, + "stdout_sha256": "af4e08e8b27149114752421a04c6b1e5fe4df47e9bfb76098bd2457084cede50", + "gc_schedule": { + "forced_collections": 1061, + "copying_minors": 1061, + "moved_objects": 168, + "loop_polls": 1055 + } + } + ], + "runtime_suite": { + "passed": 3439, + "failed": 1, + "ignored": 4, + "failure": "gc::tests::telemetry_verifier::emergency_full_trace_is_excluded_from_ordinary_pause_stats" + }, + "codegen_suite": { + "passed": 1460, + "failed": 3, + "ignored": 1, + "failures": [ + "codegen::spec_preserve_none_tests::the_clone_entry_is_shrink_wrapped_frameless", + "native_emit::tests::split_native_construction_propagates_shadow_backend_to_workers", + "native_emit::tests::split_native_construction_lowers_precise_roots_before_rs4gc" + ], + "build": "candidate build; final native fixtures rechecked on the final build" + }, + "broader_string_parity": { + "build": "initial candidate build; the final build reran all three focused fixtures above", + "summary": { + "parity_pass": 47, + "parity_fail": 1, + "compile_fail": 19, + "crash_fail": 0, + "node_fail": 0, + "skipped": 1, + "total_run": 48, + "parity_percentage": 97.9 + }, + "failures": { + "parity": [ + "test_gap_tolocalestring_locale_options_9414" + ], + "compile": [ + "test_gap_2786_2880_2782_2789_string_semantics", + "test_gap_5591_method_string_coercion", + "test_gap_6370_tostring_coercion_own_override", + "test_gap_9713_dynamic_number_tostring", + "test_gap_bigint_tolocalestring_intl_gate", + "test_gap_dynamic_string_length_generic_tower", + "test_gap_gc_string_repeat_reentrant_count", + "test_gap_json_direct_strings", + "test_gap_number_string_2864_2948_2855", + "test_gap_object_string_wrappers", + "test_gap_object_tolocalestring_primitive_receiver", + "test_gap_response_boxed_string_body_5453", + "test_gap_sso_concat_string_index", + "test_gap_string_locale_2781_2845_2897", + "test_issue_317_string_matchall", + "test_issue_8432_string_accumulators", + "test_issue_9810_virtual_string_indices", + "test_parity_querystring", + "test_parity_string_decoder" + ], + "crash": [] + } + } +} From 086d18a6f7fc6d39177a7ec4e492dfae32ffa54d Mon Sep 17 00:00:00 2001 From: Ralph Kuepper Date: Fri, 11 Sep 2026 15:20:10 +0200 Subject: [PATCH 3/9] docs: associate string slice changelog with PR 10075 (cherry picked from commit 6183863dde4c28f0aa2f1732522be7fa2d1beb87) --- changelog.d/{10061-string-slice.md => 10075-string-slice.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{10061-string-slice.md => 10075-string-slice.md} (100%) diff --git a/changelog.d/10061-string-slice.md b/changelog.d/10075-string-slice.md similarity index 100% rename from changelog.d/10061-string-slice.md rename to changelog.d/10075-string-slice.md From 5db10b2c6ea3dd95a81c001b3307bb42b8130626 Mon Sep 17 00:00:00 2001 From: Ralph Kuepper Date: Fri, 11 Sep 2026 15:10:11 +0200 Subject: [PATCH 4/9] fix(runtime): make dense array shift drains linear Conflicts resolved on the train (#10072/#10074 landed first): - array/concat_reverse.rs, array/sort.rs: kept main's newer structure -- #10072's three-arm linear append in js_array_concat, and the publish_sorted_values / with_sorted_indices refactor -- then applied this change's actual intent to it, replacing every open-coded `header + size_of::()` element base with `crate::array::array_elements_ptr`. That includes four sites in sort.rs that postdate this branch's base and so were never converted here. - array/push_pop.rs: union -- this change's offset-aware accessor plus main's TAG_HOLE -> undefined conversion, which spread needs and this branch predates. - scripts/gc_runtime_root_holders.json: merged structurally, not textually. The PASS1_MARKED re-audit prose is base + main's #10055/#10054 paragraphs + this change's #10060 paragraph, and each `window.sources` pin is taken from the side that actually moved it (gc/census.rs from here, gc/mod.rs from main) so neither is reverted to the base hash. - test-parity/gc_repsel_corpus.txt: both new entries kept. --- benchmarks/array-shift-10060/array-push.ts | 91 ++++++++++++++++++ .../array-shift-10060/array-shift-queue.ts | 89 ++++++++++++++++++ benchmarks/array-shift-10060/run.py | 94 +++++++++++++++++++ changelog.d/10060-array-shift-queue.md | 11 +++ crates/perry-codegen/src/array_storage.rs | 25 +++++ crates/perry-codegen/src/expr/array_pop.rs | 2 +- crates/perry-codegen/src/expr/array_push.rs | 12 +-- .../src/expr/element_shape_guard.rs | 2 +- .../perry-codegen/src/expr/i32_fast_path.rs | 8 +- crates/perry-codegen/src/expr/index.rs | 4 +- crates/perry-codegen/src/expr/index_get.rs | 4 +- .../src/expr/index_get/guarded_array.rs | 20 ++-- .../expr/index_get/inline_dyn_typed_array.rs | 7 +- crates/perry-codegen/src/expr/index_set.rs | 8 +- .../src/expr/index_set_guarded.rs | 4 +- .../src/expr/index_set_packed_loop.rs | 12 +-- .../src/expr/logical_collections.rs | 4 +- .../perry-codegen/src/expr/masked_window.rs | 8 +- .../src/expr/ptr_numarray_access.rs | 8 +- .../perry-codegen/src/expr/string_window.rs | 4 +- .../perry-codegen/src/expr/this_super_call.rs | 2 +- crates/perry-codegen/src/lib.rs | 1 + .../src/stmt/cached_field_index_return.rs | 5 +- crates/perry-codegen/src/stmt/loops.rs | 12 ++- .../src/stmt/stable_packed_loop.rs | 10 +- crates/perry-ext-better-sqlite3/src/lib.rs | 3 +- crates/perry-ext-http/src/client_overload.rs | 3 +- .../src/client_request_surface.rs | 2 +- crates/perry-ext-http/src/server/types.rs | 3 +- crates/perry-runtime/src/array/alloc.rs | 14 +-- .../src/array/collection_tag_tests.rs | 2 +- .../perry-runtime/src/array/concat_reverse.rs | 23 ++--- .../src/array/element_shape_tests.rs | 2 +- crates/perry-runtime/src/array/fill_extend.rs | 4 +- crates/perry-runtime/src/array/flat_clone.rs | 36 +++---- .../src/array/forwarding_tests.rs | 2 +- crates/perry-runtime/src/array/from_concat.rs | 17 ++-- crates/perry-runtime/src/array/generic.rs | 8 +- .../perry-runtime/src/array/generic_object.rs | 2 +- crates/perry-runtime/src/array/header.rs | 24 +++-- .../src/array/header_gc_slots.rs | 12 ++- crates/perry-runtime/src/array/immutable.rs | 22 ++--- crates/perry-runtime/src/array/indexing.rs | 18 ++-- .../src/array/indexing_support.rs | 2 +- .../perry-runtime/src/array/iter_methods.rs | 10 +- crates/perry-runtime/src/array/join.rs | 2 +- crates/perry-runtime/src/array/jsvalue_api.rs | 2 +- crates/perry-runtime/src/array/mod.rs | 5 + .../perry-runtime/src/array/numeric_range.rs | 4 +- crates/perry-runtime/src/array/push_pop.rs | 79 ++++++++-------- .../perry-runtime/src/array/reduce_right.rs | 2 +- crates/perry-runtime/src/array/search.rs | 2 +- .../src/array/shift_queue_tests.rs | 93 ++++++++++++++++++ crates/perry-runtime/src/array/sort.rs | 21 +++-- .../perry-runtime/src/array/splice_slice.rs | 11 ++- .../src/array/spread_dense_tests.rs | 2 +- crates/perry-runtime/src/array/storage.rs | 72 ++++++++++++++ crates/perry-runtime/src/array/subclass.rs | 24 ++--- .../src/array/subclass_elements.rs | 6 +- crates/perry-runtime/src/array/tests.rs | 4 +- .../src/array/tests_from_string_codepoints.rs | 2 +- crates/perry-runtime/src/async_hooks.rs | 4 +- crates/perry-runtime/src/buffer/encode.rs | 2 +- crates/perry-runtime/src/buffer/from.rs | 3 +- crates/perry-runtime/src/buffer/iter.rs | 3 +- crates/perry-runtime/src/builtins/console.rs | 2 +- .../perry-runtime/src/builtins/formatting.rs | 14 +-- .../src/builtins/formatting/errors.rs | 2 +- .../src/builtins/formatting/util_format.rs | 4 +- crates/perry-runtime/src/builtins/globals.rs | 12 +-- crates/perry-runtime/src/builtins/table.rs | 20 ++-- .../src/child_process/registry.rs | 12 +-- .../src/child_process/value_util.rs | 2 +- crates/perry-runtime/src/gc/census.rs | 6 +- crates/perry-runtime/src/gc/fromspace_scan.rs | 5 +- crates/perry-runtime/src/gc/heap_snapshot.rs | 4 +- .../tests/array_pointer_slot_enumeration.rs | 2 +- crates/perry-runtime/src/gc/tests/copying.rs | 21 +++-- .../copying/all_pointer_elements_7469.rs | 4 +- .../src/gc/tests/copying/shift_queue.rs | 86 +++++++++++++++++ .../gc/tests/copying/survival_and_malloc.rs | 2 +- .../src/gc/tests/helper_stores.rs | 5 +- .../src/gc/tests/promote_in_place.rs | 2 +- .../tests/runtime_roots/callback_scanners.rs | 2 +- .../tests/runtime_roots/transient_handles.rs | 2 +- crates/perry-runtime/src/gc/tests/support.rs | 2 +- .../src/gc/tests/young_log_tests.rs | 5 +- crates/perry-runtime/src/gc/verify.rs | 3 +- .../src/json/construction_array.rs | 31 ++---- crates/perry-runtime/src/json/parser.rs | 3 +- crates/perry-runtime/src/json/replacer.rs | 19 ++-- crates/perry-runtime/src/json/stringify.rs | 8 +- .../src/json/stringify_data_record.rs | 4 +- .../perry-runtime/src/json/stringify_flat.rs | 20 ++-- .../src/json/stringify_nested_records.rs | 8 +- .../src/json/stringify_primitive_array.rs | 4 +- .../src/json/stringify_primitive_object.rs | 5 +- .../src/json/stringify_record_output.rs | 27 +++--- .../src/json/stringify_shape_template.rs | 4 +- .../src/json/stringify_tojson_probe.rs | 3 +- crates/perry-runtime/src/json_tape.rs | 6 +- crates/perry-runtime/src/node_stream_json.rs | 3 +- crates/perry-runtime/src/object/alloc.rs | 30 ++++-- crates/perry-runtime/src/object/assert.rs | 2 +- .../perry-runtime/src/object/delete_rest.rs | 21 +++-- .../src/object/field_get_set/entries_shape.rs | 6 +- .../src/object/field_get_set/enumeration.rs | 20 ++-- .../object/field_set_by_name/fast_paths.rs | 2 +- crates/perry-runtime/src/object/gc_slots.rs | 2 +- crates/perry-runtime/src/object/groupby.rs | 2 +- .../src/object/has_own_helpers.rs | 2 +- .../perry-runtime/src/object/keys_lookup.rs | 2 +- .../native_module/namespace_builders.rs | 3 +- .../src/object/object_ops/accessors.rs | 4 +- crates/perry-runtime/src/object/spill.rs | 8 +- crates/perry-runtime/src/param_type_guard.rs | 6 +- .../perry-runtime/src/promise/combinators.rs | 2 +- .../perry-runtime/src/promise/then_probe.rs | 2 +- crates/perry-runtime/src/proxy/put_value.rs | 2 +- crates/perry-runtime/src/set.rs | 2 +- crates/perry-runtime/src/string/char_ops.rs | 4 +- crates/perry-runtime/src/string/split.rs | 4 +- crates/perry-runtime/src/thread.rs | 12 +-- crates/perry-runtime/src/typed_feedback.rs | 8 +- .../perry-runtime/src/typedarray/transform.rs | 4 +- crates/perry-runtime/src/util_promisify.rs | 16 ++-- .../perry-stdlib/src/async_local_storage.rs | 2 +- crates/perry-stdlib/src/sqlite/better.rs | 3 +- scripts/addr_class_allowlist.txt | 1 + scripts/gc_runtime_root_holders.json | 4 +- test-files/test_gap_array_shift_observable.ts | 39 ++++++++ test-files/test_gap_gc_array_shift_queue.ts | 60 ++++++++++++ test-parity/gc_repsel_corpus.txt | 1 + 133 files changed, 1163 insertions(+), 476 deletions(-) create mode 100644 benchmarks/array-shift-10060/array-push.ts create mode 100644 benchmarks/array-shift-10060/array-shift-queue.ts create mode 100644 benchmarks/array-shift-10060/run.py create mode 100644 changelog.d/10060-array-shift-queue.md create mode 100644 crates/perry-codegen/src/array_storage.rs create mode 100644 crates/perry-runtime/src/array/shift_queue_tests.rs create mode 100644 crates/perry-runtime/src/array/storage.rs create mode 100644 crates/perry-runtime/src/gc/tests/copying/shift_queue.rs create mode 100644 test-files/test_gap_array_shift_observable.ts create mode 100644 test-files/test_gap_gc_array_shift_queue.ts diff --git a/benchmarks/array-shift-10060/array-push.ts b/benchmarks/array-shift-10060/array-push.ts new file mode 100644 index 0000000000..d2622b0755 --- /dev/null +++ b/benchmarks/array-shift-10060/array-push.ts @@ -0,0 +1,91 @@ +// @runtime {"name": "array-push", "category": "arrays", "verification": "checksum", "sources": [{"file": "crates/perry-runtime/src/array/push_pop.rs", "function": "js_array_push_f64"}], "hypothesis": "Ordinary single-element append control for the array-shift representation change.", "notes": "", "asynchronous": false, "output_stderr": false, "fresh_input": true} +// Standalone file. Shared helpers/driver are inlined by common.py. + +let seed = 0x12345678; +function rnd(): number { + seed ^= seed << 13; seed ^= seed >>> 17; seed ^= seed << 5; + return (seed >>> 0) / 4294967296; +} +function numbers(n: number): number[] { + const a: number[] = []; + for (let i = 0; i < n; i++) a.push(Math.floor(rnd() * 1000000)); + return a; +} +function hashArray(a: number[]): number { + let h = a.length; + for (let i = 0; i < a.length; i++) h = (h * 31 + a[i]) % 1000000007; + return h; +} +// Bounded checksum work avoids making string slicing/indexing part of every +// string benchmark's asymptotic cost. The workload itself consumes its result. +function hashString(s: string): number { + let h = s.length; + const step = Math.max(1, Math.floor(s.length / 32)); + for (let i = 0; i < s.length; i += step) h = (h * 31 + s.charCodeAt(i)) % 1000000007; + return h; +} + +function setup(n: number): number[] { return numbers(n); } +function run(q: number[]): number { + let h = 0; + const a: number[] = []; + for (let i = 0; i < q.length; i++) a.push(q[i]); + h = hashArray(a); + return h; +} + +// Size is the final argument: both native Perry and Node expose it reliably. +const n = Number(process.argv[process.argv.length - 1]); +if (!(n > 0)) throw new Error("Expected a positive size argument"); +function benchmarkMain(): void { + seed = 0x12345678; + + let checksum = 0; + let seen = false; + let warmMs = 0; + let warmRuns = 0; + while (warmMs < 200 || warmRuns < 5) { + seed = 0x12345678; + const input = setup(n); + const start = performance.now(); + const value = run(input); + const elapsed = performance.now() - start; + if (!(elapsed >= 0)) throw new Error("Invalid monotonic timer"); + warmMs += elapsed; + warmRuns++; + if (seen && value !== checksum) throw new Error("CORRECTNESS: unstable checksum during warmup"); + checksum = value; + seen = true; + } + const samples: number[] = []; + let runs = 0; + for (let sample = 0; sample < 7; sample++) { + let elapsed = 0; + let count = 0; + // Mutable workloads prepare fresh input BEFORE each timer; immutable + // workloads reuse setup. Neither preparation nor validation is measured. + while (elapsed < 20) { + seed = 0x12345678; + const input = setup(n); + const start = performance.now(); + const value = run(input); + const duration = performance.now() - start; + if (!(duration >= 0)) throw new Error("Invalid monotonic timer"); + elapsed += duration; + count++; + if (value !== checksum) throw new Error("CORRECTNESS: unstable checksum during sampling"); + } + samples.push(elapsed / count); + runs += count; + } + // Do not depend on Array.sort to compute the median of a sort benchmark. + for (let i = 1; i < samples.length; i++) { + const v = samples[i]; + let j = i - 1; + while (j >= 0 && samples[j] > v) { samples[j + 1] = samples[j]; j--; } + samples[j + 1] = v; + } + console.log(JSON.stringify({name: "array-push", category: "arrays", n, + ms_per_run: samples[3], runs, checksum})); +} +benchmarkMain(); diff --git a/benchmarks/array-shift-10060/array-shift-queue.ts b/benchmarks/array-shift-10060/array-shift-queue.ts new file mode 100644 index 0000000000..0591bd15be --- /dev/null +++ b/benchmarks/array-shift-10060/array-shift-queue.ts @@ -0,0 +1,89 @@ +// @runtime {"name": "array-shift-queue", "category": "arrays", "verification": "checksum", "sources": [{"file": "crates/perry-runtime/src/array/push_pop.rs", "function": "js_array_shift_f64"}], "hypothesis": "The generic shift path may move surviving elements on every removal.", "notes": "", "asynchronous": false, "output_stderr": false, "fresh_input": true} +// Standalone file. Shared helpers/driver are inlined by common.py. + +let seed = 0x12345678; +function rnd(): number { + seed ^= seed << 13; seed ^= seed >>> 17; seed ^= seed << 5; + return (seed >>> 0) / 4294967296; +} +function numbers(n: number): number[] { + const a: number[] = []; + for (let i = 0; i < n; i++) a.push(Math.floor(rnd() * 1000000)); + return a; +} +function hashArray(a: number[]): number { + let h = a.length; + for (let i = 0; i < a.length; i++) h = (h * 31 + a[i]) % 1000000007; + return h; +} +// Bounded checksum work avoids making string slicing/indexing part of every +// string benchmark's asymptotic cost. The workload itself consumes its result. +function hashString(s: string): number { + let h = s.length; + const step = Math.max(1, Math.floor(s.length / 32)); + for (let i = 0; i < s.length; i += step) h = (h * 31 + s.charCodeAt(i)) % 1000000007; + return h; +} + +function setup(n: number): number[] { return numbers(n); } +function run(q: number[]): number { + let h = 0; + while (q.length) h = (h * 31 + q.shift()!) % 1000000007; + return h; +} + +// Size is the final argument: both native Perry and Node expose it reliably. +const n = Number(process.argv[process.argv.length - 1]); +if (!(n > 0)) throw new Error("Expected a positive size argument"); +function benchmarkMain(): void { + seed = 0x12345678; + + let checksum = 0; + let seen = false; + let warmMs = 0; + let warmRuns = 0; + while (warmMs < 200 || warmRuns < 5) { + seed = 0x12345678; + const input = setup(n); + const start = performance.now(); + const value = run(input); + const elapsed = performance.now() - start; + if (!(elapsed >= 0)) throw new Error("Invalid monotonic timer"); + warmMs += elapsed; + warmRuns++; + if (seen && value !== checksum) throw new Error("CORRECTNESS: unstable checksum during warmup"); + checksum = value; + seen = true; + } + const samples: number[] = []; + let runs = 0; + for (let sample = 0; sample < 7; sample++) { + let elapsed = 0; + let count = 0; + // Mutable workloads prepare fresh input BEFORE each timer; immutable + // workloads reuse setup. Neither preparation nor validation is measured. + while (elapsed < 20) { + seed = 0x12345678; + const input = setup(n); + const start = performance.now(); + const value = run(input); + const duration = performance.now() - start; + if (!(duration >= 0)) throw new Error("Invalid monotonic timer"); + elapsed += duration; + count++; + if (value !== checksum) throw new Error("CORRECTNESS: unstable checksum during sampling"); + } + samples.push(elapsed / count); + runs += count; + } + // Do not depend on Array.sort to compute the median of a sort benchmark. + for (let i = 1; i < samples.length; i++) { + const v = samples[i]; + let j = i - 1; + while (j >= 0 && samples[j] > v) { samples[j + 1] = samples[j]; j--; } + samples[j + 1] = v; + } + console.log(JSON.stringify({name: "array-shift-queue", category: "arrays", n, + ms_per_run: samples[3], runs, checksum})); +} +benchmarkMain(); diff --git a/benchmarks/array-shift-10060/run.py b/benchmarks/array-shift-10060/run.py new file mode 100644 index 0000000000..1db00531f7 --- /dev/null +++ b/benchmarks/array-shift-10060/run.py @@ -0,0 +1,94 @@ +"""Sequential checksum-gated issue #10060 benchmark (60 s per process).""" +import argparse +import hashlib +import json +import math +import os +from pathlib import Path +import platform +import subprocess + + +def artifact_hashes(perry, node, sources): + windows = os.name == "nt" + paths = {"perry": perry, "node": Path(node)} + for name in ("runtime", "stdlib"): + paths[name] = perry.parent / (f"perry_{name}.lib" if windows else f"libperry_{name}.a") + paths.update({source.name: source for source in sources}) + return {name: hashlib.sha256(path.read_bytes()).hexdigest() for name, path in paths.items()} + + +def slope(rows): + if len(rows) < 2: + return None + x = [math.log(n) for n, _ in rows] + y = [math.log(t) for _, t in rows] + mx, my = sum(x) / len(x), sum(y) / len(y) + return sum((a - mx) * (b - my) for a, b in zip(x, y)) / sum( + (a - mx) ** 2 for a in x + ) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--perry", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--node", default="node") + args = parser.parse_args() + # Execute Node itself: a toolchain shim may spawn a child that inherits + # the pipes and survives termination of the shim at the process timeout. + args.node = subprocess.check_output([args.node, "-p", "process.execPath"], text=True).strip() + root = Path(__file__).resolve().parent + env = dict(os.environ, PERRY_RUNTIME_DIR=str(args.perry.resolve().parent), TZ="UTC", + LC_ALL="en_US.UTF-8") + result = { + "revision": subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip(), + "node": subprocess.check_output([args.node, "--version"], text=True).strip(), + "host": platform.platform(), + "cpu": platform.processor(), + "artifact_sha256": artifact_hashes(args.perry.resolve(), args.node, + [root / f"{name}.ts" for name in + ("array-shift-queue", "array-push")]), + "workloads": {}, + } + for name in ("array-shift-queue", "array-push"): + source = root / f"{name}.ts" + binary = args.output.resolve().parent / (name + (".exe" if os.name == "nt" else "")) + subprocess.run([str(args.perry.resolve()), "compile", str(source), + "--no-auto-optimize", "--no-cache", "-o", str(binary)], env=env, check=True) + rows, stopped = [], set() + for n in (100, 1000, 10000, 100000, 1000000): + pair = {} + for engine, command in (("node", [args.node, str(source)]), + ("perry", [str(binary)])): + if engine in stopped: + row = {"status": "SKIPPED"} + else: + try: + p = subprocess.run(command + [str(n)], env=env, capture_output=True, + text=True, timeout=60) + except subprocess.TimeoutExpired: + stopped.add(engine) + row = {"status": "TIMEOUT"} + else: + if p.returncode: + raise RuntimeError(f"{engine} {name} {n}: {p.returncode}\n{p.stdout}\n{p.stderr}") + row = dict(json.loads(p.stdout), status="OK") + pair[engine] = row + print(name, n, engine, json.dumps(row), flush=True) + if all(r["status"] == "OK" for r in pair.values()): + assert pair["node"]["checksum"] == pair["perry"]["checksum"], pair + rows.append({"n": n, **pair}) + result["workloads"][name] = {"rows": rows} + args.output.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8") + common = [r for r in rows if all(r[e]["status"] == "OK" for e in ("node", "perry"))] + result["workloads"][name].update( + common_sizes=[r["n"] for r in common], + common_slopes={e: slope([(r["n"], r[e]["ms_per_run"]) for r in common]) + for e in ("node", "perry")}, + ) + args.output.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8") + + +if __name__ == "__main__": + main() diff --git a/changelog.d/10060-array-shift-queue.md b/changelog.d/10060-array-shift-queue.md new file mode 100644 index 0000000000..f8a9b105c8 --- /dev/null +++ b/changelog.d/10060-array-shift-queue.md @@ -0,0 +1,11 @@ +Make dense `Array.shift()` advance a logical front offset instead of moving +and rebuilding the GC layout of every survivor. The offset is derived from +the existing allocation size and remaining capacity, preserving the eight-byte +array header and clearing each removed slot immediately. Indexing, bulk +mutators, JSON, and GC tracing use the logical storage start; growth normalizes +the backing store and remembers copied young references. + +Keep observable shifts on the property-aware path, including custom prototypes, +sealed/frozen arrays, and non-writable length. Check final length writability +after the indexed operations, preserving their side effects before an exception. +Add queue, moving-GC, and Node-parity regressions and a reproducible benchmark. diff --git a/crates/perry-codegen/src/array_storage.rs b/crates/perry-codegen/src/array_storage.rs new file mode 100644 index 0000000000..9f2aa45f06 --- /dev/null +++ b/crates/perry-codegen/src/array_storage.rs @@ -0,0 +1,25 @@ +//! Logical array element addresses, including a lazy queue front offset. + +use crate::block::LlBlock; +use crate::types::{I32, I64}; + +impl LlBlock { + /// The caller has proved `array` is a live, non-forwarded GC_TYPE_ARRAY. + /// Its backing ends at `array + GcHeader.size - GC_HEADER_SIZE`; capacity + /// counts the slots remaining after the queue front. Match runtime + /// `array::storage::array_elements_ptr` without a call or an extra header. + pub(crate) fn array_elements_addr(&mut self, array: &str) -> String { + let size_addr = self.sub(I64, array, "4"); + let size_ptr = self.inttoptr(I64, &size_addr); + let size = self.load(I32, &size_ptr); + let size = self.zext(I32, &size, I64); + let capacity_addr = self.add(I64, array, "4"); + let capacity_ptr = self.inttoptr(I64, &capacity_addr); + let capacity = self.load(I32, &capacity_ptr); + let capacity = self.zext(I32, &capacity, I64); + let bytes = self.shl(I64, &capacity, "3"); + let end = self.add(I64, array, &size); + let end = self.sub(I64, &end, "8"); + self.sub(I64, &end, &bytes) + } +} diff --git a/crates/perry-codegen/src/expr/array_pop.rs b/crates/perry-codegen/src/expr/array_pop.rs index 843a19eab3..b589c8d71e 100644 --- a/crates/perry-codegen/src/expr/array_pop.rs +++ b/crates/perry-codegen/src/expr/array_pop.rs @@ -184,7 +184,7 @@ pub(crate) fn lower_array_pop_inline(ctx: &mut FnCtx<'_>, recv_box: &str) -> Str let blk = ctx.block(); let new_length_i64 = blk.zext(I32, &new_length, I64); let elem_off = blk.shl(I64, &new_length_i64, "3"); - let elements_addr = blk.add(I64, &payload, "8"); + let elements_addr = blk.array_elements_addr(&payload); let elem_addr = blk.add(I64, &elements_addr, &elem_off); let elem_ptr = blk.inttoptr(I64, &elem_addr); let elem = blk.load(DOUBLE, &elem_ptr); diff --git a/crates/perry-codegen/src/expr/array_push.rs b/crates/perry-codegen/src/expr/array_push.rs index c99c243ba9..4a6c613d1c 100644 --- a/crates/perry-codegen/src/expr/array_push.rs +++ b/crates/perry-codegen/src/expr/array_push.rs @@ -184,8 +184,8 @@ fn emit_dynamic_pointer_push_store( let length = blk.safe_load_i32_from_ptr(arr_handle); let length_i64 = blk.zext(I32, &length, I64); let byte_offset = blk.shl(I64, &length_i64, "3"); - let with_header = blk.add(I64, &byte_offset, "8"); - let element_addr = blk.add(I64, arr_handle, &with_header); + let elements_addr = blk.array_elements_addr(arr_handle); + let element_addr = blk.add(I64, &elements_addr, &byte_offset); let element_ptr = blk.inttoptr(I64, &element_addr); // GC_STORE_AUDIT(BARRIERED): the common store remains unconditional; // only proven no-op bookkeeping is bypassed below, and the caller @@ -282,8 +282,8 @@ fn emit_numeric_push_store_pointer_tested( let length = blk.safe_load_i32_from_ptr(arr_handle); let length_i64 = blk.zext(I32, &length, I64); let byte_offset = blk.shl(I64, &length_i64, "3"); - let with_header = blk.add(I64, &byte_offset, "8"); - let element_addr = blk.add(I64, arr_handle, &with_header); + let elements_addr = blk.array_elements_addr(arr_handle); + let element_addr = blk.add(I64, &elements_addr, &byte_offset); let element_ptr = blk.inttoptr(I64, &element_addr); // GC_STORE_AUDIT(BARRIERED): the slot write itself is unconditional; // only the bookkeeping moves behind the live test below, and the @@ -1367,8 +1367,8 @@ fn lower_inner(ctx: &mut FnCtx<'_>, expr: &Expr, value_discarded: bool) -> Resul let length = blk.safe_load_i32_from_ptr(&payload); let length_i64 = blk.zext(I32, &length, I64); let byte_offset = blk.shl(I64, &length_i64, "3"); - let with_header = blk.add(I64, &byte_offset, "8"); - let element_addr = blk.add(I64, &payload, &with_header); + let elements_addr = blk.array_elements_addr(&payload); + let element_addr = blk.add(I64, &elements_addr, &byte_offset); let element_ptr = blk.inttoptr(I64, &element_addr); let value_bits = if let Some(value_bits) = v_bits.as_deref() { emit_jsvalue_slot_store_with_value_bits_on_block( diff --git a/crates/perry-codegen/src/expr/element_shape_guard.rs b/crates/perry-codegen/src/expr/element_shape_guard.rs index 289ff1155b..9e90b0859b 100644 --- a/crates/perry-codegen/src/expr/element_shape_guard.rs +++ b/crates/perry-codegen/src/expr/element_shape_guard.rs @@ -307,7 +307,7 @@ pub(crate) fn emit_element_shape_loop_preheader_check( }; // Elements base: `arr + size_of::()`. - let base_addr = blk.add(I64, &handle1, "8"); + let base_addr = blk.array_elements_addr(&handle1); let elements_base = blk.inttoptr(I64, &base_addr); // Hoisted loop-invariant words for the residual check. The volatile gate diff --git a/crates/perry-codegen/src/expr/i32_fast_path.rs b/crates/perry-codegen/src/expr/i32_fast_path.rs index ac66adbab5..c5290dffdd 100644 --- a/crates/perry-codegen/src/expr/i32_fast_path.rs +++ b/crates/perry-codegen/src/expr/i32_fast_path.rs @@ -1195,8 +1195,8 @@ fn lower_packed_i32_loop_index_get(ctx: &mut FnCtx<'_>, e: &Expr) -> Result (String, String) { let idx_i64 = blk.zext(I32, idx_i32, I64); let byte_offset = blk.shl(I64, &idx_i64, "3"); // *8 - let with_header = blk.add(I64, &byte_offset, "8"); // +8 for header - let element_addr = blk.add(I64, arr_handle, &with_header); + let elements_addr = blk.array_elements_addr(arr_handle); + let element_addr = blk.add(I64, &elements_addr, &byte_offset); let element_ptr = blk.inttoptr(I64, &element_addr); (element_addr, element_ptr) } diff --git a/crates/perry-codegen/src/expr/index_get.rs b/crates/perry-codegen/src/expr/index_get.rs index 2c567c7338..b328aa85d3 100644 --- a/crates/perry-codegen/src/expr/index_get.rs +++ b/crates/perry-codegen/src/expr/index_get.rs @@ -1089,8 +1089,8 @@ fn lower_bounded_array_index_get_checked( let fast_blk = ctx.block(); let idx_i64 = fast_blk.zext(I32, idx_i32, I64); let byte_offset = fast_blk.shl(I64, &idx_i64, "3"); - let with_header = fast_blk.add(I64, &byte_offset, "8"); - let element_addr = fast_blk.add(I64, &arr_handle, &with_header); + let elements_addr = fast_blk.array_elements_addr(&arr_handle); + let element_addr = fast_blk.add(I64, &elements_addr, &byte_offset); let element_ptr = fast_blk.inttoptr(I64, &element_addr); let fast_raw = fast_blk.load(DOUBLE, &element_ptr); // `new Array(n)` slots are TAG_HOLE internally; JavaScript reads expose diff --git a/crates/perry-codegen/src/expr/index_get/guarded_array.rs b/crates/perry-codegen/src/expr/index_get/guarded_array.rs index ff889c781a..b86355026e 100644 --- a/crates/perry-codegen/src/expr/index_get/guarded_array.rs +++ b/crates/perry-codegen/src/expr/index_get/guarded_array.rs @@ -43,8 +43,8 @@ pub(super) fn lower_trusted_plain_array_index_get( let blk = ctx.block(); let idx_i64 = blk.zext(I32, idx_i32, I64); let byte_offset = blk.shl(I64, &idx_i64, "3"); - let with_header = blk.add(I64, &byte_offset, "8"); - let element_addr = blk.add(I64, array_handle, &with_header); + let elements_addr = blk.array_elements_addr(array_handle); + let element_addr = blk.add(I64, &elements_addr, &byte_offset); let element_ptr = blk.inttoptr(I64, &element_addr); let raw = blk.load(DOUBLE, &element_ptr); let raw_bits = blk.bitcast_double_to_i64(&raw); @@ -62,8 +62,8 @@ fn lower_trusted_numeric_array_index_get( let blk = ctx.block(); let idx_i64 = blk.zext(I32, idx_i32, I64); let byte_offset = blk.shl(I64, &idx_i64, "3"); - let with_header = blk.add(I64, &byte_offset, "8"); - let element_addr = blk.add(I64, array_handle, &with_header); + let elements_addr = blk.array_elements_addr(array_handle); + let element_addr = blk.add(I64, &elements_addr, &byte_offset); let element_ptr = blk.inttoptr(I64, &element_addr); let raw = blk.load(DOUBLE, &element_ptr); if coerce_numeric_fallback { @@ -583,8 +583,8 @@ pub(super) fn lower_guarded_array_index_get( // does this load. let idx_i64 = fast_blk.zext(I32, idx_i32, I64); let byte_offset = fast_blk.shl(I64, &idx_i64, "3"); - let with_header = fast_blk.add(I64, &byte_offset, "8"); - let element_addr = fast_blk.add(I64, &arr_handle, &with_header); + let elements_addr = fast_blk.array_elements_addr(&arr_handle); + let element_addr = fast_blk.add(I64, &elements_addr, &byte_offset); let element_ptr = fast_blk.inttoptr(I64, &element_addr); let raw = fast_blk.load(DOUBLE, &element_ptr); if coerce_numeric_fallback { @@ -605,8 +605,8 @@ pub(super) fn lower_guarded_array_index_get( } else { let idx_i64 = fast_blk.zext(I32, idx_i32, I64); let byte_offset = fast_blk.shl(I64, &idx_i64, "3"); - let with_header = fast_blk.add(I64, &byte_offset, "8"); - let element_addr = fast_blk.add(I64, &arr_handle, &with_header); + let elements_addr = fast_blk.array_elements_addr(&arr_handle); + let element_addr = fast_blk.add(I64, &elements_addr, &byte_offset); let element_ptr = fast_blk.inttoptr(I64, &element_addr); let fast_raw = fast_blk.load(DOUBLE, &element_ptr); // `new Array(n)` slots are TAG_HOLE internally; JavaScript reads expose @@ -720,8 +720,8 @@ pub(super) fn lower_packed_f64_loop_index_get( let arr_handle = blk.and(I64, &arr_bits, POINTER_MASK_I64); let idx_i64 = blk.zext(I32, idx_i32, I64); let byte_offset = blk.shl(I64, &idx_i64, "3"); - let with_header = blk.add(I64, &byte_offset, "8"); - let element_addr = blk.add(I64, &arr_handle, &with_header); + let elements_addr = blk.array_elements_addr(&arr_handle); + let element_addr = blk.add(I64, &elements_addr, &byte_offset); let element_ptr = blk.inttoptr(I64, &element_addr); blk.load(DOUBLE, &element_ptr) }; diff --git a/crates/perry-codegen/src/expr/index_get/inline_dyn_typed_array.rs b/crates/perry-codegen/src/expr/index_get/inline_dyn_typed_array.rs index 1ca6db0f2b..53c1fa6cac 100644 --- a/crates/perry-codegen/src/expr/index_get/inline_dyn_typed_array.rs +++ b/crates/perry-codegen/src/expr/index_get/inline_dyn_typed_array.rs @@ -517,7 +517,7 @@ pub(super) fn lower_inline_dyn_typed_array_get( .cond_br(&elem_ok, &elem_load_label, &object_miss_label); ctx.current_block = elem_load_idx; let elem_bytes = ctx.block().shl(I64, &object_idx_i64, "3"); - let elem_elements_addr = ctx.block().add(I64, &elem_store_i64, "8"); + let elem_elements_addr = ctx.block().array_elements_addr(&elem_store_i64); let elem_addr = ctx.block().add(I64, &elem_elements_addr, &elem_bytes); let elem_ptr = ctx.block().inttoptr(I64, &elem_addr); let elem_raw = ctx.block().load(DOUBLE, &elem_ptr); @@ -581,10 +581,11 @@ pub(super) fn lower_inline_dyn_typed_array_get( ); ctx.current_block = object_array_load_idx; - let array_element_word = ctx.block().add(I64, &object_idx_i64, "1"); + let array_base = ctx.block().array_elements_addr(&object_raw); + let array_base_ptr = ctx.block().inttoptr(I64, &array_base); let array_element_ptr = ctx.block() - .gep_inbounds(I64, &array_ptr, &[(I64, &array_element_word)]); + .gep_inbounds(I64, &array_base_ptr, &[(I64, &object_idx_i64)]); let array_raw = ctx.block().load(DOUBLE, &array_element_ptr); let array_raw_bits = ctx.block().bitcast_double_to_i64(&array_raw); let array_is_hole = ctx diff --git a/crates/perry-codegen/src/expr/index_set.rs b/crates/perry-codegen/src/expr/index_set.rs index 67317672ca..8f16e700ca 100644 --- a/crates/perry-codegen/src/expr/index_set.rs +++ b/crates/perry-codegen/src/expr/index_set.rs @@ -1094,8 +1094,8 @@ pub(crate) fn lower( // facts before doing this store. let idx_i64 = blk.zext(I32, &idx_i32, I64); let byte_offset = blk.shl(I64, &idx_i64, "3"); - let with_header = blk.add(I64, &byte_offset, "8"); - let element_addr = blk.add(I64, &arr_handle, &with_header); + let elements_addr = blk.array_elements_addr(&arr_handle); + let element_addr = blk.add(I64, &elements_addr, &byte_offset); let element_ptr = blk.inttoptr(I64, &element_addr); // GC_STORE_AUDIT(POINTER_FREE): guarded raw-f64 // numeric store β€” the (canonical) value is a @@ -1157,8 +1157,8 @@ pub(crate) fn lower( // ptr = arr_handle + 8 + idx*8 let idx_i64 = blk.zext(I32, &idx_i32, I64); let byte_offset = blk.shl(I64, &idx_i64, "3"); - let with_header = blk.add(I64, &byte_offset, "8"); - let element_addr = blk.add(I64, &arr_handle, &with_header); + let elements_addr = blk.array_elements_addr(&arr_handle); + let element_addr = blk.add(I64, &elements_addr, &byte_offset); let element_ptr = blk.inttoptr(I64, &element_addr); let value_bits = emit_jsvalue_slot_store_on_block( blk, diff --git a/crates/perry-codegen/src/expr/index_set_guarded.rs b/crates/perry-codegen/src/expr/index_set_guarded.rs index 3a898592ea..7c618a2585 100644 --- a/crates/perry-codegen/src/expr/index_set_guarded.rs +++ b/crates/perry-codegen/src/expr/index_set_guarded.rs @@ -218,8 +218,8 @@ pub(super) fn emit_guarded_inbounds_array_store( let arr_handle = live_handle.clone(); let idx_i64 = blk.zext(I32, idx_i32, I64); let byte_offset = blk.shl(I64, &idx_i64, "3"); - let with_header = blk.add(I64, &byte_offset, "8"); - let element_addr = blk.add(I64, &arr_handle, &with_header); + let elements_addr = blk.array_elements_addr(&arr_handle); + let element_addr = blk.add(I64, &elements_addr, &byte_offset); let element_ptr = blk.inttoptr(I64, &element_addr); // Same call, same argument order, as `lower_index_set_fast`'s // in-bounds arm: the guard proved the slot holds a valid value, so the diff --git a/crates/perry-codegen/src/expr/index_set_packed_loop.rs b/crates/perry-codegen/src/expr/index_set_packed_loop.rs index 623bd018b1..8ed1e5528a 100644 --- a/crates/perry-codegen/src/expr/index_set_packed_loop.rs +++ b/crates/perry-codegen/src/expr/index_set_packed_loop.rs @@ -117,8 +117,8 @@ pub(super) fn lower_packed_f64_range_loop_index_set( let blk = ctx.block(); let idx_i64 = blk.zext(I32, idx_i32, I64); let byte_offset = blk.shl(I64, &idx_i64, "3"); - let with_header = blk.add(I64, &byte_offset, "8"); - let element_addr = blk.add(I64, &arr_handle, &with_header); + let elements_addr = blk.array_elements_addr(&arr_handle); + let element_addr = blk.add(I64, &elements_addr, &byte_offset); let element_ptr = blk.inttoptr(I64, &element_addr); // GC_STORE_AUDIT(POINTER_FREE): statically-genuine packed store β€” // the RHS predicate proves an unboxed double, never a heap pointer. @@ -228,8 +228,8 @@ pub(super) fn lower_packed_f64_range_loop_index_set( let blk = ctx.block(); let idx_i64 = blk.zext(I32, idx_i32, I64); let byte_offset = blk.shl(I64, &idx_i64, "3"); - let with_header = blk.add(I64, &byte_offset, "8"); - let element_addr = blk.add(I64, &arr_handle, &with_header); + let elements_addr = blk.array_elements_addr(&arr_handle); + let element_addr = blk.add(I64, &elements_addr, &byte_offset); let element_ptr = blk.inttoptr(I64, &element_addr); // GC_STORE_AUDIT(POINTER_FREE): range-guarded packed numeric element // store β€” the inline tag check above proved `val_double` is a genuine @@ -422,8 +422,8 @@ pub(super) fn lower_packed_numeric_loop_index_set( let arr_handle = blk.and(I64, &arr_bits, POINTER_MASK_I64); let idx_i64 = blk.zext(I32, idx_i32, I64); let byte_offset = blk.shl(I64, &idx_i64, "3"); - let with_header = blk.add(I64, &byte_offset, "8"); - let element_addr = blk.add(I64, &arr_handle, &with_header); + let elements_addr = blk.array_elements_addr(&arr_handle); + let element_addr = blk.add(I64, &elements_addr, &byte_offset); let element_ptr = blk.inttoptr(I64, &element_addr); // GC_STORE_AUDIT(POINTER_FREE): packed numeric-array element store β€” // `slot_value` is a raw numeric f64 (canonicalized via diff --git a/crates/perry-codegen/src/expr/logical_collections.rs b/crates/perry-codegen/src/expr/logical_collections.rs index 27d46ae0c9..3cdd2b28b4 100644 --- a/crates/perry-codegen/src/expr/logical_collections.rs +++ b/crates/perry-codegen/src/expr/logical_collections.rs @@ -280,8 +280,8 @@ fn lower_captureless_some_inline( let blk = ctx.block(); let i64_i = blk.zext(I32, &i, I64); let byte_offset = blk.shl(I64, &i64_i, "3"); - let with_header = blk.add(I64, &byte_offset, "8"); - let elem_addr = blk.add(I64, &raw, &with_header); + let elements_addr = blk.array_elements_addr(&raw); + let elem_addr = blk.add(I64, &elements_addr, &byte_offset); let elem_ptr = blk.inttoptr(I64, &elem_addr); let bits = blk.load(I64, &elem_ptr); let is_hole = blk.icmp_eq(I64, &bits, TAG_HOLE_I64); diff --git a/crates/perry-codegen/src/expr/masked_window.rs b/crates/perry-codegen/src/expr/masked_window.rs index 0513bf122d..3d248b848b 100644 --- a/crates/perry-codegen/src/expr/masked_window.rs +++ b/crates/perry-codegen/src/expr/masked_window.rs @@ -54,8 +54,8 @@ fn emit_raw_window_load( let blk = ctx.block(); let idx_i64 = blk.zext(I32, idx_i32, I64); let byte_offset = blk.shl(I64, &idx_i64, "3"); - let with_header = blk.add(I64, &byte_offset, "8"); - let element_addr = blk.add(I64, &arr_handle, &with_header); + let elements_addr = blk.array_elements_addr(&arr_handle); + let element_addr = blk.add(I64, &elements_addr, &byte_offset); let element_ptr = blk.inttoptr(I64, &element_addr); blk.load(DOUBLE, &element_ptr) } @@ -292,8 +292,8 @@ pub(crate) fn lower_masked_window_index_set( let blk = ctx.block(); let idx_i64 = blk.zext(I32, idx_i32, I64); let byte_offset = blk.shl(I64, &idx_i64, "3"); - let with_header = blk.add(I64, &byte_offset, "8"); - let element_addr = blk.add(I64, &arr_handle, &with_header); + let elements_addr = blk.array_elements_addr(&arr_handle); + let element_addr = blk.add(I64, &elements_addr, &byte_offset); let element_ptr = blk.inttoptr(I64, &element_addr); // GC_STORE_AUDIT(POINTER_FREE): masked-window dense store β€” the // matcher/lowering proved `val_double` is a genuine (unboxed) double, diff --git a/crates/perry-codegen/src/expr/ptr_numarray_access.rs b/crates/perry-codegen/src/expr/ptr_numarray_access.rs index 03b4527beb..062a4ae28e 100644 --- a/crates/perry-codegen/src/expr/ptr_numarray_access.rs +++ b/crates/perry-codegen/src/expr/ptr_numarray_access.rs @@ -53,8 +53,8 @@ fn lower_num_array_guard_free_get( let arr_handle = blk.and(I64, &arr_bits, POINTER_MASK_I64); let idx_i64 = blk.zext(I32, idx_i32, I64); let byte_offset = blk.shl(I64, &idx_i64, "3"); - let with_header = blk.add(I64, &byte_offset, "8"); - let element_addr = blk.add(I64, &arr_handle, &with_header); + let elements_addr = blk.array_elements_addr(&arr_handle); + let element_addr = blk.add(I64, &elements_addr, &byte_offset); let element_ptr = blk.inttoptr(I64, &element_addr); let raw = blk.load(DOUBLE, &element_ptr); if fact.density == crate::collectors::NumArrayDensity::HolesOk { @@ -223,8 +223,8 @@ pub(crate) fn try_lower_num_array_guard_free_set( let arr_handle = blk.and(I64, &arr_bits, POINTER_MASK_I64); let idx_i64 = blk.zext(I32, &idx_i32, I64); let byte_offset = blk.shl(I64, &idx_i64, "3"); - let with_header = blk.add(I64, &byte_offset, "8"); - let element_addr = blk.add(I64, &arr_handle, &with_header); + let elements_addr = blk.array_elements_addr(&arr_handle); + let element_addr = blk.add(I64, &elements_addr, &byte_offset); let element_ptr = blk.inttoptr(I64, &element_addr); // GC_STORE_AUDIT(POINTER_FREE): canonical raw-f64 store under the // `Ptr` local proof β€” never a GC pointer, no barrier, no diff --git a/crates/perry-codegen/src/expr/string_window.rs b/crates/perry-codegen/src/expr/string_window.rs index dffa23dec3..f1e8d14cb7 100644 --- a/crates/perry-codegen/src/expr/string_window.rs +++ b/crates/perry-codegen/src/expr/string_window.rs @@ -56,8 +56,8 @@ pub(crate) fn try_lower_index_get( let block = ctx.block(); let index_i64 = block.zext(I32, &index_i32, I64); let byte_offset = block.shl(I64, &index_i64, "3"); - let slot_offset = block.add(I64, &byte_offset, "8"); - let slot_addr = block.add(I64, &handle, &slot_offset); + let elements_addr = block.array_elements_addr(&handle); + let slot_addr = block.add(I64, &elements_addr, &byte_offset); let slot_ptr = block.inttoptr(I64, &slot_addr); Ok(Some(block.load(DOUBLE, &slot_ptr))) } diff --git a/crates/perry-codegen/src/expr/this_super_call.rs b/crates/perry-codegen/src/expr/this_super_call.rs index 852a9d6ada..6f9afcc045 100644 --- a/crates/perry-codegen/src/expr/this_super_call.rs +++ b/crates/perry-codegen/src/expr/this_super_call.rs @@ -283,7 +283,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { if array_parent { let len_i32 = ctx.block().call(I32, "js_array_length", &[(I64, &arr)]); let len = ctx.block().zext(I32, &len_i32, I64); - let elems_addr = ctx.block().add(I64, &arr, "8"); + let elems_addr = ctx.block().array_elements_addr(&arr); let elems_ptr = ctx.block().inttoptr(I64, &elems_addr); let result = ctx.block().call( DOUBLE, diff --git a/crates/perry-codegen/src/lib.rs b/crates/perry-codegen/src/lib.rs index e27a96238b..9eb72e7496 100644 --- a/crates/perry-codegen/src/lib.rs +++ b/crates/perry-codegen/src/lib.rs @@ -4,6 +4,7 @@ //! `clang -c` to build an object file linked against `libperry_runtime.a`. //! This is Perry's sole native code generation backend (since v0.5.0). +mod array_storage; pub mod block; pub(crate) mod boxed_vars; pub mod codegen; diff --git a/crates/perry-codegen/src/stmt/cached_field_index_return.rs b/crates/perry-codegen/src/stmt/cached_field_index_return.rs index 709d085f83..30339319ab 100644 --- a/crates/perry-codegen/src/stmt/cached_field_index_return.rs +++ b/crates/perry-codegen/src/stmt/cached_field_index_return.rs @@ -298,10 +298,11 @@ pub(super) fn try_emit_cached_field_index_return( ctx.current_block = array_load_idx; let index_i64 = ctx.block().zext(I32, &index_i32, I64); - let element_word = ctx.block().add(I64, &index_i64, "1"); + let elements = ctx.block().array_elements_addr(&array_raw); + let elements_ptr = ctx.block().inttoptr(I64, &elements); let element_ptr = ctx .block() - .gep_inbounds(I64, &array_ptr, &[(I64, &element_word)]); + .gep_inbounds(I64, &elements_ptr, &[(I64, &index_i64)]); let raw_value = ctx.block().load(DOUBLE, &element_ptr); let raw_bits = ctx.block().bitcast_double_to_i64(&raw_value); let is_hole = ctx diff --git a/crates/perry-codegen/src/stmt/loops.rs b/crates/perry-codegen/src/stmt/loops.rs index bb3da4296d..b471d5f88b 100644 --- a/crates/perry-codegen/src/stmt/loops.rs +++ b/crates/perry-codegen/src/stmt/loops.rs @@ -4901,8 +4901,10 @@ fn lower_object_array_write_versioned_for( let object_ptr = { let blk = ctx.block(); let inner_i64 = blk.sext(I32, &inner, I64); - let element_word = blk.add(I64, &inner_i64, "1"); - let element_ptr = blk.gep_inbounds(I64, &array_ptr, &[(I64, &element_word)]); + let handle = blk.ptrtoint(&array_ptr, I64); + let elements = blk.array_elements_addr(&handle); + let elements_ptr = blk.inttoptr(I64, &elements); + let element_ptr = blk.gep_inbounds(I64, &elements_ptr, &[(I64, &inner_i64)]); let object_box = blk.load(DOUBLE, &element_ptr); let object_bits = blk.bitcast_double_to_i64(&object_box); let object_handle = blk.and(I64, &object_bits, crate::nanbox::POINTER_MASK_I64); @@ -4969,8 +4971,10 @@ fn lower_object_array_write_versioned_for( let object_ptr = { let blk = ctx.block(); let inner_i64 = blk.sext(I32, &inner, I64); - let element_word = blk.add(I64, &inner_i64, "1"); - let element_ptr = blk.gep_inbounds(I64, g_array_ptr, &[(I64, &element_word)]); + let handle = blk.ptrtoint(g_array_ptr, I64); + let elements = blk.array_elements_addr(&handle); + let elements_ptr = blk.inttoptr(I64, &elements); + let element_ptr = blk.gep_inbounds(I64, &elements_ptr, &[(I64, &inner_i64)]); let object_box = blk.load(DOUBLE, &element_ptr); let object_bits = blk.bitcast_double_to_i64(&object_box); let object_handle = blk.and(I64, &object_bits, crate::nanbox::POINTER_MASK_I64); diff --git a/crates/perry-codegen/src/stmt/stable_packed_loop.rs b/crates/perry-codegen/src/stmt/stable_packed_loop.rs index e2479aec36..dd87f04fb4 100644 --- a/crates/perry-codegen/src/stmt/stable_packed_loop.rs +++ b/crates/perry-codegen/src/stmt/stable_packed_loop.rs @@ -694,7 +694,7 @@ fn build_numeric_access( contiguous_u32_prefix: bool, ) -> StablePackedNumericAccess { let (is_plain, payload) = plain_payload_base(ctx, descriptor, live_raw); - let plain_base = ctx.block().add(I64, &payload, "8"); + let plain_base = ctx.block().array_elements_addr(&payload); let element_base = descriptor_word(ctx, descriptor, 4); let packed_bounds = descriptor_word(ctx, descriptor, 5); @@ -764,8 +764,8 @@ fn build_numeric_access( ); let has_spill = ctx.block().icmp_ne(I64, &spill, "0"); let safe_spill = ctx.block().select(I1, &has_spill, I64, &spill, live_raw); - let spill_offset = ctx.block().add(I64, &element_bytes, "8"); - let object_spill_base = ctx.block().add(I64, &safe_spill, &spill_offset); + let spill_base = ctx.block().array_elements_addr(&safe_spill); + let object_spill_base = ctx.block().add(I64, &spill_base, &element_bytes); let contiguous_base = contiguous_u32_prefix.then(|| { // Mode-2 admission rejects prefixes that cross the inline/spill // boundary (and rejects plain Arrays), so storage selection belongs @@ -1062,8 +1062,8 @@ pub(crate) fn try_lower_index_get( ctx.current_block = plain_idx; let byte_offset = ctx.block().shl(I64, &idx_i64, "3"); - let with_header = ctx.block().add(I64, &byte_offset, "8"); - let element_addr = ctx.block().add(I64, &payload, &with_header); + let elements = ctx.block().array_elements_addr(&payload); + let element_addr = ctx.block().add(I64, &elements, &byte_offset); let element_ptr = ctx.block().inttoptr(I64, &element_addr); let plain_raw = ctx.block().load(DOUBLE, &element_ptr); let plain_bits = ctx.block().bitcast_double_to_i64(&plain_raw); diff --git a/crates/perry-ext-better-sqlite3/src/lib.rs b/crates/perry-ext-better-sqlite3/src/lib.rs index f3bc5da226..d695c5069b 100644 --- a/crates/perry-ext-better-sqlite3/src/lib.rs +++ b/crates/perry-ext-better-sqlite3/src/lib.rs @@ -86,7 +86,8 @@ unsafe fn params_from_array(arr_ptr: *const ArrayHeader) -> Vec()) as *const u64; + let elements = + perry_runtime::array::array_elements_ptr(arr_ptr as *const ArrayHeader) as *const u64; let mut params: Vec> = Vec::with_capacity(len); for i in 0..len { diff --git a/crates/perry-ext-http/src/client_overload.rs b/crates/perry-ext-http/src/client_overload.rs index 46acf237c1..bfc9469344 100644 --- a/crates/perry-ext-http/src/client_overload.rs +++ b/crates/perry-ext-http/src/client_overload.rs @@ -65,7 +65,8 @@ pub(crate) unsafe fn parse_client_args(args_array: i64) -> ClientArgs { return out; } let len = (*arr_ptr).length as usize; - let elements = (arr_ptr as *const u8).add(std::mem::size_of::()) as *const u64; + let elements = + perry_runtime::array::array_elements_ptr(arr_ptr as *const ArrayHeader) as *const u64; for i in 0..len { let bits = *elements.add(i); // The response callback is the (single) function argument β€” match diff --git a/crates/perry-ext-http/src/client_request_surface.rs b/crates/perry-ext-http/src/client_request_surface.rs index faad8fe08e..737a5d3fba 100644 --- a/crates/perry-ext-http/src/client_request_surface.rs +++ b/crates/perry-ext-http/src/client_request_surface.rs @@ -144,7 +144,7 @@ extern "C" fn client_once_wrapper(closure: *const RawClosureHeader, rest: f64) - if array.is_null() { return js_closure_call_array(callback, std::ptr::null(), 0); } - let args = array.add(1) as *const f64; + let args = perry_runtime::array::array_elements_ptr(array) as *const f64; js_closure_call_array(callback, args, (*array).length as i64) } } diff --git a/crates/perry-ext-http/src/server/types.rs b/crates/perry-ext-http/src/server/types.rs index 2233a31d57..93bb7e43b0 100644 --- a/crates/perry-ext-http/src/server/types.rs +++ b/crates/perry-ext-http/src/server/types.rs @@ -166,7 +166,8 @@ pub unsafe fn parse_listen_args(args_array: i64) -> ListenArgs { return out; } let len = (*arr_ptr).length as usize; - let elements = (arr_ptr as *const u8).add(std::mem::size_of::()) as *const u64; + let elements = + perry_runtime::array::array_elements_ptr(arr_ptr as *const ArrayHeader) as *const u64; for i in 0..len { let bits = *elements.add(i); let v = JsValue::from_bits(bits); diff --git a/crates/perry-runtime/src/array/alloc.rs b/crates/perry-runtime/src/array/alloc.rs index b4aa3667bc..7f20e653d0 100644 --- a/crates/perry-runtime/src/array/alloc.rs +++ b/crates/perry-runtime/src/array/alloc.rs @@ -50,7 +50,7 @@ pub extern "C" fn js_array_alloc(capacity: u32) -> *mut ArrayHeader { // HOLE-initialize the whole capacity so the unused [length, capacity) // slack never holds stale arena bits that the whole-heap from-space // scan misreads as live from-space pointers. - let elements_ptr = (ptr as *mut u8).add(std::mem::size_of::()) as *mut u64; + let elements_ptr = crate::array::array_elements_ptr(ptr as *const ArrayHeader) as *mut u64; for i in 0..actual_capacity as usize { // GC_STORE_AUDIT(INIT): initialization of a just-allocated array // that is not yet reachable from any root, and TAG_HOLE is a @@ -128,7 +128,7 @@ pub extern "C" fn js_array_alloc_with_length(capacity: u32) -> *mut ArrayHeader unsafe { (*ptr).length = capacity; // Set length = requested capacity (*ptr).capacity = actual_capacity; - let elements_ptr = (ptr as *mut u8).add(std::mem::size_of::()) as *mut u64; + let elements_ptr = crate::array::array_elements_ptr(ptr as *const ArrayHeader) as *mut u64; for i in 0..capacity as usize { // GC_STORE_AUDIT(POINTER_FREE): TAG_HOLE is a non-pointer sentinel for fresh array slots. std::ptr::write(elements_ptr.add(i), crate::value::TAG_HOLE); @@ -156,7 +156,7 @@ pub(crate) fn js_array_alloc_with_length_exact(capacity: u32) -> *mut ArrayHeade unsafe { (*ptr).length = capacity; (*ptr).capacity = capacity; - let elements_ptr = (ptr as *mut u8).add(std::mem::size_of::()) as *mut u64; + let elements_ptr = crate::array::array_elements_ptr(ptr as *const ArrayHeader) as *mut u64; for i in 0..capacity as usize { // GC_STORE_AUDIT(POINTER_FREE): TAG_HOLE is a non-pointer sentinel for fresh array slots. std::ptr::write(elements_ptr.add(i), crate::value::TAG_HOLE); @@ -249,7 +249,7 @@ pub extern "C" fn js_array_from_f64(elements: *const f64, count: u32) -> *mut Ar let arr = js_array_alloc(count); unsafe { (*arr).length = count; - let arr_elements = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + let arr_elements = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut f64; // GC_STORE_AUDIT(BARRIERED): bulk array initialization is followed by layout/barrier rebuild. ptr::copy_nonoverlapping(elements, arr_elements, count as usize); rebuild_array_layout(arr); @@ -296,7 +296,7 @@ unsafe fn js_array_from_arraylike_with_missing( let arr = js_array_alloc(len); (*arr).length = len; clear_array_numeric_layout(arr); - let elements = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + let elements = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut f64; for i in 0..len { let key_str = i.to_string(); let key = crate::string::js_string_from_bytes(key_str.as_ptr(), key_str.len() as u32); @@ -368,7 +368,7 @@ unsafe fn store_codepoint_string( index: usize, string: *mut crate::string::StringHeader, ) { - let elements = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + let elements = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut f64; let value = crate::value::js_nanbox_string(string as i64); // GC_STORE_AUDIT(BARRIERED): codepoint array slot is followed by a runtime write barrier. ptr::write(elements.add(index), value); @@ -537,7 +537,7 @@ pub extern "C" fn js_array_from_values(values: *const f64, n: u32) -> *mut Array return arr; } let parent = arr as u64; - let elems = unsafe { (arr as *mut u8).add(std::mem::size_of::()) as *mut f64 }; + let elems = unsafe { crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut f64 }; for i in 0..n as usize { let v = unsafe { *values.add(i) }; let slot = unsafe { elems.add(i) }; diff --git a/crates/perry-runtime/src/array/collection_tag_tests.rs b/crates/perry-runtime/src/array/collection_tag_tests.rs index d1b50fd4a6..a403e34e04 100644 --- a/crates/perry-runtime/src/array/collection_tag_tests.rs +++ b/crates/perry-runtime/src/array/collection_tag_tests.rs @@ -193,7 +193,7 @@ fn a_stale_registry_entry_over_recycled_bytes_does_not_read_as_a_map() { let recycled = addr as *mut ArrayHeader; (*recycled).length = 2; (*recycled).capacity = 2; - let elements = (addr as *mut u8).add(std::mem::size_of::()) as *mut f64; + let elements = crate::array::array_elements_ptr(addr as *const ArrayHeader) as *mut f64; // GC_STORE_AUDIT(POINTER_FREE): raw f64 numerics into a buffer this // test allocated and re-stamped itself; no heap pointer is stored. std::ptr::write(elements, 111.0); diff --git a/crates/perry-runtime/src/array/concat_reverse.rs b/crates/perry-runtime/src/array/concat_reverse.rs index cf534d0767..14888ea7be 100644 --- a/crates/perry-runtime/src/array/concat_reverse.rs +++ b/crates/perry-runtime/src/array/concat_reverse.rs @@ -141,9 +141,8 @@ pub extern "C" fn js_array_concat( if source.is_null() { return None; } - let source_elements = (source as *const u8) - .add(std::mem::size_of::()) - as *const f64; + let source_elements = + crate::array::array_elements_ptr(source) as *const f64; Some(*source_elements.add(i)) }) else { @@ -179,9 +178,8 @@ pub extern "C" fn js_array_concat( if source.is_null() { return None; } - let source_elements = (source as *const u8) - .add(std::mem::size_of::()) - as *const f64; + let source_elements = + crate::array::array_elements_ptr(source) as *const f64; Some(*source_elements.add(i)) }) else { @@ -227,8 +225,7 @@ pub extern "C" fn js_array_concat( if source.is_null() || result.is_null() { return result; } - let source_elements = - (source as *const u8).add(std::mem::size_of::()) as *const f64; + let source_elements = crate::array::array_elements_ptr(source) as *const f64; for i in 0..src_len as usize { let source_value = *source_elements.add(i); // Array iteration observes a hole as `undefined`; the internal @@ -268,13 +265,13 @@ pub extern "C" fn js_array_concat_new( let mut result = js_array_alloc(total); if !a.is_null() && a_len > 0 { - let src = (a as *const u8).add(std::mem::size_of::()) as *const f64; + let src = crate::array::array_elements_ptr(a as *const ArrayHeader) as *const f64; for i in 0..a_len as usize { result = js_array_push_f64(result, *src.add(i)); } } if !b.is_null() && b_len > 0 { - let src = (b as *const u8).add(std::mem::size_of::()) as *const f64; + let src = crate::array::array_elements_ptr(b as *const ArrayHeader) as *const f64; for i in 0..b_len as usize { result = js_array_push_f64(result, *src.add(i)); } @@ -319,7 +316,7 @@ pub extern "C" fn js_array_reverse(arr: *mut ArrayHeader) -> *mut ArrayHeader { if crate::array::array_iteration_is_exotic(arr) { return reverse_array_spec_path(arr); } - let elements = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + let elements = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut f64; let mut i = 0usize; let mut j = len - 1; while i < j { @@ -560,7 +557,7 @@ pub extern "C" fn js_array_fill(arr: *mut ArrayHeader, value: f64) -> *mut Array if len == 0 { return arr; } - let elements = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + let elements = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut f64; for i in 0..len { // GC_STORE_AUDIT(BARRIERED): fill slot writes are followed by layout/barrier rebuild. *elements.add(i) = value; @@ -644,7 +641,7 @@ pub extern "C" fn js_array_fill_range( if s >= e { return arr; } - let elements = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + let elements = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut f64; for i in s..e { // GC_STORE_AUDIT(BARRIERED): fill range writes are followed by layout/barrier rebuild. *elements.add(i as usize) = value; diff --git a/crates/perry-runtime/src/array/element_shape_tests.rs b/crates/perry-runtime/src/array/element_shape_tests.rs index fa93b4f76c..36adee7595 100644 --- a/crates/perry-runtime/src/array/element_shape_tests.rs +++ b/crates/perry-runtime/src/array/element_shape_tests.rs @@ -108,7 +108,7 @@ fn a_scan_establishes_the_invariant_for_an_array_built_outside_the_funnels() { // fresh allocation: nothing establishes the invariant on the way in. let arr = js_array_alloc(4); unsafe { - let elements = (arr as *mut u8).add(std::mem::size_of::()) as *mut u64; + let elements = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut u64; for i in 0..4 { // GC_STORE_AUDIT(INIT): fresh `js_array_alloc(4)` slots, filled the // way an inline array literal's codegen fills them β€” the point of diff --git a/crates/perry-runtime/src/array/fill_extend.rs b/crates/perry-runtime/src/array/fill_extend.rs index 29853f7dab..a3f1ed5c0e 100644 --- a/crates/perry-runtime/src/array/fill_extend.rs +++ b/crates/perry-runtime/src/array/fill_extend.rs @@ -83,7 +83,7 @@ pub extern "C" fn js_array_fill_f64_const_extend( } return fallback; } - let elements_ptr = (out as *mut u8).add(std::mem::size_of::()) as *mut f64; + let elements_ptr = crate::array::array_elements_ptr(out as *const ArrayHeader) as *mut f64; for i in 0..end as usize { // GC_STORE_AUDIT(POINTER_FREE): bulk numeric fill writes raw f64s only. ptr::write(elements_ptr.add(i), number); @@ -191,7 +191,7 @@ pub extern "C" fn js_array_fill_f64_iota_extend( } return fallback; } - let elements_ptr = (out as *mut u8).add(std::mem::size_of::()) as *mut f64; + let elements_ptr = crate::array::array_elements_ptr(out as *const ArrayHeader) as *mut f64; for i in 0..end as usize { // GC_STORE_AUDIT(POINTER_FREE): bulk iota fill writes raw f64s only. ptr::write(elements_ptr.add(i), i as f64); diff --git a/crates/perry-runtime/src/array/flat_clone.rs b/crates/perry-runtime/src/array/flat_clone.rs index 3d83e99c5f..c69be29769 100644 --- a/crates/perry-runtime/src/array/flat_clone.rs +++ b/crates/perry-runtime/src/array/flat_clone.rs @@ -139,7 +139,7 @@ pub unsafe extern "C" fn js_short_packed_spread_values(value: f64, out: *mut f64 if len > 4 || (len != 0 && out.is_null()) { return -1; } - let elements = (arr as *const u8).add(std::mem::size_of::()) as *const u64; + let elements = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const u64; for index in 0..len { let bits = std::ptr::read(elements.add(index)); if bits == crate::value::TAG_HOLE { @@ -194,9 +194,9 @@ pub(crate) fn dense_spread_copy(value: f64) -> *mut ArrayHeader { } if len > 0 { let src_elements = - (src as *const u8).add(std::mem::size_of::()) as *const u64; + crate::array::array_elements_ptr(src as *const ArrayHeader) as *const u64; let dst_elements = - (result as *mut u8).add(std::mem::size_of::()) as *mut u64; + crate::array::array_elements_ptr(result as *const ArrayHeader) as *mut u64; // GC_STORE_AUDIT(BARRIERED): bulk copy into an unpublished array, // followed by the exact layout/barrier rebuild below. ptr::copy_nonoverlapping(src_elements, dst_elements, len as usize); @@ -328,7 +328,7 @@ unsafe fn js_array_flat_into( crate::array::array_spec_get(live_src, i as u32) } else { let elements = - (live_src as *const u8).add(std::mem::size_of::()) as *const f64; + crate::array::array_elements_ptr(live_src as *const ArrayHeader) as *const f64; let element = *elements.add(i); // Per FlattenIntoArray, holes are absent and skipped. if element.to_bits() == crate::value::TAG_HOLE { @@ -362,7 +362,7 @@ pub extern "C" fn js_array_flat(arr: *const ArrayHeader) -> *mut ArrayHeader { } unsafe { let len = (*arr).length as usize; - let elements = (arr as *const u8).add(std::mem::size_of::()) as *const f64; + let elements = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64; let mut result = js_array_alloc(0); for i in 0..len { @@ -376,9 +376,9 @@ pub extern "C" fn js_array_flat(arr: *const ArrayHeader) -> *mut ArrayHeader { let sub_len = (*sub_arr).length as usize; // Sanity check: if length is unreasonably large, treat as non-array. if sub_len <= 1_000_000 { - let sub_elements = (sub_arr as *const u8) - .add(std::mem::size_of::()) - as *const f64; + let sub_elements = + crate::array::array_elements_ptr(sub_arr as *const ArrayHeader) + as *const f64; for j in 0..sub_len { let sub = *sub_elements.add(j); // Skip holes in the flattened sub-array too. @@ -641,9 +641,9 @@ pub extern "C" fn js_array_clone(src: *const ArrayHeader) -> *mut ArrayHeader { crate::value::js_nanbox_get_pointer(result_h.get_nanbox_f64()) as *mut ArrayHeader; if len > 0 { let src_elements = - (src as *const u8).add(std::mem::size_of::()) as *const f64; + crate::array::array_elements_ptr(src as *const ArrayHeader) as *const f64; let dst_elements = - (result as *mut u8).add(std::mem::size_of::()) as *mut f64; + crate::array::array_elements_ptr(result as *const ArrayHeader) as *mut f64; // GC_STORE_AUDIT(BARRIERED): clone bulk copy is followed by exact layout/barrier rebuild. ptr::copy_nonoverlapping(src_elements, dst_elements, len as usize); (*result).length = len; @@ -699,13 +699,16 @@ pub extern "C" fn js_array_entries(arr: *const ArrayHeader) -> *mut ArrayHeader let result = js_array_alloc(len); (*result).length = len; clear_array_numeric_layout(result); - let src_elements = (arr as *const u8).add(std::mem::size_of::()) as *const f64; - let dst_elements = (result as *mut u8).add(std::mem::size_of::()) as *mut f64; + let src_elements = + crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64; + let dst_elements = + crate::array::array_elements_ptr(result as *const ArrayHeader) as *mut f64; for i in 0..len as usize { // Build a 2-element [index, value] pair as an inner array. let pair = js_array_alloc(2); (*pair).length = 2; - let pair_elems = (pair as *mut u8).add(std::mem::size_of::()) as *mut f64; + let pair_elems = + crate::array::array_elements_ptr(pair as *const ArrayHeader) as *mut f64; // GC_STORE_AUDIT(BARRIERED): entries pair slots are immediately recorded via note_array_slot. *pair_elems.add(0) = i as f64; *pair_elems.add(1) = *src_elements.add(i); @@ -744,7 +747,8 @@ pub extern "C" fn js_array_keys(arr: *const ArrayHeader) -> *mut ArrayHeader { let len = (*arr).length; let result = js_array_alloc(len); (*result).length = len; - let dst_elements = (result as *mut u8).add(std::mem::size_of::()) as *mut f64; + let dst_elements = + crate::array::array_elements_ptr(result as *const ArrayHeader) as *mut f64; for i in 0..len as usize { // GC_STORE_AUDIT(POINTER_FREE): keys array stores numeric indices only. *dst_elements.add(i) = i as f64; @@ -786,9 +790,9 @@ pub extern "C" fn js_array_values(arr: *const ArrayHeader) -> *mut ArrayHeader { crate::value::js_nanbox_get_pointer(result_h.get_nanbox_f64()) as *mut ArrayHeader; if len > 0 { let src_elements = - (arr as *const u8).add(std::mem::size_of::()) as *const f64; + crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64; let dst_elements = - (result as *mut u8).add(std::mem::size_of::()) as *mut f64; + crate::array::array_elements_ptr(result as *const ArrayHeader) as *mut f64; // GC_STORE_AUDIT(BARRIERED): values bulk copy is followed by layout/barrier rebuild. ptr::copy_nonoverlapping(src_elements, dst_elements, len as usize); (*result).length = len; diff --git a/crates/perry-runtime/src/array/forwarding_tests.rs b/crates/perry-runtime/src/array/forwarding_tests.rs index 05623d23fc..d9b1714cf1 100644 --- a/crates/perry-runtime/src/array/forwarding_tests.rs +++ b/crates/perry-runtime/src/array/forwarding_tests.rs @@ -22,7 +22,7 @@ fn growth_of_old_array_keeps_forwarding_target_out_of_copying_nursery() { unsafe { (*initial).length = 0; (*initial).capacity = capacity; - let elements = (initial as *mut u8).add(std::mem::size_of::()) as *mut u64; + let elements = crate::array::array_elements_ptr(initial as *const ArrayHeader) as *mut u64; for i in 0..capacity as usize { // GC_STORE_AUDIT(INIT): initialize unpublished fresh array storage // with the non-pointer hole sentinel before exposing the array. diff --git a/crates/perry-runtime/src/array/from_concat.rs b/crates/perry-runtime/src/array/from_concat.rs index ef18dc8ff5..0903c98cd0 100644 --- a/crates/perry-runtime/src/array/from_concat.rs +++ b/crates/perry-runtime/src/array/from_concat.rs @@ -284,7 +284,8 @@ pub extern "C" fn js_array_concat_variadic( if !result_is_plain { unsafe { let len = (*result).length as usize; - let elems = (result as *const u8).add(std::mem::size_of::()) as *const f64; + let elems = + crate::array::array_elements_ptr(result as *const ArrayHeader) as *const f64; for i in 0..len { let v = *elems.add(i); if v.to_bits() != crate::value::TAG_HOLE { @@ -915,7 +916,7 @@ unsafe fn dense_concat_array_source(src: *const ArrayHeader) -> Option<(*const A if len > (*src).capacity { return None; } - let elems = (src as *const u8).add(std::mem::size_of::()) as *const f64; + let elems = crate::array::array_elements_ptr(src as *const ArrayHeader) as *const f64; for i in 0..len as usize { if (*elems.add(i)).to_bits() == crate::value::TAG_HOLE { return None; @@ -982,13 +983,13 @@ unsafe fn try_concat_all_dense( // Pass 2: copy. Nothing below allocates or bails, so no GC can move a // source or the result mid-copy and no shared-demote runs twice β€” which // is what makes the single deferred rebuild sound. - let dst = (result as *mut u8).add(std::mem::size_of::()) as *mut f64; + let dst = crate::array::array_elements_ptr(result as *const ArrayHeader) as *mut f64; let mut off: usize = 0; let copy_array = |src: *const ArrayHeader, len: u32, off: &mut usize| { if len == 0 { return; } - let elems = (src as *const u8).add(std::mem::size_of::()) as *const f64; + let elems = crate::array::array_elements_ptr(src as *const ArrayHeader) as *const f64; // GC_STORE_AUDIT(BARRIERED): all-dense concat bulk copy; one exact // layout/barrier rebuild follows after all sources are copied. std::ptr::copy_nonoverlapping(elems, dst.add(*off), len as usize); @@ -1082,7 +1083,7 @@ unsafe fn try_append_spread_array_dense( // punt those to the slow path. String addrefs happen only after the copy // has committed below, so a mid-scan bail can't leave the fallback path // double-retaining an already-addref'd string. - let src_elems = (src as *const u8).add(std::mem::size_of::()) as *const f64; + let src_elems = crate::array::array_elements_ptr(src as *const ArrayHeader) as *const f64; for i in 0..src_len as usize { if (*src_elems.add(i)).to_bits() == crate::value::TAG_HOLE { return None; @@ -1107,8 +1108,8 @@ unsafe fn try_append_spread_array_dense( if result.is_null() || src.is_null() { return None; } - let src_elems = (src as *const u8).add(std::mem::size_of::()) as *const f64; - let dst_elems = (result as *mut u8).add(std::mem::size_of::()) as *mut f64; + let src_elems = crate::array::array_elements_ptr(src as *const ArrayHeader) as *const f64; + let dst_elems = crate::array::array_elements_ptr(result as *const ArrayHeader) as *mut f64; // GC_STORE_AUDIT(BARRIERED): concat bulk copy is followed by exact layout/barrier rebuild. std::ptr::copy_nonoverlapping( src_elems, @@ -1146,7 +1147,7 @@ fn append_spread_array(result: *mut ArrayHeader, src: *const ArrayHeader) -> *mu unsafe { let len = (*materialized).length; let elems = - (materialized as *const u8).add(std::mem::size_of::()) as *const f64; + crate::array::array_elements_ptr(materialized as *const ArrayHeader) as *const f64; let mut out = result; // ECMA-262 Β§23.1.3.5 step 5.c.iii: each index goes through // `HasProperty(E, k)` / `Get(E, k)` β€” a hole filled by an inherited diff --git a/crates/perry-runtime/src/array/generic.rs b/crates/perry-runtime/src/array/generic.rs index 5c3d47cc7b..c8726cc2bc 100644 --- a/crates/perry-runtime/src/array/generic.rs +++ b/crates/perry-runtime/src/array/generic.rs @@ -564,7 +564,7 @@ pub(super) fn al_has(recv: f64, k: i64) -> bool { if k >= (*arr).length as i64 { return false; } - let el = *((arr as *const u8).add(std::mem::size_of::()) as *const f64) + let el = *(crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64) .add(k as usize); if el.to_bits() != TAG_HOLE { return true; @@ -759,7 +759,7 @@ pub extern "C" fn js_arraylike_map(recv: f64, cb: f64, this_arg: f64) -> f64 { ) }); let elems = - unsafe { (result as *mut u8).add(std::mem::size_of::()) as *mut f64 }; + unsafe { crate::array::array_elements_ptr(result as *const ArrayHeader) as *mut f64 }; unsafe { // GC_STORE_AUDIT(BARRIERED): note_array_slot below re-stores this slot with the barrier. ptr::write(elems.add(k as usize), mapped); @@ -1207,7 +1207,7 @@ pub extern "C" fn js_arraylike_at(recv: f64, index: f64) -> f64 { fn materialize(recv: f64) -> *mut ArrayHeader { let len = al_length(recv); let arr = js_array_alloc_with_length(len.max(0) as u32); - let elems = unsafe { (arr as *mut u8).add(std::mem::size_of::()) as *mut f64 }; + let elems = unsafe { crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut f64 }; for k in 0..len { if !al_has(recv, k) { continue; // leave the hole @@ -1283,7 +1283,7 @@ pub extern "C" fn js_arraylike_slice( value_h.set_nanbox_f64(al_get(recv_h.get_nanbox_f64(), k)); let value = value_h.get_nanbox_f64(); result_h.with_mut_ptr::(|result| unsafe { - let elems = (result as *mut u8).add(std::mem::size_of::()) as *mut f64; + let elems = crate::array::array_elements_ptr(result as *const ArrayHeader) as *mut f64; // GC_STORE_AUDIT(BARRIERED): note_array_slot below re-stores this // slot with the write barrier after the direct dense write. ptr::write(elems.add(n as usize), value); diff --git a/crates/perry-runtime/src/array/generic_object.rs b/crates/perry-runtime/src/array/generic_object.rs index 7fac83396a..8428855b3b 100644 --- a/crates/perry-runtime/src/array/generic_object.rs +++ b/crates/perry-runtime/src/array/generic_object.rs @@ -305,7 +305,7 @@ pub(crate) fn object_splice(recv: f64, args_ptr: *const f64, args_len: usize) -> } let removed = js_array_alloc_with_length(delete_count.max(0) as u32); let removed_elems = - unsafe { (removed as *mut u8).add(std::mem::size_of::()) as *mut f64 }; + unsafe { crate::array::array_elements_ptr(removed as *const ArrayHeader) as *mut f64 }; for k in 0..delete_count { let from = actual_start + k; if al_has(recv, from) { diff --git a/crates/perry-runtime/src/array/header.rs b/crates/perry-runtime/src/array/header.rs index 361a53730c..16dec37462 100644 --- a/crates/perry-runtime/src/array/header.rs +++ b/crates/perry-runtime/src/array/header.rs @@ -3,6 +3,7 @@ //! pulls these basics in via `use super::*;`. pub(crate) use super::header_gc_slots::*; +pub(super) use super::storage::array_elements_ptr; use std::cell::RefCell; use std::collections::HashMap; @@ -853,15 +854,16 @@ pub(crate) fn clean_arr_ptr(arr: *const ArrayHeader) -> *const ArrayHeader { // #9371: a large pre-sized holey array grows its dense prefix on // demand, so a legitimate sparse header can have capacity above // the old one-million cutoff while it is still below `length`. - // Prove the capacity against the tracked allocation's exact byte - // size instead of imposing a second semantic threshold. Corrupt - // length/capacity words still fail closed unless they describe - // precisely the allocation the GC owns at this address. + // Remaining capacity must fit the tracked allocation. A consumed + // queue prefix accounts for any whole-slot difference from the + // allocation's physical capacity. let sparse_array_shape = tracked_obj_type == Some(crate::gc::GC_TYPE_ARRAY) && hdr.length > hdr.capacity && tracked_header.is_some_and(|gc_header| { - checked_array_allocation_size(hdr.capacity as usize) - == Some((*gc_header.as_ptr()).size as usize) + checked_array_allocation_size(hdr.capacity as usize).is_some_and(|minimum| { + let size = (*gc_header.as_ptr()).size as usize; + size >= minimum && (size - minimum) % 8 == 0 + }) }); if sparse_array_shape { return cleaned; @@ -1130,7 +1132,8 @@ pub(crate) fn normalize_array_receiver(arr: *const ArrayHeader) -> *const ArrayH pub struct ArrayHeader { /// Number of elements in the array pub length: u32, - /// Capacity (allocated space for elements) + /// Available slots from logical element zero to the end of the allocation. + /// Dense shift advances that start by reducing this value. See storage.rs. pub capacity: u32, } @@ -1218,7 +1221,7 @@ pub(crate) fn canonicalize_array_numeric_store_value_from_flags(flags: u16, valu #[inline] unsafe fn array_slot_bits(arr: *const ArrayHeader, index: usize) -> u64 { - let slot = (arr as *const u8).add(std::mem::size_of::()) as *const u64; + let slot = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const u64; *slot.add(index) } @@ -1981,8 +1984,3 @@ pub(super) fn checked_array_allocation_size(capacity: usize) -> Option { .and_then(|elements| std::mem::size_of::().checked_add(elements)) .and_then(|payload| crate::gc::GC_HEADER_SIZE.checked_add(payload)) } - -#[inline] -pub(super) unsafe fn array_elements_ptr(arr: *mut ArrayHeader) -> *mut u64 { - (arr as *mut u8).add(std::mem::size_of::()) as *mut u64 -} diff --git a/crates/perry-runtime/src/array/header_gc_slots.rs b/crates/perry-runtime/src/array/header_gc_slots.rs index 8aa0df040f..d67434c628 100644 --- a/crates/perry-runtime/src/array/header_gc_slots.rs +++ b/crates/perry-runtime/src/array/header_gc_slots.rs @@ -14,18 +14,20 @@ pub(crate) unsafe fn gc_element_slot_range( } let length = (*arr).length as usize; let capacity = (*arr).capacity as usize; - if capacity > 16_000_000 { + if capacity > 16_000_000 || capacity > super::array_physical_capacity(arr) { return None; } if length > capacity { // Preserve the old corruption fail-closed behavior while admitting - // legitimate sparse headers: the claimed capacity must exactly match - // the GC allocation that owns this payload. + // legitimate sparse headers: the remaining capacity must fit the + // tracked allocation, allowing the consumed prefix of a dense queue. let Some(gc_header) = crate::value::addr_class::try_read_tracked_gc_header(arr as usize) else { return None; }; - if checked_array_allocation_size(capacity) != Some((*gc_header.as_ptr()).size as usize) { + if checked_array_allocation_size(super::array_physical_capacity(arr)) + != Some((*gc_header.as_ptr()).size as usize) + { return None; } } @@ -169,7 +171,7 @@ pub(crate) unsafe fn rebuild_array_layout(arr: *mut ArrayHeader) { return; } // #7480: this is the post-hoc funnel most bulk element mutators use β€” - // `shift`, `unshift`, `splice`, `fill`, `copyWithin`, and `reverse` all + // Generic `shift`, `unshift`, `splice`, `fill`, `copyWithin`, and `reverse` // mutate slots with bare `ptr::write` / `ptr::copy` and then land here. // NOT `sort`: its default path writes the rank permutation back through // `RootedArrayElems::set`, so it revokes through the STORE funnel diff --git a/crates/perry-runtime/src/array/immutable.rs b/crates/perry-runtime/src/array/immutable.rs index b1d642d4fa..c5666e6cab 100644 --- a/crates/perry-runtime/src/array/immutable.rs +++ b/crates/perry-runtime/src/array/immutable.rs @@ -57,8 +57,8 @@ pub extern "C" fn js_array_to_reversed(arr: *const ArrayHeader) -> *mut ArrayHea let len = (*arr).length as usize; let new_arr = js_array_alloc(len as u32); (*new_arr).length = len as u32; - let src = (arr as *const u8).add(std::mem::size_of::()) as *const f64; - let dst = (new_arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + let src = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64; + let dst = crate::array::array_elements_ptr(new_arr as *const ArrayHeader) as *mut f64; for i in 0..len { // GC_STORE_AUDIT(BARRIERED): reversed copy initializes a fresh array rebuilt below. *dst.add(i) = *src.add(len - 1 - i); @@ -103,8 +103,8 @@ pub extern "C" fn js_array_to_sorted_default(arr: *const ArrayHeader) -> *mut Ar // Clone the array let new_arr = js_array_alloc(len as u32); (*new_arr).length = len as u32; - let src = (arr as *const u8).add(std::mem::size_of::()) as *const f64; - let dst = (new_arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + let src = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64; + let dst = crate::array::array_elements_ptr(new_arr as *const ArrayHeader) as *mut f64; // GC_STORE_AUDIT(BARRIERED): sorted clone copy initializes a fresh array rebuilt below. // toSorted reads via Get (no HasProperty skip): holes become present // `undefined` elements in the dense copy (ECMA-262 Β§23.1.3.34). @@ -163,8 +163,8 @@ pub extern "C" fn js_array_to_sorted_with_comparator( // Clone the array let new_arr = js_array_alloc(len as u32); (*new_arr).length = len as u32; - let src = (arr as *const u8).add(std::mem::size_of::()) as *const f64; - let dst = (new_arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + let src = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64; + let dst = crate::array::array_elements_ptr(new_arr as *const ArrayHeader) as *mut f64; // GC_STORE_AUDIT(BARRIERED): comparator sorted clone copy initializes a fresh array rebuilt below. // toSorted reads via Get (no HasProperty skip): holes become present // `undefined` elements in the dense copy (ECMA-262 Β§23.1.3.34). @@ -198,7 +198,7 @@ pub extern "C" fn js_array_to_spliced( } unsafe { let len = (*arr).length as isize; - let src = (arr as *const u8).add(std::mem::size_of::()) as *const f64; + let src = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64; // Normalize start index (ECMA ToIntegerOrInfinity). NaN -> 0, // +Infinity -> len, -Infinity -> 0. Avoid `f as isize` on non-finite. @@ -249,7 +249,7 @@ pub extern "C" fn js_array_to_spliced( let new_len = (len - dc + items_count as isize) as usize; let new_arr = js_array_alloc(new_len as u32); (*new_arr).length = new_len as u32; - let dst = (new_arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + let dst = crate::array::array_elements_ptr(new_arr as *const ArrayHeader) as *mut f64; // Copy elements before start // GC_STORE_AUDIT(BARRIERED): toSpliced result writes are followed by layout/barrier rebuild. @@ -324,10 +324,10 @@ pub extern "C" fn js_array_with( throw_invalid_index(index); } let idx = resolved as isize; - let src = (arr as *const u8).add(std::mem::size_of::()) as *const f64; + let src = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64; let new_arr = js_array_alloc(len as u32); (*new_arr).length = len as u32; - let dst = (new_arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + let dst = crate::array::array_elements_ptr(new_arr as *const ArrayHeader) as *mut f64; // GC_STORE_AUDIT(BARRIERED): with() clone and replacement are followed by layout/barrier rebuild. std::ptr::copy_nonoverlapping(src, dst, len as usize); *dst.add(idx as usize) = value; @@ -416,7 +416,7 @@ pub extern "C" fn js_array_copy_within( } let len = len_i64 as isize; let (t, s, e) = (t as isize, s as isize, e as isize); - let elements = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + let elements = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut f64; let count = (e - s).min(len - t); if count <= 0 { diff --git a/crates/perry-runtime/src/array/indexing.rs b/crates/perry-runtime/src/array/indexing.rs index 63c63d3c55..1043700e60 100644 --- a/crates/perry-runtime/src/array/indexing.rs +++ b/crates/perry-runtime/src/array/indexing.rs @@ -144,7 +144,7 @@ pub(crate) unsafe fn array_has_own_index(arr: *const ArrayHeader, index: u32) -> return true; } if index < (*arr).length && index < (*arr).capacity { - let elements = (arr as *const u8).add(std::mem::size_of::()) as *const u64; + let elements = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const u64; if ptr::read(elements.add(index as usize)) != crate::value::TAG_HOLE { return true; } @@ -403,7 +403,8 @@ pub extern "C" fn js_array_get_f64_unchecked(arr: *const ArrayHeader, index: u32 } return array_oob_prototype_get(arr as usize, index); } - let elements_ptr = (arr as *const u8).add(std::mem::size_of::()) as *const f64; + let elements_ptr = + crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64; let raw = *elements_ptr.add(index as usize); // Issue #323: translate HOLE sentinel (set by `new Array(n)`) back to // `undefined`. The sentinel is internal β€” user code only ever sees @@ -435,7 +436,7 @@ pub extern "C" fn js_array_numeric_get_f64_unboxed(arr: *mut ArrayHeader, index: && index < (*arr).length { let elements_ptr = - (arr as *const u8).add(std::mem::size_of::()) as *const f64; + crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64; return *elements_ptr.add(index as usize); } @@ -632,7 +633,8 @@ pub extern "C" fn js_array_get_f64(arr: *const ArrayHeader, index: u32) -> f64 { } return array_oob_prototype_get(arr as usize, index); } - let elements_ptr = (arr as *const u8).add(std::mem::size_of::()) as *const f64; + let elements_ptr = + crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64; let raw = *elements_ptr.add(index as usize); // Issue #323: translate HOLE sentinel back to `undefined` (see // `js_array_alloc_with_length` for context). Per OrdinaryGet a hole @@ -724,7 +726,8 @@ pub extern "C" fn js_array_numeric_set_f64_unboxed( clear_array_numeric_layout(arr); return 0; }; - let elements_ptr = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + let elements_ptr = + crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut f64; // GC_STORE_AUDIT(POINTER_FREE): RawF64-layout payload slot β€” // `number` is a plain f64, never a NaN-boxed pointer, so no // write barrier is needed. @@ -1302,8 +1305,7 @@ pub(crate) fn try_strict_dense_index_set( let length = (*arr).length; let capacity = (*arr).capacity; if index < length && length <= capacity && length <= 100_000_000 { - let elements = (arr as *mut u8) - .add(std::mem::size_of::()) + let elements = crate::array::array_elements_ptr(arr as *const ArrayHeader) .cast::(); let slot = elements.add(index as usize); let old = ptr::read(slot); @@ -1347,7 +1349,7 @@ pub(crate) fn try_strict_dense_index_set( if index >= (*resolved).length || index >= (*resolved).capacity { return None; } - let elements = (resolved as *mut u8).add(std::mem::size_of::()) as *mut f64; + let elements = crate::array::array_elements_ptr(resolved as *const ArrayHeader) as *mut f64; if flags & crate::gc::GC_ARRAY_RAW_F64_LAYOUT != 0 { if let Some(number) = number { // GC_STORE_AUDIT(POINTER_FREE): `GC_ARRAY_RAW_F64_LAYOUT` diff --git a/crates/perry-runtime/src/array/indexing_support.rs b/crates/perry-runtime/src/array/indexing_support.rs index a3836cd490..0780319ebe 100644 --- a/crates/perry-runtime/src/array/indexing_support.rs +++ b/crates/perry-runtime/src/array/indexing_support.rs @@ -259,7 +259,7 @@ pub(crate) unsafe fn keys_array_slot( && index < (*keys).capacity { let elements = - (keys as *const u8).add(std::mem::size_of::()) as *const f64; + crate::array::array_elements_ptr(keys as *const ArrayHeader) as *const f64; let raw = std::ptr::read(elements.add(index as usize)); if raw.to_bits() != crate::value::TAG_HOLE { return crate::value::JSValue::from_bits(raw.to_bits()); diff --git a/crates/perry-runtime/src/array/iter_methods.rs b/crates/perry-runtime/src/array/iter_methods.rs index 81acaa8cfd..f15edd4130 100644 --- a/crates/perry-runtime/src/array/iter_methods.rs +++ b/crates/perry-runtime/src/array/iter_methods.rs @@ -14,7 +14,7 @@ fn array_receiver_value(arr: *const ArrayHeader) -> f64 { #[inline(always)] unsafe fn array_elements_ptr(arr: *const ArrayHeader) -> *const f64 { - (arr as *const u8).add(std::mem::size_of::()) as *const f64 + crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64 } #[inline(always)] @@ -401,7 +401,7 @@ pub extern "C" fn js_array_map( if is_plain { let result = result_arr(&result_rooted); let result_elements = - (result as *mut u8).add(std::mem::size_of::()) as *mut f64; + crate::array::array_elements_ptr(result as *const ArrayHeader) as *mut f64; // GC_STORE_AUDIT(INIT): plain result is unpublished; slot layout noted below. ptr::write(result_elements.add(i), mapped); let mapped_bits = mapped.to_bits(); @@ -1176,9 +1176,9 @@ pub extern "C" fn js_array_flatMap( // read through a stale ArrayHeader pointer. let sub_arr = crate::array::flattenable_array_ptr(sub_rooted.get_nanbox_f64()); debug_assert!(!sub_arr.is_null()); - let sub_elements = (sub_arr as *const u8) - .add(std::mem::size_of::()) - as *const f64; + let sub_elements = + crate::array::array_elements_ptr(sub_arr as *const ArrayHeader) + as *const f64; let Some(sub_element) = present_array_element(sub_elements, j) else { continue; }; diff --git a/crates/perry-runtime/src/array/join.rs b/crates/perry-runtime/src/array/join.rs index 9b066064f1..addf39e107 100644 --- a/crates/perry-runtime/src/array/join.rs +++ b/crates/perry-runtime/src/array/join.rs @@ -17,7 +17,7 @@ const CAPACITY_SAMPLE_SIZE: usize = 32; #[inline(always)] unsafe fn array_elements_ptr(arr: *const ArrayHeader) -> *const f64 { - unsafe { (arr as *const u8).add(std::mem::size_of::()) as *const f64 } + unsafe { crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64 } } #[cold] diff --git a/crates/perry-runtime/src/array/jsvalue_api.rs b/crates/perry-runtime/src/array/jsvalue_api.rs index 5073bfc1e2..6bd94669bf 100644 --- a/crates/perry-runtime/src/array/jsvalue_api.rs +++ b/crates/perry-runtime/src/array/jsvalue_api.rs @@ -32,7 +32,7 @@ pub extern "C" fn js_array_from_jsvalue(elements: *const u64, count: u32) -> *mu let arr = js_array_alloc(count); unsafe { (*arr).length = count; - let arr_elements = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + let arr_elements = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut f64; // Each u64 contains NaN-boxed JSValue bits, store as f64 bits for i in 0..count as usize { let bits = *elements.add(i); diff --git a/crates/perry-runtime/src/array/mod.rs b/crates/perry-runtime/src/array/mod.rs index 1c454050ef..39fdbee176 100644 --- a/crates/perry-runtime/src/array/mod.rs +++ b/crates/perry-runtime/src/array/mod.rs @@ -38,9 +38,14 @@ mod prototype_addr; mod push_pop; mod reduce_right; mod search; +#[cfg(test)] +mod shift_queue_tests; mod sort; mod species; mod splice_slice; +mod storage; +pub use storage::array_elements_ptr; +pub(crate) use storage::{array_front_offset, array_physical_capacity}; mod subclass; pub(crate) mod subclass_elements; diff --git a/crates/perry-runtime/src/array/numeric_range.rs b/crates/perry-runtime/src/array/numeric_range.rs index d18ef23575..4f1b013100 100644 --- a/crates/perry-runtime/src/array/numeric_range.rs +++ b/crates/perry-runtime/src/array/numeric_range.rs @@ -75,7 +75,7 @@ fn array_numeric_range_add_impl(receiver: f64, start: f64, end: Option, del if start >= end { return i64::from(start); } - let elements = (arr as *mut u8).add(std::mem::size_of::()) as *mut u64; + let elements = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut u64; // One fused pass instead of validate-then-mutate. The all-or-nothing // contract the two-pass version provided was stronger than the source // semantics require: each element gets exactly one `+ delta` either @@ -227,7 +227,7 @@ pub unsafe extern "C" fn js_array_fill_range_strided_tagged( if start >= end { return i64::from(start); } - let elements = (arr as *mut u8).add(std::mem::size_of::()) as *mut u64; + let elements = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut u64; let mut index = start; // GC_STORE_AUDIT(POINTER_FREE): the stored constant is a non-pointer // NaN-box (boolean/null/undefined/number bits), the receiver's layout diff --git a/crates/perry-runtime/src/array/push_pop.rs b/crates/perry-runtime/src/array/push_pop.rs index 59345da6e2..f5bb39c5bf 100644 --- a/crates/perry-runtime/src/array/push_pop.rs +++ b/crates/perry-runtime/src/array/push_pop.rs @@ -177,16 +177,22 @@ pub extern "C" fn js_array_grow(arr: *mut ArrayHeader, min_capacity: u32) -> *mu } } as *mut ArrayHeader; let arr = arr_handle.get_raw_mut_ptr::(); - // GC_STORE_AUDIT(BARRIERED): array growth copy transfers layout and replays write barriers below. - ptr::copy_nonoverlapping(arr as *const u8, new_ptr as *mut u8, old_size); - + let shifted = array_front_offset(arr) != 0; + (*new_ptr).length = (*arr).length; (*new_ptr).capacity = new_capacity; + // GC_STORE_AUDIT(BARRIERED): growth normalizes the logical backing + // range, transfers its layout, and replays its write barriers below. + ptr::copy_nonoverlapping( + array_elements_ptr(arr), + array_elements_ptr(new_ptr), + old_capacity as usize, + ); // HOLE-initialize the newly added [old_capacity, new_capacity) slack // so it never holds stale arena bits the whole-heap from-space scan // misreads as live from-space pointers. { let new_elems = - (new_ptr as *mut u8).add(std::mem::size_of::()) as *mut u64; + crate::array::array_elements_ptr(new_ptr as *const ArrayHeader) as *mut u64; for i in old_capacity as usize..new_capacity as usize { // GC_STORE_AUDIT(INIT): initialization of the freshly grown // array's added [old_capacity, new_capacity) slack β€” storage @@ -217,12 +223,14 @@ pub extern "C" fn js_array_grow(arr: *mut ArrayHeader, min_capacity: u32) -> *mu // store's dirty-page coverage can be TRANSLATED to the new address // instead of re-derived from 3 M slot values. Falls back to the full // value-derived replay whenever the translation declines. - if !crate::gc::relocate_copied_old_object_dirty_pages( - new_ptr as usize, - arr as usize, - new_ptr as usize, - old_size, - ) { + if shifted + || !crate::gc::relocate_copied_old_object_dirty_pages( + new_ptr as usize, + arr as usize, + new_ptr as usize, + old_size, + ) + { replay_array_growth_write_barriers(new_ptr); } @@ -536,7 +544,7 @@ pub(super) fn proxy_array_mutator( let (v, removed) = removed_handle.across_mut::(|| { proxy_get_str_key(p(), from.as_bytes()) }); - let elems = (removed as *mut u8).add(std::mem::size_of::()) + let elems = crate::array::array_elements_ptr(removed as *const ArrayHeader) as *mut f64; // GC_STORE_AUDIT(BARRIERED): note_array_slot re-stores // the slot with the barrier. @@ -1034,8 +1042,7 @@ pub extern "C" fn js_array_push_spread_f64( if source.is_null() { break; } - let elements = - (source as *const u8).add(std::mem::size_of::()) as *const f64; + let elements = crate::array::array_elements_ptr(source) as *const f64; let source_value = *elements.add(i); let value = if source_value.to_bits() == crate::value::TAG_HOLE { f64::from_bits(crate::value::TAG_UNDEFINED) @@ -1098,9 +1105,8 @@ pub extern "C" fn js_array_pop_f64(arr: *mut ArrayHeader) -> f64 { } if length <= capacity && length <= 100_000_000 { let new_length = length - 1; - let elements = (arr as *mut u8) - .add(std::mem::size_of::()) - .cast::(); + let elements = + crate::array::array_elements_ptr(arr as *const ArrayHeader).cast::(); let value = ptr::read(elements.add(new_length as usize)); if value.to_bits() != crate::value::TAG_HOLE { (*arr).length = new_length; @@ -1154,7 +1160,8 @@ pub extern "C" fn js_array_pop_f64(arr: *mut ArrayHeader) -> f64 { None => crate::array::array_iteration_is_exotic(arr), }; if !exotic { - let elements_ptr = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + let elements_ptr = + crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut f64; let value = *elements_ptr.add(new_length as usize); (*arr).length = new_length; // #9462: the popped slot can be a HOLE β€” `[1, ,].pop()`, @@ -1268,7 +1275,7 @@ fn try_truncate_plain_array_to_zero(arr: *mut ArrayHeader) -> bool { if cur == 0 { return true; } - let elements = (arr as *mut u8).add(std::mem::size_of::()) as *mut u64; + let elements = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut u64; for i in 0..cur { // GC_STORE_AUDIT(BARRIERED): the suffix becomes unreachable when // length is published below; rebuild_array_layout then rebuilds @@ -1393,7 +1400,8 @@ pub extern "C" fn js_array_set_length(arr: *mut ArrayHeader, new_length: f64) { // rebuild from the surviving slots), so the head needs no // handle scope. `pooled.length = 0` in an object pool is this // branch every time. - let elements = (arr as *mut u8).add(std::mem::size_of::()) as *mut u64; + let elements = + crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut u64; for i in n..cur { // GC_STORE_AUDIT(BARRIERED): the suffix becomes unreachable // when length is published below; rebuild_array_layout then @@ -1527,13 +1535,13 @@ pub extern "C" fn js_array_shift_f64(arr: *mut ArrayHeader) -> f64 { if arr.is_null() { return TAG_UNDEFINED_F64; } - if array_is_frozen(arr) { - throw_frozen_array_mutation(); - } - guard_writable_length(arr); unsafe { let length = (*arr).length; if length == 0 { + if array_is_frozen(arr) { + throw_frozen_array_mutation(); + } + guard_writable_length(arr); return TAG_UNDEFINED_F64; } @@ -1542,21 +1550,16 @@ pub extern "C" fn js_array_shift_f64(arr: *mut ArrayHeader) -> f64 { // descriptors and prototype properties require the specified live // HasProperty/Get/Set/Delete order; their accessors can also freeze the // receiver or make `length` non-writable before the final length Set. - if crate::array::array_iteration_is_exotic(arr) { + if crate::array::array_iteration_is_exotic(arr) + || (crate::object::prototype_chain::array_static_proto_recorded() + && array_custom_prototype(arr).is_some()) + || array_object_flags_resolved(arr) + & (crate::gc::OBJ_FLAG_FROZEN | crate::gc::OBJ_FLAG_SEALED) + != 0 + { return shift_array_spec_path(arr); } - - // `TAG_HOLE` is an internal storage sentinel. Even on the dense path, - // Get(O, "0") must expose it as `undefined`. - let value = crate::array::js_array_get_f64(arr, 0); - let elements_ptr = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; - - // Shift all elements down - // GC_STORE_AUDIT(BARRIERED): shift memmove is followed by layout/barrier rebuild. - ptr::copy(elements_ptr.add(1), elements_ptr, (length - 1) as usize); - (*arr).length = length - 1; - rebuild_array_layout(arr); - value + super::storage::shift_dense(arr) } } @@ -1675,7 +1678,7 @@ pub extern "C" fn js_array_unshift_f64(arr: *mut ArrayHeader, value: f64) -> *mu }; let value = value_handle.get_nanbox_f64(); - let elements_ptr = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + let elements_ptr = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut f64; // Shift all elements up // GC_STORE_AUDIT(BARRIERED): unshift memmove and new slot are followed by layout/barrier rebuild. @@ -1755,7 +1758,7 @@ pub extern "C" fn js_array_unshift_variadic( } else { arr }; - let elements_ptr = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + let elements_ptr = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut f64; // Shift existing elements up by `n`. // GC_STORE_AUDIT(BARRIERED): memmove + new slots followed by layout/barrier rebuild. ptr::copy(elements_ptr, elements_ptr.add(n), length as usize); diff --git a/crates/perry-runtime/src/array/reduce_right.rs b/crates/perry-runtime/src/array/reduce_right.rs index 7c9b9cf2ba..d77ab55453 100644 --- a/crates/perry-runtime/src/array/reduce_right.rs +++ b/crates/perry-runtime/src/array/reduce_right.rs @@ -5,7 +5,7 @@ use crate::closure::ClosureHeader; #[inline(always)] unsafe fn array_elements_ptr(arr: *const ArrayHeader) -> *const f64 { - (arr as *const u8).add(std::mem::size_of::()) as *const f64 + crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64 } #[inline(always)] diff --git a/crates/perry-runtime/src/array/search.rs b/crates/perry-runtime/src/array/search.rs index 080885a395..007ca0a365 100644 --- a/crates/perry-runtime/src/array/search.rs +++ b/crates/perry-runtime/src/array/search.rs @@ -3,7 +3,7 @@ use super::*; #[inline(always)] unsafe fn array_elements_ptr(arr: *const ArrayHeader) -> *const f64 { - (arr as *const u8).add(std::mem::size_of::()) as *const f64 + crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64 } #[inline(always)] diff --git a/crates/perry-runtime/src/array/shift_queue_tests.rs b/crates/perry-runtime/src/array/shift_queue_tests.rs new file mode 100644 index 0000000000..f18dad9817 --- /dev/null +++ b/crates/perry-runtime/src/array/shift_queue_tests.rs @@ -0,0 +1,93 @@ +use super::*; + +#[test] +fn shift_queue_drain_keeps_survivors_at_their_original_addresses() { + let _triggers = crate::gc::GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + let arr = js_array_alloc(10_000); + for i in 0..10_000 { + assert_eq!(js_array_push_f64(arr, i as f64), arr); + } + let original = unsafe { array_elements_ptr(arr) }; + for i in 0..10_000 { + assert_eq!(js_array_shift_f64(arr), i as f64); + unsafe { + assert_eq!(*original.add(i), crate::value::TAG_HOLE); + if i < 9_999 { + assert_eq!(array_elements_ptr(arr), original.add(i + 1)); + assert_eq!(js_array_get_f64(arr, 0), (i + 1) as f64); + } + } + } + assert_eq!(js_array_length(arr), 0); + assert_eq!( + js_array_shift_f64(arr).to_bits(), + crate::value::TAG_UNDEFINED + ); + unsafe { + assert_eq!(array_elements_ptr(arr), original); + assert_eq!((*arr).capacity, 10_000); + } + assert_eq!(js_array_push_f64(arr, 42.0), arr); + assert_eq!(js_array_pop_f64(arr), 42.0); +} + +#[test] +fn shift_queue_aliases_growth_holes_and_refill() { + let _triggers = crate::gc::GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + let alias = js_array_alloc(16); + let mut arr = alias; + for cycle in 0..5 { + for i in 0..16 { + arr = js_array_push_f64(arr, (cycle * 16 + i) as f64); + } + for i in 0..8 { + assert_eq!(js_array_shift_f64(alias), (cycle * 16 + i) as f64); + } + for i in 16..80 { + arr = js_array_push_f64(arr, (cycle * 16 + i) as f64); + } + assert_eq!(clean_arr_ptr_mut(alias), arr); + for i in 8..80 { + assert_eq!(js_array_get_f64(alias, i - 8), (cycle * 16 + i) as f64); + } + while js_array_length(alias) > 0 { + js_array_shift_f64(alias); + } + } + arr = js_array_push_f64(arr, 1.0); + arr = js_array_push_f64(arr, 2.0); + arr = js_array_push_f64(arr, 3.0); + assert_eq!(js_array_delete(arr, 1), 1); + assert_eq!(js_array_shift_f64(arr), 1.0); + assert_eq!( + js_array_shift_f64(arr).to_bits(), + crate::value::TAG_UNDEFINED + ); + assert_eq!(js_array_shift_f64(arr), 3.0); +} + +#[test] +fn shift_queue_shared_mutators_use_the_logical_start() { + let _triggers = crate::gc::GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + let mut arr = js_array_alloc(16); + for i in 0..6 { + arr = js_array_push_f64(arr, i as f64); + } + assert_eq!(js_array_shift_f64(arr), 0.0); + assert_eq!(js_array_shift_f64(arr), 1.0); + arr = js_array_unshift_f64(arr, 9.0); + assert_eq!(js_array_shift_f64(arr), 9.0); + assert_eq!(js_array_pop_f64(arr), 5.0); + assert_eq!(js_array_get_f64(arr, 0), 2.0); + js_array_set_length(arr, 1.0); + assert_eq!(js_array_get_f64(arr, 0), 2.0); + js_array_set_length(arr, 3.0); + assert_eq!( + js_array_get_f64(arr, 1).to_bits(), + crate::value::TAG_UNDEFINED + ); + assert_eq!( + js_array_get_f64(arr, 2).to_bits(), + crate::value::TAG_UNDEFINED + ); +} diff --git a/crates/perry-runtime/src/array/sort.rs b/crates/perry-runtime/src/array/sort.rs index 994cd44857..c4293bc7a9 100644 --- a/crates/perry-runtime/src/array/sort.rs +++ b/crates/perry-runtime/src/array/sort.rs @@ -110,7 +110,7 @@ impl<'s> RootedArrayElems<'s> { #[inline(always)] pub(crate) unsafe fn get(&self, index: usize) -> f64 { let arr = self.arr(); - *((arr as *const u8).add(std::mem::size_of::()) as *const f64).add(index) + *(crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64).add(index) } /// Barriered store (`note_array_slot`): keeps the layout side-table and @@ -192,7 +192,7 @@ unsafe fn sort_permutation( let arr = roots.get(0) as *const ArrayHeader; let comparator = roots.get(1) as *const ClosureHeader; let elements = - (arr as *const u8).add(std::mem::size_of::()) as *const f64; + crate::array::array_elements_ptr(arr) as *const f64; c.less_equal_at( comparator, *elements.add(a as usize), @@ -210,7 +210,7 @@ unsafe fn apply_sorted_indices(data: &RootedArrayElems<'_>, order: &mut [u32]) { // receiver write-back. This replaces O(n log n) barriered element writes // with O(n) writes, without suppressing any collection in the comparator. let arr = data.arr(); - let elements = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + let elements = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut f64; mark_array_layout_unknown(arr); for start in 0..order.len() { if order[start] as usize == start { @@ -493,8 +493,8 @@ unsafe fn publish_sorted_values( // No user code or allocation in this region. Resolve each root once, // copy the dense prefix, and rebuild layout/barriers after all stores. let source = - (values.arr() as *const u8).add(std::mem::size_of::()) as *const f64; - let dest = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + crate::array::array_elements_ptr(values.arr() as *const ArrayHeader) as *const f64; + let dest = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut f64; mark_array_layout_unknown(arr); if let Some(order) = order { debug_assert_eq!(order.len(), count); @@ -632,7 +632,7 @@ unsafe fn sort_needs_spec_path(arr: *const ArrayHeader, objproto_keys: &[u32]) - return false; } let length = (*arr).length as usize; - let elements = (arr as *const u8).add(std::mem::size_of::()) as *const f64; + let elements = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64; (0..length).any(|i| (*elements.add(i)).to_bits() == crate::value::TAG_HOLE) } @@ -814,7 +814,7 @@ unsafe fn sort_array_receiver( if length <= 1 { return arr; } - let elements_ptr = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + let elements_ptr = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut f64; // ECMAScript SortIndexedProperties + CompareArrayElements: array holes // are excluded from the sort and trail every element, and `undefined` @@ -842,7 +842,8 @@ unsafe fn sort_array_receiver( { // Re-derive after the temp allocation above (which can GC). let arr = arr_handle.get_raw_mut_ptr::(); - let elements_ptr = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + let elements_ptr = + crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut f64; for i in 0..length { let v = *elements_ptr.add(i); let bits = v.to_bits(); @@ -886,8 +887,8 @@ unsafe fn sort_array_receiver( { // Re-derive after the temp allocation above (which can GC). let arr = arr_handle.get_raw_mut_ptr::(); - let recv_elems = (arr as *const u8).add(std::mem::size_of::()) as *const f64; - let dest = (temp.arr() as *mut u8).add(std::mem::size_of::()) as *mut f64; + let recv_elems = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64; + let dest = crate::array::array_elements_ptr(temp.arr() as *const ArrayHeader) as *mut f64; // GC_STORE_AUDIT(BARRIERED): initializing the private snapshot has no // safepoint before the layout/remembered-edge rebuild immediately below. std::ptr::copy_nonoverlapping(recv_elems, dest, length); diff --git a/crates/perry-runtime/src/array/splice_slice.rs b/crates/perry-runtime/src/array/splice_slice.rs index fd89d16f97..d2792708fa 100644 --- a/crates/perry-runtime/src/array/splice_slice.rs +++ b/crates/perry-runtime/src/array/splice_slice.rs @@ -80,7 +80,7 @@ pub extern "C" fn js_array_splice( let deleted_is_plain = crate::array::species::species_result_is_plain_array(deleted_box); let deleted = crate::value::js_nanbox_get_pointer(deleted_box) as *mut ArrayHeader; - let elements_ptr = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + let elements_ptr = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut f64; // Copy deleted elements to return array. ECMA-262 Β§23.1.3.31 step // 12.b: each removed index goes through HasProperty/Get β€” a hole @@ -100,7 +100,7 @@ pub extern "C" fn js_array_splice( if deleted_is_plain { (*deleted).length = actual_delete; let deleted_elements = - (deleted as *mut u8).add(std::mem::size_of::()) as *mut f64; + crate::array::array_elements_ptr(deleted as *const ArrayHeader) as *mut f64; // Hole reads also consult recorded custom prototypes, which are // not covered by array_iteration_is_exotic's canonical-proto flags. let src_exotic = crate::array::array_iteration_is_exotic(arr) @@ -132,7 +132,7 @@ pub extern "C" fn js_array_splice( } else { arr }; - let elements_ptr = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + let elements_ptr = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut f64; // Shift elements after the splice point let tail_start = start_idx + actual_delete; @@ -270,12 +270,13 @@ pub extern "C" fn js_array_slice( // (has Array.prototype or Object.prototype indexed properties) so // inherited indices appear in the result just as [[Get]] would return // them (ECMA-262 Β§23.1.3.25 step 8b "If HasProperty(O, from)…"). - let src_elements = (arr as *const u8).add(std::mem::size_of::()) as *const f64; + let src_elements = + crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64; let src_exotic = crate::array::array_iteration_is_exotic(arr); if is_plain { (*result).length = slice_len; let dst_elements = - (result as *mut u8).add(std::mem::size_of::()) as *mut f64; + crate::array::array_elements_ptr(result as *const ArrayHeader) as *mut f64; for i in 0..slice_len as usize { let src_idx = start_idx as usize + i; let v = if src_exotic { diff --git a/crates/perry-runtime/src/array/spread_dense_tests.rs b/crates/perry-runtime/src/array/spread_dense_tests.rs index 456913440d..412ab191d5 100644 --- a/crates/perry-runtime/src/array/spread_dense_tests.rs +++ b/crates/perry-runtime/src/array/spread_dense_tests.rs @@ -26,7 +26,7 @@ fn dense(values: &[f64]) -> *mut ArrayHeader { } unsafe fn slot_bits(arr: *const ArrayHeader, index: usize) -> u64 { - let elements = (arr as *const u8).add(std::mem::size_of::()) as *const u64; + let elements = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const u64; std::ptr::read(elements.add(index)) } diff --git a/crates/perry-runtime/src/array/storage.rs b/crates/perry-runtime/src/array/storage.rs new file mode 100644 index 0000000000..819d2280aa --- /dev/null +++ b/crates/perry-runtime/src/array/storage.rs @@ -0,0 +1,72 @@ +//! Dense queues keep their live elements in a contiguous suffix of the allocation. +//! +//! `capacity` is the capacity remaining AFTER the front offset. The GC header +//! already records the allocation's byte size, so subtracting the remaining +//! capacity from its physical capacity recovers the offset. All length/capacity +//! bounds and sparse-index rules keep their usual meaning. No extra allocation, +//! metadata slot, header word, side table, or survivor copy is needed. +//! +//! GC layouts index the logical live range returned by `array_elements_ptr`. +//! Removing the front invalidates indexed masks, but preserves pointer-free and +//! all-pointer proofs. Surviving slots stay at the same addresses, so their +//! old-to-young barriers remain valid. Growth copies the logical range into a +//! new, unshifted allocation and replays its barriers normally. + +use super::ArrayHeader; + +#[inline] +pub(crate) unsafe fn array_physical_capacity(arr: *const ArrayHeader) -> usize { + let header = (arr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + ((*header).size as usize - crate::gc::GC_HEADER_SIZE - std::mem::size_of::()) / 8 +} + +/// Offset of logical element zero within the original inline backing store. +/// The caller must supply a live, forwarding-resolved real array. +#[inline] +pub(crate) unsafe fn array_front_offset(arr: *const ArrayHeader) -> usize { + array_physical_capacity(arr) - (*arr).capacity as usize +} + +/// Address of the logical element storage, including any lazy queue offset. +/// +/// # Safety +/// `arr` must be a live, forwarding-resolved GC_TYPE_ARRAY. As with other GC +/// interior pointers, the result must not be retained across a safepoint. +#[inline] +pub unsafe fn array_elements_ptr(arr: *const ArrayHeader) -> *mut u64 { + (arr.add(1) as *mut u64).add(array_front_offset(arr)) +} + +/// Remove one ordinary dense slot without relocating any surviving element. +/// Receiver/property checks happen before entry and nothing here can collect. +pub(super) unsafe fn shift_dense(arr: *mut ArrayHeader) -> f64 { + let front = array_front_offset(arr); + let first = array_elements_ptr(arr); + let bits = *first; + // GC_STORE_AUDIT(BARRIERED): clearing a removed slot introduces no edge; + // the live range and logical layout are updated below without a safepoint. + first.write(crate::value::TAG_HOLE); + (*arr).length -= 1; + super::element_shape::clear_element_shape(arr); + if (*arr).length == 0 { + (*arr).capacity += front as u32; + super::rebuild_array_layout(arr); + } else { + (*arr).capacity -= 1; + let flags = super::header::array_object_flags_resolved(arr); + let layout = flags & (crate::gc::GC_LAYOUT_STATE_MASK | crate::gc::GC_LAYOUT_ALL_POINTERS); + if layout != crate::gc::GC_LAYOUT_POINTER_FREE + && layout != (crate::gc::GC_LAYOUT_SIDE_MASK | crate::gc::GC_LAYOUT_ALL_POINTERS) + && flags & crate::gc::GC_LAYOUT_STATE_MASK != 0 + { + // Per-index masks described the old logical indices. UNKNOWN + // traces all live slots; no survivor is reclassified here. + crate::gc::layout_mark_unknown(arr.cast()); + } + } + if bits == crate::value::TAG_HOLE { + f64::from_bits(crate::value::TAG_UNDEFINED) + } else { + f64::from_bits(bits) + } +} diff --git a/crates/perry-runtime/src/array/subclass.rs b/crates/perry-runtime/src/array/subclass.rs index 28074a6a12..a17db4f826 100644 --- a/crates/perry-runtime/src/array/subclass.rs +++ b/crates/perry-runtime/src/array/subclass.rs @@ -789,8 +789,7 @@ unsafe fn ensure_subclass_numeric_prefix( if spill.is_null() || slot >= (*spill).length { return false; } - (spill as *mut u8) - .add(std::mem::size_of::()) + crate::array::array_elements_ptr(spill as *const ArrayHeader) .cast::() .add(slot as usize) }; @@ -864,7 +863,7 @@ fn layout_field_value(obj: *const ObjectHeader, slot: u32, live_inline_slots: u3 let spill = (*meta).spill as *const ArrayHeader; if !spill.is_null() && slot < (*spill).length { let elements = - (spill as *const u8).add(std::mem::size_of::()) as *const u64; + crate::array::array_elements_ptr(spill as *const ArrayHeader) as *const u64; return JSValue::from_bits(*elements.add(slot as usize)); } } @@ -1032,9 +1031,7 @@ unsafe fn store_dense_slot( return false; } note_packed_subclass_spill_store(obj, meta); - let elements = (spill as *mut u8) - .add(std::mem::size_of::()) - .cast::(); + let elements = crate::array::array_elements_ptr(spill as *const ArrayHeader).cast::(); // GC_STORE_AUDIT(BARRIERED): the `note_array_slot` below records layout and emits the spill slot barrier. ptr::write(elements.add(slot as usize), value_bits); note_array_slot(spill, slot as usize, value_bits); @@ -1079,9 +1076,7 @@ unsafe fn store_dense_nonpointer_number_slot( if spill.is_null() || slot >= (*spill).length || slot >= (*spill).capacity { return false; } - let elements = (spill as *mut u8) - .add(std::mem::size_of::()) - .cast::(); + let elements = crate::array::array_elements_ptr(spill as *const ArrayHeader).cast::(); // GC_STORE_AUDIT(POINTER_FREE): raw Number into a spill slot the caller proved pointer-free; no edge changes. ptr::write(elements.add(slot as usize), value_bits); true @@ -1112,9 +1107,7 @@ unsafe fn clear_retired_dense_slot( if spill.is_null() || slot >= (*spill).length { return; } - let elements = (spill as *mut u8) - .add(std::mem::size_of::()) - .cast::(); + let elements = crate::array::array_elements_ptr(spill as *const ArrayHeader).cast::(); // GC_STORE_AUDIT(POINTER_FREE): retiring a spill tail slot to `undefined` publishes no edge. ptr::write(elements.add(slot as usize), crate::value::TAG_UNDEFINED); note_array_slot(spill, slot as usize, crate::value::TAG_UNDEFINED); @@ -1147,9 +1140,7 @@ unsafe fn clear_retired_dense_numeric_tail_slot( if spill.is_null() || slot >= (*spill).length { return; } - let elements = (spill as *mut u8) - .add(std::mem::size_of::()) - .cast::(); + let elements = crate::array::array_elements_ptr(spill as *const ArrayHeader).cast::(); // GC_STORE_AUDIT(POINTER_FREE): retiring a spill tail slot to `undefined` publishes no edge. ptr::write(elements.add(slot as usize), crate::value::TAG_UNDEFINED); } @@ -1622,7 +1613,8 @@ pub fn array_subclass_dense_snapshot(recv: f64) -> f64 { crate::array::array_length_range_error(); } let result = js_array_alloc_with_length(len as u32); - let elems = unsafe { (result as *mut u8).add(std::mem::size_of::()) as *mut f64 }; + let elems = + unsafe { crate::array::array_elements_ptr(result as *const ArrayHeader) as *mut f64 }; for k in 0..len { let v = al_get(recv, k); unsafe { diff --git a/crates/perry-runtime/src/array/subclass_elements.rs b/crates/perry-runtime/src/array/subclass_elements.rs index 6c8c98bc4a..ac82be8800 100644 --- a/crates/perry-runtime/src/array/subclass_elements.rs +++ b/crates/perry-runtime/src/array/subclass_elements.rs @@ -233,8 +233,7 @@ pub(crate) fn key_of_value(key: f64) -> Option { #[inline] unsafe fn slot_bits(elements: *const ArrayHeader, index: u32) -> u64 { - *(elements as *const u8) - .add(std::mem::size_of::()) + *crate::array::array_elements_ptr(elements as *const ArrayHeader) .cast::() .add(index as usize) } @@ -626,8 +625,7 @@ pub(super) fn elements_index_get(elements: *const ArrayHeader, index: u32) -> Op if index >= (*elements).length { return None; } - let slot = (elements as *const u8) - .add(std::mem::size_of::()) + let slot = crate::array::array_elements_ptr(elements as *const ArrayHeader) .cast::() .add(index as usize); let bits = *slot; diff --git a/crates/perry-runtime/src/array/tests.rs b/crates/perry-runtime/src/array/tests.rs index 2248ba2b3b..78f5ffdde0 100644 --- a/crates/perry-runtime/src/array/tests.rs +++ b/crates/perry-runtime/src/array/tests.rs @@ -63,7 +63,7 @@ fn assert_canonical_raw_slot(arr: *mut ArrayHeader, index: u32, expected: f64) { } unsafe fn raw_slot_bits(arr: *mut ArrayHeader, index: usize) -> u64 { - let elements = (arr as *const u8).add(std::mem::size_of::()) as *const u64; + let elements = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const u64; *elements.add(index) } @@ -752,7 +752,7 @@ fn test_new_array_holes_flag_walk_free_guard_and_sound_downgrade() { // loop's raw-f64 loads. let int32_value = f64::from_bits(crate::value::INT32_TAG | 7u64); js_array_set_f64(arr, 0, int32_value); - let elements = (arr as *const u8).add(std::mem::size_of::()) as *const u64; + let elements = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const u64; assert_eq!( *elements, // slot 0 7.0f64.to_bits(), diff --git a/crates/perry-runtime/src/array/tests_from_string_codepoints.rs b/crates/perry-runtime/src/array/tests_from_string_codepoints.rs index 017aee8d20..a00fcf347d 100644 --- a/crates/perry-runtime/src/array/tests_from_string_codepoints.rs +++ b/crates/perry-runtime/src/array/tests_from_string_codepoints.rs @@ -15,7 +15,7 @@ use crate::string::{ /// The `index`-th element of the result, as (bytes, flags). unsafe fn element(arr: *mut ArrayHeader, index: usize) -> (Vec, u32) { - let elements = (arr as *const u8).add(std::mem::size_of::()) as *const f64; + let elements = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64; let value = *elements.add(index); let part = crate::value::js_nanbox_get_pointer(value) as *const StringHeader; assert!(!part.is_null(), "element {index} is not a string"); diff --git a/crates/perry-runtime/src/async_hooks.rs b/crates/perry-runtime/src/async_hooks.rs index 54ba59d03a..f49f8828fe 100644 --- a/crates/perry-runtime/src/async_hooks.rs +++ b/crates/perry-runtime/src/async_hooks.rs @@ -1584,7 +1584,7 @@ pub extern "C" fn js_async_resource_run_in_async_scope( } else { let len = js_array_length(arr) as i64; let data = unsafe { - (arr as *const u8).add(std::mem::size_of::()) as *const f64 + crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64 }; unsafe { js_closure_call_array(callback as i64, data, len) } } @@ -1845,7 +1845,7 @@ fn call_callback_with_rest(callback_value: f64, this_arg: f64, rest: f64) -> f64 let data = if arr.is_null() || len == 0 { ptr::null() } else { - unsafe { (arr as *const u8).add(std::mem::size_of::()) as *const f64 } + unsafe { crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64 } }; unsafe { js_closure_call_array(callback as i64, data, len) } }; diff --git a/crates/perry-runtime/src/buffer/encode.rs b/crates/perry-runtime/src/buffer/encode.rs index 1f088eb4db..4149d45e71 100644 --- a/crates/perry-runtime/src/buffer/encode.rs +++ b/crates/perry-runtime/src/buffer/encode.rs @@ -297,7 +297,7 @@ pub fn buffer_to_array(buf_ptr: *const BufferHeader) -> *mut ArrayHeader { return result; } let src = buffer_data(buf_ptr); - let dst = (result as *mut u8).add(std::mem::size_of::()) as *mut f64; + let dst = crate::array::array_elements_ptr(result as *const ArrayHeader) as *mut f64; for i in 0..len { *dst.add(i) = (*src.add(i)) as f64; } diff --git a/crates/perry-runtime/src/buffer/from.rs b/crates/perry-runtime/src/buffer/from.rs index 2428d9eb8a..508403948f 100644 --- a/crates/perry-runtime/src/buffer/from.rs +++ b/crates/perry-runtime/src/buffer/from.rs @@ -1251,7 +1251,8 @@ fn js_buffer_concat_impl( unsafe { let len = (*arr_ptr).length as usize; - let arr_data = (arr_ptr as *const u8).add(std::mem::size_of::()) as *const f64; + let arr_data = + crate::array::array_elements_ptr(arr_ptr as *const ArrayHeader) as *const f64; // Helper to strip NaN-boxing tags from buffer element pointers let strip_nanbox = |bits: u64| -> u64 { diff --git a/crates/perry-runtime/src/buffer/iter.rs b/crates/perry-runtime/src/buffer/iter.rs index d2e56aa13e..f25e665a96 100644 --- a/crates/perry-runtime/src/buffer/iter.rs +++ b/crates/perry-runtime/src/buffer/iter.rs @@ -94,7 +94,8 @@ use crate::iter_result::make_iter_result; unsafe fn make_pair_array(idx: u32, byte: u8) -> f64 { let pair = crate::array::js_array_alloc(2); (*pair).length = 2; - let elems = (pair as *mut u8).add(std::mem::size_of::()) as *mut f64; + let elems = + crate::array::array_elements_ptr(pair as *const crate::array::ArrayHeader) as *mut f64; *elems.add(0) = idx as f64; *elems.add(1) = byte as f64; crate::array::note_array_slot(pair, 0, (idx as f64).to_bits()); diff --git a/crates/perry-runtime/src/builtins/console.rs b/crates/perry-runtime/src/builtins/console.rs index 05f925ff0f..5c7f6c93d4 100644 --- a/crates/perry-runtime/src/builtins/console.rs +++ b/crates/perry-runtime/src/builtins/console.rs @@ -1365,7 +1365,7 @@ pub extern "C" fn js_console_assert_spread(cond: f64, args_arr_handle: i64) { eprintln!("Assertion failed"); return; } - let elements = (arr_ptr as *const u8).add(std::mem::size_of::()) + let elements = crate::array::array_elements_ptr(arr_ptr as *const crate::array::ArrayHeader) as *const f64; let first = JSValue::from_bits((*elements).to_bits()); let formatted = js_util_format(arr_ptr); diff --git a/crates/perry-runtime/src/builtins/formatting.rs b/crates/perry-runtime/src/builtins/formatting.rs index c766243fa2..d1d7cf66ce 100644 --- a/crates/perry-runtime/src/builtins/formatting.rs +++ b/crates/perry-runtime/src/builtins/formatting.rs @@ -685,9 +685,9 @@ pub(crate) fn format_jsvalue(value: f64, depth: usize) -> String { }; return inspect_finish_circular(ptr as usize, empty); } - let data_ptr = (maybe_arr as *const u8) - .add(std::mem::size_of::()) - as *const f64; + let data_ptr = crate::array::array_elements_ptr( + maybe_arr as *const crate::array::ArrayHeader, + ) as *const f64; // #9415: a hole slot is `TAG_HOLE`, whose bits read back as // a NaN, so element-by-element recursion printed // `new Array(3)` as `[ NaN, NaN, NaN ]`. Runs of holes are @@ -1418,9 +1418,9 @@ fn format_jsvalue_for_json(value: f64, depth: usize) -> String { if length > 1_000_000 { return inspect_finish_circular(ptr as usize, "[Array]".to_string()); } - let data_ptr = (maybe_arr as *const u8) - .add(std::mem::size_of::()) - as *const f64; + let data_ptr = crate::array::array_elements_ptr( + maybe_arr as *const crate::array::ArrayHeader, + ) as *const f64; // #9415: the same hole grouping the `format_jsvalue` // array arm does. This is the twin that renders an // array reached as an object FIELD, so without it @@ -1605,7 +1605,7 @@ pub extern "C" fn js_array_print(arr_ptr: *const crate::array::ArrayHeader) { unsafe { let length = (*arr_ptr).length as usize; - let data_ptr = (arr_ptr as *const u8).add(std::mem::size_of::()) + let data_ptr = crate::array::array_elements_ptr(arr_ptr as *const crate::array::ArrayHeader) as *const f64; let mut parts: Vec = Vec::with_capacity(length); diff --git a/crates/perry-runtime/src/builtins/formatting/errors.rs b/crates/perry-runtime/src/builtins/formatting/errors.rs index d940f895d9..781d48fd37 100644 --- a/crates/perry-runtime/src/builtins/formatting/errors.rs +++ b/crates/perry-runtime/src/builtins/formatting/errors.rs @@ -72,7 +72,7 @@ unsafe fn format_error_array(arr_ptr: *const crate::array::ArrayHeader, depth: u return "[]".to_string(); } let data_ptr = - (arr_ptr as *const u8).add(std::mem::size_of::()) as *const f64; + crate::array::array_elements_ptr(arr_ptr as *const crate::array::ArrayHeader) as *const f64; let mut out = String::from("["); for i in 0..length { out.push('\n'); diff --git a/crates/perry-runtime/src/builtins/formatting/util_format.rs b/crates/perry-runtime/src/builtins/formatting/util_format.rs index 07acd6adf4..2af6479eca 100644 --- a/crates/perry-runtime/src/builtins/formatting/util_format.rs +++ b/crates/perry-runtime/src/builtins/formatting/util_format.rs @@ -67,7 +67,7 @@ unsafe fn util_format_json_array_has_cycle(ptr: *const u8, stack: &mut Vec()) as *const f64; + let elements = crate::array::array_elements_ptr(ptr as *const crate::ArrayHeader) as *const f64; let found = (0..len).any(|i| { let value = *elements.add(i); let bits = value.to_bits(); @@ -243,7 +243,7 @@ pub extern "C" fn js_util_format(arr_ptr: *const crate::array::ArrayHeader) -> f } unsafe { let length = (*arr_ptr).length as usize; - let data_ptr = (arr_ptr as *const u8).add(std::mem::size_of::()) + let data_ptr = crate::array::array_elements_ptr(arr_ptr as *const crate::array::ArrayHeader) as *const f64; // No format string β†’ empty result. Node returns "" for diff --git a/crates/perry-runtime/src/builtins/globals.rs b/crates/perry-runtime/src/builtins/globals.rs index 54b3a1a1f4..a25fc16f91 100644 --- a/crates/perry-runtime/src/builtins/globals.rs +++ b/crates/perry-runtime/src/builtins/globals.rs @@ -837,16 +837,16 @@ fn js_structured_clone_inner(value: f64, depth: usize) -> f64 { for i in 0..len as usize { let new_arr = pointer_addr(structured_clone_memo_value(memo_index)).unwrap() as *mut crate::array::ArrayHeader; - let elements = (new_arr as *mut u8) - .add(std::mem::size_of::()) - as *mut f64; + let elements = crate::array::array_elements_ptr( + new_arr as *const crate::array::ArrayHeader, + ) as *mut f64; let elem = *elements.add(i); let cloned = js_structured_clone_inner(elem, depth + 1); let new_arr = pointer_addr(structured_clone_memo_value(memo_index)).unwrap() as *mut crate::array::ArrayHeader; - let elements = (new_arr as *mut u8) - .add(std::mem::size_of::()) - as *mut f64; + let elements = crate::array::array_elements_ptr( + new_arr as *const crate::array::ArrayHeader, + ) as *mut f64; // GC_STORE_AUDIT(BARRIERED): note_array_slot below re-stores this slot with the barrier. *elements.add(i) = cloned; crate::array::note_array_slot(new_arr, i, cloned.to_bits()); diff --git a/crates/perry-runtime/src/builtins/table.rs b/crates/perry-runtime/src/builtins/table.rs index 5f24be7cd3..70e16807a5 100644 --- a/crates/perry-runtime/src/builtins/table.rs +++ b/crates/perry-runtime/src/builtins/table.rs @@ -259,7 +259,7 @@ fn table_properties_from_value(value: f64) -> Option> { return None; } let length = (*arr_ptr).length as usize; - let data_ptr = (arr_ptr as *const u8).add(std::mem::size_of::()) + let data_ptr = crate::array::array_elements_ptr(arr_ptr as *const crate::array::ArrayHeader) as *const f64; let mut out = Vec::with_capacity(length); for i in 0..length { @@ -329,9 +329,9 @@ pub extern "C" fn js_console_table_with_properties(value: f64, properties: f64) return; } let length = (*arr_ptr).length as usize; - let data_ptr = (arr_ptr as *const u8) - .add(std::mem::size_of::()) - as *const f64; + let data_ptr = + crate::array::array_elements_ptr(arr_ptr as *const crate::array::ArrayHeader) + as *const f64; if length == 0 { render_table(&["(index)".to_string()], &[]); @@ -452,9 +452,9 @@ pub extern "C" fn js_console_table_with_properties(value: f64, properties: f64) let sub = JSValue::from_bits(elem.to_bits()) .as_pointer::(); let sub_len = ((*sub).length as usize).min(max_len); - let sub_data = (sub as *const u8) - .add(std::mem::size_of::()) - as *const f64; + let sub_data = + crate::array::array_elements_ptr(sub as *const crate::array::ArrayHeader) + as *const f64; for (j, slot) in present.iter_mut().enumerate().take(sub_len) { if (*sub_data.add(j)).to_bits() != crate::value::TAG_HOLE { *slot = true; @@ -482,9 +482,9 @@ pub extern "C" fn js_console_table_with_properties(value: f64, properties: f64) if get_gc_type(elem) == crate::gc::GC_TYPE_ARRAY { let sub = elem_jsval.as_pointer::(); let sub_len = (*sub).length as usize; - let sub_data = (sub as *const u8) - .add(std::mem::size_of::()) - as *const f64; + let sub_data = crate::array::array_elements_ptr( + sub as *const crate::array::ArrayHeader, + ) as *const f64; for &j in &columns { // A slot this row does not own renders as an empty // cell, exactly like a short row's missing tail β€” diff --git a/crates/perry-runtime/src/child_process/registry.rs b/crates/perry-runtime/src/child_process/registry.rs index dd4c121915..8fdbe801ba 100644 --- a/crates/perry-runtime/src/child_process/registry.rs +++ b/crates/perry-runtime/src/child_process/registry.rs @@ -139,9 +139,9 @@ pub extern "C" fn js_child_process_spawn_background( if args_ptr != 0 { let arr_ptr = args_ptr as *const crate::array::ArrayHeader; let args_len = (*arr_ptr).length as usize; - let args_data = (arr_ptr as *const u8) - .add(std::mem::size_of::()) - as *const f64; + let args_data = + crate::array::array_elements_ptr(arr_ptr as *const crate::array::ArrayHeader) + as *const f64; for i in 0..args_len { let arg_val = *args_data.add(i); if let Some(arg_str) = extract_string_from_nanboxed(arg_val) { @@ -271,9 +271,9 @@ pub extern "C" fn js_child_process_spawn_detached( if args_ptr != 0 { let arr_ptr = args_ptr as *const crate::array::ArrayHeader; let args_len = (*arr_ptr).length as usize; - let args_data = (arr_ptr as *const u8) - .add(std::mem::size_of::()) - as *const f64; + let args_data = + crate::array::array_elements_ptr(arr_ptr as *const crate::array::ArrayHeader) + as *const f64; for i in 0..args_len { let arg_val = *args_data.add(i); if let Some(arg_str) = extract_string_from_nanboxed(arg_val) { diff --git a/crates/perry-runtime/src/child_process/value_util.rs b/crates/perry-runtime/src/child_process/value_util.rs index 028de7240e..18dbb2f640 100644 --- a/crates/perry-runtime/src/child_process/value_util.rs +++ b/crates/perry-runtime/src/child_process/value_util.rs @@ -195,7 +195,7 @@ pub(crate) unsafe fn cp_read_arg_strings(args_ptr: i64) -> Vec { let arr = args_ptr as *const crate::array::ArrayHeader; let n = (*arr).length as usize; let data = - (arr as *const u8).add(std::mem::size_of::()) as *const f64; + crate::array::array_elements_ptr(arr as *const crate::array::ArrayHeader) as *const f64; for i in 0..n { if let Some(s) = cp_value_to_string(*data.add(i)) { out.push(s); diff --git a/crates/perry-runtime/src/gc/census.rs b/crates/perry-runtime/src/gc/census.rs index 8b7d53b679..903df26e6c 100644 --- a/crates/perry-runtime/src/gc/census.rs +++ b/crates/perry-runtime/src/gc/census.rs @@ -468,7 +468,9 @@ impl Census { ) { let header_bytes = GC_HEADER_SIZE + std::mem::size_of::(); let slot_capacity = size.saturating_sub(header_bytes) / 8; - let length = ((*arr).length as usize).min(slot_capacity); + let length = ((*arr).length as usize) + .min((*arr).capacity as usize) + .min(slot_capacity); self.arr_length += length as u64; self.arr_capacity += slot_capacity as u64; self.arr_buckets[size_bucket(size)].add(size); @@ -476,7 +478,7 @@ impl Census { self.arr_shape_keys.add(size); } let elems = - (arr as *const u8).add(std::mem::size_of::()) as *const u64; + crate::array::array_elements_ptr(arr as *const crate::array::ArrayHeader) as *const u64; for i in 0..length { self.arr_slot_tags[slot_kind(*elems.add(i))] += 1; } diff --git a/crates/perry-runtime/src/gc/fromspace_scan.rs b/crates/perry-runtime/src/gc/fromspace_scan.rs index 41b1382566..e606c0e452 100644 --- a/crates/perry-runtime/src/gc/fromspace_scan.rs +++ b/crates/perry-runtime/src/gc/fromspace_scan.rs @@ -225,8 +225,9 @@ unsafe fn scan_object(header: *mut GcHeader, report: &mut FromSpaceScanReport) { // shrinking scan cannot silently read as a cleaner heap. if (*header).obj_type == crate::gc::GC_TYPE_ARRAY { let arr = user as *const crate::array::ArrayHeader; - let live_words = - std::mem::size_of::() / 8 + (*arr).length as usize; + let live_words = std::mem::size_of::() / 8 + + crate::array::array_front_offset(arr) + + (*arr).length as usize; if live_words < payload_words { report.array_slack_words_skipped += payload_words - live_words; payload_words = live_words; diff --git a/crates/perry-runtime/src/gc/heap_snapshot.rs b/crates/perry-runtime/src/gc/heap_snapshot.rs index 887e3adcb3..50a47a3991 100644 --- a/crates/perry-runtime/src/gc/heap_snapshot.rs +++ b/crates/perry-runtime/src/gc/heap_snapshot.rs @@ -222,7 +222,7 @@ unsafe fn object_field_name( if field_index >= (*keys).length as usize { return None; } - let elements = (keys_addr + std::mem::size_of::()) as *const u64; + let elements = crate::array::array_elements_ptr(keys_addr as *const crate::array::ArrayHeader); read_heap_string(*elements.add(field_index), 256) } @@ -331,7 +331,7 @@ pub fn gc_build_v8_heap_snapshot_json() -> String { let (elems_base, elems_len) = if rec.obj_type == GC_TYPE_ARRAY { let arr = rec.user as *const crate::array::ArrayHeader; ( - rec.user + std::mem::size_of::(), + unsafe { crate::array::array_elements_ptr(arr) as usize }, unsafe { (*arr).length } as usize * 8, ) } else { diff --git a/crates/perry-runtime/src/gc/tests/array_pointer_slot_enumeration.rs b/crates/perry-runtime/src/gc/tests/array_pointer_slot_enumeration.rs index ca5c1679a6..00976de64e 100644 --- a/crates/perry-runtime/src/gc/tests/array_pointer_slot_enumeration.rs +++ b/crates/perry-runtime/src/gc/tests/array_pointer_slot_enumeration.rs @@ -66,7 +66,7 @@ fn array_slot_enumeration_reports_a_pointer_element_the_layout_omits() { let index = unsafe { (*arr).length } as usize; unsafe { let elements = - (arr as *mut u8).add(std::mem::size_of::()) as *mut u64; + crate::array::array_elements_ptr(arr as *const crate::array::ArrayHeader) as *mut u64; std::ptr::write(elements.add(index), ptr_bits(planted)); (*arr).length = index as u32 + 1; } diff --git a/crates/perry-runtime/src/gc/tests/copying.rs b/crates/perry-runtime/src/gc/tests/copying.rs index 292ff1fb81..dbbab1bb9e 100644 --- a/crates/perry-runtime/src/gc/tests/copying.rs +++ b/crates/perry-runtime/src/gc/tests/copying.rs @@ -6,6 +6,7 @@ mod latch; mod pointer_publish_7154; mod promise_side_tables; mod promoted_remembered_7803; +mod shift_queue; mod survival_and_malloc; mod verify_malloc_borrow; mod verify_parent_context; @@ -1426,7 +1427,7 @@ fn large_object_old_born_array_slot_write_keeps_young_child_alive() { ); let elements = unsafe { - (arr as *mut u8).add(std::mem::size_of::()) as *mut u64 + crate::array::array_elements_ptr(arr as *const crate::array::ArrayHeader) as *mut u64 }; let trace = collect_minor_trace(GcTriggerKind::Direct); let rewritten = unsafe { (*elements & POINTER_MASK) as usize }; @@ -1462,7 +1463,7 @@ fn large_object_array_literal_direct_store_keeps_young_child_alive_and_excludes_ GC_TYPE_ARRAY )); let elements = unsafe { - (arr as *mut u8).add(std::mem::size_of::()) as *mut u64 + crate::array::array_elements_ptr(arr as *const crate::array::ArrayHeader) as *mut u64 }; unsafe { *elements = ptr_bits(child); @@ -1507,7 +1508,7 @@ fn large_object_inline_push_store_keeps_young_child_alive_and_excludes_parent() )); let elements = unsafe { - (arr as *mut u8).add(std::mem::size_of::()) as *mut u64 + crate::array::array_elements_ptr(arr as *const crate::array::ArrayHeader) as *mut u64 }; let slot = unsafe { let length = (*arr).length as usize; @@ -1794,7 +1795,7 @@ fn test_copying_minor_copies_transitive_young_graph() { unsafe { (*arr).length = 1; let elements = - (arr as *mut u8).add(std::mem::size_of::()) as *mut u64; + crate::array::array_elements_ptr(arr as *const crate::array::ArrayHeader) as *mut u64; *elements = ptr_bits(child); layout_note_slot(arr as usize, 0, *elements); } @@ -1803,8 +1804,9 @@ fn test_copying_minor_copies_transitive_young_graph() { let _ = gc_collect_minor(); let arr_after = (js_shadow_slot_get(0) & POINTER_MASK) as usize; let child_after = unsafe { - let elements = (arr_after as *mut u8).add(std::mem::size_of::()) - as *mut u64; + let elements = + crate::array::array_elements_ptr(arr_after as *const crate::array::ArrayHeader) + as *mut u64; (*elements & POINTER_MASK) as usize }; @@ -1822,7 +1824,7 @@ fn test_copying_minor_moves_layout_masked_transitive_object() { unsafe { (*arr).length = 1; let elements = - (arr as *mut u8).add(std::mem::size_of::()) as *mut u64; + crate::array::array_elements_ptr(arr as *const crate::array::ArrayHeader) as *mut u64; *elements = ptr_bits(child as usize); layout_note_slot(arr as usize, 0, *elements); } @@ -1831,8 +1833,9 @@ fn test_copying_minor_moves_layout_masked_transitive_object() { let trace = collect_minor_trace(GcTriggerKind::Direct); let arr_after = (js_shadow_slot_get(0) & POINTER_MASK) as usize; let child_after = unsafe { - let elements = (arr_after as *mut u8).add(std::mem::size_of::()) - as *mut u64; + let elements = + crate::array::array_elements_ptr(arr_after as *const crate::array::ArrayHeader) + as *mut u64; (*elements & POINTER_MASK) as usize }; diff --git a/crates/perry-runtime/src/gc/tests/copying/all_pointer_elements_7469.rs b/crates/perry-runtime/src/gc/tests/copying/all_pointer_elements_7469.rs index 2484d87519..7cee428a6e 100644 --- a/crates/perry-runtime/src/gc/tests/copying/all_pointer_elements_7469.rs +++ b/crates/perry-runtime/src/gc/tests/copying/all_pointer_elements_7469.rs @@ -43,13 +43,13 @@ unsafe fn elided_inline_push(arr: *mut ArrayHeader, value_bits: u64) { length < (*arr).capacity as usize, "the elided inline push models the in-capacity arm only" ); - let elements = (arr as *mut u8).add(std::mem::size_of::()) as *mut u64; + let elements = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut u64; std::ptr::write(elements.add(length), value_bits); (*arr).length = length as u32 + 1; } unsafe fn element_bits(arr: *mut ArrayHeader, index: usize) -> u64 { - let elements = (arr as *mut u8).add(std::mem::size_of::()) as *const u64; + let elements = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const u64; *elements.add(index) } diff --git a/crates/perry-runtime/src/gc/tests/copying/shift_queue.rs b/crates/perry-runtime/src/gc/tests/copying/shift_queue.rs new file mode 100644 index 0000000000..583a4273e8 --- /dev/null +++ b/crates/perry-runtime/src/gc/tests/copying/shift_queue.rs @@ -0,0 +1,86 @@ +//! Moving-collector witnesses for the array queue's logical slot range. +use super::*; +use crate::array::{self, ArrayHeader}; + +#[test] +fn shift_queue_mixed_survivors_move_and_removed_slots_are_not_roots() { + let _guard = CopyingNurseryTestGuard::new(1); + let arr = array::js_array_alloc(16); + let removed = young_leaf(); + array::js_array_push_f64(arr, f64::from_bits(ptr_bits(removed))); + array::js_array_push_f64(arr, 12.0); + let child = young_leaf(); + array::js_array_push_f64(arr, f64::from_bits(ptr_bits(child))); + let original_slots = unsafe { array::array_elements_ptr(arr) }; + assert_eq!(array::js_array_shift_f64(arr).to_bits(), ptr_bits(removed)); + unsafe { + assert_eq!(*original_slots, crate::value::TAG_HOLE); + let slots = test_heap_child_slots_for_user(arr.cast()); + assert!(slots + .iter() + .any(|s| matches!(s, HeapChildSlot::Child(slot, _) if *slot == original_slots.add(2)))); + assert!(slots + .iter() + .all(|s| !matches!(s, HeapChildSlot::Child(slot, _) if *slot == original_slots))); + } + js_shadow_slot_set(0, ptr_bits(arr as usize)); + let trace = collect_minor_trace(GcTriggerKind::Direct); + assert_copied_minor_trace(&trace, true, CopiedMinorFallbackReason::None, false); + assert!(trace.copying_nursery.copied_objects >= 2); + let moved = (js_shadow_slot_get(0) & POINTER_MASK) as *mut ArrayHeader; + assert_ne!(moved, arr); + assert_eq!(array::js_array_get_f64(moved, 0), 12.0); + let moved_child = (array::js_array_get_f64(moved, 1).to_bits() & POINTER_MASK) as usize; + assert_ne!(moved_child, child); + assert!(crate::arena::pointer_in_nursery(moved_child)); + unsafe { + assert_eq!(*((moved.add(1)) as *const u64), crate::value::TAG_HOLE); + } + js_shadow_slot_set(0, crate::value::TAG_UNDEFINED); +} + +#[test] +fn shift_queue_old_destination_remembers_young_edges_across_growth_and_refill() { + let _guard = CopyingNurseryTestGuard::new(1); + let capacity = OLD_BORN_ELEMENTS; + let arr = array::js_array_alloc(capacity); + assert!(crate::arena::pointer_in_old_gen(arr as usize)); + js_shadow_slot_set(0, ptr_bits(arr as usize)); + let mut current = arr; + for cycle in 0..3 { + for _ in 0..capacity { + let child = young_leaf(); + current = array::js_array_push_f64(current, f64::from_bits(ptr_bits(child))); + } + for _ in 0..capacity / 2 { + array::js_array_shift_f64(current); + } + // This append crosses the remaining capacity on the first refill and + // normalizes shifted storage while installing a growth forwarding stub. + for _ in 0..capacity { + let child = young_leaf(); + current = array::js_array_push_f64(current, f64::from_bits(ptr_bits(child))); + } + js_shadow_slot_set(0, ptr_bits(current as usize)); + let before = array::js_array_get_f64(current, 0).to_bits() & POINTER_MASK; + let trace = collect_minor_trace(GcTriggerKind::Direct); + assert_copied_minor_trace(&trace, true, CopiedMinorFallbackReason::None, false); + assert!( + trace.copying_nursery.copied_objects > 0, + "cycle {cycle} must copy children" + ); + let after = array::js_array_get_f64(current, 0).to_bits() & POINTER_MASK; + assert_ne!(after, before); + assert_eq!(array::js_array_length(arr), array::js_array_length(current)); + for i in 0..array::js_array_length(current) { + let bits = array::js_array_get_f64(current, i).to_bits(); + assert!(crate::arena::pointer_in_nursery( + (bits & POINTER_MASK) as usize + )); + } + while array::js_array_length(current) != 0 { + array::js_array_shift_f64(current); + } + } + js_shadow_slot_set(0, crate::value::TAG_UNDEFINED); +} diff --git a/crates/perry-runtime/src/gc/tests/copying/survival_and_malloc.rs b/crates/perry-runtime/src/gc/tests/copying/survival_and_malloc.rs index 19aedfa34f..9fff379308 100644 --- a/crates/perry-runtime/src/gc/tests/copying/survival_and_malloc.rs +++ b/crates/perry-runtime/src/gc/tests/copying/survival_and_malloc.rs @@ -728,7 +728,7 @@ fn test_copying_minor_falls_back_for_transitive_pinned_young_child() { let elements = unsafe { (*arr).length = 1; let elements = - (arr as *mut u8).add(std::mem::size_of::()) as *mut u64; + crate::array::array_elements_ptr(arr as *const crate::array::ArrayHeader) as *mut u64; *elements = ptr_bits(child); layout_note_slot(arr as usize, 0, *elements); crate::gc::pin_object(header_from_user_ptr(child as *const u8)); diff --git a/crates/perry-runtime/src/gc/tests/helper_stores.rs b/crates/perry-runtime/src/gc/tests/helper_stores.rs index 1dd70c97a8..b3752e2f70 100644 --- a/crates/perry-runtime/src/gc/tests/helper_stores.rs +++ b/crates/perry-runtime/src/gc/tests/helper_stores.rs @@ -202,7 +202,7 @@ fn regex_global_result_array_preserves_young_match_strings() { assert!(crate::arena::pointer_in_old_gen(result as usize)); js_shadow_slot_set(0, ptr_bits(result as usize)); let elements = unsafe { - (result as *mut u8).add(std::mem::size_of::()) as *mut u64 + crate::array::array_elements_ptr(result as *const crate::array::ArrayHeader) as *mut u64 }; let first_match = unsafe { (*elements & POINTER_MASK) as usize }; assert!(crate::arena::pointer_in_nursery(first_match)); @@ -210,7 +210,8 @@ fn regex_global_result_array_preserves_young_match_strings() { let trace = collect_minor_trace(GcTriggerKind::Direct); let result_after = (js_shadow_slot_get(0) & POINTER_MASK) as *mut crate::array::ArrayHeader; let elements_after = unsafe { - (result_after as *mut u8).add(std::mem::size_of::()) as *mut u64 + crate::array::array_elements_ptr(result_after as *const crate::array::ArrayHeader) + as *mut u64 }; assert_verified_copied_minor(&trace); diff --git a/crates/perry-runtime/src/gc/tests/promote_in_place.rs b/crates/perry-runtime/src/gc/tests/promote_in_place.rs index be38b8f59e..d605781532 100644 --- a/crates/perry-runtime/src/gc/tests/promote_in_place.rs +++ b/crates/perry-runtime/src/gc/tests/promote_in_place.rs @@ -429,7 +429,7 @@ fn an_untraced_promotion_indexes_the_objects_it_could_not_prove_live() { (*arr).length = 1; (*arr).capacity = 1; let elements = - (arr as *mut u8).add(std::mem::size_of::()) as *mut u64; + crate::array::array_elements_ptr(arr as *const crate::array::ArrayHeader) as *mut u64; *elements = 0; (arr, elements) }; diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs index ce179eb513..af1ee42c93 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs @@ -1679,7 +1679,7 @@ fn test_array_sort_comparator_rooted_buffers_survive_copied_minor_gc() { }); unsafe { assert_eq!((*sorted).length as usize, count); - let elems = (sorted as *const u8).add(std::mem::size_of::()) + let elems = crate::array::array_elements_ptr(sorted as *const crate::array::ArrayHeader) as *const f64; for i in 0..count { let got = string_value_content(*elems.add(i)); diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/transient_handles.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/transient_handles.rs index f067842e41..90f4357ba6 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/transient_handles.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/transient_handles.rs @@ -426,7 +426,7 @@ fn test_transient_runtime_handle_array_push_gc() { unsafe { assert_eq!((*grown).length, 200_001); let elements = - (grown as *const u8).add(std::mem::size_of::()) as *const u64; + crate::array::array_elements_ptr(grown as *const crate::ArrayHeader) as *const u64; let stored = *elements.add(200_000); assert_eq!(stored & TAG_MASK, STRING_TAG); let stored_ptr = (stored & POINTER_MASK) as *const crate::StringHeader; diff --git a/crates/perry-runtime/src/gc/tests/support.rs b/crates/perry-runtime/src/gc/tests/support.rs index 69b650516e..34c4fcc20e 100644 --- a/crates/perry-runtime/src/gc/tests/support.rs +++ b/crates/perry-runtime/src/gc/tests/support.rs @@ -924,7 +924,7 @@ pub(super) unsafe fn alloc_old_test_array( (*arr).length = length; (*arr).capacity = length; let elements = - (arr as *mut u8).add(std::mem::size_of::()) as *mut u64; + crate::array::array_elements_ptr(arr as *const crate::array::ArrayHeader) as *mut u64; for i in 0..length as usize { *elements.add(i) = 0; } diff --git a/crates/perry-runtime/src/gc/tests/young_log_tests.rs b/crates/perry-runtime/src/gc/tests/young_log_tests.rs index 27ed7c6afe..817d9e1436 100644 --- a/crates/perry-runtime/src/gc/tests/young_log_tests.rs +++ b/crates/perry-runtime/src/gc/tests/young_log_tests.rs @@ -487,7 +487,8 @@ const INDEXED_KEYS: u32 = 40; /// NaN-boxed layout `keys_array_dense_slots` reads. unsafe fn young_indexed_keys_array() -> (*mut crate::array::ArrayHeader, Vec>) { let arr = crate::array::js_array_alloc_with_length(INDEXED_KEYS); - let slots = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + let slots = + crate::array::array_elements_ptr(arr as *const crate::array::ArrayHeader) as *mut f64; let mut names = Vec::new(); for i in 0..INDEXED_KEYS { let name = format!("young_key_{i:04}"); @@ -730,7 +731,7 @@ fn shape_mutation_to_new_young_key_rearms_minor_log() { (*keys).length = 1; (*keys).capacity = 1; let slot = - (keys as *mut u8).add(std::mem::size_of::()) as *mut f64; + crate::array::array_elements_ptr(keys as *const crate::array::ArrayHeader) as *mut f64; *slot = f64::from_bits(string_bits(old_leaf())); let id = crate::object::shapes::shape_descriptor_ensure(keys, 1, 0).expect("shape"); let (owner, _) = alloc_old_test_object(0); diff --git a/crates/perry-runtime/src/gc/verify.rs b/crates/perry-runtime/src/gc/verify.rs index 732354f9fe..cc6f9822c0 100644 --- a/crates/perry-runtime/src/gc/verify.rs +++ b/crates/perry-runtime/src/gc/verify.rs @@ -986,7 +986,8 @@ pub(super) unsafe fn verify_array_pointer_slots_enumerated_for( if length == 0 || length > capacity || length > 16_000_000 { return; } - let elements = (user as *const u8).add(std::mem::size_of::()); + let elements = + crate::array::array_elements_ptr(user as *const crate::array::ArrayHeader) as *mut u8; let elements_addr = elements as usize; let words = elements as *const u64; diff --git a/crates/perry-runtime/src/json/construction_array.rs b/crates/perry-runtime/src/json/construction_array.rs index b3bd1bec3f..3e7cceabf0 100644 --- a/crates/perry-runtime/src/json/construction_array.rs +++ b/crates/perry-runtime/src/json/construction_array.rs @@ -33,7 +33,7 @@ impl ConstructionArray { let ptr = raw.cast::(); (*ptr).length = 0; (*ptr).capacity = capacity; - let slots = raw.add(std::mem::size_of::()).cast::(); + let slots = crate::array::array_elements_ptr(raw as *const ArrayHeader).cast::(); for index in 0..capacity as usize { // GC_STORE_AUDIT(INIT): initialize all physical slack for the // whole-heap verifier; live length advances only after writes. @@ -71,10 +71,7 @@ impl ConstructionArray { return; } let length = (*self.ptr).length as usize; - let slot = self - .ptr - .cast::() - .add(std::mem::size_of::()) + let slot = crate::array::array_elements_ptr(self.ptr) .cast::() .add(length); // GC_STORE_AUDIT(INIT): no marking phase or callbacks; @@ -97,17 +94,9 @@ impl ConstructionArray { let capacity = (*self.ptr).capacity.saturating_mul(2).max(16); let mut next = Self::new(batch, capacity); let length = (*self.ptr).length as usize; - let old_slots = self - .ptr - .cast::() - .add(std::mem::size_of::()) - .cast::(); + let old_slots = crate::array::array_elements_ptr(self.ptr).cast::(); debug_assert!(next.batched); - let new_slots = next - .ptr - .cast::() - .add(std::mem::size_of::()) - .cast::(); + let new_slots = crate::array::array_elements_ptr(next.ptr).cast::(); // GC_STORE_AUDIT(INIT): completed nursery or old destination; aggregate // layout and page-level remembering are installed before publication. std::ptr::copy_nonoverlapping(old_slots, new_slots, length); @@ -131,11 +120,7 @@ impl ConstructionArray { // Preserve selective tracing for mixed arrays. Build its mask in // one pass over the completed payload, rather than updating a // per-object table for every element while parsing. - let slots = self - .ptr - .cast::() - .add(std::mem::size_of::()) - .cast::(); + let slots = crate::array::array_elements_ptr(self.ptr).cast::(); crate::gc::layout_rebuild_from_slots( self.ptr.cast(), slots, @@ -150,11 +135,7 @@ impl ConstructionArray { } if self.any_pointer { if let Some(batch) = batch { - let slots = self - .ptr - .cast::() - .add(std::mem::size_of::()) - .cast::(); + let slots = crate::array::array_elements_ptr(self.ptr).cast::(); batch.finish_json_slots(self.ptr.cast(), slots, (*self.ptr).length as usize); } } diff --git a/crates/perry-runtime/src/json/parser.rs b/crates/perry-runtime/src/json/parser.rs index c49ded8c6e..832b3f4641 100644 --- a/crates/perry-runtime/src/json/parser.rs +++ b/crates/perry-runtime/src/json/parser.rs @@ -394,7 +394,8 @@ impl<'a> DirectParser<'a> { ) -> *mut ArrayHeader { let length = (*arr).length; if length < (*arr).capacity { - let elements_ptr = (arr as *mut u8).add(std::mem::size_of::()) as *mut u64; + let elements_ptr = + crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut u64; let value_bits = value.bits(); let slot = elements_ptr.add(length as usize); // GC_STORE_AUDIT(INIT): JSON.parse suppresses GC and notes layout for same-parse arrays below. diff --git a/crates/perry-runtime/src/json/replacer.rs b/crates/perry-runtime/src/json/replacer.rs index 86acb6ae85..636497507e 100644 --- a/crates/perry-runtime/src/json/replacer.rs +++ b/crates/perry-runtime/src/json/replacer.rs @@ -441,9 +441,9 @@ pub(crate) unsafe fn stringify_object_with_replacer_pretty( let mut first = true; for f in 0..actual_fields { let obj = obj_root.get_raw_const_ptr::(); - let keys_elements = (keys_root.get_raw_const_ptr::()) - .add(std::mem::size_of::()) - as *const f64; + let keys_elements = + crate::array::array_elements_ptr(keys_root.get_raw_const_ptr::()) + as *const f64; let fields_ptr = (obj_root.get_raw_const_ptr::()) .add(std::mem::size_of::()) as *const f64; let replacer = replacer_root.get_raw_const_ptr::(); @@ -619,7 +619,8 @@ pub(crate) unsafe fn stringify_array_with_replacer_pretty( } } let arr_base = arr_root.get_raw_const_ptr::(); - let elements = arr_base.add(std::mem::size_of::()) as *const f64; + let elements = + crate::array::array_elements_ptr(arr_base as *const crate::ArrayHeader) as *const f64; let replacer = replacer_root.get_raw_const_ptr::(); let elem = *elements.add(i as usize); // #5989: a sparse-array HOLE slot must surface to toJSON / the replacer @@ -1013,7 +1014,7 @@ pub(crate) unsafe fn stringify_object_pretty( }; let keys_len = (*keys_arr).length; let keys_elements = - (keys_arr as *const u8).add(std::mem::size_of::()) as *const f64; + crate::array::array_elements_ptr(keys_arr as *const crate::ArrayHeader) as *const f64; let fields_ptr = (ptr as *const u8).add(std::mem::size_of::()) as *const f64; // Iterate keys_len, not min(...): β‰₯9-field objects keep overflow values in @@ -1132,7 +1133,7 @@ pub(crate) unsafe fn stringify_array_pretty( } let arr = ptr as *const crate::ArrayHeader; let len = (*arr).length; - let elements = (arr as *const u8).add(std::mem::size_of::()) as *const f64; + let elements = crate::array::array_elements_ptr(arr as *const crate::ArrayHeader) as *const f64; if len == 0 { buf.push_str("[]"); @@ -1208,7 +1209,7 @@ pub(crate) unsafe fn stringify_object_with_array_replacer( }; let keys_len = (*keys_arr).length; let keys_elements = - (keys_arr as *const u8).add(std::mem::size_of::()) as *const f64; + crate::array::array_elements_ptr(keys_arr as *const crate::ArrayHeader) as *const f64; let fields_ptr = (ptr as *const u8).add(std::mem::size_of::()) as *const f64; // Iterate keys_len, not min(...): β‰₯9-field objects keep overflow values in @@ -1396,7 +1397,7 @@ pub(crate) unsafe fn stringify_array_with_array_replacer( let arr = ptr as *const crate::ArrayHeader; let len = (*arr).length; - let elements = (arr as *const u8).add(std::mem::size_of::()) as *const f64; + let elements = crate::array::array_elements_ptr(arr as *const crate::ArrayHeader) as *const f64; if len == 0 { buf.push_str("[]"); STRINGIFY_STACK.with(|s| s.borrow_mut().pop()); @@ -1455,7 +1456,7 @@ pub(crate) unsafe fn extract_string_array(ptr: *const u8) -> Vec { } let arr = ptr as *const crate::ArrayHeader; let len = (*arr).length; - let elements = (arr as *const u8).add(std::mem::size_of::()) as *const f64; + let elements = crate::array::array_elements_ptr(arr as *const crate::ArrayHeader) as *const f64; let mut result: Vec = Vec::new(); for i in 0..len { let elem = *elements.add(i as usize); diff --git a/crates/perry-runtime/src/json/stringify.rs b/crates/perry-runtime/src/json/stringify.rs index 22d4bd2d9f..e68a0f40be 100644 --- a/crates/perry-runtime/src/json/stringify.rs +++ b/crates/perry-runtime/src/json/stringify.rs @@ -1144,9 +1144,9 @@ pub(crate) unsafe fn stringify_object_inner(ptr: *const u8, buf: &mut String, de let key_at = |f: u32| -> f64 { obj_handle.with_const_ptr(|obj: *const crate::ObjectHeader| { let keys_arr = crate::object::object_keys_array(obj); - let keys_elements = (keys_arr as *const u8) - .add(std::mem::size_of::()) - as *const f64; + let keys_elements = + crate::array::array_elements_ptr(keys_arr as *const crate::ArrayHeader) + as *const f64; *keys_elements.add(f as usize) }) }; @@ -1555,7 +1555,7 @@ pub(crate) unsafe fn stringify_array_depth(ptr: *const u8, buf: &mut String, dep let arr_handle = scope.root_raw_const_ptr(arr); let elem_at = |i: usize| -> f64 { arr_handle.with_const_ptr(|arr: *const crate::ArrayHeader| { - *((arr as *const u8).add(std::mem::size_of::()) as *const f64) + *(crate::array::array_elements_ptr(arr as *const crate::ArrayHeader) as *const f64) .add(i) }) }; diff --git a/crates/perry-runtime/src/json/stringify_data_record.rs b/crates/perry-runtime/src/json/stringify_data_record.rs index 5a0053e86f..e7a6ba367e 100644 --- a/crates/perry-runtime/src/json/stringify_data_record.rs +++ b/crates/perry-runtime/src/json/stringify_data_record.rs @@ -111,9 +111,7 @@ unsafe fn primitive_array(arr: *const crate::ArrayHeader) -> bool { return false; } let elements = std::slice::from_raw_parts( - (arr as *const u8) - .add(std::mem::size_of::()) - .cast::(), + crate::array::array_elements_ptr(arr as *const crate::ArrayHeader).cast::(), (*arr).length as usize, ); // Even a raw-f64 array is checked for tag collisions before borrowing it diff --git a/crates/perry-runtime/src/json/stringify_flat.rs b/crates/perry-runtime/src/json/stringify_flat.rs index 1e23cccf20..08b688fbee 100644 --- a/crates/perry-runtime/src/json/stringify_flat.rs +++ b/crates/perry-runtime/src/json/stringify_flat.rs @@ -301,7 +301,7 @@ pub(super) unsafe fn try_object(bits: u64) -> Option { #[inline] unsafe fn emit_one_field_object(obj: *const crate::ObjectHeader) -> Option { let keys = crate::object::object_keys_array(obj); - let key_bits = slot(keys.cast(), std::mem::size_of::(), 0); + let key_bits = slot(crate::array::array_elements_ptr(keys).cast(), 0, 0); let value_bits = slot(obj.cast(), std::mem::size_of::(), 0); let key = key_piece(key_bits)?; let value = scalar_piece(value_bits)?; @@ -323,7 +323,7 @@ unsafe fn emit_one_field_object(obj: *const crate::ObjectHeader) -> Option(), 0), + slot(crate::array::array_elements_ptr(keys).cast(), 0, 0), output.add(1), ); // GC_STORE_AUDIT(POINTER_FREE): JSON byte-buffer payload. @@ -382,11 +382,7 @@ unsafe fn emit_two_field_parsed_string_object( let mut bytes = 2u32; let mut units = 2u32; for i in 0..2 { - key_plan[i] = key_piece(slot( - keys.cast(), - std::mem::size_of::(), - i, - ))?; + key_plan[i] = key_piece(slot(crate::array::array_elements_ptr(keys).cast(), 0, i))?; value_plan[i] = if i == proven_index { proven_value } else { @@ -435,7 +431,7 @@ unsafe fn emit_two_field_parsed_string_object( } at += emit_piece( key_plan[i], - slot(keys.cast(), std::mem::size_of::(), i), + slot(crate::array::array_elements_ptr(keys).cast(), 0, i), output.add(at), ); // GC_STORE_AUDIT(POINTER_FREE): JSON byte-buffer payload. @@ -488,11 +484,7 @@ unsafe fn emit_object(obj: *const crate::ObjectHeader, fields: usize) -> Option< let mut bytes = 2u32; let mut units = 2u32; for i in 0..fields { - key_plan[i] = key_piece(slot( - keys.cast(), - std::mem::size_of::(), - i, - ))?; + key_plan[i] = key_piece(slot(crate::array::array_elements_ptr(keys).cast(), 0, i))?; value_plan[i] = scalar_piece(slot( obj.cast(), std::mem::size_of::(), @@ -535,7 +527,7 @@ unsafe fn emit_object(obj: *const crate::ObjectHeader, fields: usize) -> Option< } at += emit_piece( key_plan[i], - slot(keys.cast(), std::mem::size_of::(), i), + slot(crate::array::array_elements_ptr(keys).cast(), 0, i), output.add(at), ); // GC_STORE_AUDIT(POINTER_FREE): JSON byte-buffer payload. diff --git a/crates/perry-runtime/src/json/stringify_nested_records.rs b/crates/perry-runtime/src/json/stringify_nested_records.rs index be203caff9..bf86576239 100644 --- a/crates/perry-runtime/src/json/stringify_nested_records.rs +++ b/crates/perry-runtime/src/json/stringify_nested_records.rs @@ -161,9 +161,7 @@ impl Emitter { }; plan.offsets[0] = self.prefixes.len() as u32; let mut scratch = [0; crate::value::SHORT_STRING_MAX_LEN]; - let keys = (keys_array as *const u8) - .wrapping_add(std::mem::size_of::()) - .cast::(); + let keys = crate::array::array_elements_ptr(keys_array); for i in 0..len { let bytes = crate::string::js_string_key_bytes(JSValue::from_bits(*keys.add(i)), &mut scratch)?; @@ -375,9 +373,7 @@ pub(super) unsafe fn try_emit( { return false; } - let elements = (arr as *const u8) - .add(std::mem::size_of::()) - .cast::(); + let elements = crate::array::array_elements_ptr(arr as *const crate::ArrayHeader).cast::(); let Some(first) = record(*elements) else { return false; }; diff --git a/crates/perry-runtime/src/json/stringify_primitive_array.rs b/crates/perry-runtime/src/json/stringify_primitive_array.rs index bec7958e53..537cb250d2 100644 --- a/crates/perry-runtime/src/json/stringify_primitive_array.rs +++ b/crates/perry-runtime/src/json/stringify_primitive_array.rs @@ -23,7 +23,7 @@ pub(super) unsafe fn try_emit(arr: *const crate::ArrayHeader, buf: &mut String) if (*arr).length > (*arr).capacity || flags & crate::gc::OBJ_FLAG_ARRAY_DESCRIPTORS != 0 { return false; } - let data = (arr as *const u8).add(std::mem::size_of::()) as *const f64; + let data = crate::array::array_elements_ptr(arr as *const crate::ArrayHeader) as *const f64; let len = (*arr).length as usize; let elements = std::slice::from_raw_parts(data, len); // The existing layout flag proves every live slot is an unboxed number, @@ -52,7 +52,7 @@ pub(super) unsafe fn try_emit(arr: *const crate::ArrayHeader, buf: &mut String) pub(super) unsafe fn emit_validated(arr: *const crate::ArrayHeader, buf: &mut String) { let header = crate::gc::header_from_trusted_user_ptr(arr.cast()); let flags = (*header)._reserved; - let data = (arr as *const u8).add(std::mem::size_of::()) as *const f64; + let data = crate::array::array_elements_ptr(arr as *const crate::ArrayHeader) as *const f64; let elements = std::slice::from_raw_parts(data, (*arr).length as usize); if flags & crate::gc::GC_ARRAY_RAW_F64_LAYOUT != 0 { buf.push('['); diff --git a/crates/perry-runtime/src/json/stringify_primitive_object.rs b/crates/perry-runtime/src/json/stringify_primitive_object.rs index c3344cc104..8bf7a06909 100644 --- a/crates/perry-runtime/src/json/stringify_primitive_object.rs +++ b/crates/perry-runtime/src/json/stringify_primitive_object.rs @@ -75,9 +75,8 @@ unsafe fn emit( let fields = (obj as *const u8) .add(std::mem::size_of::()) .cast::(); - let key_slots = (keys as *const u8) - .add(std::mem::size_of::()) - .cast::(); + let key_slots = + crate::array::array_elements_ptr(keys as *const crate::ArrayHeader).cast::(); buf.push('{'); let mut first = true; for j in 0..(*keys).length as usize { diff --git a/crates/perry-runtime/src/json/stringify_record_output.rs b/crates/perry-runtime/src/json/stringify_record_output.rs index 47f91f55bd..582fb35cc2 100644 --- a/crates/perry-runtime/src/json/stringify_record_output.rs +++ b/crates/perry-runtime/src/json/stringify_record_output.rs @@ -293,7 +293,7 @@ unsafe fn key_prefix_plan( plan.data[at] = if i == 0 { b'{' } else { b',' }; at += 1; units += 1; - let key_bits = slot(keys.cast(), ARRAY_BYTES, i); + let key_bits = slot(crate::array::array_elements_ptr(keys).cast(), 0, i); let key = key_piece(key_bits)?; let (key_bytes, key_units) = key.lengths(); let needed = at.checked_add(key_bytes as usize)?.checked_add(1)?; @@ -384,7 +384,9 @@ unsafe fn emit_repeated_output( return None; } for j in 0..len as usize { - if slot(arr.cast(), ARRAY_BYTES, j) != cached.element_bits[element + j] { + if slot(crate::array::array_elements_ptr(arr).cast(), 0, j) + != cached.element_bits[element + j] + { cached.receiver = 0; return None; } @@ -433,7 +435,8 @@ unsafe fn emit_cached_record_uncached( value_plan[i].write(Field::Array { start: used, len }); let (mut ab, mut au) = (2u32, 2u32); for j in 0..len { - let value = record_value_piece(slot(arr.cast(), ARRAY_BYTES, j))?; + let value = + record_value_piece(slot(crate::array::array_elements_ptr(arr).cast(), 0, j))?; elements[used + j].write(value); let (eb, eu) = value.lengths(); let comma = u32::from(j != 0); @@ -486,7 +489,7 @@ unsafe fn emit_cached_record_uncached( } at += emit_piece( elements[start + j].assume_init(), - slot(arr.cast(), ARRAY_BYTES, j), + slot(crate::array::array_elements_ptr(arr).cast(), 0, j), output.add(at), ); } @@ -533,7 +536,8 @@ unsafe fn emit_cached_record_memo( value_plan[i].write(Field::Array { start: used, len }); let (mut ab, mut au) = (2u32, 2u32); for j in 0..len { - let value = record_value_piece(slot(arr.cast(), ARRAY_BYTES, j))?; + let value = + record_value_piece(slot(crate::array::array_elements_ptr(arr).cast(), 0, j))?; elements[used + j].write(value); let (eb, eu) = value.lengths(); let comma = u32::from(j != 0); @@ -592,7 +596,7 @@ unsafe fn emit_cached_record_memo( output.add(at).write(b','); at += 1; } - let element_bits = slot(arr.cast(), ARRAY_BYTES, j); + let element_bits = slot(crate::array::array_elements_ptr(arr).cast(), 0, j); if repeated_candidate { signature = repeated_signature_mix(signature, element_bits); } @@ -642,7 +646,8 @@ unsafe fn emit_cached_record_memo( cached.array_lengths[i] = len as u8; let arr = (bits & POINTER_MASK) as *const crate::ArrayHeader; for j in 0..len { - cached.element_bits[start + j] = slot(arr.cast(), ARRAY_BYTES, j); + cached.element_bits[start + j] = + slot(crate::array::array_elements_ptr(arr).cast(), 0, j); } } } @@ -693,7 +698,7 @@ unsafe fn emit_record(obj: *const crate::ObjectHeader, fields: usize) -> Option< let mut used = 0; let (mut bytes, mut units) = (2u32, 2u32); for i in 0..fields { - let key = key_piece(slot(keys.cast(), ARRAY_BYTES, i))?; + let key = key_piece(slot(crate::array::array_elements_ptr(keys).cast(), 0, i))?; key_plan[i].write(key); let (kb, ku) = key.lengths(); let bits = slot(obj.cast(), OBJECT_BYTES, i); @@ -708,7 +713,7 @@ unsafe fn emit_record(obj: *const crate::ObjectHeader, fields: usize) -> Option< value_plan[i].write(Field::Array { start: used, len }); let (mut ab, mut au) = (2u32, 2u32); for j in 0..len { - let value = scalar_piece(slot(arr.cast(), ARRAY_BYTES, j))?; + let value = scalar_piece(slot(crate::array::array_elements_ptr(arr).cast(), 0, j))?; elements[used + j].write(value); let (eb, eu) = value.lengths(); let comma = u32::from(j != 0); @@ -754,7 +759,7 @@ unsafe fn emit_record(obj: *const crate::ObjectHeader, fields: usize) -> Option< } at += emit_piece( key_plan[i].assume_init(), - slot(keys.cast(), ARRAY_BYTES, i), + slot(crate::array::array_elements_ptr(keys).cast(), 0, i), output.add(at), ); // GC_STORE_AUDIT(POINTER_FREE): JSON byte-buffer payload. @@ -776,7 +781,7 @@ unsafe fn emit_record(obj: *const crate::ObjectHeader, fields: usize) -> Option< } at += emit_piece( elements[start + j].assume_init(), - slot(arr.cast(), ARRAY_BYTES, j), + slot(crate::array::array_elements_ptr(arr).cast(), 0, j), output.add(at), ); } diff --git a/crates/perry-runtime/src/json/stringify_shape_template.rs b/crates/perry-runtime/src/json/stringify_shape_template.rs index fd9d8700c0..6d23906d76 100644 --- a/crates/perry-runtime/src/json/stringify_shape_template.rs +++ b/crates/perry-runtime/src/json/stringify_shape_template.rs @@ -195,7 +195,7 @@ pub(crate) unsafe fn build_shape_prefix_template(first_elem_bits: u64) -> Option // inline limit are read through `template_field_bits`, which routes them to // `js_object_get_field`'s overflow fallback. let keys_elements = - (keys_arr as *const u8).add(std::mem::size_of::()) as *const f64; + crate::array::array_elements_ptr(keys_arr as *const crate::ArrayHeader) as *const f64; let mut prefixes: Vec = Vec::with_capacity(shape_fields as usize); let mut key_sso = [0u8; crate::value::SHORT_STRING_MAX_LEN]; let mut own_keys_exclude_to_json = true; @@ -298,7 +298,7 @@ unsafe fn set_to_json_key_for_template_field(keys_arr: *mut crate::ArrayHeader, return; } let keys_elements = - (keys_arr as *const u8).add(std::mem::size_of::()) as *const f64; + crate::array::array_elements_ptr(keys_arr as *const crate::ArrayHeader) as *const f64; let key_bits = (*keys_elements.add(f)).to_bits(); let mut key_sso = [0u8; crate::value::SHORT_STRING_MAX_LEN]; let key_str = crate::string::js_string_key_bytes(JSValue::from_bits(key_bits), &mut key_sso) diff --git a/crates/perry-runtime/src/json/stringify_tojson_probe.rs b/crates/perry-runtime/src/json/stringify_tojson_probe.rs index b0d8c83b4a..630a7ec044 100644 --- a/crates/perry-runtime/src/json/stringify_tojson_probe.rs +++ b/crates/perry-runtime/src/json/stringify_tojson_probe.rs @@ -76,7 +76,8 @@ unsafe fn keys_array_may_carry_to_json(keys: *mut crate::ArrayHeader) -> bool { // `ensure_key_in_keys_array` / the shape allocators), so read the element // slots raw β€” same layout walk `stringify_object_inner` does β€” instead of // paying the exported `js_array_get` validation per element. - let elements = (keys as *const u8).add(std::mem::size_of::()) as *const f64; + let elements = + crate::array::array_elements_ptr(keys as *const crate::ArrayHeader) as *const f64; for i in 0..key_count { let stored = JSValue::from_bits((*elements.add(i)).to_bits()); if key_may_carry_to_json(stored) { diff --git a/crates/perry-runtime/src/json_tape.rs b/crates/perry-runtime/src/json_tape.rs index 8e6715c063..ffe70a0699 100644 --- a/crates/perry-runtime/src/json_tape.rs +++ b/crates/perry-runtime/src/json_tape.rs @@ -1929,9 +1929,9 @@ pub unsafe fn force_materialize_lazy(hdr: *mut LazyArrayHeader) -> *mut crate::a }; let value_handle = elem_scope.root_nanbox_u64(value.bits()); let arr_ptr = array_from_nanbox_handle(&arr_handle); - let elements_ptr = (arr_ptr as *mut u8) - .add(std::mem::size_of::()) - as *mut u64; + let elements_ptr = + crate::array::array_elements_ptr(arr_ptr as *const crate::array::ArrayHeader) + as *mut u64; let value_bits = value_handle.get_nanbox_u64(); // GC_STORE_AUDIT(BARRIERED): note_array_slot below re-stores this slot with the barrier. *elements_ptr.add(i) = value_bits; diff --git a/crates/perry-runtime/src/node_stream_json.rs b/crates/perry-runtime/src/node_stream_json.rs index eef9db88ff..7ecd41b177 100644 --- a/crates/perry-runtime/src/node_stream_json.rs +++ b/crates/perry-runtime/src/node_stream_json.rs @@ -45,7 +45,8 @@ pub(crate) unsafe fn try_stringify_node_stream_json(ptr: *const u8, buf: &mut St if key_count > 65_536 || key_count > (*keys).capacity as usize { return false; } - let elements = (keys as *const u8).add(std::mem::size_of::()) as *const f64; + let elements = + crate::array::array_elements_ptr(keys as *const crate::ArrayHeader) as *const f64; let mut readable_idx: Option = None; let mut writable_idx: Option = None; for i in 0..key_count { diff --git a/crates/perry-runtime/src/object/alloc.rs b/crates/perry-runtime/src/object/alloc.rs index cd6d6e359a..7003d1766f 100644 --- a/crates/perry-runtime/src/object/alloc.rs +++ b/crates/perry-runtime/src/object/alloc.rs @@ -465,7 +465,9 @@ pub extern "C" fn js_build_class_keys_array( // in a loop, which cascaded via block-persistence into every // subsequent iteration's allocations. let arr = crate::array::js_array_alloc_with_length_longlived(num_keys as u32); - let elements_ptr = unsafe { (arr as *mut u8).add(8) as *mut f64 }; + let elements_ptr = unsafe { + crate::array::array_elements_ptr(arr as *const crate::array::ArrayHeader) as *mut f64 + }; for (i, key_bytes) in keys.iter().enumerate() { let str_ptr = crate::string::js_string_from_bytes_longlived( key_bytes.as_ptr(), @@ -563,7 +565,9 @@ pub extern "C" fn js_object_alloc_class_with_keys( // Issue #179: shape-cache keys_array lives in the longlived arena // (see `js_build_class_keys_array` for the rationale). let arr = crate::array::js_array_alloc_with_length_longlived(num_keys as u32); - let elements_ptr = unsafe { (arr as *mut u8).add(8) as *mut f64 }; + let elements_ptr = unsafe { + crate::array::array_elements_ptr(arr as *const crate::array::ArrayHeader) as *mut f64 + }; for (i, key_bytes) in keys.iter().enumerate() { let str_ptr = crate::string::js_string_from_bytes_longlived( key_bytes.as_ptr(), @@ -667,8 +671,13 @@ pub extern "C" fn js_object_alloc_class_dynamic_parent( }; let merged_len = parent_len as usize + own_keys.len(); let arr = crate::array::js_array_alloc_with_length_longlived(merged_len as u32); - let dst = unsafe { (arr as *mut u8).add(8) as *mut f64 }; - let src = unsafe { (parent_arr as *mut u8).add(8) as *const f64 }; + let dst = unsafe { + crate::array::array_elements_ptr(arr as *const crate::array::ArrayHeader) as *mut f64 + }; + let src = unsafe { + crate::array::array_elements_ptr(parent_arr as *const crate::array::ArrayHeader) + as *const f64 + }; unsafe { for i in 0..parent_len as usize { let bits = (*src.add(i)).to_bits(); @@ -796,7 +805,10 @@ pub extern "C" fn js_object_alloc_with_shape( key_bytes.len() as u32, ); let arr = arr_handle.get_raw_mut_ptr::(); - let elements_ptr = unsafe { (arr as *mut u8).add(8) as *mut f64 }; + let elements_ptr = unsafe { + crate::array::array_elements_ptr(arr as *const crate::array::ArrayHeader) + as *mut f64 + }; let nanboxed = f64::from_bits( crate::value::STRING_TAG | (str_ptr as u64 & crate::value::POINTER_MASK), ); @@ -978,11 +990,15 @@ pub unsafe extern "C" fn js_object_clone_with_extra( // js_array_push. Pre-size the keys capacity to avoid immediate reallocation on append. let src_keys_arr = crate::object::object_keys_array(src_ptr); let new_keys_arr = crate::array::js_array_alloc(src_field_count + extra_count); - let new_keys_elements = (new_keys_arr as *mut u8).add(8) as *mut f64; + let new_keys_elements = + crate::array::array_elements_ptr(new_keys_arr as *const crate::array::ArrayHeader) + as *mut f64; if !src_keys_arr.is_null() && (src_keys_arr as usize) >= 0x10000 { let src_key_len = (*src_keys_arr).length as usize; - let src_key_elements = (src_keys_arr as *const u8).add(8) as *const f64; + let src_key_elements = + crate::array::array_elements_ptr(src_keys_arr as *const crate::array::ArrayHeader) + as *const f64; let copy_count = src_key_len.min(src_field_count as usize); for i in 0..copy_count { // GC_STORE_AUDIT(INIT): cloned keys array is unpublished; layout is rebuilt before publication. diff --git a/crates/perry-runtime/src/object/assert.rs b/crates/perry-runtime/src/object/assert.rs index 624d0f9153..29fcd1862a 100644 --- a/crates/perry-runtime/src/object/assert.rs +++ b/crates/perry-runtime/src/object/assert.rs @@ -659,7 +659,7 @@ fn array_has_index(arr: *const crate::array::ArrayHeader, index: u32) -> bool { } unsafe { let elements = - (arr as *const u8).add(std::mem::size_of::()) as *const u64; + crate::array::array_elements_ptr(arr as *const crate::array::ArrayHeader) as *const u64; std::ptr::read(elements.add(index as usize)) != crate::value::TAG_HOLE } } diff --git a/crates/perry-runtime/src/object/delete_rest.rs b/crates/perry-runtime/src/object/delete_rest.rs index cfa797bacd..a9c0fb2e96 100644 --- a/crates/perry-runtime/src/object/delete_rest.rs +++ b/crates/perry-runtime/src/object/delete_rest.rs @@ -408,9 +408,10 @@ pub extern "C" fn js_object_delete_field( obj = reloaded_obj; keys = crate::object::object_keys_array(obj); let src_elements = - (keys as *const u8).add(std::mem::size_of::()) as *const f64; + crate::array::array_elements_ptr(keys as *const crate::ArrayHeader) as *const f64; let dst_elements = - (keys_cloned as *mut u8).add(std::mem::size_of::()) as *mut f64; + crate::array::array_elements_ptr(keys_cloned as *const crate::ArrayHeader) + as *mut f64; if key_count != 0 { // GC_STORE_AUDIT(INIT): the clone is unpublished; its layout // is rebuilt before set_object_keys_array publishes the edge. @@ -484,8 +485,9 @@ pub extern "C" fn js_object_delete_field( if !stable { (*obj_gc)._reserved &= !crate::gc::OBJ_FLAG_STABLE_TOMBSTONES; } - let elements = (keys as *mut u8).add(std::mem::size_of::()) - as *mut f64; + let elements = + crate::array::array_elements_ptr(keys as *const crate::ArrayHeader) + as *mut f64; // Barriered stores, exactly the Map delete's idiom: the // hole overwrites a key POINTER and the clear overwrites // the value, so SATB marking must shade both children. @@ -549,7 +551,7 @@ pub extern "C" fn js_object_delete_field( // cannot reuse an old count-matching token for a new slot order. super::shapes::retire_owned_shape_history(obj, keys); let elements = - (keys as *mut u8).add(std::mem::size_of::()) as *mut f64; + crate::array::array_elements_ptr(keys as *const crate::ArrayHeader) as *mut f64; // Overlapping ranges inside ONE allocation: `copy` (memmove). // // Unlike the clone arm below, this destination is the LIVE, @@ -579,9 +581,10 @@ pub extern "C" fn js_object_delete_field( } else { let keys_cloned = crate::array::js_array_alloc(new_count.max(1) as u32 + 4); let src_elements = - (keys as *const u8).add(std::mem::size_of::()) as *const f64; + crate::array::array_elements_ptr(keys as *const crate::ArrayHeader) as *const f64; let dst_elements = - (keys_cloned as *mut u8).add(std::mem::size_of::()) as *mut f64; + crate::array::array_elements_ptr(keys_cloned as *const crate::ArrayHeader) + as *mut f64; // Copy keys [0..i) ++ [i+1..N) into [0..new_count) as two contiguous // runs. These were scalar element loops, which is O(resident keys) of // load/store pairs on a path that already allocates and rebuilds a @@ -847,7 +850,7 @@ unsafe fn try_delete_stable_sso(obj: *mut ObjectHeader, key: JSValue) -> Option< let mut key_buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; let key_bytes = crate::string::js_string_key_bytes(key, &mut key_buf)?; - let elements = (keys as *mut u8).add(std::mem::size_of::()) as *mut f64; + let elements = crate::array::array_elements_ptr(keys as *const crate::ArrayHeader) as *mut f64; // The one-live-key cycle appends its sole live key at the tail. Validate // that constructive position directly; broader small receivers retain // the byte-lookup fallback. @@ -1577,7 +1580,7 @@ unsafe fn squeeze_holes_and_delete( reserved_floor: usize, ) { let keys = keys as *mut crate::ArrayHeader; - let elements = (keys as *mut u8).add(std::mem::size_of::()) as *mut f64; + let elements = crate::array::array_elements_ptr(keys as *const crate::ArrayHeader) as *mut f64; let fields_ptr = (obj as *mut u8).add(std::mem::size_of::()) as *mut u64; let floor = reserved_floor.min(key_count); let mut out = floor; diff --git a/crates/perry-runtime/src/object/field_get_set/entries_shape.rs b/crates/perry-runtime/src/object/field_get_set/entries_shape.rs index 0113826716..2e373ef769 100644 --- a/crates/perry-runtime/src/object/field_get_set/entries_shape.rs +++ b/crates/perry-runtime/src/object/field_get_set/entries_shape.rs @@ -63,9 +63,9 @@ pub(super) fn js_object_entries_shape(obj: *const ObjectHeader) -> *mut ArrayHea if length > 100_000 { return crate::array::js_array_alloc(0); } - let elements = (arr as *const u8) - .add(std::mem::size_of::()) - as *const u64; + let elements = + crate::array::array_elements_ptr(arr as *const crate::array::ArrayHeader) + as *const u64; let result = crate::array::js_array_alloc(length); for i in 0..length { if std::ptr::read(elements.add(i as usize)) == crate::value::TAG_HOLE { diff --git a/crates/perry-runtime/src/object/field_get_set/enumeration.rs b/crates/perry-runtime/src/object/field_get_set/enumeration.rs index beb29a4cf6..a02efb07de 100644 --- a/crates/perry-runtime/src/object/field_get_set/enumeration.rs +++ b/crates/perry-runtime/src/object/field_get_set/enumeration.rs @@ -1079,7 +1079,7 @@ pub(crate) unsafe fn keys_contain_array_index(keys: *const ArrayHeader) -> bool { let len = (*keys).length as usize; let elements = - (keys as *const u8).add(std::mem::size_of::()) as *const f64; + crate::array::array_elements_ptr(keys as *const ArrayHeader) as *const f64; let mut sso_buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; for i in 0..len { let key_val = crate::JSValue::from_bits((*elements.add(i)).to_bits()); @@ -1395,9 +1395,9 @@ fn js_object_keys_shape(obj: *const ObjectHeader) -> *mut ArrayHeader { dense_limit.saturating_add(names.len() as u32), ); if dense_limit > 0 { - let elements = (arr as *const u8) - .add(std::mem::size_of::()) - as *const u64; + let elements = crate::array::array_elements_ptr( + arr as *const crate::array::ArrayHeader, + ) as *const u64; for i in 0..dense_limit { if std::ptr::read(elements.add(i as usize)) == crate::value::TAG_HOLE { continue; @@ -1415,9 +1415,9 @@ fn js_object_keys_shape(obj: *const ObjectHeader) -> *mut ArrayHeader { } return result; } - let elements = (arr as *const u8) - .add(std::mem::size_of::()) - as *const u64; + let elements = + crate::array::array_elements_ptr(arr as *const crate::array::ArrayHeader) + as *const u64; // Index properties may carry a non-default descriptor // (`Object.defineProperty(arr, i, { enumerable: false })`). // Object.keys / for-in must skip non-enumerable indices β€” but @@ -1754,9 +1754,9 @@ fn js_object_values_shape(obj: *const ObjectHeader) -> *mut ArrayHeader { if length > 100_000 { return crate::array::js_array_alloc(0); } - let elements = (arr as *const u8) - .add(std::mem::size_of::()) - as *const u64; + let elements = + crate::array::array_elements_ptr(arr as *const crate::array::ArrayHeader) + as *const u64; let result = crate::array::js_array_alloc(length); for i in 0..length { if std::ptr::read(elements.add(i as usize)) == crate::value::TAG_HOLE { diff --git a/crates/perry-runtime/src/object/field_set_by_name/fast_paths.rs b/crates/perry-runtime/src/object/field_set_by_name/fast_paths.rs index 26e887a9f0..4ee613192f 100644 --- a/crates/perry-runtime/src/object/field_set_by_name/fast_paths.rs +++ b/crates/perry-runtime/src/object/field_set_by_name/fast_paths.rs @@ -398,7 +398,7 @@ unsafe fn try_readd_stable_tombstone_sso_no_grow( shape.live_inline_slot_count }; - let elements = (keys as *mut u8).add(std::mem::size_of::()) as *mut f64; + let elements = crate::array::array_elements_ptr(keys as *const ArrayHeader) as *mut f64; crate::gc::runtime_store_external_jsvalue_slot( keys as usize, elements.add(new_index as usize) as usize, diff --git a/crates/perry-runtime/src/object/gc_slots.rs b/crates/perry-runtime/src/object/gc_slots.rs index 9caea737b9..4dd728aeef 100644 --- a/crates/perry-runtime/src/object/gc_slots.rs +++ b/crates/perry-runtime/src/object/gc_slots.rs @@ -84,7 +84,7 @@ pub(crate) unsafe fn rebuild_array_layout_from_slots(arr: *mut ArrayHeader) { return; } let len = (*arr).length as usize; - let slots = (arr as *mut u8).add(std::mem::size_of::()) as *mut u64; + let slots = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut u64; crate::gc::layout_rebuild_from_slots(arr as *mut u8, slots, len); if crate::arena::pointer_in_old_gen(arr as usize) { for i in 0..len { diff --git a/crates/perry-runtime/src/object/groupby.rs b/crates/perry-runtime/src/object/groupby.rs index 53be141034..da54812c90 100644 --- a/crates/perry-runtime/src/object/groupby.rs +++ b/crates/perry-runtime/src/object/groupby.rs @@ -316,7 +316,7 @@ unsafe fn group_by_make_array( let len = items_for_key.len(); let arr = crate::array::js_array_alloc(len as u32); (*arr).length = len as u32; - let arr_data = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + let arr_data = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut f64; for i in 0..len { // GC_STORE_AUDIT(INIT): groupBy result array is unpublished; layout is rebuilt before publication. std::ptr::write(arr_data.add(i), items_for_key.get(i)); diff --git a/crates/perry-runtime/src/object/has_own_helpers.rs b/crates/perry-runtime/src/object/has_own_helpers.rs index 83de47f064..2eeef70084 100644 --- a/crates/perry-runtime/src/object/has_own_helpers.rs +++ b/crates/perry-runtime/src/object/has_own_helpers.rs @@ -130,7 +130,7 @@ pub(super) unsafe fn array_own_key_present( return false; } let elements = - (arr as *const u8).add(std::mem::size_of::()) as *const u64; + crate::array::array_elements_ptr(arr as *const crate::array::ArrayHeader) as *const u64; std::ptr::read(elements.add(index as usize)) != crate::value::TAG_HOLE } diff --git a/crates/perry-runtime/src/object/keys_lookup.rs b/crates/perry-runtime/src/object/keys_lookup.rs index 6c3c266e70..50e827606c 100644 --- a/crates/perry-runtime/src/object/keys_lookup.rs +++ b/crates/perry-runtime/src/object/keys_lookup.rs @@ -32,7 +32,7 @@ pub(crate) unsafe fn keys_array_dense_slots( } let len = (*arr).length.min((*arr).capacity) as usize; ( - (arr as *const u8).add(std::mem::size_of::()) as *const f64, + crate::array::array_elements_ptr(arr as *const crate::array::ArrayHeader) as *const f64, len, ) } diff --git a/crates/perry-runtime/src/object/native_module/namespace_builders.rs b/crates/perry-runtime/src/object/native_module/namespace_builders.rs index 109a17a1ae..2ea2051b3e 100644 --- a/crates/perry-runtime/src/object/native_module/namespace_builders.rs +++ b/crates/perry-runtime/src/object/native_module/namespace_builders.rs @@ -98,7 +98,8 @@ pub(crate) unsafe fn http_methods_array() -> f64 { "UNSUBSCRIBE", ]; let arr = crate::array::js_array_alloc_with_length_longlived(METHODS.len() as u32); - let elements_ptr = (arr as *mut u8).add(8) as *mut f64; + let elements_ptr = + crate::array::array_elements_ptr(arr as *const crate::array::ArrayHeader) as *mut f64; for (i, m) in METHODS.iter().enumerate() { let bytes = m.as_bytes(); let str_ptr = diff --git a/crates/perry-runtime/src/object/object_ops/accessors.rs b/crates/perry-runtime/src/object/object_ops/accessors.rs index 951fb7e516..bd65d98daf 100644 --- a/crates/perry-runtime/src/object/object_ops/accessors.rs +++ b/crates/perry-runtime/src/object/object_ops/accessors.rs @@ -177,8 +177,8 @@ pub extern "C" fn js_object_get_own_field_or_undef( if key_count > (*keys).capacity as usize || key_count > 65536 { return f64::from_bits(TAG_UNDEF); } - let key_slots = - (keys as *const u8).add(std::mem::size_of::()) as *const f64; + let key_slots = crate::array::array_elements_ptr(keys as *const crate::array::ArrayHeader) + as *const f64; let alloc_limit = std::cmp::max( crate::object::object_live_slot_count(obj), crate::object::INLINE_SLOT_FLOOR as u32, diff --git a/crates/perry-runtime/src/object/spill.rs b/crates/perry-runtime/src/object/spill.rs index e2d734b87a..1f6a32110a 100644 --- a/crates/perry-runtime/src/object/spill.rs +++ b/crates/perry-runtime/src/object/spill.rs @@ -58,7 +58,7 @@ pub(crate) fn object_spill_enabled() -> bool { /// the exact triple the retired side-table Vec store performed. #[inline] unsafe fn spill_elements(spill: *const crate::array::ArrayHeader) -> *mut u64 { - (spill as *mut u8).add(std::mem::size_of::()) as *mut u64 + crate::array::array_elements_ptr(spill as *const crate::array::ArrayHeader) as *mut u64 } #[inline] @@ -314,9 +314,9 @@ fn spill_set_slow(obj_ptr: usize, field_index: usize, vbits: u64) { let old = (*meta).spill as *const crate::array::ArrayHeader; if !old.is_null() { let old_len = (*old).length as usize; - let elements = (old as *const u8) - .add(std::mem::size_of::()) - as *const u64; + let elements = + crate::array::array_elements_ptr(old as *const crate::array::ArrayHeader) + as *const u64; for i in 0..old_len { let bits = *elements.add(i); if bits != crate::value::TAG_HOLE && bits != crate::value::TAG_UNDEFINED { diff --git a/crates/perry-runtime/src/param_type_guard.rs b/crates/perry-runtime/src/param_type_guard.rs index 2ef6d6200b..260b124cb6 100644 --- a/crates/perry-runtime/src/param_type_guard.rs +++ b/crates/perry-runtime/src/param_type_guard.rs @@ -417,7 +417,7 @@ impl GuardState<'_> { return ObjectKeys::Invalid; } ObjectKeys::Present { - slots: (keys as *const u8).add(std::mem::size_of::()) as *const f64, + slots: crate::array::array_elements_ptr(keys as *const ArrayHeader) as *const f64, len: key_len, } } @@ -534,7 +534,7 @@ impl GuardState<'_> { return true; } let elements = - (array as *const u8).add(std::mem::size_of::()) as *const f64; + crate::array::array_elements_ptr(array as *const ArrayHeader) as *const f64; for index in 0..length { let element = JSValue::from_bits(std::ptr::read(elements.add(index)).to_bits()); if element.bits() == TAG_HOLE || !self.matches(element, child, depth + 1) { @@ -560,7 +560,7 @@ impl GuardState<'_> { return true; } let elements = - (array as *const u8).add(std::mem::size_of::()) as *const f64; + crate::array::array_elements_ptr(array as *const ArrayHeader) as *const f64; for index in 0..count { let Some(child) = read_u32(node, 5 + index * 4) else { return false; diff --git a/crates/perry-runtime/src/promise/combinators.rs b/crates/perry-runtime/src/promise/combinators.rs index 3fafb91e14..536ad5e507 100644 --- a/crates/perry-runtime/src/promise/combinators.rs +++ b/crates/perry-runtime/src/promise/combinators.rs @@ -239,7 +239,7 @@ pub extern "C" fn js_promise_try( } else { let len = unsafe { (*args).length as usize }; let data = unsafe { - (args as *const u8).add(std::mem::size_of::()) as *const f64 + crate::array::array_elements_ptr(args as *const crate::array::ArrayHeader) as *const f64 }; (data, len) }; diff --git a/crates/perry-runtime/src/promise/then_probe.rs b/crates/perry-runtime/src/promise/then_probe.rs index b9931047ea..bbe5bf89fa 100644 --- a/crates/perry-runtime/src/promise/then_probe.rs +++ b/crates/perry-runtime/src/promise/then_probe.rs @@ -360,7 +360,7 @@ unsafe fn own_then_scan(obj: *const ObjectHeader) -> OwnScan { return OwnScan::Unknown; } let elements = - (keys as *const u8).add(std::mem::size_of::()) as *const f64; + crate::array::array_elements_ptr(keys as *const crate::array::ArrayHeader) as *const f64; for i in 0..len as usize { let raw = std::ptr::read(elements.add(i)); let bits = raw.to_bits(); diff --git a/crates/perry-runtime/src/proxy/put_value.rs b/crates/perry-runtime/src/proxy/put_value.rs index 1f4e8acdbb..9a6858ca65 100644 --- a/crates/perry-runtime/src/proxy/put_value.rs +++ b/crates/perry-runtime/src/proxy/put_value.rs @@ -1504,7 +1504,7 @@ fn object_array_numeric_write_slots( } let elements = unsafe { - (arr as *const u8).add(std::mem::size_of::()) as *const f64 + crate::array::array_elements_ptr(arr as *const crate::array::ArrayHeader) as *const f64 }; let first_bits = unsafe { (*elements.add(receiver_start as usize)).to_bits() }; if first_bits == crate::value::TAG_HOLE { diff --git a/crates/perry-runtime/src/set.rs b/crates/perry-runtime/src/set.rs index 95629053e2..2011b8ead8 100644 --- a/crates/perry-runtime/src/set.rs +++ b/crates/perry-runtime/src/set.rs @@ -1980,7 +1980,7 @@ pub extern "C" fn js_set_to_array(set: *const SetHeader) -> *mut crate::array::A let set = set_handle.get_raw_const_ptr::(); let result = result_handle.get_raw_mut_ptr::(); let src = (*set).elements as *const f64; - let dst = (result as *mut u8).add(std::mem::size_of::()) + let dst = crate::array::array_elements_ptr(result as *const crate::array::ArrayHeader) as *mut f64; // GC_STORE_AUDIT(BARRIERED): Set-to-array bulk copy is followed by exact layout/barrier rebuild. ptr::copy_nonoverlapping(src, dst, size); diff --git a/crates/perry-runtime/src/string/char_ops.rs b/crates/perry-runtime/src/string/char_ops.rs index 614d18ad30..7368506cb4 100644 --- a/crates/perry-runtime/src/string/char_ops.rs +++ b/crates/perry-runtime/src/string/char_ops.rs @@ -339,7 +339,9 @@ pub extern "C" fn js_string_to_char_array(s: i64) -> i64 { i = end; } let arr = crate::array::js_array_alloc_with_length(spans.len() as u32); - let elements = unsafe { (arr as *mut u8).add(8) as *mut f64 }; + let elements = unsafe { + crate::array::array_elements_ptr(arr as *const crate::array::ArrayHeader) as *mut f64 + }; for (i, &(start, end)) in spans.iter().enumerate() { let seq = &bytes[start..end]; let ch_ptr = js_string_from_bytes(seq.as_ptr(), seq.len() as u32); diff --git a/crates/perry-runtime/src/string/split.rs b/crates/perry-runtime/src/string/split.rs index 286fccf888..0b3302f5c2 100644 --- a/crates/perry-runtime/src/string/split.rs +++ b/crates/perry-runtime/src/string/split.rs @@ -13,7 +13,7 @@ unsafe fn store_split_string(arr: *mut ArrayHeader, index: usize, string: *mut S const STRING_TAG: u64 = 0x7FFF_0000_0000_0000; const POINTER_MASK: u64 = 0x0000_FFFF_FFFF_FFFF; - let elements_ptr = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + let elements_ptr = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut f64; let value_bits = STRING_TAG | (string as u64 & POINTER_MASK); // GC_STORE_AUDIT(BARRIERED): split result string slot is followed by a runtime write barrier. std::ptr::write(elements_ptr.add(index), f64::from_bits(value_bits)); @@ -702,7 +702,7 @@ fn split_single_element(s: *const StringHeader) -> *mut ArrayHeader { let (arr, s) = s_handle.across_const::(|| crate::array::js_array_alloc(1)); unsafe { (*arr).length = 1; - let elements_ptr = (arr as *mut u8).add(std::mem::size_of::()) as *mut f64; + let elements_ptr = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *mut f64; let nanboxed = STRING_TAG | (s as u64 & POINTER_MASK); // GC_STORE_AUDIT(BARRIERED): slot recorded via note_array_slot. std::ptr::write(elements_ptr, f64::from_bits(nanboxed)); diff --git a/crates/perry-runtime/src/thread.rs b/crates/perry-runtime/src/thread.rs index 389cf4c2a6..6d18eb1c03 100644 --- a/crates/perry-runtime/src/thread.rs +++ b/crates/perry-runtime/src/thread.rs @@ -644,9 +644,9 @@ unsafe fn serialize_object(obj: *const crate::object::ObjectHeader) -> Serialize if keys_arr.is_null() || i >= (*keys_arr).length as usize { return false; } - let keys_elements = (keys_arr as *const u8) - .add(std::mem::size_of::()) - as *const f64; + let keys_elements = + crate::array::array_elements_ptr(keys_arr as *const crate::array::ArrayHeader) + as *const f64; (*keys_elements.add(i)).to_bits() == crate::value::TAG_HOLE }; @@ -666,9 +666,9 @@ unsafe fn serialize_object(obj: *const crate::object::ObjectHeader) -> Serialize let keys = if !crate::object::object_keys_array(obj).is_null() { let keys_arr = crate::object::object_keys_array(obj); let keys_len = (*keys_arr).length as usize; - let keys_elements = (keys_arr as *const u8) - .add(std::mem::size_of::()) - as *const f64; + let keys_elements = + crate::array::array_elements_ptr(keys_arr as *const crate::array::ArrayHeader) + as *const f64; let mut key_strings = Vec::with_capacity(keys_len); for i in 0..keys_len { let key_bits = (*keys_elements.add(i)).to_bits(); diff --git a/crates/perry-runtime/src/typed_feedback.rs b/crates/perry-runtime/src/typed_feedback.rs index 9535f9cad8..4b7b5773e7 100644 --- a/crates/perry-runtime/src/typed_feedback.rs +++ b/crates/perry-runtime/src/typed_feedback.rs @@ -571,7 +571,7 @@ fn array_element_kind(addr: usize, index: Option, len: u64, layout_kind: u8 return STABLE_VALUE_UNDEFINED; } unsafe { - let elements = (addr as *const u8).add(std::mem::size_of::()) as *const u64; + let elements = crate::array::array_elements_ptr(addr as *const ArrayHeader) as *const u64; stable_value_kind(*elements.add(index as usize)) } } @@ -1505,7 +1505,7 @@ pub extern "C" fn js_string_array_range_loop_guard( return 0; } let elements = - (raw_addr as *const u8).add(std::mem::size_of::()) as *const f64; + crate::array::array_elements_ptr(raw_addr as *const ArrayHeader) as *const f64; for index in min_idx..max_idx_exclusive { let value = *elements.add(index as usize); if !crate::value::JSValue::from_bits(value.to_bits()).is_any_string() { @@ -1738,7 +1738,7 @@ fn packed_i32_array_loop_guard(arr: *const ArrayHeader) -> bool { return false; } let elements = - (raw_addr as *const u8).add(std::mem::size_of::()) as *const f64; + crate::array::array_elements_ptr(raw_addr as *const ArrayHeader) as *const f64; for i in 0..len { let value = *elements.add(i); if !value.is_finite() @@ -1765,7 +1765,7 @@ fn packed_u32_array_loop_guard(arr: *const ArrayHeader) -> bool { return false; } let elements = - (raw_addr as *const u8).add(std::mem::size_of::()) as *const f64; + crate::array::array_elements_ptr(raw_addr as *const ArrayHeader) as *const f64; for i in 0..len { let value = *elements.add(i); if !value.is_finite() || value.fract() != 0.0 || value < 0.0 || value > u32::MAX as f64 diff --git a/crates/perry-runtime/src/typedarray/transform.rs b/crates/perry-runtime/src/typedarray/transform.rs index 4340503e6a..2b39fe2185 100644 --- a/crates/perry-runtime/src/typedarray/transform.rs +++ b/crates/perry-runtime/src/typedarray/transform.rs @@ -27,8 +27,8 @@ pub fn typed_array_to_array(ta: *const TypedArrayHeader) -> *mut crate::array::A if len == 0 { return result; } - let dst = - (result as *mut u8).add(std::mem::size_of::()) as *mut f64; + let dst = crate::array::array_elements_ptr(result as *const crate::array::ArrayHeader) + as *mut f64; for i in 0..len { *dst.add(i) = load_at(ta, i); } diff --git a/crates/perry-runtime/src/util_promisify.rs b/crates/perry-runtime/src/util_promisify.rs index ad551dd3e6..37e80998d9 100644 --- a/crates/perry-runtime/src/util_promisify.rs +++ b/crates/perry-runtime/src/util_promisify.rs @@ -371,7 +371,7 @@ extern "C" fn outer_thunk(closure: *const ClosureHeader, rest_value: f64) -> f64 let mut combined = js_array_alloc((rest_len + 1) as u32); if !rest_arr_ptr.is_null() && rest_len > 0 { let rest_data = unsafe { - (rest_arr_ptr as *const u8).add(std::mem::size_of::()) as *const f64 + crate::array::array_elements_ptr(rest_arr_ptr as *const ArrayHeader) as *const f64 }; for i in 0..rest_len { let v = unsafe { *rest_data.add(i) }; @@ -396,7 +396,7 @@ extern "C" fn outer_thunk(closure: *const ClosureHeader, rest_value: f64) -> f64 called = true; let arr = combined_handle.get_raw_const_ptr::(); let data = unsafe { - (arr as *const u8).add(std::mem::size_of::()) as *const f64 + crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64 }; let n = js_array_length(arr) as usize; unsafe { @@ -494,7 +494,7 @@ extern "C" fn gkp_outer_thunk(closure: *const ClosureHeader, rest_value: f64) -> let mut combined = js_array_alloc((rest_len + 1) as u32); if !rest_arr_ptr.is_null() && rest_len > 0 { let rest_data = unsafe { - (rest_arr_ptr as *const u8).add(std::mem::size_of::()) as *const f64 + crate::array::array_elements_ptr(rest_arr_ptr as *const ArrayHeader) as *const f64 }; for i in 0..rest_len { let v = unsafe { *rest_data.add(i) }; @@ -516,7 +516,7 @@ extern "C" fn gkp_outer_thunk(closure: *const ClosureHeader, rest_value: f64) -> called = true; let arr = combined_handle.get_raw_const_ptr::(); let data = unsafe { - (arr as *const u8).add(std::mem::size_of::()) as *const f64 + crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64 }; let n = js_array_length(arr) as usize; unsafe { @@ -642,7 +642,9 @@ extern "C" fn deprecate_outer_thunk(closure: *const ClosureHeader, rest_value: f let rest_data = if rest_arr_ptr.is_null() { std::ptr::null() } else { - unsafe { (rest_arr_ptr as *const u8).add(std::mem::size_of::()) as *const f64 } + unsafe { + crate::array::array_elements_ptr(rest_arr_ptr as *const ArrayHeader) as *const f64 + } }; unsafe { crate::closure::js_native_call_value(fn_handle.get_nanbox_f64(), rest_data, rest_len) } @@ -678,7 +680,7 @@ extern "C" fn callbackify_outer_thunk(closure: *const ClosureHeader, rest_value: } let rest_data = unsafe { - (rest_arr_ptr as *const u8).add(std::mem::size_of::()) as *const f64 + crate::array::array_elements_ptr(rest_arr_ptr as *const ArrayHeader) as *const f64 }; let callback_value = unsafe { *rest_data.add(rest_len - 1) }; let callback_handle = scope.root_nanbox_f64(callback_value); @@ -699,7 +701,7 @@ extern "C" fn callbackify_outer_thunk(closure: *const ClosureHeader, rest_value: // trap so any sync exception unwinds past us. let returned = unsafe { let arr = original_args_handle.get_raw_const_ptr::(); - let data = (arr as *const u8).add(std::mem::size_of::()) as *const f64; + let data = crate::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64; crate::closure::js_native_call_value(fn_handle.get_nanbox_f64(), data, original_arg_len) }; // #9539: `returned` is a heap value that outlives two closure allocations, diff --git a/crates/perry-stdlib/src/async_local_storage.rs b/crates/perry-stdlib/src/async_local_storage.rs index 5fc45afd9e..2c482b0c51 100644 --- a/crates/perry-stdlib/src/async_local_storage.rs +++ b/crates/perry-stdlib/src/async_local_storage.rs @@ -63,7 +63,7 @@ unsafe fn call_with_forwarded_args(cb: *const ClosureHeader, args_array: i64) -> let data = if arr.is_null() || len == 0 { std::ptr::null() } else { - (arr as *const u8).add(std::mem::size_of::()) as *const f64 + perry_runtime::array::array_elements_ptr(arr as *const ArrayHeader) as *const f64 }; js_closure_call_array(closure_env, data, len) } diff --git a/crates/perry-stdlib/src/sqlite/better.rs b/crates/perry-stdlib/src/sqlite/better.rs index 6bf20864e9..421142cf76 100644 --- a/crates/perry-stdlib/src/sqlite/better.rs +++ b/crates/perry-stdlib/src/sqlite/better.rs @@ -153,7 +153,8 @@ pub(crate) unsafe fn params_from_array( return vec![]; } let len = (*arr_ptr).length as usize; - let elements = (arr_ptr as *const u8).add(std::mem::size_of::()) as *const f64; + let elements = + perry_runtime::array::array_elements_ptr(arr_ptr as *const ArrayHeader) as *const f64; let mut params: Vec> = Vec::with_capacity(len); for i in 0..len { diff --git a/scripts/addr_class_allowlist.txt b/scripts/addr_class_allowlist.txt index 814403f2d2..4ab811e64c 100644 --- a/scripts/addr_class_allowlist.txt +++ b/scripts/addr_class_allowlist.txt @@ -175,3 +175,4 @@ crates/perry-runtime/src/arena/page_meta/tests.rs | (0x1020_0000, 0x1030_0000, 9 crates/perry-runtime/src/hot_diag/receiver_repr.rs | let derived_ptr = derived.as_ptr() as *const crate::gc::GcHeader; | #9973 debug-only trust-the-tag audit. `derived` is already the canonical ownership-derived header pointer; this only re-types it so the two sides of the comparison have one type. `#[cfg(debug_assertions)]`, so release builds carry no such probe. crates/perry-runtime/src/hot_diag/receiver_repr.rs | (addr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; | #9973 debug-only trust-the-tag audit, and the raw read is the POINT of it: the block compares the ownership-derived header against a direct byte-offset read and counts disagreements in DIRECT_MISMATCH. Routing this side through `try_read_gc_header` would validate the address first and return None for exactly the implausible cases the audit exists to catch, so the canonical predicate cannot stand in here. `#[cfg(debug_assertions)]`; absent from release builds. crates/perry-runtime/src/json/stringify_record_output.rs | as *const crate::gc::GcHeader | #10004 repeated-output hit: `arr` is reread from a field of the live receiver already classified by `try_object`, and its full tagged bits must equal the array token admitted by `dense_array`; no allocation or GC occurs between that reread and this header check. Moving GC changes the receiver or field token into a miss before this read. Repeating the tracked-range lookup here would tax every admitted memo hit without strengthening that ownership proof. +crates/perry-runtime/src/array/storage.rs | let header = (arr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; | #10060 logical array storage primitive: its unsafe contract requires a live, forwarding-resolved GC_TYPE_ARRAY supplied by the allocator, collector, or a validated array receiver. It never accepts a NaN-box payload or registry handle; the preceding allocation header is part of that ownership proof. No allocation or safepoint occurs during the read. diff --git a/scripts/gc_runtime_root_holders.json b/scripts/gc_runtime_root_holders.json index 042e1fdc70..75e447ed80 100644 --- a/scripts/gc_runtime_root_holders.json +++ b/scripts/gc_runtime_root_holders.json @@ -295,7 +295,7 @@ "file": "crates/perry-runtime/src/gc/census.rs", "name": "PASS1_MARKED", "verdict": "non_moving_snapshot", - "why": "Real GC header addresses, deliberately untraced so the diagnostic does not keep its observed objects alive. Populated only at the end of mark propagation of a synchronous full cycle; consumed at sweep entry in the same run_to_completion invocation. The intervening full-cycle phases do not relocate or run JS callbacks. The Vec is used for membership comparisons and dropped with the census before sweep. Budgeted and minor cycles skip both boundaries. Pin re-audited 2026-09-05 after #9760 touched `gc/mod.rs`: that change is `mod heap_stats;` plus a `pub(crate) use` re-export and alters no mark/sweep control flow. `heap_stats()` is reached only from `js_bun_jsc_heap_stats` (the JS-facing `bun:jsc.heapStats()`), i.e. from mutator code, never inside a cycle, and its own module contract forbids allocation or collection during its walk. The mark-complete β†’ sweep-entry window is unchanged. Re-audited 2026-09-05 (train125) after #9769 and #9771 touched pinned files. #9769 adds one `reg_scanner!` registration to `gc/mod.rs`; #9771 adds a feature-gated `alloc_census_init()` there and a feature-gated Rust-heap dump inside `take_census`. `alloc-census` is not in the default feature set, and decisively: `census_take_if_armed_at_full_sweep_start` does `PASS1_MARKED.with(|p| p.borrow_mut().take())` BEFORE calling `take_census`, so the snapshot has already left the thread-local by the time #9771's code runs β€” it cannot affect the window. Neither change alters mark/sweep control flow. Re-audited 2026-09-06 after #9831 touched `gc/policy.rs`. Its hunks are (a) the tiny-parse pressure guard's pricing (`tiny_parse_pressure_headroom_bytes`, `tiny_parse_pressure_due*`, a `Cell` byte-count base) consulted from JSON.parse's mutator-side boundaries (`gc_bump_malloc_trigger`, `gc_collect_pending_suppressed_parse`, `gc_schedule_parse_boundary_collection_if_pressure`), none of which is reachable from inside a cycle, and (b) one extra `Cell` store in `note_collection_finished_arena_occupancy`, which runs from `publish_reclaim_outcome` in the Publish subphase β€” after `step_sweep` has already consumed the snapshot. Mark/sweep control flow between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start` is untouched. Re-audited 2026-09-05 (train126) after #9755 restructured `gc/cycle.rs`. Its hunks are all root-scan machinery (`RootScanSubphase`, `RootScanCycleState`, the mutable-scanner iteration state), which runs BEFORE mark propagation completes; `gc/mod.rs` gains only a `mod young_log;` declaration. The bracketing is unchanged β€” `census_pass1_if_armed` is still inside `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` inside `step_sweep` β€” and a synchronous full mark-sweep still moves nothing between them. Re-pinned 2026-09-05 for the #9740 hot-TLS conversion of this file: the sole change is `thread_local!` β†’ `crate::perry_thread_local!`, a macro-name swap with identical declaration syntax and `.with()` call sites. No control flow, no phase boundary, and no storage semantics change. Re-audited 2026-09-06 (train128) after #9794's GC diagnostics touched `gc/mod.rs` and `gc/policy.rs`: both gain diagnostic module declarations and counters only β€” no mark/sweep control flow, and the census bracketing in `step_mark_propagation` / `step_sweep` is unchanged. Re-audited for #9794's GC diagnostics: `gc/mod.rs` gains `mod diag_sites;` / `mod survival_diag;`, a re-export, a `diag_sites::full_started(...)` call at TRIGGER time (before mark propagation begins), and exit-time reporting. Nothing executes between mark-complete and sweep-entry, so the window is unchanged. Re-audited 2026-09-06 for the retained array-growth verifier fix: the cycle.rs change passes the existing non-copying evacuation verifier an explicit all-forwarded policy. That call remains in minor finalization, outside the synchronous full-cycle census window; its root and heap reads do not allocate GC objects, move objects, or invoke JS callbacks. The mark-complete and sweep-entry boundaries are unchanged. Re-audited 2026-09-05 after #9830 touched `gc/policy.rs`. That change is (a) six `thread_local! {` blocks rewritten as `crate::perry_thread_local! {` and (b) one `#[cfg(test)]` accessor listing the trigger path's hot-slot indices. The macro keeps the same storage, the same `.with()` at every read and write, and the same destructor registration (the teardown guard exists exactly when `needs_drop` holds, which is what `std::thread_local!` already decided); no value, predicate or branch in the file changes, so no mark or sweep control flow does. The one new behaviour is on a declaration's FIRST read: `HotKey::resolve_and_cache` takes a mutex and allocates a key through the GLOBAL allocator. Even if a first read landed inside this window it would be sound β€” the window's contract is that nothing relocates and no JS callback runs, and a mimalloc allocation does neither. `census_pass1_if_armed` is still inside `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` inside `step_sweep`; the bracketing is untouched. Re-audited 2026-09-06 (train132) after #9860 and #9845 touched `gc/mod.rs`. Both hunks are re-export lists and nothing else: #9860 adds `idle_reclaim_elapsed_starts` / `IDLE_RECLAIM_REARM_MS`, and #9845 adds `owner_is_dead_copied_minor_from_space_of_type`. No mark or sweep control flow changes. #9845's substantive work sits in `gc/oldgen.rs` and `gc/copying.rs`, neither pinned: the copying-minor arm (`finalize_dead_copied_minor_from_space_regexps`) runs on a MINOR, which skips both census boundaries; the full-cycle arm (`collect_dead_registered_regexps_post_trace`, from `with_dead_collection_finalize`) walks the RegExp registry building a Vec of addresses β€” no GC allocation, no JS callback, so it cannot relocate the snapshot's subjects β€” and it is reached from the sweep body, i.e. AFTER `census_take_if_armed_at_full_sweep_start` has already `take()`n the snapshot out of the thread-local. The mark-complete -> sweep-entry window is unchanged. Re-audited 2026-09-07 for #9965 after 1ec9e0e8a touched `gc/cycle.rs` and `gc/mod.rs`: `gc/mod.rs:216-217` only declares and imports the failure-attribution module, while `gc/cycle.rs:1414-1417` reads the trigger and diagnostic counters immediately before evacuation verification inside `atomic_finalize_minor_prelude`. Full cycles bypass `MinorPrelude` at `gc/cycle.rs:1192-1196`; evacuation remains guarded by the minor-only context at `gc/cycle.rs:1330-1372`. The snapshot store remains at `gc/cycle.rs:963-964` after synchronous full marking, and its take remains at `gc/cycle.rs:1454-1457` before sweep. No new write, relocation, collection, or JS callback was added to that full-cycle interval, so the PASS1_MARKED window is unaffected. Re-audited 2026-09-07 for the regex census rows: all new work is in `take_census` after `census_take_if_armed_at_full_sweep_start` has taken PASS1_MARKED out of TLS; neither boundary nor the intervening cycle control flow changed. Re-audited 2026-09-08 (train144) after #9976 and #9977 touched pinned files. `gc/mod.rs` gains exactly three lines: `mod copying_phase;` and `mod regex_census;` (declarations) and one `reg_scanner!(regex::site_test::scan_roots_mut)` registration. A scanner registration adds a root SOURCE for the mutable-root walks; it does not move either census boundary and runs nowhere between them. `gc/census.rs` widens `side_tables()` to `pub(super)`, extends it with regex rows and adds a test module β€” all census REPORTING, which runs from the diagnostic dump, not inside a cycle. Mark/sweep control flow between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start` is untouched. Re-audited 2026-09-08 for #9849 JSON construction deferral. `gc/mod.rs` adds the `json_defer` module/re-export and a trusted-header layout helper used only by already-validated JSON emitters; neither changes or runs in collector phase control flow. `gc/policy.rs` adds JSON completion scheduling, construction-grace checks, and safepoint deferral predicates. These are called from mutator-side JSON allocation/output boundaries and ordinary safepoint entry; they do not alter `step_mark_propagation`, `step_sweep`, or invoke callbacks or relocation between the census boundaries. The mark-complete to sweep-entry window is unchanged. The follow-up adds a cfg(test)-only one-shot boolean for deterministic explicit-pressure fixtures; it is absent from production builds and cannot affect the census window. The first predicate read consumes it, so post-parse accounting exercises normal pricing. Re-audited 2026-09-09 for bounded tiny-JSON completion polling. The policy.rs changes split the mutator-side pending-parse check into an inlined empty fast path plus an outlined debt-service path, and amortize the mutator-side arena-pressure read across 64 bounded parse completions. Neither function is reachable from step_mark_propagation or step_sweep; neither census boundary nor the synchronous full-cycle interval between them changes. Re-audited 2026-09-09 for lazy JSON record batches: policy.rs only widens gc_budgeted_cycle_active visibility from pub(super) to pub(crate). Its body remains a read-only Cell query. The new caller is lazy_get materialization in the mutator; run_to_completion, step_mark_propagation, census snapshot consumption at step_sweep, and the synchronous non-moving window are unchanged. Re-audited 2026-09-09 for completed JSON-output debt: the added gc_service_json_output_sweep function calls the existing trigger check from a rooted mutator boundary and reports whether its malloc-count request remains due. It is not called from any census or collector phase; the synchronous mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-09 for the JSON byte-debt carry: the same mutator-only service helper now distinguishes requests satisfied before its call from those satisfied by its trigger check. The added enum contains no payload, both count reads are scalar, and no census boundary or collector phase changed. Re-audited 2026-09-11 for #10055: gc/mod.rs only registers the weak UTF-16 index scanner during gc_init. It neither marks strings nor allocates GC objects or runs JS; offset vectors use the Rust allocator. The mark-complete to sweep-entry census window and cycle control flow are unchanged. Re-audited 2026-09-11 for #10054: gc/mod.rs adds only the trim-cache mutable-root scanner registration in gc_init. Its scanner visits two existing string slots without allocating or invoking JS. Root scanning still precedes mark completion, and neither census boundary nor the synchronous mark-complete to sweep-entry window changes.", + "why": "Real GC header addresses, deliberately untraced so the diagnostic does not keep its observed objects alive. Populated only at the end of mark propagation of a synchronous full cycle; consumed at sweep entry in the same run_to_completion invocation. The intervening full-cycle phases do not relocate or run JS callbacks. The Vec is used for membership comparisons and dropped with the census before sweep. Budgeted and minor cycles skip both boundaries. Pin re-audited 2026-09-05 after #9760 touched `gc/mod.rs`: that change is `mod heap_stats;` plus a `pub(crate) use` re-export and alters no mark/sweep control flow. `heap_stats()` is reached only from `js_bun_jsc_heap_stats` (the JS-facing `bun:jsc.heapStats()`), i.e. from mutator code, never inside a cycle, and its own module contract forbids allocation or collection during its walk. The mark-complete β†’ sweep-entry window is unchanged. Re-audited 2026-09-05 (train125) after #9769 and #9771 touched pinned files. #9769 adds one `reg_scanner!` registration to `gc/mod.rs`; #9771 adds a feature-gated `alloc_census_init()` there and a feature-gated Rust-heap dump inside `take_census`. `alloc-census` is not in the default feature set, and decisively: `census_take_if_armed_at_full_sweep_start` does `PASS1_MARKED.with(|p| p.borrow_mut().take())` BEFORE calling `take_census`, so the snapshot has already left the thread-local by the time #9771's code runs β€” it cannot affect the window. Neither change alters mark/sweep control flow. Re-audited 2026-09-06 after #9831 touched `gc/policy.rs`. Its hunks are (a) the tiny-parse pressure guard's pricing (`tiny_parse_pressure_headroom_bytes`, `tiny_parse_pressure_due*`, a `Cell` byte-count base) consulted from JSON.parse's mutator-side boundaries (`gc_bump_malloc_trigger`, `gc_collect_pending_suppressed_parse`, `gc_schedule_parse_boundary_collection_if_pressure`), none of which is reachable from inside a cycle, and (b) one extra `Cell` store in `note_collection_finished_arena_occupancy`, which runs from `publish_reclaim_outcome` in the Publish subphase β€” after `step_sweep` has already consumed the snapshot. Mark/sweep control flow between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start` is untouched. Re-audited 2026-09-05 (train126) after #9755 restructured `gc/cycle.rs`. Its hunks are all root-scan machinery (`RootScanSubphase`, `RootScanCycleState`, the mutable-scanner iteration state), which runs BEFORE mark propagation completes; `gc/mod.rs` gains only a `mod young_log;` declaration. The bracketing is unchanged β€” `census_pass1_if_armed` is still inside `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` inside `step_sweep` β€” and a synchronous full mark-sweep still moves nothing between them. Re-pinned 2026-09-05 for the #9740 hot-TLS conversion of this file: the sole change is `thread_local!` β†’ `crate::perry_thread_local!`, a macro-name swap with identical declaration syntax and `.with()` call sites. No control flow, no phase boundary, and no storage semantics change. Re-audited 2026-09-06 (train128) after #9794's GC diagnostics touched `gc/mod.rs` and `gc/policy.rs`: both gain diagnostic module declarations and counters only β€” no mark/sweep control flow, and the census bracketing in `step_mark_propagation` / `step_sweep` is unchanged. Re-audited for #9794's GC diagnostics: `gc/mod.rs` gains `mod diag_sites;` / `mod survival_diag;`, a re-export, a `diag_sites::full_started(...)` call at TRIGGER time (before mark propagation begins), and exit-time reporting. Nothing executes between mark-complete and sweep-entry, so the window is unchanged. Re-audited 2026-09-06 for the retained array-growth verifier fix: the cycle.rs change passes the existing non-copying evacuation verifier an explicit all-forwarded policy. That call remains in minor finalization, outside the synchronous full-cycle census window; its root and heap reads do not allocate GC objects, move objects, or invoke JS callbacks. The mark-complete and sweep-entry boundaries are unchanged. Re-audited 2026-09-05 after #9830 touched `gc/policy.rs`. That change is (a) six `thread_local! {` blocks rewritten as `crate::perry_thread_local! {` and (b) one `#[cfg(test)]` accessor listing the trigger path's hot-slot indices. The macro keeps the same storage, the same `.with()` at every read and write, and the same destructor registration (the teardown guard exists exactly when `needs_drop` holds, which is what `std::thread_local!` already decided); no value, predicate or branch in the file changes, so no mark or sweep control flow does. The one new behaviour is on a declaration's FIRST read: `HotKey::resolve_and_cache` takes a mutex and allocates a key through the GLOBAL allocator. Even if a first read landed inside this window it would be sound β€” the window's contract is that nothing relocates and no JS callback runs, and a mimalloc allocation does neither. `census_pass1_if_armed` is still inside `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` inside `step_sweep`; the bracketing is untouched. Re-audited 2026-09-06 (train132) after #9860 and #9845 touched `gc/mod.rs`. Both hunks are re-export lists and nothing else: #9860 adds `idle_reclaim_elapsed_starts` / `IDLE_RECLAIM_REARM_MS`, and #9845 adds `owner_is_dead_copied_minor_from_space_of_type`. No mark or sweep control flow changes. #9845's substantive work sits in `gc/oldgen.rs` and `gc/copying.rs`, neither pinned: the copying-minor arm (`finalize_dead_copied_minor_from_space_regexps`) runs on a MINOR, which skips both census boundaries; the full-cycle arm (`collect_dead_registered_regexps_post_trace`, from `with_dead_collection_finalize`) walks the RegExp registry building a Vec of addresses β€” no GC allocation, no JS callback, so it cannot relocate the snapshot's subjects β€” and it is reached from the sweep body, i.e. AFTER `census_take_if_armed_at_full_sweep_start` has already `take()`n the snapshot out of the thread-local. The mark-complete -> sweep-entry window is unchanged. Re-audited 2026-09-07 for #9965 after 1ec9e0e8a touched `gc/cycle.rs` and `gc/mod.rs`: `gc/mod.rs:216-217` only declares and imports the failure-attribution module, while `gc/cycle.rs:1414-1417` reads the trigger and diagnostic counters immediately before evacuation verification inside `atomic_finalize_minor_prelude`. Full cycles bypass `MinorPrelude` at `gc/cycle.rs:1192-1196`; evacuation remains guarded by the minor-only context at `gc/cycle.rs:1330-1372`. The snapshot store remains at `gc/cycle.rs:963-964` after synchronous full marking, and its take remains at `gc/cycle.rs:1454-1457` before sweep. No new write, relocation, collection, or JS callback was added to that full-cycle interval, so the PASS1_MARKED window is unaffected. Re-audited 2026-09-07 for the regex census rows: all new work is in `take_census` after `census_take_if_armed_at_full_sweep_start` has taken PASS1_MARKED out of TLS; neither boundary nor the intervening cycle control flow changed. Re-audited 2026-09-08 (train144) after #9976 and #9977 touched pinned files. `gc/mod.rs` gains exactly three lines: `mod copying_phase;` and `mod regex_census;` (declarations) and one `reg_scanner!(regex::site_test::scan_roots_mut)` registration. A scanner registration adds a root SOURCE for the mutable-root walks; it does not move either census boundary and runs nowhere between them. `gc/census.rs` widens `side_tables()` to `pub(super)`, extends it with regex rows and adds a test module β€” all census REPORTING, which runs from the diagnostic dump, not inside a cycle. Mark/sweep control flow between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start` is untouched. Re-audited 2026-09-08 for #9849 JSON construction deferral. `gc/mod.rs` adds the `json_defer` module/re-export and a trusted-header layout helper used only by already-validated JSON emitters; neither changes or runs in collector phase control flow. `gc/policy.rs` adds JSON completion scheduling, construction-grace checks, and safepoint deferral predicates. These are called from mutator-side JSON allocation/output boundaries and ordinary safepoint entry; they do not alter `step_mark_propagation`, `step_sweep`, or invoke callbacks or relocation between the census boundaries. The mark-complete to sweep-entry window is unchanged. The follow-up adds a cfg(test)-only one-shot boolean for deterministic explicit-pressure fixtures; it is absent from production builds and cannot affect the census window. The first predicate read consumes it, so post-parse accounting exercises normal pricing. Re-audited 2026-09-09 for bounded tiny-JSON completion polling. The policy.rs changes split the mutator-side pending-parse check into an inlined empty fast path plus an outlined debt-service path, and amortize the mutator-side arena-pressure read across 64 bounded parse completions. Neither function is reachable from step_mark_propagation or step_sweep; neither census boundary nor the synchronous full-cycle interval between them changes. Re-audited 2026-09-09 for lazy JSON record batches: policy.rs only widens gc_budgeted_cycle_active visibility from pub(super) to pub(crate). Its body remains a read-only Cell query. The new caller is lazy_get materialization in the mutator; run_to_completion, step_mark_propagation, census snapshot consumption at step_sweep, and the synchronous non-moving window are unchanged. Re-audited 2026-09-09 for completed JSON-output debt: the added gc_service_json_output_sweep function calls the existing trigger check from a rooted mutator boundary and reports whether its malloc-count request remains due. It is not called from any census or collector phase; the synchronous mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-09 for the JSON byte-debt carry: the same mutator-only service helper now distinguishes requests satisfied before its call from those satisfied by its trigger check. The added enum contains no payload, both count reads are scalar, and no census boundary or collector phase changed. Re-audited 2026-09-11 for #10055: gc/mod.rs only registers the weak UTF-16 index scanner during gc_init. It neither marks strings nor allocates GC objects or runs JS; offset vectors use the Rust allocator. The mark-complete to sweep-entry census window and cycle control flow are unchanged. Re-audited 2026-09-11 for #10054: gc/mod.rs adds only the trim-cache mutable-root scanner registration in gc_init. Its scanner visits two existing string slots without allocating or invoking JS. Root scanning still precedes mark completion, and neither census boundary nor the synchronous mark-complete to sweep-entry window changes. Re-audited 2026-09-11 for #10060: the census array classifier now reads the logical element start and bounds its scan by the remaining capacity. The helper only reads the existing GC/header words and performs pointer arithmetic; it cannot allocate, collect, or call JS. This classifier runs in take_census after PASS1_MARKED has been taken out of TLS. Neither census boundary nor the mark-complete to sweep-entry control flow changed.", "window": { "start": { "file": "crates/perry-runtime/src/gc/census.rs", @@ -310,7 +310,7 @@ "function": "run_to_completion" }, "sources": { - "crates/perry-runtime/src/gc/census.rs": "0fd14b011bdbe0ae561e105d6d1acf0e9cc03e8a86ddef12685e307d1adee55a", + "crates/perry-runtime/src/gc/census.rs": "40a1d0744405a0c2e71ae578a0767ec0fb0100aeeecdbd877c94658af6067169", "crates/perry-runtime/src/gc/cycle.rs": "77eadaf7c4157308c3b14be5e1ff11d7198b84b503a50ff2d244851248d3e800", "crates/perry-runtime/src/gc/mod.rs": "db54308d6acf29c7ad4f7b360490976d82329ba0667dad9c6a732d1112548c04", "crates/perry-runtime/src/gc/policy.rs": "a701257f2e2310adabe16e33c0afcd935c7cd28e1ddc157b4974b48e2688cc4f", diff --git a/test-files/test_gap_array_shift_observable.ts b/test-files/test_gap_array_shift_observable.ts new file mode 100644 index 0000000000..05d71089e0 --- /dev/null +++ b/test-files/test_gap_array_shift_observable.ts @@ -0,0 +1,39 @@ +// #10060: shift must preserve live indexed operations and abrupt completion order. +const inherited: number[] = [1, , 3]; +const proto = Object.create(Array.prototype); +proto[1] = 8; +Object.setPrototypeOf(inherited, proto); +console.log("inherited", inherited.shift(), inherited[0], inherited[1], inherited.length); + +const observed: any[] = [10, 20, 30]; +let log = ""; +Object.defineProperty(observed, "0", { + configurable: true, + get() { log += "get0;"; return 10; }, + set(v) { log += "set0=" + v + ";"; }, +}); +Object.defineProperty(observed, "1", { + configurable: true, + get() { log += "get1;"; return 20; }, + set(v) { log += "set1=" + v + ";"; }, +}); +console.log("accessor", observed.shift(), log, observed.length); + +const locked: number[] = [1, 2, 3]; +Object.defineProperty(locked, "length", { writable: false }); +try { locked.shift(); } catch (e) { console.log("length-error", e instanceof TypeError); } +console.log("length-effects", locked.length, locked[0], locked[1], 2 in locked); + +const sealed: number[] = [1, 2, 3]; +Object.seal(sealed); +try { sealed.shift(); } catch (e) { console.log("sealed-error", e instanceof TypeError); } +console.log("sealed-effects", sealed.join(",")); + +const frozen: any[] = [1, 2]; +let reads = 0; +Object.defineProperty(frozen, "0", { get() { reads++; return 1; } }); +Object.freeze(frozen); +try { frozen.shift(); } catch (e) { console.log("frozen-error", e instanceof TypeError, reads); } +const empty: number[] = []; +Object.defineProperty(empty, "length", { writable: false }); +try { empty.shift(); } catch (e) { console.log("empty-error", e instanceof TypeError); } diff --git a/test-files/test_gap_gc_array_shift_queue.ts b/test-files/test_gap_gc_array_shift_queue.ts new file mode 100644 index 0000000000..36a46eb96f --- /dev/null +++ b/test-files/test_gap_gc_array_shift_queue.ts @@ -0,0 +1,60 @@ +// #10060: shifted storage must agree across generated indexing, runtime +// mutators, JSON, aliases, forwarding, and moving collection. +declare function gc(): void; + +function collect(): void { + if (typeof gc === "function") gc(); +} + +function shiftedNumbers(): number[] { + const q: number[] = []; + for (let i = 0; i < 20; i++) q.push(i); + for (let i = 0; i < 7; i++) q.shift(); + return q; +} + +const q = shiftedNumbers(); +const alias = q; +q[0] = 40; +q.push(20); +console.log("index", q.length, alias[0], q[3], q.pop()); +console.log("slice", JSON.stringify(q.slice(0, 4))); +q.reverse(); +q.copyWithin(1, 3, 5); +q.fill(60, 2, 4); +q.unshift(50, 51); +console.log("mutate", JSON.stringify(q), JSON.stringify(q.splice(2, 3, 70))); +console.log("map", q.map((n: number) => n + 1).join(",")); +console.log("concat", JSON.stringify(q.concat([80, 81]))); +q.length = 2; +q.length = 5; +console.log("holes", JSON.stringify(q), 2 in q); +while (q.length) q.shift(); +q.push(90); +console.log("reuse", alias[0], q.shift(), q.length, q.shift()); + +function mixedQueue(size: number): void { + const values: any[] = []; + const other = values; + let checksum = 0; + for (let cycle = 0; cycle < 3; cycle++) { + for (let i = 0; i < size; i++) { + values.push(i % 2 === 0 ? { id: i, text: "item-" + i } : i); + } + for (let i = 0; i < size / 2; i++) { + const v = values.shift(); + checksum += typeof v === "number" ? v : v.id; + } + collect(); + for (let i = 0; i < size; i++) values.push({ id: i, text: "new-" + i }); + collect(); + while (other.length) { + const v = other.shift(); + checksum += typeof v === "number" ? v : v.id; + } + } + console.log("mixed", size, checksum, values.length, other.length); +} +mixedQueue(32); +mixedQueue(4096); + diff --git a/test-parity/gc_repsel_corpus.txt b/test-parity/gc_repsel_corpus.txt index 1e14f29a38..50bd738e32 100644 --- a/test-parity/gc_repsel_corpus.txt +++ b/test-parity/gc_repsel_corpus.txt @@ -861,3 +861,4 @@ test_gap_gc_coalesce_local_root test_gap_gc_for_in_proxy_callback_roots test_gap_gc_string_suffix_cursor +test_gap_gc_array_shift_queue From caa23ad5d4249ee916b7f89d39c0a8884a97281a Mon Sep 17 00:00:00 2001 From: Ralph Kuepper Date: Fri, 11 Sep 2026 15:19:51 +0200 Subject: [PATCH 5/9] fix(ffi): read shifted arrays through the existing accessor (cherry picked from commit 1e79ee9e431c8a3791cac534ecb08b58d53df0c9) --- benchmarks/array-shift-10060/.gitignore | 4 ++++ crates/perry-codegen/src/expr/element_shape_guard.rs | 2 +- crates/perry-ext-better-sqlite3/src/lib.rs | 4 +--- crates/perry-ext-http/src/client_overload.rs | 4 +--- crates/perry-ext-http/src/client_request_surface.rs | 4 +++- crates/perry-ext-http/src/server/types.rs | 4 +--- crates/perry-ffi/src/types.rs | 3 ++- test-files/test_gap_gc_array_shift_queue.ts | 1 - 8 files changed, 13 insertions(+), 13 deletions(-) create mode 100644 benchmarks/array-shift-10060/.gitignore diff --git a/benchmarks/array-shift-10060/.gitignore b/benchmarks/array-shift-10060/.gitignore new file mode 100644 index 0000000000..5691a6d1bb --- /dev/null +++ b/benchmarks/array-shift-10060/.gitignore @@ -0,0 +1,4 @@ +*.exe +/array-shift-queue +/array-push +__pycache__/ diff --git a/crates/perry-codegen/src/expr/element_shape_guard.rs b/crates/perry-codegen/src/expr/element_shape_guard.rs index 9e90b0859b..5db287d909 100644 --- a/crates/perry-codegen/src/expr/element_shape_guard.rs +++ b/crates/perry-codegen/src/expr/element_shape_guard.rs @@ -306,7 +306,7 @@ pub(crate) fn emit_element_shape_loop_preheader_check( } }; - // Elements base: `arr + size_of::()`. + // Logical elements base, including a consumed queue prefix. let base_addr = blk.array_elements_addr(&handle1); let elements_base = blk.inttoptr(I64, &base_addr); diff --git a/crates/perry-ext-better-sqlite3/src/lib.rs b/crates/perry-ext-better-sqlite3/src/lib.rs index d695c5069b..08ce72f7cf 100644 --- a/crates/perry-ext-better-sqlite3/src/lib.rs +++ b/crates/perry-ext-better-sqlite3/src/lib.rs @@ -86,12 +86,10 @@ unsafe fn params_from_array(arr_ptr: *const ArrayHeader) -> Vec> = Vec::with_capacity(len); for i in 0..len { - let bits = *elements.add(i); + let bits = perry_ffi::js_array_get(arr_ptr, i as u32).bits(); let val = JsValue::from_bits(bits); if val.is_null() || val.is_undefined() { diff --git a/crates/perry-ext-http/src/client_overload.rs b/crates/perry-ext-http/src/client_overload.rs index bfc9469344..9fb80894b6 100644 --- a/crates/perry-ext-http/src/client_overload.rs +++ b/crates/perry-ext-http/src/client_overload.rs @@ -65,10 +65,8 @@ pub(crate) unsafe fn parse_client_args(args_array: i64) -> ClientArgs { return out; } let len = (*arr_ptr).length as usize; - let elements = - perry_runtime::array::array_elements_ptr(arr_ptr as *const ArrayHeader) as *const u64; for i in 0..len { - let bits = *elements.add(i); + let bits = perry_ffi::js_array_get(arr_ptr, i as u32).bits(); // The response callback is the (single) function argument β€” match // it by value type, not position. if js_value_is_closure(bits as i64) != 0 { diff --git a/crates/perry-ext-http/src/client_request_surface.rs b/crates/perry-ext-http/src/client_request_surface.rs index 737a5d3fba..169ac2913d 100644 --- a/crates/perry-ext-http/src/client_request_surface.rs +++ b/crates/perry-ext-http/src/client_request_surface.rs @@ -144,7 +144,9 @@ extern "C" fn client_once_wrapper(closure: *const RawClosureHeader, rest: f64) - if array.is_null() { return js_closure_call_array(callback, std::ptr::null(), 0); } - let args = perry_runtime::array::array_elements_ptr(array) as *const f64; + // The rest ABI creates this fresh, unshifted argument array for this + // invocation; no JS callback has run since it was packed. + let args = (array as *const u8).add(8) as *const f64; js_closure_call_array(callback, args, (*array).length as i64) } } diff --git a/crates/perry-ext-http/src/server/types.rs b/crates/perry-ext-http/src/server/types.rs index 93bb7e43b0..0e5c780e0b 100644 --- a/crates/perry-ext-http/src/server/types.rs +++ b/crates/perry-ext-http/src/server/types.rs @@ -166,10 +166,8 @@ pub unsafe fn parse_listen_args(args_array: i64) -> ListenArgs { return out; } let len = (*arr_ptr).length as usize; - let elements = - perry_runtime::array::array_elements_ptr(arr_ptr as *const ArrayHeader) as *const u64; for i in 0..len { - let bits = *elements.add(i); + let bits = perry_ffi::js_array_get(arr_ptr, i as u32).bits(); let v = JsValue::from_bits(bits); // The completion callback is the (single) function argument β€” match it // by value type, not position, so it's picked up wherever it floats. diff --git a/crates/perry-ffi/src/types.rs b/crates/perry-ffi/src/types.rs index 7032185e39..887a463659 100644 --- a/crates/perry-ffi/src/types.rs +++ b/crates/perry-ffi/src/types.rs @@ -67,11 +67,12 @@ pub struct StringHeader { const _: () = assert!(std::mem::size_of::() == 20); /// Header for a runtime-allocated JS array. +/// Element storage may have a consumed queue prefix; use `js_array_get` to read it. #[repr(C)] pub struct ArrayHeader { /// Number of elements currently in the array. pub length: u32, - /// Allocated element capacity. + /// Available slots from logical element zero to the backing allocation end. pub capacity: u32, } diff --git a/test-files/test_gap_gc_array_shift_queue.ts b/test-files/test_gap_gc_array_shift_queue.ts index 36a46eb96f..dfe7e7b136 100644 --- a/test-files/test_gap_gc_array_shift_queue.ts +++ b/test-files/test_gap_gc_array_shift_queue.ts @@ -57,4 +57,3 @@ function mixedQueue(size: number): void { } mixedQueue(32); mixedQueue(4096); - From 2143d5de01a62f53a5f0fb06a81d570ef112bbc5 Mon Sep 17 00:00:00 2001 From: Ralph Kuepper Date: Fri, 11 Sep 2026 15:39:00 +0200 Subject: [PATCH 6/9] docs: record array shift scaling and GC validation (cherry picked from commit b27feab6f58da936f7d7dc78e7856613f29f384b) --- benchmarks/array-shift-10060/README.md | 191 ++++++++++++++ benchmarks/array-shift-10060/after.json | 249 ++++++++++++++++++ benchmarks/array-shift-10060/before.json | 235 +++++++++++++++++ .../array-shift-10060/checksum-reference.json | 7 + benchmarks/array-shift-10060/comparison.json | 67 +++++ 5 files changed, 749 insertions(+) create mode 100644 benchmarks/array-shift-10060/README.md create mode 100644 benchmarks/array-shift-10060/after.json create mode 100644 benchmarks/array-shift-10060/before.json create mode 100644 benchmarks/array-shift-10060/checksum-reference.json create mode 100644 benchmarks/array-shift-10060/comparison.json diff --git a/benchmarks/array-shift-10060/README.md b/benchmarks/array-shift-10060/README.md new file mode 100644 index 0000000000..264f1c3629 --- /dev/null +++ b/benchmarks/array-shift-10060/README.md @@ -0,0 +1,191 @@ +# Array.shift queue drain (#10060) + +`array-shift-queue.ts` is the complete standalone source embedded in +[#10060](https://github.com/PerryTS/perry/issues/10060), including its original +driver. `array-push.ts` is an ordinary push control using the same seeded input, +driver, and ordered checksum helpers. Both workloads use the same executable logic before +and after the change. + +## Representation and memory cost + +The eight-byte `ArrayHeader` still contains length and capacity. Capacity now +counts available slots from **logical element zero to the physical allocation +end**. The existing GC allocation size gives physical capacity; their difference +is the queue front offset. Runtime and generated indexing share this formula. +Native bindings use the existing `perry_ffi::js_array_get` accessor; general array +elements can no longer be read by assuming a fixed eight-byte header offset. +Fresh compiler-created rest arguments and internal shape-key arrays remain +unshifted. + +A dense shift reads and clears one slot, decreases length and remaining capacity, +and invalidates element-shape evidence. It performs no survivor copy, allocation, +or per-survivor layout/barrier rebuild. The final shift resets capacity to the +full physical capacity, allowing empty-array reuse. A complete dense drain is +linear in the number of elements, rather than repeatedly processing lengths +`n-1, n-2, ...`. + +GC enumeration uses the logical live range. Pointer-free and all-pointer layout +proofs survive a shift; index-specific mixed masks become UNKNOWN and trace the +live slots. Removed physical slots contain HOLE, so retaining the allocation does +not retain the removed values. Survivor addresses stay fixed and existing +old-to-young dirty-page coverage stays valid. Growth copies the logical range to +a fresh unshifted allocation, transfers layout state, and replays barriers; +shifted sources cannot use the old address-translation shortcut. Array-growth +forwarding continues to preserve aliases. + +There is no extra header word, side table, or allocation for a pure drain. Both +versions retain the original backing capacity throughout a pure drain; consuming +a prefix does not release its bytes to the allocator. The 10,000-element unit +witness keeps the exact original allocation throughout the drain, then restores +its capacity from one remaining slot to 10,000 on empty. This is a retained +capacity accounting claim, not an RSS measurement. Alternating shifts and pushes +can exhaust the remaining tail and grow earlier; growth normalizes storage and +remains geometric in the live capacity requirement. Mixed arrays trade their +indexed tracing mask for a conservative live-slot scan until layout is rebuilt. +Ordinary unshifted accesses also pay the extra allocation-size/capacity loads; +the push control below measures one consequence of that shared representation. + +Receivers with indexed descriptors, custom prototypes, sparse storage or +sealed/frozen restrictions take the live property-aware shift path. Indexed +operations happen before the final length write, preserving partial effects and +exception order when length is non-writable. + +## Reproduction + +Measured on Windows 11 (10.0.26200), x86-64 AMD Ryzen 5 7640HS, 6 cores / 12 +logical processors, with Node **v26.5.1**. The before compiler and both archives +were rebuilt from pristine main +`603b074ace01464bc66fc07cc8d532f26ccf5a0f`. The after JSON records the implementation +commit; later commits only add evidence and name the changelog fragment. + +Both builds used the same Rust nightly 1.100.0 toolchain and release profile, +with `CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16`: + +```sh +cargo build --release --locked -p perry -p perry-runtime-static -p perry-stdlib-static +python benchmarks/array-shift-10060/run.py \ + --perry target/release/perry --output benchmarks/array-shift-10060/after.json +``` + +On Windows the compiler path ends in `.exe`; `LLVM_SYS_221_PREFIX` and PATH point +to the local LLVM 22 installation. The driver selects matching runtime archives +beside the compiler, compiles with `--no-auto-optimize --no-cache`, resolves Node's +actual executable past any launcher shim, and runs each process sequentially. +To measure the baseline, pass its separately built compiler and use `before.json` +as the output. JSON includes compiler, runtime, stdlib, Node and queue-source +SHA-256 hashes. + +Input setup is outside the timer and resets the PRNG before **every** invocation. +Warmup requires both at least 200 ms of measured work and five runs. Each of seven +samples contains at least 20 ms of measured work; the result is the median +per-invocation time. Every invocation checks checksum consistency, and completed +Node/Perry pairs must match. The 60-second process timeout includes setup, warmup +and sampling. A timed-out engine skips later sizes, exactly as in the issue. +Timer/checksum overhead is included. Timings are evidence from this host, not a +universal Node shift complexity claim. + +## Measurements + +Times are median milliseconds per complete workload invocation. Raw results are +[before.json](before.json), [after.json](after.json), and [comparison.json](comparison.json). + +### Queue drain + +| n | Node before | Perry before | Node after | Perry after | +|---:|---:|---:|---:|---:|---:| +| 100 | 0.002051 | 0.016557 | 0.001818 | 0.001971 | +| 1,000 | 0.039113 | 1.388167 | 0.036471 | 0.019170 | +| 10,000 | 0.393892 | 401.922800 | 0.363189 | 0.192403 | +| 100,000 | 519.977100 | TIMEOUT | 394.911100 | 1.933736 | +| 1,000,000 | TIMEOUT | SKIPPED | TIMEOUT | 19.305250 | + +The 100,000-element Perry process now completes within the original timeout. +The 10,000-element drain falls from 401.923 ms to 0.192 ms (about 2,089x on +this host). The one-million-element Perry drain takes 19.305 ms. Node retains its +60-second timeout at one million in both sweeps. + +Log(time)/log(n) least-squares slopes, with explicit completed size sets: + +- Common to **both engines, before and after**, `[100, 1000, 10000]`: Perry + **2.193 -> 0.995**; Node **1.142 -> 1.150**. +- After only, common `[100, 1000, 10000, 100000]`: Perry **0.998**, Node **1.701**. +- After Perry over `[100, 1000, 10000, 100000, 1000000]`: **0.999**. No Node + one-million point is used in any fit. + +All completed Node/Perry pairs match their order-sensitive checksums. For one +million elements, Perry returns **755413900**, matching an independent untimed +Node traversal of the same seeded sequence; [checksum-reference.json](checksum-reference.json) +contains that reference program. This does not convert the timed-out Node shift +process into a completed benchmark sample. + +### Ordinary push control + +| n | Node before | Perry before | Node after | Perry after | +|---:|---:|---:|---:|---:|---:| +| 100 | 0.002307 | 0.004118 | 0.001779 | 0.003272 | +| 1,000 | 0.023579 | 0.029980 | 0.019149 | 0.023626 | +| 10,000 | 0.213551 | 0.322097 | 0.191966 | 0.248349 | +| 100,000 | 2.103980 | 3.468786 | 1.919945 | 2.633288 | +| 1,000,000 | 24.169400 | 36.186200 | 21.401300 | 25.561100 | + +Common sizes for every control fit are `[100, 1000, 10000, 100000, 1000000]`. +Perry slopes: **0.995 -> 0.983**; Node: **0.999 -> 1.016**. Every checksum +matches. Ordinary push remains linear and shows no slowdown in these runs. Node +also improves in the later sweep, so small constant-factor differences should be +treated as host/run variability on this shared development machine. + +## Validation + +The release compiler and matching runtime/stdlib archives build successfully. +Tests use the repository's pinned Node v26.5.1. Runtime Rust tests run with one +thread; test-profile overrides are `DEBUG=0`, `OPT_LEVEL=1`, `CODEGEN_UNITS=16`. + +- Five new runtime/collector tests pass, including a no-copy 10,000-element + drain, holes, aliases, growth, empty reuse, actual relocation of mixed-array + survivors, and old-to-young edges through repeated growth/refill. +- Both new TypeScript fixtures match Node. The queue fixture also passes with + `PERRY_GC_FORCE_EVACUATE=1`, `PERRY_GC_VERIFY_EVACUATION=1`, + `PERRY_GC_FROMSPACE_SCAN_ABORT=1`, and `PERRY_GC_DIAG=1`: **12 copying minors**, + with actual copied objects and no stale-pointer diagnostic. It is registered + in `test-parity/gc_repsel_corpus.txt`. +- Seven existing array/JSON fixtures match Node: array splice spread/dispatch, + proxy array mutators, short JSON array storage, primitive JSON arrays, JSON + array-element overflow fields, and grown-array stringify. The queue covers + indexing, slice/reverse/copyWithin/fill/unshift/splice/map/concat and length + changes. The observable fixture covers custom-prototype holes, indexed + getters/setters, non-writable length, sealed/frozen arrays and exception order. +- All **144 array-related codegen tests** and **33 FFI tests** pass. Production + `cargo check --lib` also passes separately for `perry-ext-http` and + `perry-ext-better-sqlite3`, preserving their FFI-only dependency boundary. +- Changed-crate formatting, test registration, GC store-site inventory, address + classification, GC root-holder audit and its self-test pass. The census pin was + refreshed after reviewing its unchanged non-moving collector window. No + recorded raw-handle/unrooted-local debt ceiling was raised. + +Broader checks were run and compared with pristine main; they are not claimed +as clean full-suite runs: + +| Check | Result on this Windows host | Pristine-main comparison | +|---|---|---| +| Runtime unit suite | 3,440 passed, 1 failed, 4 ignored | Same allocator telemetry failure reproduced independently | +| Full codegen unit suite | 1,457 passed, 3 failed, 1 ignored | Exactly the same tests and counts | +| Standard-library unit suite | 59 passed, then abort | Same young-log assertion at the same test after 59 passes | +| HTTP unit binary | Link failure, 8 missing async/TLS symbols | Same missing symbols | +| better-sqlite3 unit binary | Link failure, 12 missing async/FFI symbols | Same missing symbols | + +The runtime failure is +`emergency_full_trace_is_excluded_from_ordinary_pause_stats` (Windows reports +allocator trimming as `executed`, while the test expects `unsupported`). The +codegen failures are `the_clone_entry_is_shrink_wrapped_frameless`, +`split_native_construction_lowers_precise_roots_before_rs4gc`, and +`split_native_construction_propagates_shadow_backend_to_workers`. The stdlib abort +is `listeners_provider_roots_readable_snapshot_across_array_allocation`, asserting +that `closure.dynamic_props` lacks a young-log entry. + +Five script-lint entries also fail on pristine Windows main: changeset and release +pipeline self-tests, whole-workspace `cargo fmt` (Windows command-length error +206), public benchmark artifact freshness, and the unrooted-local per-file +baseline check. Formatting each changed crate separately passes. The full +conformance corpus was not run locally. Windows exception-handling fixtures use +`PERRY_RS4GC=0` because the default Windows statepoint backend rejects funclet EH +(#7354); the new GC fixture uses the default backend and forced moving collection. diff --git a/benchmarks/array-shift-10060/after.json b/benchmarks/array-shift-10060/after.json new file mode 100644 index 0000000000..0c13abc253 --- /dev/null +++ b/benchmarks/array-shift-10060/after.json @@ -0,0 +1,249 @@ +{ + "revision": "1e79ee9e431c8a3791cac534ecb08b58d53df0c9", + "node": "v26.5.1", + "host": "Windows-11-10.0.26200-SP0", + "cpu": "AMD64 Family 25 Model 116 Stepping 1, AuthenticAMD", + "artifact_sha256": { + "perry": "93a63d8e953ac61cf9a88ea34ecaeb334bce38d48627fe545c0f84138a6f73d0", + "node": "b48b0224081224cda1f49374e2fc63d143041ade51754f0cc6608fe8510ba29e", + "runtime": "55d5307dae1afe575d772013ab6b9575d2461ccb627746be5aa21fa675843816", + "stdlib": "2f203e0e90005ad7b5437b0e639d55636db3b621e5b8506240b9d42b8fa24bd6", + "array-shift-queue.ts": "9bb352c176b20fa1d05365246f0f9161d5d4e103d23dedfb6f4e7ca0ccb012cf", + "array-push.ts": "bacabb7fd2d4d5b0860142b02f1b054dea636fd7dcd6144ca04482747a521e64" + }, + "workloads": { + "array-shift-queue": { + "rows": [ + { + "n": 100, + "node": { + "name": "array-shift-queue", + "category": "arrays", + "n": 100, + "ms_per_run": 0.0018184471315575545, + "runs": 76729, + "checksum": 534569475, + "status": "OK" + }, + "perry": { + "name": "array-shift-queue", + "category": "arrays", + "n": 100, + "ms_per_run": 0.001970571428571307, + "runs": 71052, + "checksum": 534569475, + "status": "OK" + } + }, + { + "n": 1000, + "node": { + "name": "array-shift-queue", + "category": "arrays", + "n": 1000, + "ms_per_run": 0.03647067395264273, + "runs": 3826, + "checksum": 965032923, + "status": "OK" + }, + "perry": { + "name": "array-shift-queue", + "category": "arrays", + "n": 1000, + "ms_per_run": 0.019170498084291523, + "runs": 7303, + "checksum": 965032923, + "status": "OK" + } + }, + { + "n": 10000, + "node": { + "name": "array-shift-queue", + "category": "arrays", + "n": 10000, + "ms_per_run": 0.36318928571428544, + "runs": 388, + "checksum": 663245961, + "status": "OK" + }, + "perry": { + "name": "array-shift-queue", + "category": "arrays", + "n": 10000, + "ms_per_run": 0.19240285714285316, + "runs": 733, + "checksum": 663245961, + "status": "OK" + } + }, + { + "n": 100000, + "node": { + "name": "array-shift-queue", + "category": "arrays", + "n": 100000, + "ms_per_run": 394.9110999999998, + "runs": 7, + "checksum": 987888354, + "status": "OK" + }, + "perry": { + "name": "array-shift-queue", + "category": "arrays", + "n": 100000, + "ms_per_run": 1.9337363636364195, + "runs": 77, + "checksum": 987888354, + "status": "OK" + } + }, + { + "n": 1000000, + "node": { + "status": "TIMEOUT" + }, + "perry": { + "name": "array-shift-queue", + "category": "arrays", + "n": 1000000, + "ms_per_run": 19.30525, + "runs": 14, + "checksum": 755413900, + "status": "OK" + } + } + ], + "common_sizes": [ + 100, + 1000, + 10000, + 100000 + ], + "common_slopes": { + "node": 1.7008585224432438, + "perry": 0.9976993367688158 + } + }, + "array-push": { + "rows": [ + { + "n": 100, + "node": { + "name": "array-push", + "category": "arrays", + "n": 100, + "ms_per_run": 0.0017794306049822746, + "runs": 77124, + "checksum": 397536938, + "status": "OK" + }, + "perry": { + "name": "array-push", + "category": "arrays", + "n": 100, + "ms_per_run": 0.0032718510573109764, + "runs": 47026, + "checksum": 397536938, + "status": "OK" + } + }, + { + "n": 1000, + "node": { + "name": "array-push", + "category": "arrays", + "n": 1000, + "ms_per_run": 0.01914937799043073, + "runs": 7313, + "checksum": 60977410, + "status": "OK" + }, + "perry": { + "name": "array-push", + "category": "arrays", + "n": 1000, + "ms_per_run": 0.023626446280998768, + "runs": 5955, + "checksum": 60977410, + "status": "OK" + } + }, + { + "n": 10000, + "node": { + "name": "array-push", + "category": "arrays", + "n": 10000, + "ms_per_run": 0.19196571428571285, + "runs": 726, + "checksum": 840530610, + "status": "OK" + }, + "perry": { + "name": "array-push", + "category": "arrays", + "n": 10000, + "ms_per_run": 0.2483493827160539, + "runs": 557, + "checksum": 840530610, + "status": "OK" + } + }, + { + "n": 100000, + "node": { + "name": "array-push", + "category": "arrays", + "n": 100000, + "ms_per_run": 1.9199454545454429, + "runs": 75, + "checksum": 810288076, + "status": "OK" + }, + "perry": { + "name": "array-push", + "category": "arrays", + "n": 100000, + "ms_per_run": 2.6332875000000087, + "runs": 54, + "checksum": 810288076, + "status": "OK" + } + }, + { + "n": 1000000, + "node": { + "name": "array-push", + "category": "arrays", + "n": 1000000, + "ms_per_run": 21.401299999999992, + "runs": 8, + "checksum": 863996427, + "status": "OK" + }, + "perry": { + "name": "array-push", + "category": "arrays", + "n": 1000000, + "ms_per_run": 25.561099999999897, + "runs": 7, + "checksum": 863996427, + "status": "OK" + } + } + ], + "common_sizes": [ + 100, + 1000, + 10000, + 100000, + 1000000 + ], + "common_slopes": { + "node": 1.0161452416944852, + "perry": 0.9832671902629404 + } + } + } +} diff --git a/benchmarks/array-shift-10060/before.json b/benchmarks/array-shift-10060/before.json new file mode 100644 index 0000000000..21225ee8a9 --- /dev/null +++ b/benchmarks/array-shift-10060/before.json @@ -0,0 +1,235 @@ +{ + "revision": "603b074ace01464bc66fc07cc8d532f26ccf5a0f", + "node": "v26.5.1", + "host": "Windows-11-10.0.26200-SP0", + "workloads": { + "array-shift-queue": { + "rows": [ + { + "n": 100, + "node": { + "name": "array-shift-queue", + "category": "arrays", + "n": 100, + "ms_per_run": 0.0020512923076925617, + "runs": 68006, + "checksum": 534569475, + "status": "OK" + }, + "perry": { + "name": "array-shift-queue", + "category": "arrays", + "n": 100, + "ms_per_run": 0.01655653973509805, + "runs": 7959, + "checksum": 534569475, + "status": "OK" + } + }, + { + "n": 1000, + "node": { + "name": "array-shift-queue", + "category": "arrays", + "n": 1000, + "ms_per_run": 0.039113085937499736, + "runs": 3499, + "checksum": 965032923, + "status": "OK" + }, + "perry": { + "name": "array-shift-queue", + "category": "arrays", + "n": 1000, + "ms_per_run": 1.3881666666666737, + "runs": 100, + "checksum": 965032923, + "status": "OK" + } + }, + { + "n": 10000, + "node": { + "name": "array-shift-queue", + "category": "arrays", + "n": 10000, + "ms_per_run": 0.3938921568627453, + "runs": 351, + "checksum": 663245961, + "status": "OK" + }, + "perry": { + "name": "array-shift-queue", + "category": "arrays", + "n": 10000, + "ms_per_run": 401.9228000000003, + "runs": 7, + "checksum": 663245961, + "status": "OK" + } + }, + { + "n": 100000, + "node": { + "name": "array-shift-queue", + "category": "arrays", + "n": 100000, + "ms_per_run": 519.9770999999992, + "runs": 7, + "checksum": 987888354, + "status": "OK" + }, + "perry": { + "status": "TIMEOUT" + } + }, + { + "n": 1000000, + "node": { + "status": "TIMEOUT" + }, + "perry": { + "status": "SKIPPED" + } + } + ], + "common_sizes": [ + 100, + 1000, + 10000 + ], + "common_slopes": { + "node": 1.1416748909194272, + "perry": 2.192586533751063 + } + }, + "array-push": { + "rows": [ + { + "n": 100, + "node": { + "name": "array-push", + "category": "arrays", + "n": 100, + "ms_per_run": 0.0023072095974161896, + "runs": 59952, + "checksum": 397536938, + "status": "OK" + }, + "perry": { + "name": "array-push", + "category": "arrays", + "n": 100, + "ms_per_run": 0.004117620419925205, + "runs": 33988, + "checksum": 397536938, + "status": "OK" + } + }, + { + "n": 1000, + "node": { + "name": "array-push", + "category": "arrays", + "n": 1000, + "ms_per_run": 0.02357915194346346, + "runs": 5990, + "checksum": 60977410, + "status": "OK" + }, + "perry": { + "name": "array-push", + "category": "arrays", + "n": 1000, + "ms_per_run": 0.02997994011975865, + "runs": 4575, + "checksum": 60977410, + "status": "OK" + } + }, + { + "n": 10000, + "node": { + "name": "array-push", + "category": "arrays", + "n": 10000, + "ms_per_run": 0.21355106382978487, + "runs": 644, + "checksum": 840530610, + "status": "OK" + }, + "perry": { + "name": "array-push", + "category": "arrays", + "n": 10000, + "ms_per_run": 0.32209682539681644, + "runs": 439, + "checksum": 840530610, + "status": "OK" + } + }, + { + "n": 100000, + "node": { + "name": "array-push", + "category": "arrays", + "n": 100000, + "ms_per_run": 2.1039800000000013, + "runs": 70, + "checksum": 810288076, + "status": "OK" + }, + "perry": { + "name": "array-push", + "category": "arrays", + "n": 100000, + "ms_per_run": 3.4687857142857217, + "runs": 45, + "checksum": 810288076, + "status": "OK" + } + }, + { + "n": 1000000, + "node": { + "name": "array-push", + "category": "arrays", + "n": 1000000, + "ms_per_run": 24.169399999999996, + "runs": 7, + "checksum": 863996427, + "status": "OK" + }, + "perry": { + "name": "array-push", + "category": "arrays", + "n": 1000000, + "ms_per_run": 36.18619999999987, + "runs": 7, + "checksum": 863996427, + "status": "OK" + } + } + ], + "common_sizes": [ + 100, + 1000, + 10000, + 100000, + 1000000 + ], + "common_slopes": { + "node": 0.9990871065499047, + "perry": 0.9951140051289211 + } + } + }, + "cpu": "AMD64 Family 25 Model 116 Stepping 1, AuthenticAMD", + "artifact_sha256": { + "perry": "d860bb96cfa8086b780cfc81ae274c0d191f2d1383164674715f205a6b8f67c4", + "node": "b48b0224081224cda1f49374e2fc63d143041ade51754f0cc6608fe8510ba29e", + "runtime": "e0c45bbccd7fa9f4dfced0b421f72beb110fd57e81a3e2c2134cf479664f40f5", + "stdlib": "ffaa589e113a24adecf57d3de74747690f4206bd21069c7550071347168814e4", + "array-shift-queue.ts": "9bb352c176b20fa1d05365246f0f9161d5d4e103d23dedfb6f4e7ca0ccb012cf" + } +} diff --git a/benchmarks/array-shift-10060/checksum-reference.json b/benchmarks/array-shift-10060/checksum-reference.json new file mode 100644 index 0000000000..c45ac4654b --- /dev/null +++ b/benchmarks/array-shift-10060/checksum-reference.json @@ -0,0 +1,7 @@ +{ + "node": "v26.5.1", + "n": 1000000, + "checksum": 755413900, + "method": "Untimed direct traversal of the same xorshift32 sequence, h starts at zero. This is an independent correctness reference, not a completed Node shift benchmark.", + "javascript": "let seed=0x12345678,h=0; for(let i=0;i<1000000;i++){seed ^= seed << 13; seed ^= seed >>> 17; seed ^= seed << 5;const v=Math.floor((seed >>> 0)/4294967296*1000000); h=(h*31+v)%1000000007;}console.log(h);" +} diff --git a/benchmarks/array-shift-10060/comparison.json b/benchmarks/array-shift-10060/comparison.json new file mode 100644 index 0000000000..a6779b0dab --- /dev/null +++ b/benchmarks/array-shift-10060/comparison.json @@ -0,0 +1,67 @@ +{ + "array-shift-queue": { + "common_completed_sizes_before_and_after": [ + 100, + 1000, + 10000 + ], + "slopes_common": { + "before": { + "node": 1.1416748909194272, + "perry": 2.192586533751063 + }, + "after": { + "node": 1.1502161744764081, + "perry": 0.994809667647383 + } + }, + "after_completed_slopes": { + "node": 1.7008585224432438, + "perry": 0.9985930364176875 + }, + "after_common_sizes": [ + 100, + 1000, + 10000, + 100000 + ], + "after_common_slopes": { + "node": 1.7008585224432438, + "perry": 0.9976993367688158 + } + }, + "array-push": { + "common_completed_sizes_before_and_after": [ + 100, + 1000, + 10000, + 100000, + 1000000 + ], + "slopes_common": { + "before": { + "node": 0.9990871065499047, + "perry": 0.9951140051289211 + }, + "after": { + "node": 1.0161452416944852, + "perry": 0.9832671902629404 + } + }, + "after_completed_slopes": { + "node": 1.0161452416944852, + "perry": 0.9832671902629404 + }, + "after_common_sizes": [ + 100, + 1000, + 10000, + 100000, + 1000000 + ], + "after_common_slopes": { + "node": 1.0161452416944852, + "perry": 0.9832671902629404 + } + } +} From c14f70007ac175392369c7b133fae68558cd366d Mon Sep 17 00:00:00 2001 From: Ralph Kuepper Date: Fri, 11 Sep 2026 15:40:58 +0200 Subject: [PATCH 7/9] docs: name array shift changeset for PR 10077 (cherry picked from commit 176e5ab8615f66ba1992372917ec1ee4af1efd14) --- .../{10060-array-shift-queue.md => 10077-array-shift-queue.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{10060-array-shift-queue.md => 10077-array-shift-queue.md} (100%) diff --git a/changelog.d/10060-array-shift-queue.md b/changelog.d/10077-array-shift-queue.md similarity index 100% rename from changelog.d/10060-array-shift-queue.md rename to changelog.d/10077-array-shift-queue.md From 300f0e21d1f14c60c480e11a86a1b25170a023d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 12 Sep 2026 07:45:54 +0200 Subject: [PATCH 8/9] fix(runtime): make the JSON cached read honour the array front offset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #10077 gives a dense array a queue front offset: `capacity` counts the slots remaining AFTER the front, so logical element zero is no longer the end of `ArrayHeader`. That PR converted every open-coded `header + size_of::()` element base in its own tree β€” but two places on current `main` postdate its branch point and were never converted: - `json_tape/cached_read.rs` (from #10064, landed in train161) open-codes the base on the hot materialized-array read. A materialized JSON array is an ordinary `GC_TYPE_ARRAY` that user code can `shift()`, so after this train that read would return the wrong element. Routed through `crate::array::array_elements_ptr`. - `array/sort.rs` gained four more open-coded bases with the `publish_sorted_values` / `apply_sorted_indices` refactor. Converted with the conflict resolution in the pick itself; only the rustfmt reflow lands here. Also converts the six raw-handle debt sites #10075's two new test modules introduced. `--no-raise-vs ` refuses both a ceiling on a module that was absent at the base (`string/slice_tests.rs`) and a per-module raise (`gc/tests/runtime_roots/string_slice.rs`, 1 -> 3), so the sites are converted rather than recorded: - `gc/tests/runtime_roots/string_slice.rs`: the two post-collection re-reads now run the collection inside `across_mut` / `across_const`, and the kept-slice assertions inside `with_const_ptr`. That also retires the module's one pre-existing site, so its ceiling line is DELETED β€” the deletion is the receipt β€” and the recorded total falls 945 -> 944. - `string/slice_tests.rs`: the three argument-position reads become `with_const_ptr`, which is the documented shape for a self-rooting entry point such as `js_string_slice`. --- crates/perry-runtime/src/array/sort.rs | 3 +-- .../src/gc/tests/runtime_roots/string_slice.rs | 17 ++++++++++------- .../perry-runtime/src/json_tape/cached_read.rs | 5 +++-- crates/perry-runtime/src/string/slice_tests.rs | 12 +++--------- scripts/raw_handle_debt_baseline.txt | 2 +- scripts/raw_handle_debt_files.txt | 1 - 6 files changed, 18 insertions(+), 22 deletions(-) diff --git a/crates/perry-runtime/src/array/sort.rs b/crates/perry-runtime/src/array/sort.rs index c4293bc7a9..13c033196b 100644 --- a/crates/perry-runtime/src/array/sort.rs +++ b/crates/perry-runtime/src/array/sort.rs @@ -191,8 +191,7 @@ unsafe fn sort_permutation( // no interior pointer or copied value survives a callback. let arr = roots.get(0) as *const ArrayHeader; let comparator = roots.get(1) as *const ClosureHeader; - let elements = - crate::array::array_elements_ptr(arr) as *const f64; + let elements = crate::array::array_elements_ptr(arr) as *const f64; c.less_equal_at( comparator, *elements.add(a as usize), diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/string_slice.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/string_slice.rs index dc668c301d..01fb5eb37e 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/string_slice.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/string_slice.rs @@ -21,8 +21,9 @@ fn suffix_cursor_offsets_survive_source_evacuation_and_split_slice_owns_its_byte } let kept = crate::string::js_string_slice(source, 3, 5); let kept_root = scope.root_string_ptr(kept); - gc_collect_minor(); - let source = root.get_raw_mut_ptr::(); + // The collection is what the source must survive, so take its address from + // the rooted slot after the call rather than before it. + let (_, source) = root.across_mut::(|| gc_collect_minor()); assert_ne!( source as usize, before, "the test must actually move the source" @@ -39,9 +40,10 @@ fn suffix_cursor_offsets_survive_source_evacuation_and_split_slice_owns_its_byte 214.0 ); } - let kept = kept_root.get_raw_const_ptr::(); - assert_eq!(crate::string::js_string_char_code_at(kept, 0), 56832.0); - assert_eq!(crate::string::js_string_char_code_at(kept, 1), 214.0); + kept_root.with_const_ptr(|kept: *const crate::StringHeader| { + assert_eq!(crate::string::js_string_char_code_at(kept, 0), 56832.0); + assert_eq!(crate::string::js_string_char_code_at(kept, 1), 214.0); + }); } /// #5062: `String.prototype.slice` copies the selected range out of the source @@ -85,8 +87,9 @@ fn test_transient_runtime_handle_string_slice_gc() { let result_scope = RuntimeHandleScope::new(); let result_root = result_scope.root_string_ptr(result); - drain_scheduled_minor_gc(before, "slice destination allocation"); - let result = result_root.get_raw_const_ptr::(); + let (_, result) = result_root.across_const::(|| { + drain_scheduled_minor_gc(before, "slice destination allocation") + }); unsafe { assert_eq!((*result).byte_len, SLICE_LEN as u32); diff --git a/crates/perry-runtime/src/json_tape/cached_read.rs b/crates/perry-runtime/src/json_tape/cached_read.rs index 1e200edc95..e1e41db2b9 100644 --- a/crates/perry-runtime/src/json_tape/cached_read.rs +++ b/crates/perry-runtime/src/json_tape/cached_read.rs @@ -60,8 +60,9 @@ pub unsafe fn lazy_get(hdr: *mut LazyArrayHeader, i: u32) -> JSValue { && i < (*arr).length && i < (*arr).capacity { - let elements = (arr as *const u8).add(std::mem::size_of::()) - as *const u64; + // #10077: a dense queue keeps its live elements in a suffix of the + // allocation, so logical element zero is not the header's end. + let elements = crate::array::array_elements_ptr(arr) as *const u64; let bits = *elements.add(i as usize); // Holes must still consult prototypes; sparse and out-of-bounds // reads can do the same. Those paths may invoke a getter. diff --git a/crates/perry-runtime/src/string/slice_tests.rs b/crates/perry-runtime/src/string/slice_tests.rs index 24ec857593..7207b34618 100644 --- a/crates/perry-runtime/src/string/slice_tests.rs +++ b/crates/perry-runtime/src/string/slice_tests.rs @@ -28,7 +28,7 @@ fn slice_utf16_bounds_and_lone_surrogates() { let normalize = |n: i32| if n < 0 { (len + n).max(0) } else { n.min(len) }; let a = normalize(start) as usize; let b = normalize(end) as usize; - let result = js_string_slice(root.get_raw_const_ptr(), start, end); + let result = root.with_const_ptr(|s| js_string_slice(s, start, end)); assert_eq!( units(result), expected[a..b.max(a)], @@ -47,17 +47,11 @@ fn slice_utf16_bounds_and_lone_surrogates() { ); let a = start.clamp(0, len) as usize; let b = end.clamp(0, len) as usize; - let result = js_string_substring(root.get_raw_const_ptr(), start, end); + let result = root.with_const_ptr(|s| js_string_substring(s, start, end)); assert_eq!(units(result), expected[a.min(b)..a.max(b)]); } } - assert!( - units(js_string_slice( - root.get_raw_const_ptr(), - i32::MIN, - i32::MAX - )) == expected - ); + assert!(units(root.with_const_ptr(|s| js_string_slice(s, i32::MIN, i32::MAX))) == expected); } } diff --git a/scripts/raw_handle_debt_baseline.txt b/scripts/raw_handle_debt_baseline.txt index 2ebd3a0c97..175df711d1 100644 --- a/scripts/raw_handle_debt_baseline.txt +++ b/scripts/raw_handle_debt_baseline.txt @@ -1 +1 @@ -945 +944 diff --git a/scripts/raw_handle_debt_files.txt b/scripts/raw_handle_debt_files.txt index 2f5e412399..b299d25523 100644 --- a/scripts/raw_handle_debt_files.txt +++ b/scripts/raw_handle_debt_files.txt @@ -71,7 +71,6 @@ 47 crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs 4 crates/perry-runtime/src/gc/tests/runtime_roots/fs_options_object.rs 3 crates/perry-runtime/src/gc/tests/runtime_roots/hook_dispatch_handles.rs -1 crates/perry-runtime/src/gc/tests/runtime_roots/string_slice.rs 10 crates/perry-runtime/src/gc/tests/runtime_roots/transient_handles.rs 11 crates/perry-runtime/src/json/replacer.rs 26 crates/perry-runtime/src/json/reviver.rs From 2aa97f529d45ab7041f5f3fc58f03e9ce45091f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 12 Sep 2026 07:45:54 +0200 Subject: [PATCH 9/9] chore: bump workspace version to 0.5.1537 Train163 (#10075, #10077) lands on main at 0.5.1536; neither PR bumped the version, which is the maintainer's job at merge time. Cargo.lock regenerated so every workspace member's inherited version moves with it. --- CLAUDE.md | 2 +- Cargo.lock | 158 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 3 files changed, 81 insertions(+), 81 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 66023a8565..c82586ea28 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1536 +**Current Version:** 0.5.1537 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index ea80236d42..9dc8e400fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5695,7 +5695,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "anyhow", "base64 0.22.1", @@ -5759,7 +5759,7 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "perry-dispatch", "serde", @@ -5767,7 +5767,7 @@ dependencies = [ [[package]] name = "perry-audio-miniaudio" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "cc", "libc", @@ -5776,7 +5776,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "aho-corasick", "anyhow", @@ -5794,7 +5794,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "anyhow", "perry-hir", @@ -5802,7 +5802,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "anyhow", "perry-hir", @@ -5810,7 +5810,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "anyhow", "perry-dispatch", @@ -5819,7 +5819,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "anyhow", "perry-hir", @@ -5827,7 +5827,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "anyhow", "base64 0.22.1", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "anyhow", "perry-hir", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "anyhow", "async-trait", @@ -5876,14 +5876,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "serde", "serde_json", @@ -5891,7 +5891,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1536" +version = "0.5.1537" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5902,7 +5902,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "anyhow", "clap", @@ -5917,7 +5917,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "block2", "objc2", @@ -5927,7 +5927,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "argon2", "perry-ffi", @@ -5936,7 +5936,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "perry-ffi", "reqwest", @@ -5945,7 +5945,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "bcrypt", "perry-ffi", @@ -5953,7 +5953,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "perry-ffi", "rusqlite", @@ -5961,7 +5961,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "perry-ffi", "scraper", @@ -5969,7 +5969,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "perry-ffi", "perry-runtime", @@ -5977,7 +5977,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "chrono", "cron", @@ -5987,7 +5987,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "chrono", "perry-ffi", @@ -5995,7 +5995,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "perry-ffi", "rust_decimal", @@ -6003,7 +6003,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "perry-ffi", "serde_json", @@ -6011,7 +6011,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "perry-ffi", "rand 0.10.2", @@ -6019,7 +6019,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "perry-ffi", "perry-runtime", @@ -6027,14 +6027,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "bytes", "http-body-util", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "bytes", "lazy_static", @@ -6065,7 +6065,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "base64 0.22.1", "bytes", @@ -6097,7 +6097,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "lazy_static", "perry-ffi", @@ -6107,7 +6107,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "base64 0.22.1", "jsonwebtoken", @@ -6118,7 +6118,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "lru", "perry-ffi", @@ -6127,7 +6127,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "chrono", "perry-ffi", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "bson", "futures-util", @@ -6147,7 +6147,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "chrono", "perry-ffi", @@ -6159,7 +6159,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "nanoid", "perry-ffi", @@ -6168,7 +6168,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "bytes", "perry-ffi", @@ -6183,7 +6183,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "const-oid 0.10.2", "der 0.8.1", @@ -6202,7 +6202,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "lettre", "perry-ffi", @@ -6212,7 +6212,7 @@ dependencies = [ [[package]] name = "perry-ext-parcel-watcher" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "fancy-regex", "notify", @@ -6224,7 +6224,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "perry-ffi", "printpdf", @@ -6232,7 +6232,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "perry-ffi", "sqlx", @@ -6241,7 +6241,7 @@ dependencies = [ [[package]] name = "perry-ext-qs" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "perry-ffi", "perry-runtime", @@ -6250,7 +6250,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "governor", "perry-ffi", @@ -6258,7 +6258,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "fast_image_resize", "image", @@ -6269,7 +6269,7 @@ dependencies = [ [[package]] name = "perry-ext-streams" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "lazy_static", "perry-ffi", @@ -6278,7 +6278,7 @@ dependencies = [ [[package]] name = "perry-ext-typescript" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "anyhow", "perry-ffi", @@ -6298,7 +6298,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "perry-ffi", "perry-runtime", @@ -6307,7 +6307,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "perry-ffi", "uuid", @@ -6315,7 +6315,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "perry-ffi", "regex", @@ -6325,7 +6325,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "futures-util", "lazy_static", @@ -6338,7 +6338,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "brotli", "flate2", @@ -6348,7 +6348,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "dashmap 6.2.1", "once_cell", @@ -6358,7 +6358,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "anyhow", "perry-api-manifest", @@ -6377,11 +6377,11 @@ dependencies = [ [[package]] name = "perry-native-registration" -version = "0.5.1536" +version = "0.5.1537" [[package]] name = "perry-parser" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "anyhow", "perry-diagnostics", @@ -6394,7 +6394,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "ahash", "anyhow", @@ -6457,14 +6457,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6560,14 +6560,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "anyhow", "perry-hir", @@ -6576,7 +6576,7 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "perry-ffi", "perry-ui-model", @@ -6584,7 +6584,7 @@ dependencies = [ [[package]] name = "perry-ui-android" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "base64 0.22.1", "itoa", @@ -6602,7 +6602,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "rand 0.10.2", "serde", @@ -6612,7 +6612,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "base64 0.22.1", "cairo-rs 0.22.9", @@ -6635,7 +6635,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "base64 0.22.1", "block2", @@ -6652,7 +6652,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "base64 0.22.1", "block2", @@ -6669,7 +6669,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1536" +version = "0.5.1537" [[package]] name = "perry-ui-test" @@ -6680,11 +6680,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1536" +version = "0.5.1537" [[package]] name = "perry-ui-tvos" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "base64 0.22.1", "block2", @@ -6701,7 +6701,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "base64 0.22.1", "block2", @@ -6718,7 +6718,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "block2", "libc", @@ -6732,7 +6732,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "base64 0.22.1", "libc", @@ -6751,7 +6751,7 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "base64 0.22.1", "libc", @@ -6764,7 +6764,7 @@ dependencies = [ [[package]] name = "perry-updater" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "anyhow", "base64 0.22.1", @@ -6780,7 +6780,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1536" +version = "0.5.1537" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index d5bff15a5c..815e7989cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -336,7 +336,7 @@ codegen-units = 1 codegen-units = 1 [workspace.package] -version = "0.5.1536" +version = "0.5.1537" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry"