diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs deleted file mode 100644 index 636554df98..0000000000 --- a/.git-blame-ignore-revs +++ /dev/null @@ -1,5 +0,0 @@ -# Formatting commits that should be skipped by `git blame`. -# Local setup: git config blame.ignoreRevsFile .git-blame-ignore-revs -# -# cargo fmt --all baseline (issue #245 PR-A) -# SHA will be filled in after the commit lands on main. diff --git a/.gitignore b/.gitignore index 8fa9f8b40d..db73367ad2 100644 --- a/.gitignore +++ b/.gitignore @@ -218,3 +218,13 @@ test-files/.perry-cache/ /tiny_test /wasm_test /t1 +/clusterAg +/isoA +/m +/m5 +/tests/modules/main +# generated outputs (perry --target wasm / android builds / i18n emit) +/wasm_ui_demo.html +/examples/wasm_ui_demo.html +android-build/app/src/main/jniLibs/ +/res/ diff --git a/NM_DEVIRT_PLAN.md b/NM_DEVIRT_PLAN.md deleted file mode 100644 index 522e0062e7..0000000000 --- a/NM_DEVIRT_PLAN.md +++ /dev/null @@ -1,109 +0,0 @@ -# Native-module method-dispatch devirtualization (feat/nm-method-devirt) - -Goal: let `-dead_strip` remove native-module handler code (cluster/child_process/ -dns/tls/vm/repl/inspector/perf_hooks/sqlite/dgram/…) from binaries that don't -import those modules. Today one monolithic `dispatch_native_module_method` -(437→592 arms, `native_module_dispatch.rs`) statically names every handler, so any -program creating one native namespace pins all of them. Measured ceiling for -hello-world: ~213KB (method dispatch only; constructor dispatcher in -class_registry.rs is a SEPARATE later phase). - -## Validated partition -592 arms → 37 buckets, 0 unmapped, 0 unbalanced (brace-balanced boundaries). -Buckets: assert async_hooks bigint buffer child_process cluster console crypto -dgram dns domain events fs http https inspector module net os path perf process -punycode querystring readline repl sea sqlite stream timers tls tty url util v8 -vm wasi zlib. Sub-namespace tags map to bucket: crypto.subtle/webcrypto/Certificate→crypto, -path.posix/win32→path, util.types/util/types→util, dns/promises→dns, -assert/strict & assert.instance→assert, inspector/promises & inspector.Network→inspector, -punycode.default→punycode, perf_hooks/perf_observer*/perf_histogram→perf, -v8.Serializer/Deserializer/GCProfiler/promiseHooks/startupSnapshot→v8. - -## Design (minimal disruption — vtable struct + hook UNCHANGED) -- `NmCtx { obj, args_ptr, args_len, assert_skip_prototype }` + `nm_general_closures!` - macro (general closures only: arg,i32_arg,str_to_f64,bool_to_f64,bool_tag,ptr_addr, - optional_ptr_addr,arg_bits,pack_args,pack_args_from,ptr_to_f64,typed_kind,_arg_event_ptr, - _arg_closure_ptr). Path closures (require_path_str_ptr,optional_path_str_ptr, - path_join/resolve/basename_value) inline ONLY in nm_dispatch_path. -- `dispatch_native_module_method(obj,method,args,len)` becomes a THIN ROUTER: extract - field0 name + existing normalization (current lines 128-165) → build NmCtx → - `nm_dispatch_registry_lookup(canonical) -> Optionf64>` → - call, else undefined. Still pointed at by the shared vtable.dispatch (955) and - native_arena.rs:474 — both unchanged. -- 37 `nm_dispatch_(ctx,module,method)->f64`: `let NmCtx{obj,args_ptr,args_len, - assert_skip_prototype}=*ctx; nm_general_closures!(); match (module,method){ _=>undefined }`. -- Registry (native_module_registry.rs): bucket id enum + `NM_DISPATCH_REGISTRY: - [AtomicPtr; 37]` (null init) + `nm_module_index(name)->Option` (string match, - NO fn refs) + `#[no_mangle] js_nm_install_()` storing `nm_dispatch_ as ptr` - (SOLE static ref to each bucket fn) + `js_nm_install_all()` (dynamic-require fallback). -- Codegen: at each `js_create_native_module_namespace` site (8 sites, main - static_field_meta.rs:572) ALSO emit `js_nm_install_()` for the static name, or - `js_nm_install_all()` if the module name is dynamic/unanalyzable. Runtime-internal - creators (node_v8, perf_hooks) call their `js_nm_install_v8/perf()`. -- Completeness invariant: every namespace-create site (compile-time name) emits the - matching install BEFORE any method dispatch on it → registry never misses → never - silently returns undefined. Dynamic name → install_all (correct, larger). - -## Why correct (vs cfg-gating): precise linker reachability through real edges, -sound graceful degradation (install_all), semantics never change with a build flag. - -## Status -[x] worktree off origin/main (5258a6073) -[x] partition validated (592 arms → 37 active buckets, https dropped = 0-arm) -[x] generated: NmCtx + nm_general_closures! macro + thin router + 37 nm_dispatch_ fns (native_module_dispatch.rs) -[x] registry: NmBucket + NM_DISPATCH_REGISTRY + nm_module_index + 37 js_nm_install_() + js_nm_install_all() (native_module_registry.rs) -[x] **perry-runtime compiles GREEN** (cargo build -p perry-runtime, 0 errors) -[x] CODEGEN: emit js_nm_install_() at all 5 js_create_native_module_namespace sites (nm_install.rs nm_install_symbol; externs declared in runtime_decls/objects.rs). perry builds green. -[x] CORRECTNESS verified byte-identical to node: hello-world, import os, import path, global process (cwd/pid/argv), util.format/inspect/types, querystring, assert. -[x] **MEASURED: hello-world __text 4,667,824 → 4,058,936 = −608,888 B (−13%); binary 5.4MB → 4.7MB.** (baseline = pristine origin/main perry.) -[x] FOLLOW-UP #1 DONE — dynamic getBuiltinModule/require fallback via indirect install-all hook: - - native_module_get_builtin_module_value → nm_run_install_all_hook() (opaque ptr, names no bucket) - - js_nm_enable_install_all() (black_box'd, sole ref to js_nm_install_all) armed by js_process_get_builtin_module_devirt (codegen getBuiltinModule table target) - - black_box REQUIRED: else whole-program opt devirtualizes the single-ptr indirect call → re-pins all (per-bucket array is immune, runtime-indexed). - - Verified: getBuiltinModule(dynamic+literal), require(literal), global process, static import — all byte-identical to node; hello-world __text 4,058,968 (install_all absent). RESIDUAL EDGE: require(runtimeVar) of builtin (module_require.rs:121, not armed) — narrow, likely deferred anyway. -[x] PHASE 2 DONE (commit 8406fafea) — node-module-namespaced constructor devirt: - - 8 direct-call ctor blocks (tty/fs/vm/tls/wasi/readline/repl/stream) in js_new_function_construct - → per-module nm_ctor_ fns (class_registry.rs) routed via NM_CTOR_REGISTRY, registered by the - SAME js_nm_install_() (no new codegen). Globals (URL/WeakSet/Error/TypedArray) stay inline; - http/events/zlib/sqlite already dynamic-dispatch. - - Measured: hello-world __text 4,058,968 → 3,971,252 (repl fully stripped). TOTAL from baseline - 4,667,824 → 3,971,252 = −696,572 (−14.9%); binary 5.4MB → ~4.6MB. - - Correct: new stream.Readable/Writable/Transform, global new URL/TextEncoder/WeakSet/Error/Uint8Array, - + all 6 phase-1 cases. -[ ] PHASE 3 (diminishing returns) — residual node_stream/tls/child_process/cluster pinned by INTRA-subsystem - refs (js_node_stream_from_web→readable_new) + method-dispatch internals that construct streams. Would - need devirtualizing those internal paths too. - -## Generators (in /tmp, re-runnable from git HEAD) -/tmp/nm_generate.py (dispatch file), /tmp/nm_gen_registry.py (registry). Both read -pristine source via `git show HEAD:...` so re-running is idempotent. - -## Phase 3 (submodule devirt) — DONE (commit 9e37185ce) -SUBMODULES static table → per-submodule statics + SUBMOD_REGISTRY; find_submodule via -registry; js_node_submod_install_() emitted at all 6 codegen submodule-resolution -sites; black_box'd install-all hook for dynamic require/getBuiltinModule. hello-world -__text 3,971,424 → 3,716,980 (−254KB). CUMULATIVE baseline 4,667,824 → 3,716,980 = -−950,844 (−20.4%), ~5.4MB → ~4.3MB. 9/9 correctness sweep + fs/promises (named import -and fs.promises via native) byte-identical to node. - -## console.trace — DONE (commit a5c14dbbf) -Coarse `at ` frame instead of std::backtrace::force_capture (consistent with -Error.stack; prereq for any future panic-symbolizer strip). No size change alone (the -143KB gimli is pulled by std's panic runtime, not console.trace). - -## Panic-symbolizer strip (~220KB) — ATTEMPTED, REVERTED (toolchain-fragile) -build-std + panic_immediate_abort to drop std's default panic hook + DWARF symbolizer. -Blockers found on current nightly: - 1. panic_immediate_abort is now a real STRATEGY: needs `-Cpanic=immediate-abort` - (+ -Zunstable-options) + -Zbuild-std, NOT the old `-Zbuild-std-features=panic_immediate_abort`. - 2. Native build (host==target) → host build-scripts/proc-macros use the PRECOMPILED host - core (default panic), but the rustflag forces immediate-abort on them → "core compiled - with incompatible panic strategy" (proc-macro2 build script fails). - 3. Fix requires explicit `--target ` to separate host (precompiled) from - target (build-std immediate-abort) — which then breaks the auto-opt output-path - resolution (libs move to target//release/). 3 fragile, nightly-version-specific - pieces → defer to a focused effort with proper --target + path handling. -The PERRY_MIN_SIZE=1 opt-in wiring was reverted (kept the tree clean). console.trace prereq -stays. Other no-tradeoff levers remain: json (47KB, event-loop pump), js_native_call_method -monolith (34KB devirt), feature-gating url/intl/bigint (160KB, other branch's mechanism). diff --git a/PERF_RUN_LOG.md b/PERF_RUN_LOG.md deleted file mode 100644 index e395ec7481..0000000000 --- a/PERF_RUN_LOG.md +++ /dev/null @@ -1,46 +0,0 @@ -# Perry Performance Run Log - -## 2026-06-17 - Typed feedback registration hoist - -- Start revision: `e816fc3e4af1` -- Branch: `codex/perry-performance-20260617` -- Worker assignment: single Codex pass in this worktree -- Benchmark environment: Linux `/usr/bin/time -v`; local `node` cannot execute `.ts` benchmark inputs, so Node columns and correctness comparisons were skipped by the harness -- Baseline commands: - - `cargo build --release` - - `./benchmarks/quick.sh` - - `./benchmarks/compare.sh --quick --runs 3 --warn-only --json-out /tmp/perry-baseline-e816fc3e4.json` -- Baseline results: - - quick: fibonacci 260ms/18MB, math_intensive 73ms/18MB, nested_loops 3508ms/17MB, factorial 95ms/18MB, matrix_multiply 6462ms/27MB - - compare quick medians: loop_overhead 74ms/18772KB, fibonacci 262ms/18696KB, math_intensive 70ms/18696KB, nested_loops 3383ms/17724KB, factorial 96ms/18836KB -- Selected gap and evidence: - - `nested_loops` dominated the quick compare set at 3383ms; `matrix_multiply` was the slowest `quick.sh` case at 6462ms. - - LLVM trace for `benchmarks/suite/10_nested_loops.ts` showed `js_typed_feedback_register_site(...)` emitted inside the hot `for.body.21` inner loop before each typed-feedback array guard. -- Change: - - Added `LlFunction::entry_setup_call_void` and changed typed-feedback site registration to emit once in function-entry setup instead of at every guard use site. - - Kept guard, fallback, pass, and counter calls at original use sites so runtime evidence semantics remain per-use. - - Updated benchmark harnesses to support Linux RSS measurement and skip Node `.ts` columns when the installed Node cannot run TypeScript directly. -- Post-change benchmark commands: - - `cargo build --release` - - `./benchmarks/compare.sh --quick --runs 3 --warn-only --json-out /tmp/perry-final-e816fc3e4.json` - - `./benchmarks/quick.sh` -- Post-change results: - - compare quick medians: loop_overhead 74ms/18768KB, fibonacci 261ms/18920KB, math_intensive 69ms/18944KB, nested_loops 956ms/19152KB, factorial 94ms/18896KB - - quick: fibonacci 262ms/18MB, math_intensive 55ms/18MB, nested_loops 965ms/18MB, factorial 75ms/18MB, matrix_multiply 1842ms/28MB -- Measured impact: - - `10_nested_loops` compare median: 3383ms -> 956ms, 71.7% faster - - `16_matrix_multiply` quick: 6462ms -> 1842ms, 71.5% faster -- Verification: - - `bash -n benchmarks/quick.sh` - - `bash -n benchmarks/compare.sh` - - `cargo fmt --check` - - `cargo test -p perry-codegen --test typed_feedback` - - `PERRY_BIN=target/release/perry python3 tests/test_typed_feedback_runtime_evidence.py` - - `tests/test_benchmark_output_verifier.sh` - - `target/release/perry compile --no-cache benchmarks/suite/10_nested_loops.ts -o /tmp/perry-nested-loops-final --trace llvm --quiet`; trace confirmed registration calls in entry setup only and no registration calls in `for.body.21` - - `/tmp/perry-nested-loops-final` produced `nested_loops:963` and `sum:26991000000` - - `target/release/perry compile --no-cache benchmarks/suite/16_matrix_multiply.ts -o /tmp/perry-matrix-multiply-final --quiet && /tmp/perry-matrix-multiply-final` produced `matrix_multiply:1778` and `checksum:41079519680` -- Notes: - - `benchmarks/baseline.json` is stale for this Linux environment; compare was run with `--warn-only` and the before/after comparison above uses the captured local baseline JSON. - - Follow-up candidates remain in typed array and numeric array hot paths, but this cycle stopped at the isolated registration-hoist optimization. -- PR: https://github.com/PerryTS/perry/pull/5295 diff --git a/W6_ISSUE_DRAFT.md b/W6_ISSUE_DRAFT.md deleted file mode 100644 index 78b7536624..0000000000 --- a/W6_ISSUE_DRAFT.md +++ /dev/null @@ -1,105 +0,0 @@ -# Lazy default-import of a cjs-wrapped module binds to a named class export instead of `export default` (scale-emergent) - -## Summary -In a large compiled bundle, a `require()` **inside a function** of a CommonJS-wrapped -module binds the import local to the module's **named class export** instead of its -**`export default`** (`module.exports`) object. The metadata is correct; only the -final codegen symbol binding is wrong, and only at giant-module scale. - -## Concrete instance (Next.js app-router render → HTTP 500) -- Module: `next/dist/server/lib/incremental-cache/shared-cache-controls.external.js` -- Source exports (CJS): `Object.defineProperty(exports, "SharedCacheControls", { get })` + a top-level `class SharedCacheControls`. -- `cjs_wrap` output (correct): hoists the class, emits both `export default _cjs;` and `export { SharedCacheControls };`. -- `PERRY_DUMP_EXPORTS` (recorded metadata, correct): - - `Named { local: "default", exported: "default" }` (→ `_cjs`, the exports object) - - `Named { local: "SharedCacheControls", exported: "SharedCacheControls" }` (the class) -- Importer `app-page-turbo.runtime.prod.js` does `const uw = require(".../shared-cache-controls.external.js")` **inside `IncrementalCache.getIncrementalCache`**, recorded as: - - `Default { local: "_lazyreq_26" }`, `is_adopted_require = true` (a lazy default import) -- Runtime: `typeof uw === "function"` and `uw.SharedCacheControls === undefined` → `new uw.SharedCacheControls(...)` throws **`TypeError: undefined is not a constructor`** in `IncrementalCache`'s constructor → app-router render returns HTTP 500. - -So the lazy default import `_lazyreq_26` binds to the **class** symbol instead of the -`"default"` (`_cjs`) symbol. - -## What's ruled out -- `cjs_wrap` output — correct (`export default _cjs` present). -- Runtime `module.exports` — correct (`typeof module.exports === "object"`, `.SharedCacheControls` a function, `exports === module.exports`). -- `reachability.rs` — tree-shaking only; `shared-cache-controls` is a non-barrel → module-granularity (whole module kept). -- Default-export-name collision — `__default` symbols are per-origin (`perry_fn___default`). - -## Not minimally reproducible (~13 shapes all bind correctly) -relative `.js`; node_modules pkg; `.external.js` suffix; `compilePackages` NativeCompiled; -full-subpath require; within-package sibling require; circular require; -`exports.X = X`; `module.exports.X`; getter + static-field exact class shape; -dual-importer (named + namespace); **lazy require inside a function**; -**multi-module (5) lazy default imports**. Every one returns `_cjs`/binds correctly. -The defect appears only inside the real ~600KB `app-page-turbo` module. - -## Likely area -Codegen default-import → export-symbol resolution (`import_function_prefixes` / -`perry_fn___default`). Hypotheses: the `__default` symbol for an -`export default ` (the IIFE result `_cjs`) is not emitted / not -reachable at scale, so the default import falls back to the module's other (named -class) export; or a scale-only symbol-resolution path differs. - -## Repro env -`/tmp/perry-nextjs-demo` (Next 16 standalone, `output: 'standalone'`), compiled with -`PERRY_LL_O0_THRESHOLD_BYTES=536870912 PERRY_ALLOW_PERRY_FEATURES=1 PERRY_ALLOW_EVAL=1 PERRY_ALLOW_UNIMPLEMENTED=1`. -Diagnostic: `PERRY_DUMP_EXPORTS` dump added to `bootstrap.rs enforce_package_default_exports`. - -## Context -This is the 6th wall in the Next.js app-router bring-up; walls 1–5 fixed on -`feat/nextjs-wall-46` (incl. `9970fbbe7` 0-arg class-object resolve, `af8c832b0` -readFileSync ENOENT, `6c41417ff` anon-class-expression capture). With W6 fixed the -render should advance past `IncrementalCache` construction. - ---- -## DEEP UPDATE (corrected root via runtime probes) -Earlier "binds to the class" was WRONG. Confirmed via ~10 probe cycles: -- Importer: `_lazyreq_26` is in `imported_vars`, NOT in `class_ids` → correctly reaches the getter path (dyn_extern_i18n.rs:594/625), calls `perry_fn___default`. -- Exporter: that getter IS emitted (`emit_getter=true`, `is_function_alias=false`), loads `@perry_global___55`. -- HIR: `export default _cjs` → `LocalGet(0)`; local 0 = `_cjs`, init = `Call` (the IIFE call result — correct). -- Module scope: at shared-cache-controls's OWN scope, `module.exports` (= `_cjs`) is `typeof object` (PERRY_SCC probe). -- Cross-module runtime (W6X at `new uw.SharedCacheControls`): `uw` = an UNNAMED CLOSURE (`typeof function`, `name===""`, no keys) — NOT the class, NOT the object. - -So: `perry_global___55` (the `"default"` global) holds a **closure** at runtime, even though `_cjs` is the exports **object** at its own scope. The cross-module `"default"` transfer (the module-init `perry_global__55 = LocalGet(0)` assignment, or the IIFE-result local read) **mistypes the object as a closure**, ONLY at giant-bundle scale (~14 minimal repros — incl. lazy-require, deferred, -O3 auto-optimize, exact class shape — all transfer the object correctly). Not the I64/F64 module_var_data_ids path (that's inlining-only). - -Next: runtime-probe the value written to `perry_global___55` at module-init (object vs closure) to confirm the assignment vs getter mistyping; investigate the IIFE-result local (`_cjs`, local 0) read at module-init scope at scale. - ---- -## ROOT (store-time probe, decisive) -`js_debug_val` injected at the module-global store (let_stmt.rs:785, gated PERRY_DBG_STORE on the COMPILE) shows the `"default"` Let (id 55) store-time value: -`[DEBUG_VAL] label=55 bits=0x7FFD045AB87A73B8` — tag `0x7FFD` = POINTER (runs once, deferred-init). Runtime `uw` is `typeof function`, so this pointer is the **closure**. - -So `_cjs` (local 0, init = the IIFE `Call`) holds the **IIFE closure**, not the IIFE **call result** (the exports object), at store time — i.e. `const _cjs = (function(){...; return module.exports})()` binds `_cjs` to the *function* instead of its *return value*, ONLY at giant-bundle scale. The IIFE body's `module.exports` IS an object (PERRY_SCC), so the IIFE returns the object; the bug is the Call-result binding of `_cjs`. Not reproducible in ~14 minimal repros (incl. deferred lazy-require + -O3 auto-optimize) — a scale-emergent codegen defect in the IIFE-call-result assignment for a deferred cjs-wrapped module. - -FIX area: the codegen that lowers `const x = (closure)()` (the cjs_wrap IIFE) — ensure `x` binds the Call RESULT, not the callee closure, under the giant-module / deferred-init path. Needs a scale reproduction or someone with the IIFE-call/deferred-init codegen context. - ---- -## store==load (definitive, same run) -`js_debug_val` at the store (let_stmt.rs) AND the importer getter-call (dyn_extern_i18n.rs:628), same run: -`label=55 (store) bits=0x7FFD02D428FA7130` == `label=9955 (load) bits=0x7FFD02D428FA7130` — IDENTICAL. -So the getter faithfully returns the stored value (NOT a load-side/getter bug, NOT corruption). `uw.name===""` (anonymous) ⇒ the stored value is the **IIFE function itself**, not the class (which would be `name==="SharedCacheControls"`). Definitive root: `const _cjs = (function(){...; return module.exports})()` binds `_cjs` to the IIFE **closure (callee)**, not the IIFE's **call result** (the exports object) — the IIFE body DOES run (module.exports populated) but its return value is discarded and the closure is stored. Giant-bundle-scale only (16+ repros incl. 150-module -O3 build all bind the call result correctly). The IIFE-call path is via the receiverless closure-value call (lower_call/console_promise.rs:997); it's correct in repros, so the defect is a scale-specific interaction (inlining / deferred-init / whole-program -O3) in the real bundle. Not reproducible synthetically → needs in-bundle debugging or the team's oversized-module codegen work. - ---- -## ROOT CONFIRMED (2026-06-19): deferred-require var captured by-value as a stale thunk -After exhaustively refuting prefix/global-id/FuncId collisions, -O3, GC, and the IIFE-return path (all clean), and tracing the value across the module boundary, the root is: - -`uw = require("next/dist/server/lib/incremental-cache/shared-cache-controls.external.js")` is an **adopted/deferred require** (`cjs_wrap` rewrites `const uw = require('S')` → `import uw from 'S'`; `is_deferred_require` on the import decl). The `IncrementalCache` **constructor** (a class method inside app-page-turbo's cjs-wrap IIFE) **captures `uw` by value** (`js_closure_get_capture_f64`, NON-boxed; literals_vars.rs:434) at class-definition time — when `uw` is still the **unresolved thunk/closure**. So `new uw.SharedCacheControls(...)` reads a function → `uw.SharedCacheControls === undefined` → `TypeError: undefined is not a constructor` → HTTP 500. - -### Verified value chain (one run, is_closure probe) -- perry_global store of the export = OBJECT (`is_closure=false`) -- importer getter-call result (the `uw` value via the getter) = SAME OBJECT (`is_closure=false`, identical bits) -- but the constructor's captured `uw` = FUNCTION (anonymous closure) — `W6X typeof=function` -- probe `js_dbg_closure_only` at the capture-read site: **47 by-value captures-of-closures in app-page-turbo**; `uw` is one (candidate ids 49 / 7499 / 7962 / 186xx). - -### Why the boxing analysis misses it -`boxed_vars.rs:151` boxes a var only when `(declared AND captured AND mutated) OR self-recursive-closure`. `uw` is captured but assigned once (not "mutated"), so it's snapshotted by value. The **self-recursive-closure** rule (`collect_self_recursive_closure_ids`, boxed_vars.rs:148) is the exact precedent — it boxes `let f = closure()` because "the store happens AFTER captures populate." `uw`'s deferred require is the same "value not ready at capture" shape, just with a require/import init. - -### Candidate fixes (delicate — import/capture subsystem; needs the repr decided first) -Whether `uw` is a `Stmt::Let` (boxed_vars-visible) or a pure adopted-import binding determines the site: -1. **box-when-captured**: if `uw` is a captured `Let` whose init is an adopted-require/import value → add to the boxed set (mirror the self-recursive rule), so the capture is by-reference and sees the resolved object. Verify the box actually receives the resolved value. -2. **eager-init**: a cjs-wrap-IIFE require runs at module init, so resolve it eagerly into the local before the class definition (then by-value capture = object). -3. **getter-on-read**: lower the constructor's `uw` read through the imported-var getter (`ExternFuncRef` → `perry_fn___`), consistent with init-scope reads, instead of a by-value capture. - -### Repro status -NOT minimally reproducible (the eager-resolve path works in isolation; module-level require captured by a class method passes). Needs the cjs-wrap adopted/deferred-require + cross-module-getter shape — bundle-only so far. A faithful repro likely requires a compilePackage that cjs-wraps `const uw = require('dep'); module.exports.C = class { constructor(){ new uw.Thing() } }`. diff --git a/android-build/app/src/main/jniLibs/arm64-v8a/libperry_app.so b/android-build/app/src/main/jniLibs/arm64-v8a/libperry_app.so deleted file mode 100755 index 52643c58dd..0000000000 Binary files a/android-build/app/src/main/jniLibs/arm64-v8a/libperry_app.so and /dev/null differ diff --git a/clusterAg b/clusterAg deleted file mode 100755 index 071f461c9d..0000000000 Binary files a/clusterAg and /dev/null differ diff --git a/crates/perry-codegen/src/nm_install.rs b/crates/perry-codegen/src/nm_install.rs index dacb16aed3..fb1686f884 100644 --- a/crates/perry-codegen/src/nm_install.rs +++ b/crates/perry-codegen/src/nm_install.rs @@ -1,4 +1,4 @@ -//! GENERATED (NM_DEVIRT_PLAN.md): native-module dispatch-install symbol selection. +//! GENERATED (native-module devirtualization, #5256): dispatch-install symbol selection. //! Mirrors perry-runtime `nm_module_index`. `js_create_native_module_namespace` //! sites emit the returned symbol so the per-module dispatch bucket is registered //! before any method call; unimported modules are never named → dead-stripped. diff --git a/crates/perry-runtime/src/.value.parked/dyn_index.rs b/crates/perry-runtime/src/.value.parked/dyn_index.rs deleted file mode 100644 index aac516ce76..0000000000 --- a/crates/perry-runtime/src/.value.parked/dyn_index.rs +++ /dev/null @@ -1,223 +0,0 @@ -//! Tag-aware dynamic index get/set + helpers for ambiguous index access. - -use super::*; - -/// Tag-aware dynamic index dispatch for `obj[key]` where `obj` has unknown -/// static type. Issue #514. Strings → js_string_char_at; everything else -/// uses the same `raw_ptr + 8 + idx*8` direct-read offset hack the existing -/// IndexGet fallback uses (which happens to be load-bearing for -/// Object-with-numeric-keys + TypedArrays). LAZY_ARRAY / FORWARDED arrays -/// route through `js_array_get_f64` to chase the materialized chain. -#[no_mangle] -pub extern "C" fn js_dyn_index_get(value: f64, index: f64) -> f64 { - let bits = value.to_bits(); - let jsval = JSValue::from_bits(bits); - if jsval.is_string() || jsval.is_short_string() { - let s_ptr = js_get_string_pointer_unified(value) as *const crate::StringHeader; - if s_ptr.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } - let idx_i32 = if index.is_nan() || index.is_infinite() { - 0 - } else { - index as i32 - }; - let result = crate::string::js_string_char_at(s_ptr, idx_i32); - if result.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } - return f64::from_bits(JSValue::string_ptr(result).bits()); - } - let raw_ptr = if jsval.is_pointer() { - (bits & POINTER_MASK) as usize - } else if !value.is_nan() - && bits != 0 - && bits < 0x0001_0000_0000_0000 - && (bits & 0x3) == 0 - && bits >= 0x10000 - { - bits as usize - } else { - return f64::from_bits(TAG_UNDEFINED); - }; - if raw_ptr < 0x10000 { - return f64::from_bits(TAG_UNDEFINED); - } - // Issue #957: if the index itself is a string, route through the - // by-name object getter. Pre-fix, `obj["foo"]` lowered through - // `IndexUpdate` re-entered this helper with a NaN-boxed string index - // and the `index as i32` coercion produced garbage offsets, so - // `++obj["foo"]` silently returned undefined. - let idx_bits = index.to_bits(); - let idx_top16 = idx_bits >> 48; - if idx_top16 == 0x7FFF || idx_top16 == 0x7FF9 { - let key_ptr = js_get_string_pointer_unified(index) as *const crate::StringHeader; - if !key_ptr.is_null() { - return crate::object::js_object_get_field_by_name_f64( - raw_ptr as *const crate::object::ObjectHeader, - key_ptr, - ); - } - return f64::from_bits(TAG_UNDEFINED); - } - let idx_i32 = if index.is_nan() || index.is_infinite() { - return f64::from_bits(TAG_UNDEFINED); - } else { - index as i32 - }; - if idx_i32 < 0 { - return f64::from_bits(TAG_UNDEFINED); - } - // Registry-backed Buffer (`Buffer.from(...)`, `js_buffer_alloc`, the - // `'data'`-event chunk an http/net listener receives). These carry NO - // GcHeader (see `crates/perry-runtime/src/buffer.rs` — "Buffers carry - // no GcHeader") and store one byte per element after an 8-byte - // `BufferHeader { length, capacity }`. The generic fall-through below - // does `raw_ptr - GC_HEADER_SIZE` to read an `obj_type` that doesn't - // exist for a buffer (garbage that never matches GC_TYPE_ARRAY), then - // reads an 8-byte f64 at `raw_ptr + 8 + idx*8` straight out of the - // buffer's 1-byte-per-element data region — `chunk[0]` came back as a - // denormal/garbage f64 that printed `0`, while `.toString()` / - // `.length` / `Array.from(chunk)` (which all probe BUFFER_REGISTRY) - // were correct. Probe the registry first and read the byte the same - // way the working accessors do (`js_buffer_get` → `buffer_data()`). - // Node semantics: in-range → the byte (0..255); out-of-range → undefined. - if crate::buffer::is_registered_buffer(raw_ptr) { - let buf = raw_ptr as *const crate::buffer::BufferHeader; - let len = unsafe { (*buf).length }; - if (idx_i32 as u32) >= len { - return f64::from_bits(TAG_UNDEFINED); - } - let byte_val = crate::buffer::js_buffer_get(buf, idx_i32); - return byte_val as f64; - } - if raw_ptr >= crate::gc::GC_HEADER_SIZE { - let gc_hdr = unsafe { - (raw_ptr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader - }; - let obj_type = unsafe { (*gc_hdr).obj_type }; - let gc_flags = unsafe { (*gc_hdr).gc_flags }; - if obj_type == crate::gc::GC_TYPE_LAZY_ARRAY - || (gc_flags & crate::gc::GC_FLAG_FORWARDED) != 0 - { - let arr = raw_ptr as *const crate::array::ArrayHeader; - return crate::array::js_array_get_f64(arr, idx_i32 as u32); - } - // Issue #1069: bounds-check regular arrays so out-of-range reads - // return TAG_UNDEFINED instead of whatever's in the slot. Without - // this, an empty (or short) array — most visibly the synthetic - // `arguments` array bundled by the call-site for caller arity 0 — - // returns the raw 0.0 slot value because `js_array_alloc` rounds - // capacity up to MIN_ARRAY_CAPACITY and the unchecked load reads - // past `length` into zeroed-but-allocated storage. `arguments[0]` - // on `function f() { arguments[0] }; f()` printed `0` instead of - // `undefined`. The narrow gate (GC_TYPE_ARRAY) keeps object - // numeric-key fast path unchanged. - if obj_type == crate::gc::GC_TYPE_ARRAY { - let arr = raw_ptr as *const crate::array::ArrayHeader; - let length = unsafe { (*arr).length }; - if (idx_i32 as u32) >= length { - return f64::from_bits(TAG_UNDEFINED); - } - } - } - let elem_addr = raw_ptr.wrapping_add(8 + (idx_i32 as usize) * 8); - let v = unsafe { *(elem_addr as *const f64) }; - if v.to_bits() == crate::value::TAG_HOLE { - return f64::from_bits(TAG_UNDEFINED); - } - v -} - -/// Issue #957 — tag-aware dynamic index write counterpart to -/// `js_dyn_index_get`. Used by `Expr::IndexUpdate` codegen to write back -/// the incremented value without duplicating the IndexSet dispatch tree. -/// -/// Routes by the receiver's `gc_type` byte: arrays go through -/// `js_array_set_index_or_string` (numeric/string-key spec dispatch); -/// everything else stringifies the index and routes through -/// `js_object_set_field_by_name`. Strings are immutable — no-op (matches -/// strict-mode `s[i] = x` semantics, close enough for the `++result[key]` -/// pattern this is added for). -#[no_mangle] -pub extern "C" fn js_dyn_index_set(obj: f64, index: f64, value: f64) -> f64 { - let bits = obj.to_bits(); - let jsval = JSValue::from_bits(bits); - if jsval.is_string() || jsval.is_short_string() { - return value; - } - let raw_ptr = if jsval.is_pointer() { - (bits & POINTER_MASK) as usize - } else if !obj.is_nan() - && bits != 0 - && bits < 0x0001_0000_0000_0000 - && (bits & 0x3) == 0 - && bits >= 0x10000 - { - bits as usize - } else { - return value; - }; - if raw_ptr < crate::gc::GC_HEADER_SIZE + 0x1000 { - return value; - } - let is_array = unsafe { - let gc_header = - (raw_ptr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - (*gc_header).obj_type == crate::gc::GC_TYPE_ARRAY - }; - if is_array { - crate::array::js_array_set_index_or_string( - raw_ptr as *mut crate::array::ArrayHeader, - index, - value, - ); - return value; - } - // Non-array object: stringify the index and write via the object setter. - let bits = index.to_bits(); - let top16 = bits >> 48; - let key_ptr: *const crate::StringHeader = if top16 == 0x7FFF { - (bits & 0x0000_FFFF_FFFF_FFFF) as *const crate::StringHeader - } else if top16 == 0x7FF9 { - crate::value::js_get_string_pointer_unified(index) as *const crate::StringHeader - } else { - // Numeric (or other) index — stringify and intern as a UTF-8 key. - let idx_i32 = if index.is_nan() || index.is_infinite() { - 0 - } else { - index as i32 - }; - let s = idx_i32.to_string(); - crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32) - }; - if key_ptr.is_null() { - return value; - } - crate::object::js_object_set_field_by_name( - raw_ptr as *mut crate::object::ObjectHeader, - key_ptr, - value, - ); - value -} - -/// Check if a value should trigger a destructuring default. -/// Returns 1 if the value is TAG_UNDEFINED, or a bare IEEE NaN (e.g., from -/// out-of-bounds array read), 0 otherwise. All other NaN-boxed values -/// (strings, pointers, booleans, etc.) return 0 because their NaN payload -/// does not match NaN or TAG_UNDEFINED exactly. -#[no_mangle] -pub extern "C" fn js_is_undefined_or_bare_nan(value: f64) -> i32 { - let bits = value.to_bits(); - // TAG_UNDEFINED = 0x7FFC_0000_0000_0001 - if bits == 0x7FFC_0000_0000_0001 { - return 1; - } - // Bare IEEE NaN (0.0/0.0) — produced by OOB array reads - // Canonical NaN is 0x7FF8_0000_0000_0000 on most platforms - if bits == 0x7FF8_0000_0000_0000 { - return 1; - } - 0 -} diff --git a/crates/perry-runtime/src/.value.parked/dynamic_arith.rs b/crates/perry-runtime/src/.value.parked/dynamic_arith.rs deleted file mode 100644 index 80d40a3c2c..0000000000 --- a/crates/perry-runtime/src/.value.parked/dynamic_arith.rs +++ /dev/null @@ -1,252 +0,0 @@ -//! Dynamic arithmetic dispatch: handles BigInt vs float at runtime. -//! -//! When a parameter has Type::Any (is_union=true), it may hold a BigInt -//! (NaN-boxed with BIGINT_TAG) or a regular f64. These functions check -//! the NaN-box tag at runtime and dispatch to the correct operation. - -use super::*; - -/// Convert a NaN-boxed JSValue to a *mut BigIntHeader for arithmetic. -/// If the value is already a BigInt, extracts the pointer. -/// Otherwise allocates a new BigInt from the f64 value. -#[inline] -unsafe fn coerce_to_bigint_ptr(val: f64) -> *mut crate::bigint::BigIntHeader { - let jsval = JSValue::from_bits(val.to_bits()); - if jsval.is_bigint() { - jsval.as_bigint_ptr() as *mut _ - } else { - crate::bigint::js_bigint_from_f64(val) - } -} - -/// Dynamic multiply: BigInt * BigInt if either operand is BigInt, else f64 * f64. -#[no_mangle] -pub unsafe extern "C" fn js_dynamic_mul(a: f64, b: f64) -> f64 { - let a_val = JSValue::from_bits(a.to_bits()); - let b_val = JSValue::from_bits(b.to_bits()); - if a_val.is_bigint() || b_val.is_bigint() { - let a_ptr = coerce_to_bigint_ptr(a) as *const _; - let b_ptr = coerce_to_bigint_ptr(b) as *const _; - let result = crate::bigint::js_bigint_mul(a_ptr, b_ptr); - return js_nanbox_bigint(result as i64); - } - a * b -} - -/// Dynamic add: BigInt + BigInt if either operand is BigInt, else f64 + f64. -#[no_mangle] -pub unsafe extern "C" fn js_dynamic_add(a: f64, b: f64) -> f64 { - let a_val = JSValue::from_bits(a.to_bits()); - let b_val = JSValue::from_bits(b.to_bits()); - if a_val.is_bigint() || b_val.is_bigint() { - let result = crate::bigint::js_bigint_add( - coerce_to_bigint_ptr(a) as *const _, - coerce_to_bigint_ptr(b) as *const _, - ); - return js_nanbox_bigint(result as i64); - } - a + b -} - -/// Dynamic `a + b` for type-uncertain operands. Per JS spec, when either -/// operand is a string after ToPrimitive, the result is string concatenation; -/// otherwise both operands are coerced to numbers and summed (or BigInt- -/// summed when either is BigInt). The codegen dispatches here for `+` when -/// neither operand has a statically-known type — refs #486 (hono's -/// `Node.buildRegExpStr` does `k + c.buildRegExpStr()` inside a for-of loop -/// over `Object.keys(...)` results, both operands lower to plain f64s with -/// inferred type Any, the static-string-concat fast path doesn't fire, and -/// the previous fallback called `js_number_coerce` on each side and `fadd`d -/// the results — turning `"c" + ""` into `NaN + 0 = NaN`). -#[no_mangle] -pub unsafe extern "C" fn js_dynamic_string_or_number_add(a: f64, b: f64) -> f64 { - let a_val = JSValue::from_bits(a.to_bits()); - let b_val = JSValue::from_bits(b.to_bits()); - - // String concat takes priority: either operand being a string forces - // ToPrimitive on the other side via the spec's "if either is a string, - // do concat" branch. js_string_concat_value handles the - // `string + non-string` case (it calls js_jsvalue_to_string on the - // non-string side); we use it for both orderings by pre-coercing the - // other operand to string via js_jsvalue_to_string when it ISN'T a - // string. - if a_val.is_any_string() || b_val.is_any_string() { - let a_str = if a_val.is_any_string() { - js_get_string_pointer_unified(a) as *mut crate::string::StringHeader - } else { - js_jsvalue_to_string(a) - }; - let b_str = if b_val.is_any_string() { - js_get_string_pointer_unified(b) as *mut crate::string::StringHeader - } else { - js_jsvalue_to_string(b) - }; - let result = crate::string::js_string_concat(a_str, b_str); - return f64::from_bits(JSValue::string_ptr(result).bits()); - } - - // BigInt: same as js_dynamic_add. - if a_val.is_bigint() || b_val.is_bigint() { - let result = crate::bigint::js_bigint_add( - coerce_to_bigint_ptr(a) as *const _, - coerce_to_bigint_ptr(b) as *const _, - ); - return js_nanbox_bigint(result as i64); - } - - // Both numeric — coerce non-numbers (booleans, null, undefined) the - // same way the static fallback path did. - let a_num = if a_val.is_number() || a_val.is_int32() { - a - } else { - crate::builtins::js_number_coerce(a) - }; - let b_num = if b_val.is_number() || b_val.is_int32() { - b - } else { - crate::builtins::js_number_coerce(b) - }; - a_num + b_num -} - -/// Dynamic subtract: BigInt - BigInt if either operand is BigInt, else f64 - f64. -#[no_mangle] -pub unsafe extern "C" fn js_dynamic_sub(a: f64, b: f64) -> f64 { - let a_val = JSValue::from_bits(a.to_bits()); - let b_val = JSValue::from_bits(b.to_bits()); - if a_val.is_bigint() || b_val.is_bigint() { - let result = crate::bigint::js_bigint_sub( - coerce_to_bigint_ptr(a) as *const _, - coerce_to_bigint_ptr(b) as *const _, - ); - return js_nanbox_bigint(result as i64); - } - a - b -} - -/// Dynamic divide: BigInt / BigInt if either operand is BigInt, else f64 / f64. -#[no_mangle] -pub unsafe extern "C" fn js_dynamic_div(a: f64, b: f64) -> f64 { - let a_val = JSValue::from_bits(a.to_bits()); - let b_val = JSValue::from_bits(b.to_bits()); - if a_val.is_bigint() || b_val.is_bigint() { - let result = crate::bigint::js_bigint_div( - coerce_to_bigint_ptr(a) as *const _, - coerce_to_bigint_ptr(b) as *const _, - ); - return js_nanbox_bigint(result as i64); - } - a / b -} - -/// Dynamic modulo: BigInt % BigInt if either operand is BigInt, else f64 % f64. -#[no_mangle] -pub unsafe extern "C" fn js_dynamic_mod(a: f64, b: f64) -> f64 { - let a_val = JSValue::from_bits(a.to_bits()); - let b_val = JSValue::from_bits(b.to_bits()); - if a_val.is_bigint() || b_val.is_bigint() { - let result = crate::bigint::js_bigint_mod( - coerce_to_bigint_ptr(a) as *const _, - coerce_to_bigint_ptr(b) as *const _, - ); - return js_nanbox_bigint(result as i64); - } - // Float modulo: a - trunc(a / b) * b - a - (a / b).trunc() * b -} - -/// Dynamic negate: -BigInt if operand is BigInt, else -f64. -#[no_mangle] -pub unsafe extern "C" fn js_dynamic_neg(a: f64) -> f64 { - let a_val = JSValue::from_bits(a.to_bits()); - if a_val.is_bigint() { - let result = crate::bigint::js_bigint_neg(a_val.as_bigint_ptr()); - return js_nanbox_bigint(result as i64); - } - -a -} - -/// Dynamic right shift: BigInt >> if either operand is BigInt, else i32 >> for numbers. -#[no_mangle] -pub unsafe extern "C" fn js_dynamic_shr(a: f64, b: f64) -> f64 { - let a_val = JSValue::from_bits(a.to_bits()); - let b_val = JSValue::from_bits(b.to_bits()); - if a_val.is_bigint() || b_val.is_bigint() { - let result = crate::bigint::js_bigint_shr( - coerce_to_bigint_ptr(a) as *const _, - coerce_to_bigint_ptr(b) as *const _, - ); - return js_nanbox_bigint(result as i64); - } - // JS ToInt32: f64 -> i64 -> i32 (wrapping), NOT f64 -> i32 (saturating). - // Rust `f64 as i32` saturates at i32::MAX for values >= 2^31, but JS wraps. - let ai = (a as i64) as i32; - let bi = ((b as i64) as i32) & 0x1f; - (ai >> bi) as f64 -} - -/// Dynamic left shift: BigInt << if either operand is BigInt, else i32 << for numbers. -#[no_mangle] -pub unsafe extern "C" fn js_dynamic_shl(a: f64, b: f64) -> f64 { - let a_val = JSValue::from_bits(a.to_bits()); - let b_val = JSValue::from_bits(b.to_bits()); - if a_val.is_bigint() || b_val.is_bigint() { - let result = crate::bigint::js_bigint_shl( - coerce_to_bigint_ptr(a) as *const _, - coerce_to_bigint_ptr(b) as *const _, - ); - return js_nanbox_bigint(result as i64); - } - // JS ToInt32: f64 -> i64 -> i32 (wrapping), NOT f64 -> i32 (saturating). - let ai = (a as i64) as i32; - let bi = ((b as i64) as i32) & 0x1f; - (ai << bi) as f64 -} - -/// Dynamic bitwise AND: BigInt & if either operand is BigInt, else i32 & for numbers. -#[no_mangle] -pub unsafe extern "C" fn js_dynamic_bitand(a: f64, b: f64) -> f64 { - let a_val = JSValue::from_bits(a.to_bits()); - let b_val = JSValue::from_bits(b.to_bits()); - if a_val.is_bigint() || b_val.is_bigint() { - let result = crate::bigint::js_bigint_and( - coerce_to_bigint_ptr(a) as *const _, - coerce_to_bigint_ptr(b) as *const _, - ); - return js_nanbox_bigint(result as i64); - } - // JS ToInt32: f64 -> i64 -> i32 (wrapping), NOT f64 -> i32 (saturating). - (((a as i64) as i32) & ((b as i64) as i32)) as f64 -} - -/// Dynamic bitwise OR: BigInt | if either operand is BigInt, else i32 | for numbers. -#[no_mangle] -pub unsafe extern "C" fn js_dynamic_bitor(a: f64, b: f64) -> f64 { - let a_val = JSValue::from_bits(a.to_bits()); - let b_val = JSValue::from_bits(b.to_bits()); - if a_val.is_bigint() || b_val.is_bigint() { - let result = crate::bigint::js_bigint_or( - coerce_to_bigint_ptr(a) as *const _, - coerce_to_bigint_ptr(b) as *const _, - ); - return js_nanbox_bigint(result as i64); - } - // JS ToInt32: f64 -> i64 -> i32 (wrapping), NOT f64 -> i32 (saturating). - (((a as i64) as i32) | ((b as i64) as i32)) as f64 -} - -/// Dynamic bitwise XOR: BigInt ^ if either operand is BigInt, else i32 ^ for numbers. -#[no_mangle] -pub unsafe extern "C" fn js_dynamic_bitxor(a: f64, b: f64) -> f64 { - let a_val = JSValue::from_bits(a.to_bits()); - let b_val = JSValue::from_bits(b.to_bits()); - if a_val.is_bigint() || b_val.is_bigint() { - let result = crate::bigint::js_bigint_xor( - coerce_to_bigint_ptr(a) as *const _, - coerce_to_bigint_ptr(b) as *const _, - ); - return js_nanbox_bigint(result as i64); - } - // JS ToInt32: f64 -> i64 -> i32 (wrapping), NOT f64 -> i32 (saturating). - (((a as i64) as i32) ^ ((b as i64) as i32)) as f64 -} diff --git a/crates/perry-runtime/src/.value.parked/handle.rs b/crates/perry-runtime/src/.value.parked/handle.rs deleted file mode 100644 index f1c628035f..0000000000 --- a/crates/perry-runtime/src/.value.parked/handle.rs +++ /dev/null @@ -1,124 +0,0 @@ -//! JS handle FFI registration + dispatch helpers. -//! -//! perry-jsruntime calls into these `js_set_*` setters to wire up the -//! function pointers used by the dynamic handle dispatchers in the other -//! `value` sub-modules. All `static` storage lives in `tags.rs` so the -//! generated NaN-box helpers can see them through `super::*`. - -use super::*; -use std::sync::atomic::Ordering; - -/// Set the JS handle array get function (called by perry-jsruntime) -#[no_mangle] -pub extern "C" fn js_set_handle_array_get(func: JsHandleArrayGetFn) { - JS_HANDLE_ARRAY_GET.store(func as *mut (), Ordering::SeqCst); -} - -/// Set the JS handle array length function (called by perry-jsruntime) -#[no_mangle] -pub extern "C" fn js_set_handle_array_length(func: JsHandleArrayLengthFn) { - JS_HANDLE_ARRAY_LENGTH.store(func as *mut (), Ordering::SeqCst); -} - -/// Set the JS handle object get property function (called by perry-jsruntime) -#[no_mangle] -pub extern "C" fn js_set_handle_object_get_property(func: JsHandleObjectGetPropertyFn) { - JS_HANDLE_OBJECT_GET_PROPERTY.store(func as *mut (), Ordering::SeqCst); -} - -/// Set the JS handle to string conversion function (called by perry-jsruntime) -#[no_mangle] -pub extern "C" fn js_set_handle_to_string(func: JsHandleToStringFn) { - JS_HANDLE_TO_STRING.store(func as *mut (), Ordering::SeqCst); -} - -/// Set the JS handle method call function (called by perry-jsruntime) -#[no_mangle] -pub extern "C" fn js_set_handle_call_method(func: JsHandleCallMethodFn) { - JS_HANDLE_CALL_METHOD.store(func as *mut (), Ordering::SeqCst); -} - -/// Set the native module JS property loader (called by perry-jsruntime) -/// This callback loads a native module via V8 and gets a property from it. -#[no_mangle] -pub extern "C" fn js_set_native_module_js_loader(func: JsNativeModuleJsLoaderFn) { - JS_NATIVE_MODULE_JS_LOADER.store(func as *mut (), Ordering::SeqCst); -} - -/// Set the V8 new-from-handle function (called by perry-jsruntime) -/// This callback calls V8's new_instance for JS handle constructors. -#[no_mangle] -pub extern "C" fn js_set_new_from_handle_v8(func: JsNewFromHandleV8Fn) { - JS_NEW_FROM_HANDLE_V8.store(func as *mut (), Ordering::SeqCst); -} - -/// Set the V8 handle typeof discriminator (called by perry-jsruntime). -/// Used by `js_value_typeof` so `typeof someJsFunction` returns `"function"` -/// instead of `"object"` when the handle wraps a V8 callable. (Issue #258.) -#[no_mangle] -pub extern "C" fn js_set_handle_typeof(func: JsHandleTypeofFn) { - JS_HANDLE_TYPEOF.store(func as *mut (), Ordering::SeqCst); -} - -/// Probe a V8 handle's JS `typeof` discriminator. Returns 1 for `"function"`, -/// 0 for `"object"`, and 0 if the V8 callback hasn't been registered (no V8 → -/// fall through to the default "object" classification). Internal helper for -/// `js_value_typeof`. -#[inline] -pub(crate) fn js_handle_is_function(value: f64) -> bool { - let ptr = JS_HANDLE_TYPEOF.load(Ordering::Relaxed); - if ptr.is_null() { - return false; - } - let func: JsHandleTypeofFn = unsafe { std::mem::transmute(ptr) }; - unsafe { func(value) == 1 } -} - -/// Get element from a JS handle array. Dispatches through the function pointer -/// set by perry-jsruntime, or returns TAG_UNDEFINED if JS runtime not loaded. -#[no_mangle] -pub extern "C" fn js_handle_array_get(array_handle: f64, index: i32) -> f64 { - let ptr = JS_HANDLE_ARRAY_GET.load(Ordering::Relaxed); - if ptr.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } - let func: JsHandleArrayGetFn = unsafe { std::mem::transmute(ptr) }; - func(array_handle, index) -} - -/// Get length of a JS handle array. Dispatches through the function pointer -/// set by perry-jsruntime, or returns 0 if JS runtime not loaded. -#[no_mangle] -pub extern "C" fn js_handle_array_length(array_handle: f64) -> i32 { - let ptr = JS_HANDLE_ARRAY_LENGTH.load(Ordering::Relaxed); - if ptr.is_null() { - return 0; - } - let func: JsHandleArrayLengthFn = unsafe { std::mem::transmute(ptr) }; - func(array_handle) -} - -/// Try to load a property from a native module via V8 JS runtime. -/// Returns TAG_UNDEFINED if JS runtime is not available or property not found. -pub fn native_module_try_js_property(module_name: &str, property_name: &str) -> f64 { - let loader_ptr = JS_NATIVE_MODULE_JS_LOADER.load(Ordering::Relaxed); - if loader_ptr.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } - let loader: JsNativeModuleJsLoaderFn = unsafe { std::mem::transmute(loader_ptr) }; - unsafe { - loader( - module_name.as_ptr(), - module_name.len(), - property_name.as_ptr(), - property_name.len(), - ) - } -} - -/// Check if a NaN-boxed value is a JS handle -#[inline] -pub fn is_js_handle(value: f64) -> bool { - let bits = value.to_bits(); - (bits & TAG_MASK) == JS_HANDLE_TAG -} diff --git a/crates/perry-runtime/src/.value.parked/jsvalue.rs b/crates/perry-runtime/src/.value.parked/jsvalue.rs deleted file mode 100644 index 4c1d906157..0000000000 --- a/crates/perry-runtime/src/.value.parked/jsvalue.rs +++ /dev/null @@ -1,349 +0,0 @@ -//! The `JSValue` NaN-boxed value type, its construct/inspect/coerce -//! methods, and the `Debug`/`Default` impls. - -use super::*; - -/// A JavaScript value using NaN-boxing representation -#[derive(Clone, Copy)] -#[repr(transparent)] -pub struct JSValue { - pub(super) bits: u64, -} - -impl JSValue { - /// Create undefined value - #[inline] - pub const fn undefined() -> Self { - Self { - bits: TAG_UNDEFINED, - } - } - - /// Create null value - #[inline] - pub const fn null() -> Self { - Self { bits: TAG_NULL } - } - - /// Create a boolean value - #[inline] - pub const fn bool(value: bool) -> Self { - Self { - bits: if value { TAG_TRUE } else { TAG_FALSE }, - } - } - - /// Create an f64 number value - #[inline] - pub fn number(value: f64) -> Self { - // Just reinterpret the bits - f64 values are stored directly - Self { - bits: value.to_bits(), - } - } - - /// Create an i32 value (stored in payload, faster than f64 for integers) - #[inline] - pub const fn int32(value: i32) -> Self { - Self { - bits: INT32_TAG | ((value as u32) as u64), - } - } - - /// Create a pointer value (for heap-allocated objects) - #[inline] - pub fn pointer(ptr: *const u8) -> Self { - debug_assert!( - (ptr as u64) <= POINTER_MASK, - "Pointer too large for NaN-boxing" - ); - Self { - bits: POINTER_TAG | (ptr as u64 & POINTER_MASK), - } - } - - /// Check if this is a number (not a tagged value) - #[inline] - pub fn is_number(&self) -> bool { - // A value is a number if upper 16 bits are not in our tagged range 0x7FFC-0x7FFF - // This allows IEEE NaN (0x7FF8), negative numbers, and all other f64 through - let upper = self.bits >> 48; - !(0x7FFC..=0x7FFF).contains(&upper) - } - - /// Check if this is undefined - #[inline] - pub fn is_undefined(&self) -> bool { - self.bits == TAG_UNDEFINED - } - - /// Check if this is null - #[inline] - pub fn is_null(&self) -> bool { - self.bits == TAG_NULL - } - - /// Check if this is a boolean - #[inline] - pub fn is_bool(&self) -> bool { - self.bits == TAG_TRUE || self.bits == TAG_FALSE - } - - /// Check if this is an int32 - #[inline] - pub fn is_int32(&self) -> bool { - (self.bits & !INT32_MASK) == INT32_TAG - } - - /// Check if this is a pointer (object or array) - #[inline] - pub fn is_pointer(&self) -> bool { - (self.bits & !POINTER_MASK) == POINTER_TAG - } - - /// Check if this is a heap-allocated string pointer - /// (STRING_TAG only — inline SSO values return false). This is - /// the legacy predicate that most call sites rely on: they - /// follow `is_string()` with `as_string_ptr()` assuming a real - /// `*mut StringHeader`. Keeping this strict avoids a massive - /// audit during the SSO rollout; use `is_any_string()` when - /// you want to accept both representations. - #[inline] - pub fn is_string(&self) -> bool { - (self.bits & !POINTER_MASK) == STRING_TAG - } - - /// Accepts both heap `STRING_TAG` pointers and inline - /// `SHORT_STRING_TAG` values. Use this for general "is this a - /// string?" checks that don't care about representation — - /// e.g., `typeof x === "string"`, string equality ops, string - /// concatenation. Paired with `short_string_to_buf()` / - /// `as_string_ptr()` on the respective branches to read the - /// data. - #[inline] - pub fn is_any_string(&self) -> bool { - let tag = self.bits & TAG_MASK; - tag == STRING_TAG || tag == SHORT_STRING_TAG - } - - /// Check if this is specifically an inline SSO string. - #[inline] - pub fn is_short_string(&self) -> bool { - (self.bits & TAG_MASK) == SHORT_STRING_TAG - } - - /// Check if this is a BigInt pointer - #[inline] - pub fn is_bigint(&self) -> bool { - (self.bits & !POINTER_MASK) == BIGINT_TAG - } - - /// Get as f64 (panics if not a number) - #[inline] - pub fn as_number(&self) -> f64 { - debug_assert!(self.is_number(), "Value is not a number"); - f64::from_bits(self.bits) - } - - /// Get as bool (panics if not a boolean) - #[inline] - pub fn as_bool(&self) -> bool { - debug_assert!(self.is_bool(), "Value is not a boolean"); - self.bits == TAG_TRUE - } - - /// Get as i32 (panics if not an int32) - #[inline] - pub fn as_int32(&self) -> i32 { - debug_assert!(self.is_int32(), "Value is not an int32"); - (self.bits & INT32_MASK) as i32 - } - - /// Get as pointer (panics if not a pointer) - #[inline] - pub fn as_pointer(&self) -> *const T { - debug_assert!(self.is_pointer(), "Value is not a pointer"); - (self.bits & POINTER_MASK) as *const T - } - - /// Convert to f64, coercing if necessary - pub fn to_number(&self) -> f64 { - if self.is_number() { - self.as_number() - } else if self.is_int32() { - self.as_int32() as f64 - } else if self.is_bool() { - if self.as_bool() { - 1.0 - } else { - 0.0 - } - } else if self.is_null() { - 0.0 - } else if self.is_undefined() { - f64::NAN - } else { - // Pointer types would need object-specific conversion - f64::NAN - } - } - - /// Convert to boolean (JS truthiness) - pub fn to_bool(&self) -> bool { - if self.is_bool() { - self.as_bool() - } else if self.is_number() { - let n = self.as_number(); - n != 0.0 && !n.is_nan() - } else if self.is_int32() { - self.as_int32() != 0 - } else if self.is_null() || self.is_undefined() { - false - } else { - // Pointers (objects) are truthy - true - } - } - - /// Raw bits access (for debugging) - #[inline] - pub fn bits(&self) -> u64 { - self.bits - } - - /// Create from raw bits - #[inline] - pub fn from_bits(bits: u64) -> Self { - Self { bits } - } - - /// Create a string pointer value (uses STRING_TAG for type discrimination) - #[inline] - pub fn string_ptr(ptr: *mut crate::string::StringHeader) -> Self { - debug_assert!( - (ptr as u64) <= POINTER_MASK, - "Pointer too large for NaN-boxing" - ); - Self { - bits: STRING_TAG | (ptr as u64 & POINTER_MASK), - } - } - - /// Try to encode a byte slice as an inline SSO string. Returns - /// `Some(Self)` when `bytes.len() <= SHORT_STRING_MAX_LEN`, - /// `None` otherwise. Skips all heap allocation on success. - /// - /// Semantic note: strings containing U+0000 (the NUL byte) are - /// fine — the NUL is stored verbatim in one of the 5 data bytes - /// and the length field is authoritative. Length 0 (the empty - /// string) is a valid SSO value with no data bytes read. - #[inline] - pub fn try_short_string(bytes: &[u8]) -> Option { - if bytes.len() > SHORT_STRING_MAX_LEN { - return None; - } - let mut payload: u64 = 0; - for (i, &b) in bytes.iter().enumerate() { - payload |= (b as u64) << (i * 8); - } - let len_bits = (bytes.len() as u64) << SHORT_STRING_LEN_SHIFT; - Some(Self { - bits: SHORT_STRING_TAG | len_bits | payload, - }) - } - - /// Unconditional SSO constructor. Caller must ensure - /// `bytes.len() <= SHORT_STRING_MAX_LEN`; debug-build panics on - /// violation, release-build truncates silently. - #[inline] - pub fn short_string_unchecked(bytes: &[u8]) -> Self { - debug_assert!(bytes.len() <= SHORT_STRING_MAX_LEN); - Self::try_short_string(bytes).expect("short string must fit SHORT_STRING_MAX_LEN") - } - - /// Extract the byte contents of an inline SSO string into a - /// caller-provided buffer of at least `SHORT_STRING_MAX_LEN` - /// bytes. Returns the actual length. Panics in debug builds if - /// called on a non-SSO value. - #[inline] - pub fn short_string_to_buf(&self, buf: &mut [u8; SHORT_STRING_MAX_LEN]) -> usize { - debug_assert!(self.is_short_string()); - let len = ((self.bits & SHORT_STRING_LEN_MASK) >> SHORT_STRING_LEN_SHIFT) as usize; - let data = self.bits & SHORT_STRING_DATA_MASK; - for i in 0..len { - buf[i] = ((data >> (i * 8)) & 0xFF) as u8; - } - len - } - - /// Return the length of an SSO string (0..=5). - #[inline] - pub fn short_string_len(&self) -> usize { - debug_assert!(self.is_short_string()); - ((self.bits & SHORT_STRING_LEN_MASK) >> SHORT_STRING_LEN_SHIFT) as usize - } - - /// Get string pointer (panics if not a string) - #[inline] - pub fn as_string_ptr(&self) -> *const crate::string::StringHeader { - debug_assert!(self.is_string(), "Value is not a string"); - (self.bits & POINTER_MASK) as *const crate::string::StringHeader - } - - /// Create a BigInt pointer value (uses BIGINT_TAG for type discrimination) - #[inline] - pub fn bigint_ptr(ptr: *mut crate::bigint::BigIntHeader) -> Self { - debug_assert!( - (ptr as u64) <= POINTER_MASK, - "Pointer too large for NaN-boxing" - ); - Self { - bits: BIGINT_TAG | (ptr as u64 & POINTER_MASK), - } - } - - /// Get BigInt pointer (panics if not a BigInt) - #[inline] - pub fn as_bigint_ptr(&self) -> *const crate::bigint::BigIntHeader { - debug_assert!(self.is_bigint(), "Value is not a BigInt"); - (self.bits & POINTER_MASK) as *const crate::bigint::BigIntHeader - } - - /// Create an object pointer value - #[inline] - pub fn object_ptr(ptr: *mut u8) -> Self { - Self::pointer(ptr) - } - - /// Create an array pointer value - #[inline] - pub fn array_ptr(ptr: *mut crate::array::ArrayHeader) -> Self { - Self::pointer(ptr as *const u8) - } -} - -impl std::fmt::Debug for JSValue { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.is_undefined() { - write!(f, "undefined") - } else if self.is_null() { - write!(f, "null") - } else if self.is_bool() { - write!(f, "{}", self.as_bool()) - } else if self.is_number() { - write!(f, "{}", self.as_number()) - } else if self.is_int32() { - write!(f, "{}i", self.as_int32()) - } else if self.is_pointer() { - write!(f, "", self.as_pointer::()) - } else { - write!(f, "", self.bits) - } - } -} - -impl Default for JSValue { - fn default() -> Self { - Self::undefined() - } -} diff --git a/crates/perry-runtime/src/.value.parked/mod.rs.tmp b/crates/perry-runtime/src/.value.parked/mod.rs.tmp deleted file mode 100644 index 30086802ca..0000000000 --- a/crates/perry-runtime/src/.value.parked/mod.rs.tmp +++ /dev/null @@ -1,2871 +0,0 @@ -//! JSValue representation using NaN-boxing -//! -//! NaN-boxing is a technique that encodes type information and values -//! in a 64-bit float. IEEE 754 double-precision floats have a specific -//! bit pattern for NaN (Not a Number), and we can use the unused bits -//! in the NaN payload to store pointers or small values. -//! -//! Layout (64 bits): -//! - Regular f64 values (including NaN) are stored directly -//! - Tagged values use a quiet NaN pattern (mantissa bit 51 set), with the -//! tag in the top 16 bits and the payload in the low 48 bits. Per IEEE -//! 754 §6.2.1, a NaN is quiet iff mantissa bit 51 is set; every tag -//! prefix Perry uses (0x7FF8..=0x7FFF) has that bit set, so all tagged -//! values are qNaN. Quiet matters because arithmetic on qNaN propagates -//! silently (`undefined + 1 -> NaN`) whereas sNaN would trap the FPU. -//! -//! We use the top 16 bits for tagging: -//! - 0x7FF9: short string (SSO, inline 5-byte payload) -//! - 0x7FFA: bigint pointer -//! - 0x7FFC + tag: singleton specials (undefined / null / true / false / hole) -//! - 0x7FFD: object/array pointer (48-bit payload) -//! - 0x7FFE: int32 (low 32 bits) -//! - 0x7FFF: heap string pointer (48-bit payload) -//! - Other: regular f64 (including canonical qNaN 0x7FF8_0000_0000_0000) - -/// Tag-marker for the singleton specials (undefined / null / true / false / -/// hole). 0x7FFC chosen so the first two mantissa bits are `11`: that keeps -/// it inside the qNaN encoding space (mantissa bit 51 set) while staying -/// distinct from the canonical qNaN 0x7FF8 the FPU produces from arithmetic -/// like `0/0` — code that wants to tell "Perry tagged" from "real NaN" can -/// gate on `top16 >= 0x7FFC` (see `JSValue::is_number` below). -/// -/// #854: part of the NaN-boxing tag contract documented in CLAUDE.md. -/// Kept as a named constant even when no Rust code consults it directly — -/// codegen, doc references, and external tooling all match against the -/// numeric value. -#[allow(dead_code)] -const TAG_MARKER: u64 = 0x7FFC_0000_0000_0000; - -/// Special singleton values -pub(crate) const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; -const TAG_NULL: u64 = 0x7FFC_0000_0000_0002; -const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003; -const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; - -/// Issue #323: hole sentinel for sparse arrays. Slots in `new Array(n)` are -/// initialized to this value; reads through `js_array_get_f64` translate it -/// back to TAG_UNDEFINED so user code never observes the raw bits, while -/// `Object.keys` and the `in` operator inspect slots directly to distinguish a -/// hole from an explicit `undefined` write. Bits chosen in the same 0x7FFC -/// singleton namespace, distinct from UNDEFINED/NULL/FALSE/TRUE so a NaN-box -/// payload can never be mistaken for a hole. -pub(crate) const TAG_HOLE: u64 = 0x7FFC_0000_0000_0010; - -/// Pointer tag: 0x7FFD_XXXX_XXXX_XXXX (48 bits for pointer) - objects/arrays -const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; -pub(crate) const POINTER_MASK: u64 = 0x0000_FFFF_FFFF_FFFF; - -/// Int32 tag: 0x7FFE_0000_XXXX_XXXX (32 bits for i32) -const INT32_TAG: u64 = 0x7FFE_0000_0000_0000; -const INT32_MASK: u64 = 0x0000_0000_FFFF_FFFF; - -/// String pointer tag: 0x7FFF_XXXX_XXXX_XXXX (48 bits for string pointer) -pub(crate) const STRING_TAG: u64 = 0x7FFF_0000_0000_0000; - -/// Small String Optimization (SSO) — tier 1 #2 per -/// `docs/memory-perf-roadmap.md`. A string of length 0..=5 bytes -/// encodes inline in the 48-bit NaN-box payload instead of -/// allocating a `StringHeader`. Layout: -/// -/// ```text -/// bits 63........48 47.....40 39...32 31..24 23..16 15..8 7..0 -/// 0x7FF9 tag length byte0 byte1 byte2 byte3 byte4 -/// ``` -/// -/// Length in bits 40..=47 (0..=5 — 6 valid values, 3 bits would -/// suffice but we use a full byte for alignment). Data in bits -/// 0..=39 (5 bytes, little-endian by byte index — `byte0` is the -/// first character). -/// -/// Why 5 bytes not 6: 6 bytes × 8 bits = 48 bits would fill the -/// entire payload leaving no room for length, forcing us to use 3 -/// different tag values for length buckets or a null-terminator -/// convention (which breaks strings containing U+0000). Staying at -/// 5 bytes with one tag keeps decode simple: tag check + 40-bit -/// extract. Covers "id", "name", "age", "true", "false", "null", -/// single-byte ASCII, etc. — a large fraction of real-world JSON -/// keys and short values. -/// -/// Strings with length > 5 fall through to the standard heap -/// `StringHeader` path; callers read-side use `is_string()` (which -/// accepts BOTH tags) + `string_bytes()` (which decodes either -/// form to a (ptr, len) slice view). -pub(crate) const SHORT_STRING_TAG: u64 = 0x7FF9_0000_0000_0000; -pub(crate) const SHORT_STRING_LEN_SHIFT: u64 = 40; -// Length byte at bits 40..=47 (byte index 5 from LSB). Not -// 0x00FF_0000_0000_0000 — that would be byte 6, overlapping the -// tag. -pub(crate) const SHORT_STRING_LEN_MASK: u64 = 0x0000_FF00_0000_0000; -// Data bytes at bits 0..=39 (5 bytes, byte indices 0..=4 from LSB). -pub(crate) const SHORT_STRING_DATA_MASK: u64 = 0x0000_00FF_FFFF_FFFF; -pub const SHORT_STRING_MAX_LEN: usize = 5; - -/// BigInt pointer tag: 0x7FFA_XXXX_XXXX_XXXX (48 bits for bigint pointer) -const BIGINT_TAG: u64 = 0x7FFA_0000_0000_0000; - -/// JS Handle tag: 0x7FFB_XXXX_XXXX_XXXX (48 bits for handle ID) -/// This is used by perry-jsruntime to reference V8 objects -pub(crate) const JS_HANDLE_TAG: u64 = 0x7FFB_0000_0000_0000; -pub(crate) const TAG_MASK: u64 = 0xFFFF_0000_0000_0000; - -/// Function pointers for JS handle operations (set by perry-jsruntime) -/// These allow the unified functions to dispatch to JS runtime when needed -use std::sync::atomic::{AtomicPtr, Ordering}; - -type JsHandleArrayGetFn = extern "C" fn(f64, i32) -> f64; -type JsHandleArrayLengthFn = extern "C" fn(f64) -> i32; -type JsHandleObjectGetPropertyFn = extern "C" fn(f64, *const i8, usize) -> f64; -type JsHandleToStringFn = extern "C" fn(f64) -> *mut crate::string::StringHeader; -type JsHandleCallMethodFn = unsafe extern "C" fn(f64, *const i8, usize, *const f64, usize) -> f64; -type JsNativeModuleJsLoaderFn = unsafe extern "C" fn(*const u8, usize, *const u8, usize) -> f64; -type JsNewFromHandleV8Fn = unsafe extern "C" fn(f64, *const f64, usize) -> f64; -/// Returns the JS spec `typeof` string discriminator for a V8 handle: -/// 1 = "function" (V8 callable), 0 = "object" (everything else — including arrays). -/// Negative values reserved for future use ("symbol" = 2 if V8 ever exposes it that way). -type JsHandleTypeofFn = unsafe extern "C" fn(f64) -> i32; - -static JS_HANDLE_ARRAY_GET: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); -static JS_HANDLE_ARRAY_LENGTH: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); -pub(crate) static JS_HANDLE_OBJECT_GET_PROPERTY: AtomicPtr<()> = - AtomicPtr::new(std::ptr::null_mut()); -static JS_HANDLE_TO_STRING: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); -pub static JS_HANDLE_CALL_METHOD: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); -pub static JS_NATIVE_MODULE_JS_LOADER: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); -pub static JS_NEW_FROM_HANDLE_V8: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); -pub static JS_HANDLE_TYPEOF: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); - -/// Set the JS handle array get function (called by perry-jsruntime) -#[no_mangle] -pub extern "C" fn js_set_handle_array_get(func: JsHandleArrayGetFn) { - JS_HANDLE_ARRAY_GET.store(func as *mut (), Ordering::SeqCst); -} - -/// Set the JS handle array length function (called by perry-jsruntime) -#[no_mangle] -pub extern "C" fn js_set_handle_array_length(func: JsHandleArrayLengthFn) { - JS_HANDLE_ARRAY_LENGTH.store(func as *mut (), Ordering::SeqCst); -} - -/// Set the JS handle object get property function (called by perry-jsruntime) -#[no_mangle] -pub extern "C" fn js_set_handle_object_get_property(func: JsHandleObjectGetPropertyFn) { - JS_HANDLE_OBJECT_GET_PROPERTY.store(func as *mut (), Ordering::SeqCst); -} - -/// Set the JS handle to string conversion function (called by perry-jsruntime) -#[no_mangle] -pub extern "C" fn js_set_handle_to_string(func: JsHandleToStringFn) { - JS_HANDLE_TO_STRING.store(func as *mut (), Ordering::SeqCst); -} - -/// Set the JS handle method call function (called by perry-jsruntime) -#[no_mangle] -pub extern "C" fn js_set_handle_call_method(func: JsHandleCallMethodFn) { - JS_HANDLE_CALL_METHOD.store(func as *mut (), Ordering::SeqCst); -} - -/// Set the native module JS property loader (called by perry-jsruntime) -/// This callback loads a native module via V8 and gets a property from it. -#[no_mangle] -pub extern "C" fn js_set_native_module_js_loader(func: JsNativeModuleJsLoaderFn) { - JS_NATIVE_MODULE_JS_LOADER.store(func as *mut (), Ordering::SeqCst); -} - -/// Set the V8 new-from-handle function (called by perry-jsruntime) -/// This callback calls V8's new_instance for JS handle constructors. -#[no_mangle] -pub extern "C" fn js_set_new_from_handle_v8(func: JsNewFromHandleV8Fn) { - JS_NEW_FROM_HANDLE_V8.store(func as *mut (), Ordering::SeqCst); -} - -/// Set the V8 handle typeof discriminator (called by perry-jsruntime). -/// Used by `js_value_typeof` so `typeof someJsFunction` returns `"function"` -/// instead of `"object"` when the handle wraps a V8 callable. (Issue #258.) -#[no_mangle] -pub extern "C" fn js_set_handle_typeof(func: JsHandleTypeofFn) { - JS_HANDLE_TYPEOF.store(func as *mut (), Ordering::SeqCst); -} - -/// Probe a V8 handle's JS `typeof` discriminator. Returns 1 for `"function"`, -/// 0 for `"object"`, and 0 if the V8 callback hasn't been registered (no V8 → -/// fall through to the default "object" classification). Internal helper for -/// `js_value_typeof`. -#[inline] -pub(crate) fn js_handle_is_function(value: f64) -> bool { - let ptr = JS_HANDLE_TYPEOF.load(Ordering::Relaxed); - if ptr.is_null() { - return false; - } - let func: JsHandleTypeofFn = unsafe { std::mem::transmute(ptr) }; - unsafe { func(value) == 1 } -} - -/// Get element from a JS handle array. Dispatches through the function pointer -/// set by perry-jsruntime, or returns TAG_UNDEFINED if JS runtime not loaded. -#[no_mangle] -pub extern "C" fn js_handle_array_get(array_handle: f64, index: i32) -> f64 { - let ptr = JS_HANDLE_ARRAY_GET.load(Ordering::Relaxed); - if ptr.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } - let func: JsHandleArrayGetFn = unsafe { std::mem::transmute(ptr) }; - func(array_handle, index) -} - -/// Get length of a JS handle array. Dispatches through the function pointer -/// set by perry-jsruntime, or returns 0 if JS runtime not loaded. -#[no_mangle] -pub extern "C" fn js_handle_array_length(array_handle: f64) -> i32 { - let ptr = JS_HANDLE_ARRAY_LENGTH.load(Ordering::Relaxed); - if ptr.is_null() { - return 0; - } - let func: JsHandleArrayLengthFn = unsafe { std::mem::transmute(ptr) }; - func(array_handle) -} - -/// Try to load a property from a native module via V8 JS runtime. -/// Returns TAG_UNDEFINED if JS runtime is not available or property not found. -pub fn native_module_try_js_property(module_name: &str, property_name: &str) -> f64 { - let loader_ptr = JS_NATIVE_MODULE_JS_LOADER.load(Ordering::Relaxed); - if loader_ptr.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } - let loader: JsNativeModuleJsLoaderFn = unsafe { std::mem::transmute(loader_ptr) }; - unsafe { - loader( - module_name.as_ptr(), - module_name.len(), - property_name.as_ptr(), - property_name.len(), - ) - } -} - -/// Check if a NaN-boxed value is a JS handle -#[inline] -pub fn is_js_handle(value: f64) -> bool { - let bits = value.to_bits(); - (bits & TAG_MASK) == JS_HANDLE_TAG -} - -/// A JavaScript value using NaN-boxing representation -#[derive(Clone, Copy)] -#[repr(transparent)] -pub struct JSValue { - bits: u64, -} - -impl JSValue { - /// Create undefined value - #[inline] - pub const fn undefined() -> Self { - Self { - bits: TAG_UNDEFINED, - } - } - - /// Create null value - #[inline] - pub const fn null() -> Self { - Self { bits: TAG_NULL } - } - - /// Create a boolean value - #[inline] - pub const fn bool(value: bool) -> Self { - Self { - bits: if value { TAG_TRUE } else { TAG_FALSE }, - } - } - - /// Create an f64 number value - #[inline] - pub fn number(value: f64) -> Self { - // Just reinterpret the bits - f64 values are stored directly - Self { - bits: value.to_bits(), - } - } - - /// Create an i32 value (stored in payload, faster than f64 for integers) - #[inline] - pub const fn int32(value: i32) -> Self { - Self { - bits: INT32_TAG | ((value as u32) as u64), - } - } - - /// Create a pointer value (for heap-allocated objects) - #[inline] - pub fn pointer(ptr: *const u8) -> Self { - debug_assert!( - (ptr as u64) <= POINTER_MASK, - "Pointer too large for NaN-boxing" - ); - Self { - bits: POINTER_TAG | (ptr as u64 & POINTER_MASK), - } - } - - /// Check if this is a number (not a tagged value) - #[inline] - pub fn is_number(&self) -> bool { - // A value is a number if upper 16 bits are not in our tagged range 0x7FFC-0x7FFF - // This allows IEEE NaN (0x7FF8), negative numbers, and all other f64 through - let upper = self.bits >> 48; - !(0x7FFC..=0x7FFF).contains(&upper) - } - - /// Check if this is undefined - #[inline] - pub fn is_undefined(&self) -> bool { - self.bits == TAG_UNDEFINED - } - - /// Check if this is null - #[inline] - pub fn is_null(&self) -> bool { - self.bits == TAG_NULL - } - - /// Check if this is a boolean - #[inline] - pub fn is_bool(&self) -> bool { - self.bits == TAG_TRUE || self.bits == TAG_FALSE - } - - /// Check if this is an int32 - #[inline] - pub fn is_int32(&self) -> bool { - (self.bits & !INT32_MASK) == INT32_TAG - } - - /// Check if this is a pointer (object or array) - #[inline] - pub fn is_pointer(&self) -> bool { - (self.bits & !POINTER_MASK) == POINTER_TAG - } - - /// Check if this is a heap-allocated string pointer - /// (STRING_TAG only — inline SSO values return false). This is - /// the legacy predicate that most call sites rely on: they - /// follow `is_string()` with `as_string_ptr()` assuming a real - /// `*mut StringHeader`. Keeping this strict avoids a massive - /// audit during the SSO rollout; use `is_any_string()` when - /// you want to accept both representations. - #[inline] - pub fn is_string(&self) -> bool { - (self.bits & !POINTER_MASK) == STRING_TAG - } - - /// Accepts both heap `STRING_TAG` pointers and inline - /// `SHORT_STRING_TAG` values. Use this for general "is this a - /// string?" checks that don't care about representation — - /// e.g., `typeof x === "string"`, string equality ops, string - /// concatenation. Paired with `short_string_to_buf()` / - /// `as_string_ptr()` on the respective branches to read the - /// data. - #[inline] - pub fn is_any_string(&self) -> bool { - let tag = self.bits & TAG_MASK; - tag == STRING_TAG || tag == SHORT_STRING_TAG - } - - /// Check if this is specifically an inline SSO string. - #[inline] - pub fn is_short_string(&self) -> bool { - (self.bits & TAG_MASK) == SHORT_STRING_TAG - } - - /// Check if this is a BigInt pointer - #[inline] - pub fn is_bigint(&self) -> bool { - (self.bits & !POINTER_MASK) == BIGINT_TAG - } - - /// Get as f64 (panics if not a number) - #[inline] - pub fn as_number(&self) -> f64 { - debug_assert!(self.is_number(), "Value is not a number"); - f64::from_bits(self.bits) - } - - /// Get as bool (panics if not a boolean) - #[inline] - pub fn as_bool(&self) -> bool { - debug_assert!(self.is_bool(), "Value is not a boolean"); - self.bits == TAG_TRUE - } - - /// Get as i32 (panics if not an int32) - #[inline] - pub fn as_int32(&self) -> i32 { - debug_assert!(self.is_int32(), "Value is not an int32"); - (self.bits & INT32_MASK) as i32 - } - - /// Get as pointer (panics if not a pointer) - #[inline] - pub fn as_pointer(&self) -> *const T { - debug_assert!(self.is_pointer(), "Value is not a pointer"); - (self.bits & POINTER_MASK) as *const T - } - - /// Convert to f64, coercing if necessary - pub fn to_number(&self) -> f64 { - if self.is_number() { - self.as_number() - } else if self.is_int32() { - self.as_int32() as f64 - } else if self.is_bool() { - if self.as_bool() { - 1.0 - } else { - 0.0 - } - } else if self.is_null() { - 0.0 - } else if self.is_undefined() { - f64::NAN - } else { - // Pointer types would need object-specific conversion - f64::NAN - } - } - - /// Convert to boolean (JS truthiness) - pub fn to_bool(&self) -> bool { - if self.is_bool() { - self.as_bool() - } else if self.is_number() { - let n = self.as_number(); - n != 0.0 && !n.is_nan() - } else if self.is_int32() { - self.as_int32() != 0 - } else if self.is_null() || self.is_undefined() { - false - } else { - // Pointers (objects) are truthy - true - } - } - - /// Raw bits access (for debugging) - #[inline] - pub fn bits(&self) -> u64 { - self.bits - } - - /// Create from raw bits - #[inline] - pub fn from_bits(bits: u64) -> Self { - Self { bits } - } - - /// Create a string pointer value (uses STRING_TAG for type discrimination) - #[inline] - pub fn string_ptr(ptr: *mut crate::string::StringHeader) -> Self { - debug_assert!( - (ptr as u64) <= POINTER_MASK, - "Pointer too large for NaN-boxing" - ); - Self { - bits: STRING_TAG | (ptr as u64 & POINTER_MASK), - } - } - - /// Try to encode a byte slice as an inline SSO string. Returns - /// `Some(Self)` when `bytes.len() <= SHORT_STRING_MAX_LEN`, - /// `None` otherwise. Skips all heap allocation on success. - /// - /// Semantic note: strings containing U+0000 (the NUL byte) are - /// fine — the NUL is stored verbatim in one of the 5 data bytes - /// and the length field is authoritative. Length 0 (the empty - /// string) is a valid SSO value with no data bytes read. - #[inline] - pub fn try_short_string(bytes: &[u8]) -> Option { - if bytes.len() > SHORT_STRING_MAX_LEN { - return None; - } - let mut payload: u64 = 0; - for (i, &b) in bytes.iter().enumerate() { - payload |= (b as u64) << (i * 8); - } - let len_bits = (bytes.len() as u64) << SHORT_STRING_LEN_SHIFT; - Some(Self { - bits: SHORT_STRING_TAG | len_bits | payload, - }) - } - - /// Unconditional SSO constructor. Caller must ensure - /// `bytes.len() <= SHORT_STRING_MAX_LEN`; debug-build panics on - /// violation, release-build truncates silently. - #[inline] - pub fn short_string_unchecked(bytes: &[u8]) -> Self { - debug_assert!(bytes.len() <= SHORT_STRING_MAX_LEN); - Self::try_short_string(bytes).expect("short string must fit SHORT_STRING_MAX_LEN") - } - - /// Extract the byte contents of an inline SSO string into a - /// caller-provided buffer of at least `SHORT_STRING_MAX_LEN` - /// bytes. Returns the actual length. Panics in debug builds if - /// called on a non-SSO value. - #[inline] - pub fn short_string_to_buf(&self, buf: &mut [u8; SHORT_STRING_MAX_LEN]) -> usize { - debug_assert!(self.is_short_string()); - let len = ((self.bits & SHORT_STRING_LEN_MASK) >> SHORT_STRING_LEN_SHIFT) as usize; - let data = self.bits & SHORT_STRING_DATA_MASK; - for i in 0..len { - buf[i] = ((data >> (i * 8)) & 0xFF) as u8; - } - len - } - - /// Return the length of an SSO string (0..=5). - #[inline] - pub fn short_string_len(&self) -> usize { - debug_assert!(self.is_short_string()); - ((self.bits & SHORT_STRING_LEN_MASK) >> SHORT_STRING_LEN_SHIFT) as usize - } - - /// Get string pointer (panics if not a string) - #[inline] - pub fn as_string_ptr(&self) -> *const crate::string::StringHeader { - debug_assert!(self.is_string(), "Value is not a string"); - (self.bits & POINTER_MASK) as *const crate::string::StringHeader - } - - /// Create a BigInt pointer value (uses BIGINT_TAG for type discrimination) - #[inline] - pub fn bigint_ptr(ptr: *mut crate::bigint::BigIntHeader) -> Self { - debug_assert!( - (ptr as u64) <= POINTER_MASK, - "Pointer too large for NaN-boxing" - ); - Self { - bits: BIGINT_TAG | (ptr as u64 & POINTER_MASK), - } - } - - /// Get BigInt pointer (panics if not a BigInt) - #[inline] - pub fn as_bigint_ptr(&self) -> *const crate::bigint::BigIntHeader { - debug_assert!(self.is_bigint(), "Value is not a BigInt"); - (self.bits & POINTER_MASK) as *const crate::bigint::BigIntHeader - } - - /// Create an object pointer value - #[inline] - pub fn object_ptr(ptr: *mut u8) -> Self { - Self::pointer(ptr) - } - - /// Create an array pointer value - #[inline] - pub fn array_ptr(ptr: *mut crate::array::ArrayHeader) -> Self { - Self::pointer(ptr as *const u8) - } -} - -impl std::fmt::Debug for JSValue { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.is_undefined() { - write!(f, "undefined") - } else if self.is_null() { - write!(f, "null") - } else if self.is_bool() { - write!(f, "{}", self.as_bool()) - } else if self.is_number() { - write!(f, "{}", self.as_number()) - } else if self.is_int32() { - write!(f, "{}i", self.as_int32()) - } else if self.is_pointer() { - write!(f, "", self.as_pointer::()) - } else { - write!(f, "", self.bits) - } - } -} - -impl Default for JSValue { - fn default() -> Self { - Self::undefined() - } -} - -// FFI functions for creating NaN-boxed values from raw pointers - -/// Create a NaN-boxed pointer value from an i64 raw pointer. -/// Returns the value as f64 for storage in union-typed variables. -/// If the value already has a NaN-box tag (JS_HANDLE, STRING, POINTER, etc.), -/// it is preserved as-is to prevent tag corruption. -#[no_mangle] -pub extern "C" fn js_nanbox_pointer(ptr: i64) -> f64 { - // Guard: null pointer (ptr == 0) must NOT produce null POINTER_TAG (0x7FFD_0000_0000_0000). - // Null POINTER_TAG causes crashes when code tries to dereference it as a real object pointer. - if ptr == 0 { - return f64::from_bits(TAG_NULL); - } - let bits = ptr as u64; - // If value already has a NaN-box tag (top bits in NaN range), preserve it - if bits & 0xFFF0_0000_0000_0000 >= 0x7FF0_0000_0000_0000 { - return f64::from_bits(bits); - } - let jsval = JSValue::pointer(ptr as *const u8); - f64::from_bits(jsval.bits()) -} - -/// Create a NaN-boxed string pointer value from an i64 raw pointer. -/// Returns the value as f64 for storage in union-typed variables. -/// This uses STRING_TAG (0x7FFF) to distinguish from object pointers. -/// If ptr is null, returns a NaN-boxed empty string to prevent null -/// dereference when callers access .length on the result. -#[no_mangle] -pub extern "C" fn js_nanbox_string(ptr: i64) -> f64 { - let actual_ptr = if ptr == 0 { - // Allocate an empty string instead of boxing null - crate::string::js_string_from_bytes(b"".as_ptr(), 0) as i64 - } else { - ptr - }; - let jsval = JSValue::string_ptr(actual_ptr as *mut crate::string::StringHeader); - f64::from_bits(jsval.bits()) -} - -/// Debug checkpoint function: prints checkpoint number to stderr. -/// Used to narrow down crash locations in generated code. -#[no_mangle] -pub extern "C" fn js_checkpoint(n: i32) { - use std::io::Write; - let mut stderr = std::io::stderr(); - let _ = writeln!(stderr, "[CHECKPOINT] {}", n); - let _ = stderr.flush(); -} - -/// Debug: print a value's raw bits to stderr (for diagnosing NaN-boxing issues) -#[no_mangle] -pub extern "C" fn js_debug_val(label: i32, val: f64) { - use std::io::Write; - let bits = val.to_bits(); - let _ = writeln!( - std::io::stderr(), - "[DEBUG_VAL] label={} bits=0x{:016X} f64={}", - label, - bits, - val - ); - let _ = std::io::stderr().flush(); -} - -/// Create a NaN-boxed BigInt pointer value from an i64 raw pointer. -/// Returns the value as f64 for storage in union-typed variables. -/// This uses BIGINT_TAG (0x7FFA) to distinguish from other pointer types. -#[no_mangle] -pub extern "C" fn js_nanbox_bigint(ptr: i64) -> f64 { - let jsval = JSValue::bigint_ptr(ptr as *mut crate::bigint::BigIntHeader); - f64::from_bits(jsval.bits()) -} - -// ====================================================================== -// Dynamic arithmetic dispatch: handles BigInt vs float at runtime. -// When a parameter has Type::Any (is_union=true), it may hold a BigInt -// (NaN-boxed with BIGINT_TAG) or a regular f64. These functions check -// the NaN-box tag at runtime and dispatch to the correct operation. -// ====================================================================== - -/// Convert a NaN-boxed JSValue to a *mut BigIntHeader for arithmetic. -/// If the value is already a BigInt, extracts the pointer. -/// Otherwise allocates a new BigInt from the f64 value. -#[inline] -unsafe fn coerce_to_bigint_ptr(val: f64) -> *mut crate::bigint::BigIntHeader { - let jsval = JSValue::from_bits(val.to_bits()); - if jsval.is_bigint() { - jsval.as_bigint_ptr() as *mut _ - } else { - crate::bigint::js_bigint_from_f64(val) - } -} - -/// Dynamic multiply: BigInt * BigInt if either operand is BigInt, else f64 * f64. -#[no_mangle] -pub unsafe extern "C" fn js_dynamic_mul(a: f64, b: f64) -> f64 { - let a_val = JSValue::from_bits(a.to_bits()); - let b_val = JSValue::from_bits(b.to_bits()); - if a_val.is_bigint() || b_val.is_bigint() { - let a_ptr = coerce_to_bigint_ptr(a) as *const _; - let b_ptr = coerce_to_bigint_ptr(b) as *const _; - let result = crate::bigint::js_bigint_mul(a_ptr, b_ptr); - return js_nanbox_bigint(result as i64); - } - a * b -} - -/// Dynamic add: BigInt + BigInt if either operand is BigInt, else f64 + f64. -#[no_mangle] -pub unsafe extern "C" fn js_dynamic_add(a: f64, b: f64) -> f64 { - let a_val = JSValue::from_bits(a.to_bits()); - let b_val = JSValue::from_bits(b.to_bits()); - if a_val.is_bigint() || b_val.is_bigint() { - let result = crate::bigint::js_bigint_add( - coerce_to_bigint_ptr(a) as *const _, - coerce_to_bigint_ptr(b) as *const _, - ); - return js_nanbox_bigint(result as i64); - } - a + b -} - -/// Dynamic `a + b` for type-uncertain operands. Per JS spec, when either -/// operand is a string after ToPrimitive, the result is string concatenation; -/// otherwise both operands are coerced to numbers and summed (or BigInt- -/// summed when either is BigInt). The codegen dispatches here for `+` when -/// neither operand has a statically-known type — refs #486 (hono's -/// `Node.buildRegExpStr` does `k + c.buildRegExpStr()` inside a for-of loop -/// over `Object.keys(...)` results, both operands lower to plain f64s with -/// inferred type Any, the static-string-concat fast path doesn't fire, and -/// the previous fallback called `js_number_coerce` on each side and `fadd`d -/// the results — turning `"c" + ""` into `NaN + 0 = NaN`). -#[no_mangle] -pub unsafe extern "C" fn js_dynamic_string_or_number_add(a: f64, b: f64) -> f64 { - let a_val = JSValue::from_bits(a.to_bits()); - let b_val = JSValue::from_bits(b.to_bits()); - - // String concat takes priority: either operand being a string forces - // ToPrimitive on the other side via the spec's "if either is a string, - // do concat" branch. js_string_concat_value handles the - // `string + non-string` case (it calls js_jsvalue_to_string on the - // non-string side); we use it for both orderings by pre-coercing the - // other operand to string via js_jsvalue_to_string when it ISN'T a - // string. - if a_val.is_any_string() || b_val.is_any_string() { - let a_str = if a_val.is_any_string() { - js_get_string_pointer_unified(a) as *mut crate::string::StringHeader - } else { - js_jsvalue_to_string(a) - }; - let b_str = if b_val.is_any_string() { - js_get_string_pointer_unified(b) as *mut crate::string::StringHeader - } else { - js_jsvalue_to_string(b) - }; - let result = crate::string::js_string_concat(a_str, b_str); - return f64::from_bits(JSValue::string_ptr(result).bits()); - } - - // BigInt: same as js_dynamic_add. - if a_val.is_bigint() || b_val.is_bigint() { - let result = crate::bigint::js_bigint_add( - coerce_to_bigint_ptr(a) as *const _, - coerce_to_bigint_ptr(b) as *const _, - ); - return js_nanbox_bigint(result as i64); - } - - // Both numeric — coerce non-numbers (booleans, null, undefined) the - // same way the static fallback path did. - let a_num = if a_val.is_number() || a_val.is_int32() { - a - } else { - crate::builtins::js_number_coerce(a) - }; - let b_num = if b_val.is_number() || b_val.is_int32() { - b - } else { - crate::builtins::js_number_coerce(b) - }; - a_num + b_num -} - -/// Dynamic subtract: BigInt - BigInt if either operand is BigInt, else f64 - f64. -#[no_mangle] -pub unsafe extern "C" fn js_dynamic_sub(a: f64, b: f64) -> f64 { - let a_val = JSValue::from_bits(a.to_bits()); - let b_val = JSValue::from_bits(b.to_bits()); - if a_val.is_bigint() || b_val.is_bigint() { - let result = crate::bigint::js_bigint_sub( - coerce_to_bigint_ptr(a) as *const _, - coerce_to_bigint_ptr(b) as *const _, - ); - return js_nanbox_bigint(result as i64); - } - a - b -} - -/// Dynamic divide: BigInt / BigInt if either operand is BigInt, else f64 / f64. -#[no_mangle] -pub unsafe extern "C" fn js_dynamic_div(a: f64, b: f64) -> f64 { - let a_val = JSValue::from_bits(a.to_bits()); - let b_val = JSValue::from_bits(b.to_bits()); - if a_val.is_bigint() || b_val.is_bigint() { - let result = crate::bigint::js_bigint_div( - coerce_to_bigint_ptr(a) as *const _, - coerce_to_bigint_ptr(b) as *const _, - ); - return js_nanbox_bigint(result as i64); - } - a / b -} - -/// Dynamic modulo: BigInt % BigInt if either operand is BigInt, else f64 % f64. -#[no_mangle] -pub unsafe extern "C" fn js_dynamic_mod(a: f64, b: f64) -> f64 { - let a_val = JSValue::from_bits(a.to_bits()); - let b_val = JSValue::from_bits(b.to_bits()); - if a_val.is_bigint() || b_val.is_bigint() { - let result = crate::bigint::js_bigint_mod( - coerce_to_bigint_ptr(a) as *const _, - coerce_to_bigint_ptr(b) as *const _, - ); - return js_nanbox_bigint(result as i64); - } - // Float modulo: a - trunc(a / b) * b - a - (a / b).trunc() * b -} - -/// Dynamic negate: -BigInt if operand is BigInt, else -f64. -#[no_mangle] -pub unsafe extern "C" fn js_dynamic_neg(a: f64) -> f64 { - let a_val = JSValue::from_bits(a.to_bits()); - if a_val.is_bigint() { - let result = crate::bigint::js_bigint_neg(a_val.as_bigint_ptr()); - return js_nanbox_bigint(result as i64); - } - -a -} - -/// Dynamic right shift: BigInt >> if either operand is BigInt, else i32 >> for numbers. -#[no_mangle] -pub unsafe extern "C" fn js_dynamic_shr(a: f64, b: f64) -> f64 { - let a_val = JSValue::from_bits(a.to_bits()); - let b_val = JSValue::from_bits(b.to_bits()); - if a_val.is_bigint() || b_val.is_bigint() { - let result = crate::bigint::js_bigint_shr( - coerce_to_bigint_ptr(a) as *const _, - coerce_to_bigint_ptr(b) as *const _, - ); - return js_nanbox_bigint(result as i64); - } - // JS ToInt32: f64 -> i64 -> i32 (wrapping), NOT f64 -> i32 (saturating). - // Rust `f64 as i32` saturates at i32::MAX for values >= 2^31, but JS wraps. - let ai = (a as i64) as i32; - let bi = ((b as i64) as i32) & 0x1f; - (ai >> bi) as f64 -} - -/// Dynamic left shift: BigInt << if either operand is BigInt, else i32 << for numbers. -#[no_mangle] -pub unsafe extern "C" fn js_dynamic_shl(a: f64, b: f64) -> f64 { - let a_val = JSValue::from_bits(a.to_bits()); - let b_val = JSValue::from_bits(b.to_bits()); - if a_val.is_bigint() || b_val.is_bigint() { - let result = crate::bigint::js_bigint_shl( - coerce_to_bigint_ptr(a) as *const _, - coerce_to_bigint_ptr(b) as *const _, - ); - return js_nanbox_bigint(result as i64); - } - // JS ToInt32: f64 -> i64 -> i32 (wrapping), NOT f64 -> i32 (saturating). - let ai = (a as i64) as i32; - let bi = ((b as i64) as i32) & 0x1f; - (ai << bi) as f64 -} - -/// Dynamic bitwise AND: BigInt & if either operand is BigInt, else i32 & for numbers. -#[no_mangle] -pub unsafe extern "C" fn js_dynamic_bitand(a: f64, b: f64) -> f64 { - let a_val = JSValue::from_bits(a.to_bits()); - let b_val = JSValue::from_bits(b.to_bits()); - if a_val.is_bigint() || b_val.is_bigint() { - let result = crate::bigint::js_bigint_and( - coerce_to_bigint_ptr(a) as *const _, - coerce_to_bigint_ptr(b) as *const _, - ); - return js_nanbox_bigint(result as i64); - } - // JS ToInt32: f64 -> i64 -> i32 (wrapping), NOT f64 -> i32 (saturating). - (((a as i64) as i32) & ((b as i64) as i32)) as f64 -} - -/// Dynamic bitwise OR: BigInt | if either operand is BigInt, else i32 | for numbers. -#[no_mangle] -pub unsafe extern "C" fn js_dynamic_bitor(a: f64, b: f64) -> f64 { - let a_val = JSValue::from_bits(a.to_bits()); - let b_val = JSValue::from_bits(b.to_bits()); - if a_val.is_bigint() || b_val.is_bigint() { - let result = crate::bigint::js_bigint_or( - coerce_to_bigint_ptr(a) as *const _, - coerce_to_bigint_ptr(b) as *const _, - ); - return js_nanbox_bigint(result as i64); - } - // JS ToInt32: f64 -> i64 -> i32 (wrapping), NOT f64 -> i32 (saturating). - (((a as i64) as i32) | ((b as i64) as i32)) as f64 -} - -/// Dynamic bitwise XOR: BigInt ^ if either operand is BigInt, else i32 ^ for numbers. -#[no_mangle] -pub unsafe extern "C" fn js_dynamic_bitxor(a: f64, b: f64) -> f64 { - let a_val = JSValue::from_bits(a.to_bits()); - let b_val = JSValue::from_bits(b.to_bits()); - if a_val.is_bigint() || b_val.is_bigint() { - let result = crate::bigint::js_bigint_xor( - coerce_to_bigint_ptr(a) as *const _, - coerce_to_bigint_ptr(b) as *const _, - ); - return js_nanbox_bigint(result as i64); - } - // JS ToInt32: f64 -> i64 -> i32 (wrapping), NOT f64 -> i32 (saturating). - (((a as i64) as i32) ^ ((b as i64) as i32)) as f64 -} - -/// Check if an f64 value (interpreted as NaN-boxed) represents a BigInt. -#[no_mangle] -pub extern "C" fn js_nanbox_is_bigint(value: f64) -> i32 { - let jsval = JSValue::from_bits(value.to_bits()); - if jsval.is_bigint() { - 1 - } else { - 0 - } -} - -/// Extract a BigInt pointer from a NaN-boxed f64 value. -/// Returns the pointer as i64. -#[no_mangle] -pub extern "C" fn js_nanbox_get_bigint(value: f64) -> i64 { - let bits = value.to_bits(); - let jsval = JSValue::from_bits(bits); - if jsval.is_bigint() { - return jsval.as_bigint_ptr() as i64; - } - if value.is_nan() { - return 0; - } - bits as i64 -} - -/// Check if an f64 value (interpreted as NaN-boxed) represents a pointer. -#[no_mangle] -pub extern "C" fn js_nanbox_is_pointer(value: f64) -> i32 { - let jsval = JSValue::from_bits(value.to_bits()); - if jsval.is_pointer() { - 1 - } else { - 0 - } -} - -/// Extract a pointer from a NaN-boxed f64 value. -/// Also handles raw pointer bits (bitcast from i64) for backward compatibility. -/// Handles POINTER_TAG, STRING_TAG, BIGINT_TAG, and JS_HANDLE_TAG. -/// Returns the pointer as i64. -#[no_mangle] -pub extern "C" fn js_nanbox_get_pointer(value: f64) -> i64 { - let bits = value.to_bits(); - let jsval = JSValue::from_bits(bits); - - if jsval.is_pointer() { - return jsval.as_pointer::() as i64; - } - - if jsval.is_string() { - return jsval.as_string_ptr() as i64; - } - - if jsval.is_bigint() { - return jsval.as_bigint_ptr() as i64; - } - - // JS_HANDLE_TAG (0x7FFB): used for V8 handles and Perry UI widget handles - // when values pass through inline_nanbox_pointer's "already tagged" path. - if (bits & TAG_MASK) == JS_HANDLE_TAG { - return (bits & POINTER_MASK) as i64; - } - - if bits != 0 && bits <= POINTER_MASK { - let upper = bits >> 48; - if upper == 0 || (upper > 0 && upper < 0x7FF0) { - return bits as i64; - } - } - - 0 -} - -/// Returns the pointer as i64. -#[no_mangle] -pub extern "C" fn js_nanbox_get_string_pointer(value: f64) -> i64 { - let jsval = JSValue::from_bits(value.to_bits()); - if jsval.is_string() { - jsval.as_string_ptr() as i64 - } else { - 0 - } -} - -/// Extract a string pointer from an f64 value that may be either: -/// 1. A properly NaN-boxed string (with STRING_TAG) -/// 2. A raw pointer bitcast to f64 (for locally-created strings) -/// This unified function handles both cases for function parameters. -#[no_mangle] -pub extern "C" fn js_get_string_pointer_unified(value: f64) -> i64 { - let bits = value.to_bits(); - let jsval = JSValue::from_bits(bits); - - // Check if it's a properly NaN-boxed string (STRING_TAG = 0x7FFF) - if jsval.is_string() { - return jsval.as_string_ptr() as i64; - } - - // SSO inline value (SHORT_STRING_TAG = 0x7FF9) — caller wants a - // `*const StringHeader`, so materialize the inline bytes onto the - // heap. Pre-fix this fell through every branch (SSO bits are NaN - // so the raw-pointer / number-to-string fallbacks rejected it), - // returned 0, and any consumer that did - // `js_string_equals(handle_a, handle_b)` saw "one side is null - // → not equal" — which is why `JSON.parse(...).foo === "perry"` - // returned false (SSO === heap string mixed compare). Materialize - // here defeats the SSO win for the comparison path but is the - // smallest-blast-radius correctness fix; future codegen sites can - // avoid the alloc by routing through `js_jsvalue_equals` directly. - if jsval.is_short_string() { - return crate::string::js_string_materialize_to_heap(value) as i64; - } - - // Check if it's a POINTER_TAG (0x7FFD) NaN-boxed pointer (used for cross-module returns) - if jsval.is_pointer() { - return (bits & 0x0000_FFFF_FFFF_FFFF) as i64; - } - - // Raw pointer fallback: only accept values that look like valid heap pointers. - // Must be non-NaN, non-zero, within 48-bit address space, AND at least 4-byte aligned. - // The alignment check prevents subnormal f64 numbers like 2.16e-314 (bits=0x1100000003) - // from being misidentified as pointers. - if !value.is_nan() && bits != 0 && bits < 0x0001_0000_0000_0000 { - // Must be at least 4-byte aligned (StringHeader starts with u32 length) - // and above minimum heap address - if (bits & 0x3) == 0 && bits >= 0x10000 { - return bits as i64; - } - } - - // For numeric values used as property keys (e.g., obj[pool.id], obj[Direction.Up]), - // convert the number to a string representation. - // Note: 0.0 (bits == 0) is a valid number that should produce "0", so we must - // NOT skip it. The bits != 0 guard above is only for the raw-pointer fallback. - if !value.is_nan() { - let s = crate::string::js_number_to_string(value); - if !s.is_null() { - return s as i64; - } - } - - 0 -} - -/// Check if a NaN-boxed f64 value represents a string. -#[no_mangle] -pub extern "C" fn js_nanbox_is_string(value: f64) -> i32 { - let jsval = JSValue::from_bits(value.to_bits()); - if jsval.is_string() { - 1 - } else { - 0 - } -} - -/// Tag-aware dynamic index dispatch for `obj[key]` where `obj` has unknown -/// static type. Issue #514. Strings → js_string_char_at; everything else -/// uses the same `raw_ptr + 8 + idx*8` direct-read offset hack the existing -/// IndexGet fallback uses (which happens to be load-bearing for -/// Object-with-numeric-keys + TypedArrays). LAZY_ARRAY / FORWARDED arrays -/// route through `js_array_get_f64` to chase the materialized chain. -#[no_mangle] -pub extern "C" fn js_dyn_index_get(value: f64, index: f64) -> f64 { - let bits = value.to_bits(); - let jsval = JSValue::from_bits(bits); - if jsval.is_string() || jsval.is_short_string() { - let s_ptr = js_get_string_pointer_unified(value) as *const crate::StringHeader; - if s_ptr.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } - let idx_i32 = if index.is_nan() || index.is_infinite() { - 0 - } else { - index as i32 - }; - let result = crate::string::js_string_char_at(s_ptr, idx_i32); - if result.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } - return f64::from_bits(JSValue::string_ptr(result).bits()); - } - let raw_ptr = if jsval.is_pointer() { - (bits & POINTER_MASK) as usize - } else if !value.is_nan() - && bits != 0 - && bits < 0x0001_0000_0000_0000 - && (bits & 0x3) == 0 - && bits >= 0x10000 - { - bits as usize - } else { - return f64::from_bits(TAG_UNDEFINED); - }; - if raw_ptr < 0x10000 { - return f64::from_bits(TAG_UNDEFINED); - } - // Issue #957: if the index itself is a string, route through the - // by-name object getter. Pre-fix, `obj["foo"]` lowered through - // `IndexUpdate` re-entered this helper with a NaN-boxed string index - // and the `index as i32` coercion produced garbage offsets, so - // `++obj["foo"]` silently returned undefined. - let idx_bits = index.to_bits(); - let idx_top16 = idx_bits >> 48; - if idx_top16 == 0x7FFF || idx_top16 == 0x7FF9 { - let key_ptr = js_get_string_pointer_unified(index) as *const crate::StringHeader; - if !key_ptr.is_null() { - return crate::object::js_object_get_field_by_name_f64( - raw_ptr as *const crate::object::ObjectHeader, - key_ptr, - ); - } - return f64::from_bits(TAG_UNDEFINED); - } - let idx_i32 = if index.is_nan() || index.is_infinite() { - return f64::from_bits(TAG_UNDEFINED); - } else { - index as i32 - }; - if idx_i32 < 0 { - return f64::from_bits(TAG_UNDEFINED); - } - // Registry-backed Buffer (`Buffer.from(...)`, `js_buffer_alloc`, the - // `'data'`-event chunk an http/net listener receives). These carry NO - // GcHeader (see `crates/perry-runtime/src/buffer.rs` — "Buffers carry - // no GcHeader") and store one byte per element after an 8-byte - // `BufferHeader { length, capacity }`. The generic fall-through below - // does `raw_ptr - GC_HEADER_SIZE` to read an `obj_type` that doesn't - // exist for a buffer (garbage that never matches GC_TYPE_ARRAY), then - // reads an 8-byte f64 at `raw_ptr + 8 + idx*8` straight out of the - // buffer's 1-byte-per-element data region — `chunk[0]` came back as a - // denormal/garbage f64 that printed `0`, while `.toString()` / - // `.length` / `Array.from(chunk)` (which all probe BUFFER_REGISTRY) - // were correct. Probe the registry first and read the byte the same - // way the working accessors do (`js_buffer_get` → `buffer_data()`). - // Node semantics: in-range → the byte (0..255); out-of-range → undefined. - if crate::buffer::is_registered_buffer(raw_ptr) { - let buf = raw_ptr as *const crate::buffer::BufferHeader; - let len = unsafe { (*buf).length }; - if (idx_i32 as u32) >= len { - return f64::from_bits(TAG_UNDEFINED); - } - let byte_val = crate::buffer::js_buffer_get(buf, idx_i32); - return byte_val as f64; - } - if raw_ptr >= crate::gc::GC_HEADER_SIZE { - let gc_hdr = unsafe { - (raw_ptr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader - }; - let obj_type = unsafe { (*gc_hdr).obj_type }; - let gc_flags = unsafe { (*gc_hdr).gc_flags }; - if obj_type == crate::gc::GC_TYPE_LAZY_ARRAY - || (gc_flags & crate::gc::GC_FLAG_FORWARDED) != 0 - { - let arr = raw_ptr as *const crate::array::ArrayHeader; - return crate::array::js_array_get_f64(arr, idx_i32 as u32); - } - // Issue #1069: bounds-check regular arrays so out-of-range reads - // return TAG_UNDEFINED instead of whatever's in the slot. Without - // this, an empty (or short) array — most visibly the synthetic - // `arguments` array bundled by the call-site for caller arity 0 — - // returns the raw 0.0 slot value because `js_array_alloc` rounds - // capacity up to MIN_ARRAY_CAPACITY and the unchecked load reads - // past `length` into zeroed-but-allocated storage. `arguments[0]` - // on `function f() { arguments[0] }; f()` printed `0` instead of - // `undefined`. The narrow gate (GC_TYPE_ARRAY) keeps object - // numeric-key fast path unchanged. - if obj_type == crate::gc::GC_TYPE_ARRAY { - let arr = raw_ptr as *const crate::array::ArrayHeader; - let length = unsafe { (*arr).length }; - if (idx_i32 as u32) >= length { - return f64::from_bits(TAG_UNDEFINED); - } - } - } - let elem_addr = raw_ptr.wrapping_add(8 + (idx_i32 as usize) * 8); - let v = unsafe { *(elem_addr as *const f64) }; - if v.to_bits() == crate::value::TAG_HOLE { - return f64::from_bits(TAG_UNDEFINED); - } - v -} - -/// Issue #957 — tag-aware dynamic index write counterpart to -/// `js_dyn_index_get`. Used by `Expr::IndexUpdate` codegen to write back -/// the incremented value without duplicating the IndexSet dispatch tree. -/// -/// Routes by the receiver's `gc_type` byte: arrays go through -/// `js_array_set_index_or_string` (numeric/string-key spec dispatch); -/// everything else stringifies the index and routes through -/// `js_object_set_field_by_name`. Strings are immutable — no-op (matches -/// strict-mode `s[i] = x` semantics, close enough for the `++result[key]` -/// pattern this is added for). -#[no_mangle] -pub extern "C" fn js_dyn_index_set(obj: f64, index: f64, value: f64) -> f64 { - let bits = obj.to_bits(); - let jsval = JSValue::from_bits(bits); - if jsval.is_string() || jsval.is_short_string() { - return value; - } - let raw_ptr = if jsval.is_pointer() { - (bits & POINTER_MASK) as usize - } else if !obj.is_nan() - && bits != 0 - && bits < 0x0001_0000_0000_0000 - && (bits & 0x3) == 0 - && bits >= 0x10000 - { - bits as usize - } else { - return value; - }; - if raw_ptr < crate::gc::GC_HEADER_SIZE + 0x1000 { - return value; - } - let is_array = unsafe { - let gc_header = - (raw_ptr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - (*gc_header).obj_type == crate::gc::GC_TYPE_ARRAY - }; - if is_array { - crate::array::js_array_set_index_or_string( - raw_ptr as *mut crate::array::ArrayHeader, - index, - value, - ); - return value; - } - // Non-array object: stringify the index and write via the object setter. - let bits = index.to_bits(); - let top16 = bits >> 48; - let key_ptr: *const crate::StringHeader = if top16 == 0x7FFF { - (bits & 0x0000_FFFF_FFFF_FFFF) as *const crate::StringHeader - } else if top16 == 0x7FF9 { - crate::value::js_get_string_pointer_unified(index) as *const crate::StringHeader - } else { - // Numeric (or other) index — stringify and intern as a UTF-8 key. - let idx_i32 = if index.is_nan() || index.is_infinite() { - 0 - } else { - index as i32 - }; - let s = idx_i32.to_string(); - crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32) - }; - if key_ptr.is_null() { - return value; - } - crate::object::js_object_set_field_by_name( - raw_ptr as *mut crate::object::ObjectHeader, - key_ptr, - value, - ); - value -} - -/// Check if a value should trigger a destructuring default. -/// Returns 1 if the value is TAG_UNDEFINED, or a bare IEEE NaN (e.g., from -/// out-of-bounds array read), 0 otherwise. All other NaN-boxed values -/// (strings, pointers, booleans, etc.) return 0 because their NaN payload -/// does not match NaN or TAG_UNDEFINED exactly. -#[no_mangle] -pub extern "C" fn js_is_undefined_or_bare_nan(value: f64) -> i32 { - let bits = value.to_bits(); - // TAG_UNDEFINED = 0x7FFC_0000_0000_0001 - if bits == 0x7FFC_0000_0000_0001 { - return 1; - } - // Bare IEEE NaN (0.0/0.0) — produced by OOB array reads - // Canonical NaN is 0x7FF8_0000_0000_0000 on most platforms - if bits == 0x7FF8_0000_0000_0000 { - return 1; - } - 0 -} - -/// Convert a NaN-boxed f64 value to a string pointer. -/// Handles all value types: strings (extract pointer), numbers (convert), JS handles, etc. -#[no_mangle] -pub extern "C" fn js_jsvalue_to_string(value: f64) -> *mut crate::string::StringHeader { - // Check for JS handle first - these come from the JS runtime (e.g., process.env values) - if is_js_handle(value) { - let func_ptr = JS_HANDLE_TO_STRING.load(Ordering::SeqCst); - if !func_ptr.is_null() { - let func: JsHandleToStringFn = unsafe { std::mem::transmute(func_ptr) }; - return func(value); - } - // Fallback if no handler registered - return crate::string::js_string_from_bytes(b"[JS Handle]".as_ptr(), 11); - } - - let jsval = JSValue::from_bits(value.to_bits()); - - if jsval.is_string() { - // Already a heap string — return the pointer directly. - jsval.as_string_ptr() as *mut crate::string::StringHeader - } else if jsval.is_short_string() { - // Inline SSO — materialize into a heap StringHeader so the - // caller gets a uniform `*mut StringHeader`. This defeats - // the SSO benefit for this particular conversion, but it's - // a correctness-preserving compatibility shim for the many - // call sites that currently expect a heap pointer. - crate::string::js_string_materialize_to_heap(value) - } else if jsval.is_undefined() { - crate::string::js_string_from_bytes(b"undefined".as_ptr(), 9) - } else if jsval.is_null() { - crate::string::js_string_from_bytes(b"null".as_ptr(), 4) - } else if jsval.is_bool() { - if jsval.as_bool() { - crate::string::js_string_from_bytes(b"true".as_ptr(), 4) - } else { - crate::string::js_string_from_bytes(b"false".as_ptr(), 5) - } - } else if jsval.is_int32() { - // Convert int32 to string - let n = jsval.as_int32(); - let s = n.to_string(); - crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32) - } else if jsval.is_bigint() { - // BigInt - convert to decimal string - let ptr = jsval.as_bigint_ptr(); - crate::bigint::js_bigint_to_string(ptr) - } else if jsval.is_pointer() { - // Pointer: could be an array, object, or other heap type. Arrays - // stringify via `Array.prototype.join(",")` per JS semantics; other - // objects fall back to "[object Object]". - let ptr: *const u8 = jsval.as_pointer(); - if !ptr.is_null() && (ptr as usize) >= 0x10000 { - // Symbols: detect via the side-table before any GC header read. - if crate::symbol::is_registered_symbol(ptr as usize) { - return unsafe { - crate::symbol::js_symbol_to_string(value) as *mut crate::string::StringHeader - }; - } - // Consult `[Symbol.toPrimitive]("string")` if the object has a - // custom toPrimitive method registered in the symbol side-table. - // A changed result means the user-defined method produced a - // string-hint primitive — recurse so strings pass through as-is - // and numbers get js_number_to_string. - let primitive = unsafe { crate::symbol::js_to_primitive(value, 2) }; - if primitive.to_bits() != value.to_bits() { - return js_jsvalue_to_string(primitive); - } - // Buffers: BufferHeader has no GC header, so we must detect via - // BUFFER_REGISTRY before computing gc_header (which would read - // garbage one word before the buffer). `Buffer.toString()` with - // no arg defaults to UTF-8 — Node prints the raw bytes. - if crate::buffer::is_registered_buffer(ptr as usize) { - return crate::buffer::js_buffer_to_string( - ptr as *const crate::buffer::BufferHeader, - 0, - ); - } - unsafe { - let gc_header = ptr.sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*gc_header).obj_type == crate::gc::GC_TYPE_ARRAY { - // Use js_array_join with a "," separator to match Array.prototype.toString. - let sep = crate::string::js_string_from_bytes(b",".as_ptr(), 1); - return crate::array::js_array_join( - ptr as *const crate::array::ArrayHeader, - sep as *const crate::string::StringHeader, - ); - } - } - } - crate::string::js_string_from_bytes(b"[object Object]".as_ptr(), 15) - } else { - // Regular number - use js_number_to_string - crate::string::js_number_to_string(value) - } -} - -/// Convert a NaN-boxed f64 value to a string with the given radix. -/// Handles BigInt (uses bigint_to_string_radix), numbers, strings, etc. -#[no_mangle] -pub extern "C" fn js_jsvalue_to_string_radix( - value: f64, - radix: i32, -) -> *mut crate::string::StringHeader { - let jsval = JSValue::from_bits(value.to_bits()); - - if jsval.is_bigint() { - let ptr = jsval.as_bigint_ptr(); - crate::bigint::js_bigint_to_string_radix(ptr, radix) - } else if jsval.is_string() { - jsval.as_string_ptr() as *mut crate::string::StringHeader - } else if jsval.is_int32() { - let n = jsval.as_int32(); - let s = if radix == 16 { - format!("{:x}", n) - } else if radix == 10 || radix == 0 { - n.to_string() - } else { - // General radix conversion - let mut result = String::new(); - let mut val = if n < 0 { -(n as i64) as u64 } else { n as u64 }; - let r = radix as u64; - if val == 0 { - return crate::string::js_string_from_bytes(b"0".as_ptr(), 1); - } - while val > 0 { - let digit = (val % r) as u8; - result.push(if digit < 10 { - (b'0' + digit) as char - } else { - (b'a' + digit - 10) as char - }); - val /= r; - } - if n < 0 { - result.push('-'); - } - let s: String = result.chars().rev().collect(); - return crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); - }; - crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32) - } else { - // Regular f64 number - let n = value; - if n.is_nan() { - return crate::string::js_string_from_bytes(b"NaN".as_ptr(), 3); - } - if n.is_infinite() { - if n > 0.0 { - return crate::string::js_string_from_bytes(b"Infinity".as_ptr(), 8); - } else { - return crate::string::js_string_from_bytes(b"-Infinity".as_ptr(), 9); - } - } - if radix == 10 || radix == 0 { - return crate::string::js_number_to_string(value); - } - // For hex and other radixes, convert via integer - let n_i64 = n as i64; - let s = if radix == 16 { - if n_i64 < 0 { - format!("-{:x}", -n_i64) - } else { - format!("{:x}", n_i64) - } - } else { - let mut result = String::new(); - let mut val = if n_i64 < 0 { - (-n_i64) as u64 - } else { - n_i64 as u64 - }; - let r = radix as u64; - if val == 0 { - return crate::string::js_string_from_bytes(b"0".as_ptr(), 1); - } - while val > 0 { - let digit = (val % r) as u8; - result.push(if digit < 10 { - (b'0' + digit) as char - } else { - (b'a' + digit - 10) as char - }); - val /= r; - } - if n_i64 < 0 { - result.push('-'); - } - result.chars().rev().collect() - }; - crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32) - } -} - -/// Ensure a value is a native string pointer. -/// This is specifically for fetch headers where we need to handle: -/// 1. Raw string pointers (literal strings - f64 bits ARE the pointer) -/// 2. NaN-boxed strings (STRING_TAG) -/// 3. JS handle strings (from process.env) -/// Returns the string pointer as i64. -#[no_mangle] -pub extern "C" fn js_ensure_string_ptr(value: f64) -> i64 { - let bits = value.to_bits(); - - // Check for JS handle first - these need conversion - if is_js_handle(value) { - let func_ptr = JS_HANDLE_TO_STRING.load(Ordering::SeqCst); - if !func_ptr.is_null() { - let func: JsHandleToStringFn = unsafe { std::mem::transmute(func_ptr) }; - return func(value) as i64; - } - // Fallback - create a placeholder string - return crate::string::js_string_from_bytes(b"[JS Handle]".as_ptr(), 11) as i64; - } - - // Check for NaN-boxed string (STRING_TAG) - if (bits & TAG_MASK) == STRING_TAG { - let ptr = (bits & POINTER_MASK) as i64; - if ptr != 0 { - let str_header = ptr as *const crate::string::StringHeader; - unsafe { - let length = (*str_header).byte_len; - // Make a copy of the string to ensure we have a Perry-allocated string - let data_ptr = (str_header as *const u8) - .add(std::mem::size_of::()); - let copy = crate::string::js_string_from_bytes(data_ptr, length); - return copy as i64; - } - } - return ptr; - } - - // Otherwise, treat the f64 bits directly as a pointer (raw string literal) - bits as i64 -} - -/// Compare two NaN-boxed f64 values for equality (JavaScript `===` semantics). -/// SameValueZero algorithm (ECMA-262) — used by `Array.prototype.includes`, -/// `Map` / `Set` keys. Same as strict equality except `NaN` is considered -/// equal to itself. Returns 1 if equal, 0 if not. -#[no_mangle] -pub extern "C" fn js_jsvalue_same_value_zero(a: f64, b: f64) -> i32 { - // NaN-equals-NaN under SameValueZero (the only difference from ===). - let abits = a.to_bits(); - let bbits = b.to_bits(); - if (abits >> 48) == 0x7FF8 && (bbits >> 48) == 0x7FF8 { - return 1; - } - js_jsvalue_equals(a, b) -} - -/// Handles string comparison by comparing actual string contents. -/// Handles BigInt comparison by comparing underlying bigint values (not pointers). -/// Returns 1 if equal, 0 if not. -#[no_mangle] -pub extern "C" fn js_jsvalue_equals(a: f64, b: f64) -> i32 { - let abits = a.to_bits(); - let bbits = b.to_bits(); - - // NaN === NaN is false in JS (strict equality follows IEEE 754). - // Raw IEEE NaN has top16 = 0x7FF8 (the canonical quiet-NaN). NaN- - // boxed tagged values use top16 0x7FFA–0x7FFF and never collide. - // Must come BEFORE the abits==bbits fast path: `[1, NaN, 3].indexOf(NaN)` - // routes through this helper, both sides decode to f64::NAN (same - // bit pattern), and pre-fix the fast path returned 1 (wrongly equal) - // so indexOf reported index 1 instead of -1. - let a_top16 = abits >> 48; - let b_top16 = bbits >> 48; - if a_top16 == 0x7FF8 && b_top16 == 0x7FF8 { - return 0; - } - - // Fast path: same bit pattern → equal (same number, same pointer, same boolean, etc.) - if abits == bbits { - return 1; - } - - let a_val = JSValue::from_bits(abits); - let b_val = JSValue::from_bits(bbits); - - // BigInt comparison: compare by value, not by pointer - // Two BigInt allocations with the same value must be equal under === - if a_val.is_bigint() && b_val.is_bigint() { - let a_ptr = a_val.as_bigint_ptr(); - let b_ptr = b_val.as_bigint_ptr(); - return crate::bigint::js_bigint_eq(a_ptr, b_ptr); - } - - // String comparison: compare by content, not by pointer. Must - // accept both STRING_TAG heap strings and SHORT_STRING_TAG - // inline SSO values, in any combination. - if a_val.is_any_string() && b_val.is_any_string() { - // Fast path: both SSO → identical bits ↔ identical content, - // because SSO encoding is canonical (same bytes + same - // length ⇒ same bit pattern). - if a_val.is_short_string() && b_val.is_short_string() { - return if abits == bbits { 1 } else { 0 }; - } - // Decode each side to a (ptr, len) view via a stack scratch - // buffer for the SSO side; compare by bytes. - let mut a_scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - let mut b_scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - let a_view = crate::string::str_bytes_from_jsvalue(a, &mut a_scratch); - let b_view = crate::string::str_bytes_from_jsvalue(b, &mut b_scratch); - if let (Some((a_ptr, a_len)), Some((b_ptr, b_len))) = (a_view, b_view) { - if a_len != b_len { - return 0; - } - if a_len == 0 { - return 1; - } - unsafe { - let a_slice = std::slice::from_raw_parts(a_ptr, a_len as usize); - let b_slice = std::slice::from_raw_parts(b_ptr, b_len as usize); - return if a_slice == b_slice { 1 } else { 0 }; - } - } - return 0; - } - - // Helper: check if bits represent a plain IEEE 754 number (not a NaN-boxed tagged value). - // NaN-boxing uses tags 0x7FF8-0x7FFF in the upper 16 bits. Regular numbers (positive, - // negative, zero, infinities) have upper16 outside this range. Negative numbers have - // sign bit set (upper16 >= 0x8000), so the old check `bits < 0x7FF8...` missed them. - #[inline(always)] - fn is_plain_number(bits: u64) -> bool { - let tag = bits >> 48; - !(0x7FF8..=0x7FFF).contains(&tag) - } - - // INT32 comparison: one or both operands may be NaN-boxed INT32 (0x7FFE tag). - // Convert INT32 to f64 for numeric comparison (e.g., INT32(5) === 5.0 should be true). - // This mirrors the conversion in js_jsvalue_compare. - if a_val.is_int32() || b_val.is_int32() { - let af = if a_val.is_int32() { - a_val.as_int32() as f64 - } else if is_plain_number(abits) { - a - } else { - return 0; - }; // non-numeric type → not equal - let bf = if b_val.is_int32() { - b_val.as_int32() as f64 - } else if is_plain_number(bbits) { - b - } else { - return 0; - }; // non-numeric type → not equal - return if af == bf { 1 } else { 0 }; - } - - // Regular f64 numbers (not NaN-boxed): use IEEE 754 equality - // This handles -0.0 === 0.0 correctly (both are equal per IEEE 754) - // Also correctly handles NaN !== NaN (IEEE 754 NaN comparison returns false) - if is_plain_number(abits) && is_plain_number(bbits) { - return if a == b { 1 } else { 0 }; - } - - // Different types or different NaN-boxed values → not equal - 0 -} - -/// JS Abstract Equality Comparison (==). -/// Implements the type coercion rules from ECMA-262 §7.2.14: -/// - null == undefined → true -/// - string == number → ToNumber(string) == number -/// - boolean == anything → ToNumber(boolean) == anything -/// - Same type → strict equality -#[no_mangle] -pub extern "C" fn js_jsvalue_loose_equals(a: f64, b: f64) -> i32 { - let abits = a.to_bits(); - let bbits = b.to_bits(); - - // Fast path: same bit pattern - if abits == bbits { - return 1; - } - - let a_val = JSValue::from_bits(abits); - let b_val = JSValue::from_bits(bbits); - - // null == undefined (and vice versa) - let a_null = a_val.is_null() || a_val.is_undefined(); - let b_null = b_val.is_null() || b_val.is_undefined(); - if a_null && b_null { - return 1; - } - // null/undefined != anything else - if a_null || b_null { - return 0; - } - - #[inline(always)] - fn is_plain_number(bits: u64) -> bool { - let tag = bits >> 48; - !(0x7FF8..=0x7FFF).contains(&tag) - } - - // Helper: convert a JSValue to f64 for numeric comparison - fn to_number(val: &JSValue, bits: u64, raw: f64) -> Option { - if val.is_int32() { - Some(val.as_int32() as f64) - } else if is_plain_number(bits) { - Some(raw) - } else if val.is_bool() { - Some(if val.as_bool() { 1.0 } else { 0.0 }) - } else if val.is_string() { - let ptr = val.as_string_ptr(); - if ptr.is_null() { - return Some(f64::NAN); - } - let header = unsafe { &*ptr }; - let s = unsafe { - let data = - (ptr as *const u8).add(std::mem::size_of::()); - std::str::from_utf8_unchecked(std::slice::from_raw_parts( - data, - header.byte_len as usize, - )) - }; - let trimmed = s.trim(); - if trimmed.is_empty() { - Some(0.0) - } else { - trimmed.parse::().ok() - } - } else { - None - } - } - - // If both are same type, delegate to strict equals - let a_is_num = a_val.is_int32() || is_plain_number(abits); - let b_is_num = b_val.is_int32() || is_plain_number(bbits); - let a_is_str = a_val.is_string(); - let b_is_str = b_val.is_string(); - let a_is_bool = a_val.is_bool(); - let b_is_bool = b_val.is_bool(); - - // Both strings: strict string comparison - if a_is_str && b_is_str { - let a_ptr = a_val.as_string_ptr(); - let b_ptr = b_val.as_string_ptr(); - return crate::string::js_string_equals(a_ptr, b_ptr); - } - - // Both numbers: numeric comparison - if a_is_num && b_is_num { - let af = if a_val.is_int32() { - a_val.as_int32() as f64 - } else { - a - }; - let bf = if b_val.is_int32() { - b_val.as_int32() as f64 - } else { - b - }; - return if af == bf { 1 } else { 0 }; - } - - // Boolean == anything: convert boolean to number, then recurse - if a_is_bool { - let a_num = if a_val.as_bool() { 1.0 } else { 0.0 }; - return js_jsvalue_loose_equals(a_num, b); - } - if b_is_bool { - let b_num = if b_val.as_bool() { 1.0 } else { 0.0 }; - return js_jsvalue_loose_equals(a, b_num); - } - - // String == Number: convert string to number - if a_is_str && b_is_num { - if let Some(af) = to_number(&a_val, abits, a) { - let bf = if b_val.is_int32() { - b_val.as_int32() as f64 - } else { - b - }; - return if af == bf { 1 } else { 0 }; - } - return 0; - } - if a_is_num && b_is_str { - if let Some(bf) = to_number(&b_val, bbits, b) { - let af = if a_val.is_int32() { - a_val.as_int32() as f64 - } else { - a - }; - return if af == bf { 1 } else { 0 }; - } - return 0; - } - - // BigInt comparisons - if a_val.is_bigint() && b_val.is_bigint() { - let a_ptr = a_val.as_bigint_ptr(); - let b_ptr = b_val.as_bigint_ptr(); - return crate::bigint::js_bigint_eq(a_ptr, b_ptr); - } - - 0 -} - -/// Compare two JSValues for relational ordering (< <= > >=). -/// Returns -1 if a < b, 0 if a == b, 1 if a > b. -/// Handles BigInt, String, Number, and INT32 types. -#[no_mangle] -pub extern "C" fn js_jsvalue_compare(a: f64, b: f64) -> i32 { - let abits = a.to_bits(); - let bbits = b.to_bits(); - - let a_val = JSValue::from_bits(abits); - let b_val = JSValue::from_bits(bbits); - - // BigInt comparison - if a_val.is_bigint() && b_val.is_bigint() { - let a_ptr = a_val.as_bigint_ptr(); - let b_ptr = b_val.as_bigint_ptr(); - return crate::bigint::js_bigint_cmp(a_ptr, b_ptr); - } - - // String comparison (lexicographic). Accepts SSO in either - // operand — decode via `str_bytes_from_jsvalue` into stack - // scratch, then compare slices. - if a_val.is_any_string() && b_val.is_any_string() { - let mut a_scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - let mut b_scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - let a_view = crate::string::str_bytes_from_jsvalue(a, &mut a_scratch); - let b_view = crate::string::str_bytes_from_jsvalue(b, &mut b_scratch); - if let (Some((a_ptr, a_len)), Some((b_ptr, b_len))) = (a_view, b_view) { - if (!a_ptr.is_null() || a_len == 0) && (!b_ptr.is_null() || b_len == 0) { - unsafe { - let a_bytes = std::slice::from_raw_parts(a_ptr, a_len as usize); - let b_bytes = std::slice::from_raw_parts(b_ptr, b_len as usize); - return match a_bytes.cmp(b_bytes) { - std::cmp::Ordering::Less => -1, - std::cmp::Ordering::Equal => 0, - std::cmp::Ordering::Greater => 1, - }; - } - } - } - } - - // INT32 comparison - if a_val.is_int32() && b_val.is_int32() { - let ai = a_val.as_int32(); - let bi = b_val.as_int32(); - return if ai < bi { - -1 - } else if ai > bi { - 1 - } else { - 0 - }; - } - - // Convert to f64 for numeric comparison (handles Number, INT32 mixed with Number, etc.) - // Return 2 (sentinel) for undefined/null — makes all comparisons false - // Convert to f64 — use tag check that correctly handles negative numbers - // (sign bit set → upper16 >= 0x8000, which is > 0x7FFF, not a NaN-box tag) - let a_tag = abits >> 48; - let b_tag = bbits >> 48; - let af = if a_val.is_int32() { - a_val.as_int32() as f64 - } else if a_val.is_bigint() { - crate::bigint::js_bigint_to_f64(a_val.as_bigint_ptr()) - } else if !(0x7FF8..=0x7FFF).contains(&a_tag) { - a - } else { - return 2; - }; // undefined/null/boolean → incomparable sentinel - let bf = if b_val.is_int32() { - b_val.as_int32() as f64 - } else if b_val.is_bigint() { - crate::bigint::js_bigint_to_f64(b_val.as_bigint_ptr()) - } else if !(0x7FF8..=0x7FFF).contains(&b_tag) { - b - } else { - return 2; - }; // undefined/null/boolean → incomparable sentinel - - if af < bf { - -1 - } else if af > bf { - 1 - } else { - 0 - } -} - -/// Check if a JavaScript value is truthy. -/// In JavaScript, the following values are falsy: -/// - false -/// - 0 (and -0) -/// - NaN -/// - "" (empty string) -/// - null -/// - undefined -/// Everything else is truthy. -/// Returns 1 if truthy, 0 if falsy. -#[no_mangle] -pub extern "C" fn js_is_truthy(value: f64) -> i32 { - let bits = value.to_bits(); - - // Check for special tagged values first - if bits == TAG_UNDEFINED || bits == TAG_NULL || bits == TAG_FALSE { - return 0; - } - - // TAG_TRUE is truthy - if bits == TAG_TRUE { - return 1; - } - - // Check for NaN-boxed string (empty string is falsy) - if (bits & TAG_MASK) == STRING_TAG { - let str_ptr = (bits & POINTER_MASK) as *const crate::string::StringHeader; - if str_ptr.is_null() { - return 0; - } - // Empty string is falsy - let len = crate::string::js_string_length(str_ptr); - if len == 0 { - return 0; - } - return 1; - } - - // Check for NaN-boxed pointer (objects/arrays are always truthy) - if (bits & TAG_MASK) == POINTER_TAG { - // Null pointer (0x7FFD_0000_0000_0000) is falsy — like null in JS - if (bits & POINTER_MASK) == 0 { - return 0; - } - return 1; - } - - // Check for BigInt (0n is falsy, non-zero is truthy) - if (bits & !POINTER_MASK) == BIGINT_TAG { - let ptr = (bits & POINTER_MASK) as *const u8; - if ptr.is_null() { - return 0; - } - return if crate::bigint::js_bigint_is_zero(ptr as *const crate::bigint::BigIntHeader) != 0 { - 0 - } else { - 1 - }; - } - - // Check for JS handle (always truthy - they represent objects) - if (bits & TAG_MASK) == JS_HANDLE_TAG { - return 1; - } - - // Check for int32 tag - if (bits & TAG_MASK) == INT32_TAG { - let int_val = (bits & INT32_MASK) as i32; - return if int_val == 0 { 0 } else { 1 }; - } - - // Check for SHORT_STRING_TAG (inline SSO strings): falsy iff length is 0. - // Without this branch SSO empties would fall through to the f64 path, - // produce a non-zero non-NaN value, and report truthy. - if (bits & TAG_MASK) == SHORT_STRING_TAG { - let len = (bits & SHORT_STRING_LEN_MASK) >> SHORT_STRING_LEN_SHIFT; - return if len == 0 { 0 } else { 1 }; - } - - // Check for raw pointer bits (from bitcast of string literal). This is a - // legacy path for compiled-in string literals that were emitted as raw - // pointer bitcasts rather than NaN-boxed STRING_TAG values. - // - // CAUTION: a plain f64 value (e.g., a denormal like `f64::from_bits(0x646e)` - // ≈ 1.27e-319, or any other number whose bit pattern happens to fall in - // the userspace pointer range) must NOT be misidentified as a string - // pointer — dereferencing it as `*StringHeader` will SIGSEGV. Surfaced by - // dayjs which passes `bits=0x646e` through here on a utility-object call. - // - // Defense in depth: - // 1. Reject everything below the smallest realistic heap address. On - // macOS/Linux userspace heaps live well above 0x10_0000 (1 MiB); the - // previous 0x1000 (4 KiB) threshold let small integers (including - // 0x646e) through. - // 2. Require 8-byte alignment. `StringHeader` is `repr(C)` with at - // least one usize-aligned field, so a valid pointer must have its - // low 3 bits clear. Most small denormal/integer bit patterns fail - // this check. - // Both filters together make a false-positive astronomically unlikely - // while still preserving the legacy bitcast path for real pointers. - if bits >= 0x10_0000 && bits < 0x0001_0000_0000_0000 && (bits & 0x7) == 0 { - // This could be a raw string pointer - check if it's a valid string - let str_ptr = bits as *const crate::string::StringHeader; - // Try to read the string length - empty string is falsy - let len = crate::string::js_string_length(str_ptr); - if len == 0 { - return 0; - } - return 1; - } - - // Regular f64 number: 0.0, -0.0, and NaN are falsy - if value == 0.0 || value.is_nan() { - return 0; - } - - // Everything else is truthy - 1 -} - -/// Dynamic string comparison that handles both NaN-boxed strings and raw pointer bitcasts. -/// This is needed when comparing a PropertyGet result (NaN-boxed) with a string literal (raw bitcast). -/// Returns 1 if equal, 0 if not. -#[no_mangle] -pub extern "C" fn js_dynamic_string_equals(a: f64, b: f64) -> i32 { - // Extract string pointers from both values, handling both representations - let a_ptr = extract_string_ptr(a); - let b_ptr = extract_string_ptr(b); - - if a_ptr.is_null() && b_ptr.is_null() { - return 1; - } - if a_ptr.is_null() || b_ptr.is_null() { - return 0; - } - - if crate::string::js_string_equals(a_ptr, b_ptr) != 0 { - 1 - } else { - 0 - } -} - -/// Extract a string pointer from an f64 value that might be: -/// - NaN-boxed with STRING_TAG -/// - NaN-boxed with POINTER_TAG (for strings stored as generic pointers) -/// - Raw pointer bits (from bitcast) -fn extract_string_ptr(value: f64) -> *const crate::StringHeader { - let bits = value.to_bits(); - let jsval = JSValue::from_bits(bits); - - // Check for STRING_TAG first (e.g., from PropertyGet) - if jsval.is_string() { - return jsval.as_string_ptr(); - } - - // Check for POINTER_TAG (generic pointer that might be a string) - if jsval.is_pointer() { - return jsval.as_pointer::(); - } - - // Assume raw pointer bits (from bitcast of string literal) - // In a 64-bit system, valid heap pointers are typically in the range - // 0x0000_0000_0000_0000 to 0x0000_7FFF_FFFF_FFFF - // Check if it looks like a valid pointer (not NaN, not a small number) - if bits > 0x1000 && bits < 0x0001_0000_0000_0000 { - return bits as *const crate::StringHeader; - } - - std::ptr::null() -} - -/// Unified index access that handles strings, arrays, and JS handles. -/// This is called from compiled code when the value type is not known at compile time. -/// For strings, returns the character at the given index as a NaN-boxed string. -/// For arrays, returns the element at the given index. -#[no_mangle] -pub extern "C" fn js_dynamic_array_get(value: f64, index: i32) -> f64 { - let bits = value.to_bits(); - let jsval = JSValue::from_bits(bits); - - // Check if this is a NaN-boxed string - if jsval.is_string() { - // String character access - let str_ptr = jsval.as_string_ptr(); - if !str_ptr.is_null() && index >= 0 { - let result_ptr = crate::string::js_string_char_at(str_ptr, index); - if !result_ptr.is_null() { - // NaN-box the result string pointer - return f64::from_bits(STRING_TAG | (result_ptr as u64 & POINTER_MASK)); - } - } - // Return empty string for invalid index - let empty = crate::string::js_string_from_bytes(std::ptr::null(), 0); - return f64::from_bits(STRING_TAG | (empty as u64 & POINTER_MASK)); - } - - // Check if this is a JS handle - if is_js_handle(value) { - // Try to use the JS runtime function if it's been registered - let func_ptr = JS_HANDLE_ARRAY_GET.load(Ordering::SeqCst); - if !func_ptr.is_null() { - let func: JsHandleArrayGetFn = unsafe { std::mem::transmute(func_ptr) }; - return func(value, index); - } - // JS runtime not available - return undefined - return f64::from_bits(TAG_UNDEFINED); - } - - // Not a JS handle - it's a native array/buffer pointer - let ptr = js_nanbox_get_pointer(value); - if ptr == 0 { - // Invalid pointer - return undefined - return f64::from_bits(TAG_UNDEFINED); - } - - // Check if this is a buffer (Uint8Array) - read individual bytes, not f64 values - if crate::buffer::is_registered_buffer(ptr as usize) { - let byte_val = - crate::buffer::js_buffer_get(ptr as *const crate::buffer::BufferHeader, index); - return byte_val as f64; - } - - // Call the native array get function - let result_bits = - crate::array::js_array_get_jsvalue(ptr as *const crate::array::ArrayHeader, index as u32); - let _result_top16 = result_bits >> 48; - // debug: DYNAMIC-ARRAY-GET-DEBUG disabled - f64::from_bits(result_bits) -} - -/// Unified array length access that handles both JS handle arrays and native arrays. -#[no_mangle] -pub extern "C" fn js_dynamic_array_length(arr_value: f64) -> i32 { - let bits = arr_value.to_bits(); - let _top16 = bits >> 48; - - // Check if this is a JS handle - if is_js_handle(arr_value) { - let func_ptr = JS_HANDLE_ARRAY_LENGTH.load(Ordering::SeqCst); - if !func_ptr.is_null() { - let func: JsHandleArrayLengthFn = unsafe { std::mem::transmute(func_ptr) }; - return func(arr_value); - } - return 0; - } - - // Not a JS handle - extract the pointer - let ptr = js_nanbox_get_pointer(arr_value); - if ptr == 0 { - return 0; - } - - crate::array::js_array_length(ptr as *const crate::array::ArrayHeader) as i32 -} - -/// Issue #73: safe `.length` lookup by runtime type. Called from the -/// inline PropertyGet length path when the GC-type-byte check at -/// `handle-8` doesn't prove the receiver is a GC_TYPE_ARRAY or -/// GC_TYPE_STRING. Routes by runtime registry / GC header so that a -/// Named-typed receiver that turns out to hold a Buffer, TypedArray, -/// Closure, Error, number, etc. at runtime returns a sensible length -/// instead of dereferencing garbage at `recv & 0xFFFFFFFFFFFF`. -/// -/// Returns a double so the inline caller can phi the fast and slow -/// results without another conversion. -#[no_mangle] -pub extern "C" fn js_value_length_f64(value: f64) -> f64 { - let bits = value.to_bits(); - let top16 = bits >> 48; - - // SHORT_STRING_TAG (SSO) — length is the byte count stored in - // bits 40..=47. Fast path, no heap access. For multibyte UTF-8 - // content the byte length and UTF-16 code-unit count differ, - // but SSO strings are ≤5 bytes and the vast majority are ASCII - // where they match. Non-ASCII SSO values go through a slower - // full-parse path — tolerated because the distinction doesn't - // come up in practice for 5-byte strings. - if top16 == 0x7FF9 { - return ((bits & SHORT_STRING_LEN_MASK) >> SHORT_STRING_LEN_SHIFT) as f64; - } - - // STRING_TAG — length is code-unit count from js_string_length. - if top16 == 0x7FFF { - let ptr = (bits & POINTER_MASK) as *const crate::string::StringHeader; - if ptr.is_null() || (ptr as usize) < 0x10000 { - return 0.0; - } - return crate::string::js_string_length(ptr) as f64; - } - - // POINTER_TAG — Buffer / TypedArray via registries first (they - // don't have GC headers — `buffer_alloc` + `typed_array_alloc` - // use `std::alloc` directly). Falling through to the GC-header - // path would read mimalloc bookkeeping as obj_type and return - // nonsense. - if top16 == 0x7FFD { - let handle = (bits & POINTER_MASK) as usize; - // Heap window: Darwin mimalloc lands in 3-5 TB, but Android scudo, - // Linux glibc, and Windows mimalloc all allocate much lower (often - // hundreds of GB or less). Using the Darwin-tight 2 TB floor on - // Android / Windows null-s every real pointer. See clean_arr_ptr - // for the same platform split. - #[cfg(any(target_os = "android", target_os = "linux", target_os = "windows"))] - let heap_min: usize = 0x1000; - #[cfg(not(any(target_os = "android", target_os = "linux", target_os = "windows")))] - let heap_min: usize = 0x200_0000_0000; - if handle < heap_min || handle >= 0x8000_0000_0000 { - return 0.0; - } - if crate::buffer::is_registered_buffer(handle) { - let buf = handle as *const crate::buffer::BufferHeader; - return unsafe { (*buf).length as f64 }; - } - if crate::typedarray::lookup_typed_array_kind(handle).is_some() { - let ta = handle as *const crate::typedarray::TypedArrayHeader; - return unsafe { (*ta).length as f64 }; - } - let gc_header = (handle - crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - let obj_type = unsafe { (*gc_header).obj_type }; - // Issue #233: a FORWARDED array's first 4 bytes are no longer - // length but the lower 32 bits of the forwarding pointer. - // Follow the chain via the unified array-pointer cleaner so - // `samples.length` after a grow returns the real length. - if obj_type == crate::gc::GC_TYPE_ARRAY - && unsafe { (*gc_header).gc_flags } & crate::gc::GC_FLAG_FORWARDED != 0 - { - let cleaned = crate::array::js_array_get_length(handle as i64); - return cleaned as f64; - } - match obj_type { - crate::gc::GC_TYPE_ARRAY | crate::gc::GC_TYPE_STRING => { - return unsafe { *(handle as *const u32) } as f64; - } - // Issue #179 Phase 2: lazy arrays also have `length` at - // offset 0 (cached_length). The inline-length codegen - // only recognizes GC_TYPE_ARRAY/STRING in its check so - // lazy values land here via the slow path — read the - // u32 from offset 0 just like regular arrays. - crate::gc::GC_TYPE_LAZY_ARRAY => { - return unsafe { *(handle as *const u32) } as f64; - } - // Closures, BigInts, Promises, Errors, plain Objects, Maps: - // no `.length`. Return 0 to match Perry's existing - // fallback for missing fields (JS would produce - // `undefined`, but the generic PropertyGet slow path - // already degrades to 0 here). - _ => return 0.0, - } - } - - // Raw pointer bitcast to f64 (no NaN-box tag — top16 == 0). - // TypedArrays are allocated via `std::alloc` and the codegen - // sometimes hands their pointer through as `bitcast i64 → double` - // without a POINTER_TAG. Without this path, `Int32Array.length` - // returned 0 because the value's top16 was 0, not 0x7FFD. - #[cfg(any(target_os = "android", target_os = "linux", target_os = "windows"))] - let raw_heap_min: u64 = 0x1000; - #[cfg(not(any(target_os = "android", target_os = "linux", target_os = "windows")))] - let raw_heap_min: u64 = 0x200_0000_0000; - if top16 == 0 && bits >= raw_heap_min && bits < 0x8000_0000_0000 { - let handle = bits as usize; - if crate::buffer::is_registered_buffer(handle) { - let buf = handle as *const crate::buffer::BufferHeader; - return unsafe { (*buf).length as f64 }; - } - if crate::typedarray::lookup_typed_array_kind(handle).is_some() { - let ta = handle as *const crate::typedarray::TypedArrayHeader; - return unsafe { (*ta).length as f64 }; - } - } - - // Everything else — undefined, null, booleans, int32, plain - // doubles, BigInt pointers — has no `.length`. - 0.0 -} - -/// Dynamic array find that handles both JS handle arrays and native arrays. -/// Takes the array as f64 (may be NaN-boxed or JS handle) and a callback closure. -/// Returns the found element as f64, or NaN (undefined) if not found. -#[no_mangle] -pub extern "C" fn js_dynamic_array_find( - arr_value: f64, - callback: *const crate::closure::ClosureHeader, -) -> f64 { - // Check if callback is null - if callback.is_null() { - return f64::NAN; - } - - // Check if this is a JS handle array - if is_js_handle(arr_value) { - // For JS handle arrays, iterate using dynamic access - let length = js_dynamic_array_length(arr_value); - for i in 0..length { - let element = js_dynamic_array_get(arr_value, i); - let result = crate::closure::js_closure_call1(callback, element); - // Proper truthy check: handles NaN-boxed booleans - if js_is_truthy(result) != 0 { - return element; - } - } - // Not found - return undefined (NaN) - return f64::NAN; - } - - // Not a JS handle - extract the native array pointer - let ptr = js_nanbox_get_pointer(arr_value); - if ptr == 0 { - return f64::NAN; - } - - // Use the native array find - crate::array::js_array_find(ptr as *const crate::array::ArrayHeader, callback) -} - -/// Dynamic array findIndex that handles both JS handle arrays and native arrays. -/// Takes the array as f64 (may be NaN-boxed or JS handle) and a callback closure. -/// Returns the index as f64 (-1.0 if not found). -#[no_mangle] -pub extern "C" fn js_dynamic_array_findIndex( - arr_value: f64, - callback: *const crate::closure::ClosureHeader, -) -> f64 { - // Check if this is a JS handle array - if is_js_handle(arr_value) { - // For JS handle arrays, iterate using dynamic access - let length = js_dynamic_array_length(arr_value); - for i in 0..length { - let element = js_dynamic_array_get(arr_value, i); - let result = crate::closure::js_closure_call1(callback, element); - // Proper truthy check: handles NaN-boxed booleans - if js_is_truthy(result) != 0 { - return i as f64; - } - } - // Not found - return -1.0; - } - - // Not a JS handle - extract the native array pointer - let ptr = js_nanbox_get_pointer(arr_value); - if ptr == 0 { - return -1.0; - } - - // Use the native array findIndex and convert to f64 - crate::array::js_array_findIndex(ptr as *const crate::array::ArrayHeader, callback) as f64 -} - -/// Unified object property access that handles both JS handle objects and native objects. -/// Also handles strings for property access like `.length`. -#[no_mangle] -pub unsafe extern "C" fn js_dynamic_object_get_property( - obj_value: f64, - property_name_ptr: *const i8, - property_name_len: usize, -) -> f64 { - // Check if this is a JS handle - if is_js_handle(obj_value) { - // Try to use the JS runtime function if it's been registered - let func_ptr = JS_HANDLE_OBJECT_GET_PROPERTY.load(Ordering::SeqCst); - if !func_ptr.is_null() { - let func: JsHandleObjectGetPropertyFn = unsafe { std::mem::transmute(func_ptr) }; - return func(obj_value, property_name_ptr, property_name_len); - } - // JS runtime not available - return undefined - return f64::from_bits(TAG_UNDEFINED); - } - - // Check if this is a NaN-boxed string - handle string properties like .length - let bits = obj_value.to_bits(); - if (bits & TAG_MASK) == STRING_TAG { - let str_ptr = (bits & POINTER_MASK) as *const crate::string::StringHeader; - if !str_ptr.is_null() { - // Get the property name - let name_slice = if property_name_ptr.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } else if property_name_len > 0 { - std::slice::from_raw_parts(property_name_ptr as *const u8, property_name_len) - } else { - std::ffi::CStr::from_ptr(property_name_ptr as *const std::ffi::c_char).to_bytes() - }; - - // Handle string properties - if name_slice == b"length" { - let len = crate::string::js_string_length(str_ptr); - return len as f64; - } - // Other string properties return undefined - return f64::from_bits(TAG_UNDEFINED); - } - } - - // Not a JS handle - it's a native object pointer - let ptr = js_nanbox_get_pointer(obj_value); - - if ptr == 0 { - return f64::from_bits(TAG_UNDEFINED); - } - - // Check if this is a handle-based object (small integer, not a real heap pointer) - if ptr < 0x100000 { - if let Some(dispatch) = crate::object::HANDLE_PROPERTY_DISPATCH { - return dispatch(ptr, property_name_ptr as *const u8, property_name_len); - } - return f64::from_bits(TAG_UNDEFINED); - } - - // Get the key string - let name_slice = if property_name_ptr.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } else if property_name_len > 0 { - std::slice::from_raw_parts(property_name_ptr as *const u8, property_name_len) - } else { - // Null-terminated C string - std::ffi::CStr::from_ptr(property_name_ptr as *const std::ffi::c_char).to_bytes() - }; - - let property_name = match std::str::from_utf8(name_slice) { - Ok(s) => s, - Err(_) => return f64::from_bits(TAG_UNDEFINED), - }; - - // Check if this is a ClosureHeader (CLOSURE_MAGIC at offset 12). - // ClosureHeader layout: func_ptr (8B), capture_count u32 (4B), type_tag u32 (4B), captures at 16+ - // ObjectHeader layout: object_type u32 (4B), class_id u32 (4B), parent_class_id u32 (4B), field_count u32 (4B), keys_array (8B), ... - // Without this check, the closure's capture[0] at offset 16 would be read as keys_array → crash. - if crate::closure::is_closure_ptr(ptr as usize) { - return crate::closure::closure_get_dynamic_prop(ptr as usize, property_name); - } - - // Handle Buffer/Uint8Array properties (buffer, byteOffset, byteLength, length) - // BufferHeader has same layout as ArrayHeader (length u32, capacity u32, data...) - // and doesn't have ObjectHeader fields, so we must check before treating as ObjectHeader. - if crate::buffer::is_registered_buffer(ptr as usize) { - let buf = ptr as *const crate::buffer::BufferHeader; - match property_name { - "length" | "byteLength" => { - return (*buf).length as f64; - } - "byteOffset" => { - return 0.0; - } - "buffer" => { - // Return the buffer itself (Perry doesn't separate ArrayBuffer) - return obj_value; - } - _ => { - return f64::from_bits(TAG_UNDEFINED); - } - } - } - - // Check if this is a registered Map - if crate::map::is_registered_map(ptr as usize) { - let map_ptr = ptr as *const crate::map::MapHeader; - if name_slice == b"size" { - return (*map_ptr).size as f64; - } - return f64::from_bits(TAG_UNDEFINED); - } - - // Check if this is a registered Set - if crate::set::is_registered_set(ptr as usize) { - let set_ptr = ptr as *const crate::set::SetHeader; - if name_slice == b"size" { - return (*set_ptr).size as f64; - } - return f64::from_bits(TAG_UNDEFINED); - } - - // Check the object type tag (first u32 field of both ObjectHeader and ErrorHeader) - let object_type = *(ptr as *const u32); - - // Handle native module namespace objects (e.g., `const fn = fs.lstatSync`) - // Create a bound method closure so the method reference can be called later - let obj_header = ptr as *const crate::object::ObjectHeader; - if (*obj_header).class_id == crate::object::NATIVE_MODULE_CLASS_ID { - return crate::object::js_native_module_bind_method( - obj_value, - property_name.as_ptr(), - property_name.len(), - ); - } - - // Handle Error objects specially - if object_type == crate::error::OBJECT_TYPE_ERROR { - let error_ptr = ptr as *mut crate::error::ErrorHeader; - match property_name { - "message" => { - let msg = crate::error::js_error_get_message(error_ptr); - return js_nanbox_string(msg as i64); - } - "name" => { - let name = crate::error::js_error_get_name(error_ptr); - return js_nanbox_string(name as i64); - } - "stack" => { - let stack = crate::error::js_error_get_stack(error_ptr); - return js_nanbox_string(stack as i64); - } - "cause" => { - return crate::error::js_error_get_cause(error_ptr); - } - "errors" => { - let arr = crate::error::js_error_get_errors(error_ptr); - if arr.is_null() { - return f64::from_bits(TAG_UNDEFINED); - } - return js_nanbox_pointer(arr as i64); - } - _ => { - // Error objects don't have other properties - return f64::from_bits(TAG_UNDEFINED); - } - } - } - - // Check vtable for a registered getter or method before falling back to field lookup - let class_id = (*obj_header).class_id; - if class_id != 0 { - if let Ok(registry) = crate::object::CLASS_VTABLE_REGISTRY.read() { - if let Some(ref reg) = *registry { - if let Some(vtable) = reg.get(&class_id) { - if let Some(&getter_ptr) = vtable.getters.get(property_name) { - // Methods take `this` as f64 (NaN-boxed), not i64. - // On Windows x64 ABI, i64 and f64 use different registers. - let this_f64: f64 = f64::from_bits(ptr as u64); - let f: extern "C" fn(f64) -> f64 = std::mem::transmute(getter_ptr); - return f(this_f64); - } - // If the property is a registered method, return truthy so that - // `if (obj.method)` works (method existence checks). - if vtable.methods.contains_key(property_name) { - return f64::from_bits(TAG_TRUE); - } - } - } - } - } - - // Create a Perry string for the key - let key_ptr = - crate::string::js_string_from_bytes(property_name.as_ptr(), property_name.len() as u32); - - // Call native object property access - - crate::object::js_object_get_field_by_name_f64( - ptr as *const crate::object::ObjectHeader, - key_ptr, - ) -} - -/// Dynamic method dispatch for Map/Set collection types. -/// Checks the magic tag of the object and dispatches known methods. -/// Returns TAG_UNDEFINED if the object is not a Map/Set or method is unknown. -/// This handles cases like `map.get(key).add(value)` where the intermediate -/// result type is unknown at codegen time. -#[no_mangle] -pub unsafe extern "C" fn js_collection_method_dispatch( - obj_value: f64, - method_ptr: *const u8, - method_len: usize, - arg0: f64, - arg1: f64, -) -> f64 { - let ptr = js_nanbox_get_pointer(obj_value); - if ptr == 0 || ptr < 0x10000 { - return f64::from_bits(TAG_UNDEFINED); - } - - let method = std::slice::from_raw_parts(method_ptr, method_len); - - // Check if this is a registered Map - if crate::map::is_registered_map(ptr as usize) { - let map = ptr as *mut crate::map::MapHeader; - return match method { - b"get" => crate::map::js_map_get(map, arg0), - b"set" => { - let result = crate::map::js_map_set(map, arg0, arg1); - js_nanbox_pointer(result as i64) - } - b"has" => crate::map::js_map_has(map, arg0) as f64, - b"delete" => crate::map::js_map_delete(map, arg0) as f64, - b"size" => crate::map::js_map_size(map) as f64, - b"clear" => { - crate::map::js_map_clear(map); - f64::from_bits(TAG_UNDEFINED) - } - b"entries" => { - let arr = crate::map::js_map_entries(map); - js_nanbox_pointer(arr as i64) - } - b"keys" => { - let arr = crate::map::js_map_keys(map); - js_nanbox_pointer(arr as i64) - } - b"values" => { - let arr = crate::map::js_map_values(map); - js_nanbox_pointer(arr as i64) - } - _ => f64::from_bits(TAG_UNDEFINED), - }; - } - - // Check if this is a registered Set - if crate::set::is_registered_set(ptr as usize) { - let set = ptr as *mut crate::set::SetHeader; - return match method { - b"add" => { - let result = crate::set::js_set_add(set, arg0); - js_nanbox_pointer(result as i64) - } - b"has" => crate::set::js_set_has(set, arg0) as f64, - b"delete" => crate::set::js_set_delete(set, arg0) as f64, - b"size" => crate::set::js_set_size(set) as f64, - b"clear" => { - crate::set::js_set_clear(set); - f64::from_bits(TAG_UNDEFINED) - } - _ => f64::from_bits(TAG_UNDEFINED), - }; - } - - f64::from_bits(TAG_UNDEFINED) -} - -/// Dynamic Object.keys() that handles both regular objects and Error objects. -/// Takes a raw pointer (extracted from NaN-boxed value) and returns array of keys. -#[no_mangle] -pub unsafe extern "C" fn js_dynamic_object_keys(ptr: i64) -> *mut crate::array::ArrayHeader { - if ptr == 0 { - return crate::array::js_array_alloc(0); - } - - // Check the object type tag (first u32 field of both ObjectHeader and ErrorHeader) - let object_type = *(ptr as *const u32); - - // Handle Error objects specially - they have fixed keys - if object_type == crate::error::OBJECT_TYPE_ERROR { - // Error objects have keys: "message", "name", "stack" - let keys = crate::array::js_array_alloc(3); - - let msg_key = crate::string::js_string_from_bytes(b"message".as_ptr(), 7); - crate::array::js_array_push(keys, JSValue::string_ptr(msg_key)); - - let name_key = crate::string::js_string_from_bytes(b"name".as_ptr(), 4); - crate::array::js_array_push(keys, JSValue::string_ptr(name_key)); - - let stack_key = crate::string::js_string_from_bytes(b"stack".as_ptr(), 5); - crate::array::js_array_push(keys, JSValue::string_ptr(stack_key)); - - return keys; - } - - // Regular object - delegate to js_object_keys - crate::object::js_object_keys(ptr as *const crate::object::ObjectHeader) -} - -/// Get a property from an object by name. -/// This is the main entry point used by codegen for dynamic property access. -/// Delegates to js_dynamic_object_get_property which handles JS handles, native objects, -/// strings, and error objects. -/// -/// Parameters: -/// - object: NaN-boxed f64 containing the object -/// - name_ptr: i64 pointer to the property name bytes -/// - name_len: i64 length of the property name -/// -/// Returns: NaN-boxed f64 containing the property value (or undefined) -#[no_mangle] -pub unsafe extern "C" fn js_get_property(object: f64, name_ptr: i64, name_len: i64) -> f64 { - js_dynamic_object_get_property(object, name_ptr as *const i8, name_len as usize) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_undefined() { - let v = JSValue::undefined(); - assert!(v.is_undefined()); - assert!(!v.is_null()); - assert!(!v.is_number()); - } - - #[test] - fn test_null() { - let v = JSValue::null(); - assert!(v.is_null()); - assert!(!v.is_undefined()); - } - - #[test] - fn test_bool() { - let t = JSValue::bool(true); - let f = JSValue::bool(false); - assert!(t.is_bool()); - assert!(f.is_bool()); - assert!(t.as_bool()); - assert!(!f.as_bool()); - } - - #[test] - fn test_number() { - let v = JSValue::number(42.5); - assert!(v.is_number()); - assert_eq!(v.as_number(), 42.5); - - let zero = JSValue::number(0.0); - assert!(zero.is_number()); - assert_eq!(zero.as_number(), 0.0); - - let neg = JSValue::number(-123.456); - assert!(neg.is_number()); - assert_eq!(neg.as_number(), -123.456); - } - - #[test] - fn test_int32() { - let v = JSValue::int32(42); - assert!(v.is_int32()); - assert_eq!(v.as_int32(), 42); - - let neg = JSValue::int32(-100); - assert!(neg.is_int32()); - assert_eq!(neg.as_int32(), -100); - } - - #[test] - fn test_truthiness() { - assert!(!JSValue::undefined().to_bool()); - assert!(!JSValue::null().to_bool()); - assert!(!JSValue::bool(false).to_bool()); - assert!(JSValue::bool(true).to_bool()); - assert!(!JSValue::number(0.0).to_bool()); - assert!(JSValue::number(1.0).to_bool()); - assert!(JSValue::number(-1.0).to_bool()); - assert!(!JSValue::number(f64::NAN).to_bool()); - } - - #[test] - fn test_jsvalue_equals_booleans() { - let t = f64::from_bits(TAG_TRUE); - let f = f64::from_bits(TAG_FALSE); - // Same boolean values - assert_eq!(js_jsvalue_equals(t, t), 1); - assert_eq!(js_jsvalue_equals(f, f), 1); - // Different boolean values - assert_eq!(js_jsvalue_equals(t, f), 0); - assert_eq!(js_jsvalue_equals(f, t), 0); - // Boolean vs number (strict equality: different types) - assert_eq!(js_jsvalue_equals(t, 1.0), 0); - assert_eq!(js_jsvalue_equals(f, 0.0), 0); - } - - #[test] - fn test_jsvalue_equals_int32() { - let int5 = f64::from_bits(INT32_TAG | 5); - let float5 = 5.0f64; - let int0 = f64::from_bits(INT32_TAG | 0); - let float0 = 0.0f64; - let int_neg = f64::from_bits(INT32_TAG | ((-3i32 as u32) as u64)); - let float_neg = -3.0f64; - // INT32 vs f64 with same numeric value - assert_eq!(js_jsvalue_equals(int5, float5), 1); - assert_eq!(js_jsvalue_equals(float5, int5), 1); - assert_eq!(js_jsvalue_equals(int0, float0), 1); - assert_eq!(js_jsvalue_equals(int_neg, float_neg), 1); - // INT32 vs INT32 - assert_eq!(js_jsvalue_equals(int5, int5), 1); - // INT32 vs different f64 - assert_eq!(js_jsvalue_equals(int5, 6.0), 0); - assert_eq!(js_jsvalue_equals(int5, 4.0), 0); - } - - #[test] - fn test_short_string_encoding_roundtrip() { - for s in [b"" as &[u8], b"a", b"ab", b"abc", b"abcd", b"abcde"] { - let v = JSValue::try_short_string(s).unwrap(); - assert!(v.is_short_string(), "tag mismatch for {:?}", s); - assert!(v.is_any_string(), "is_any_string should accept SSO"); - assert!(!v.is_string(), "legacy is_string should NOT accept SSO"); - assert_eq!(v.short_string_len(), s.len(), "length mismatch for {:?}", s); - let mut buf = [0u8; SHORT_STRING_MAX_LEN]; - let n = v.short_string_to_buf(&mut buf); - assert_eq!(n, s.len()); - assert_eq!(&buf[..n], s, "bytes mismatch for {:?}", s); - } - } - - #[test] - fn test_short_string_too_long_rejects() { - assert!(JSValue::try_short_string(b"abcdef").is_none()); // 6 bytes - assert!(JSValue::try_short_string(b"hello world").is_none()); // 11 bytes - } - - #[test] - fn test_short_string_embedded_nul_ok() { - // Strings with embedded U+0000 work fine in SSO — length - // is authoritative, NULs are plain data bytes. - let s = &[b'a', 0, b'b', 0, b'c']; - let v = JSValue::try_short_string(s).unwrap(); - assert_eq!(v.short_string_len(), 5); - let mut buf = [0u8; SHORT_STRING_MAX_LEN]; - let n = v.short_string_to_buf(&mut buf); - assert_eq!(&buf[..n], s); - } - - #[test] - fn test_short_string_tag_distinct_from_others() { - // Any valid SSO value must not collide with other NaN-box - // tags. `is_short_string()` is strict — returns false for - // everything except the SSO tag band. - let sso = JSValue::try_short_string(b"abcde").unwrap(); - let heap_string = JSValue { - bits: STRING_TAG | 0x1234, - }; - let pointer = JSValue { - bits: POINTER_TAG | 0x5678, - }; - let int32 = JSValue::int32(42); - let number = JSValue::number(3.14); - let undef = JSValue::undefined(); - assert!(sso.is_short_string()); - assert!(!heap_string.is_short_string()); - assert!(!pointer.is_short_string()); - assert!(!int32.is_short_string()); - assert!(!number.is_short_string()); - assert!(!undef.is_short_string()); - // is_any_string accepts both SSO and heap string, rejects others. - assert!(sso.is_any_string()); - assert!(heap_string.is_any_string()); - assert!(!pointer.is_any_string()); - assert!(!int32.is_any_string()); - assert!(!number.is_any_string()); - } - - #[test] - fn test_short_string_empty_roundtrip() { - let v = JSValue::try_short_string(b"").unwrap(); - assert!(v.is_short_string()); - assert_eq!(v.short_string_len(), 0); - let mut buf = [0u8; SHORT_STRING_MAX_LEN]; - assert_eq!(v.short_string_to_buf(&mut buf), 0); - } - - #[test] - fn test_short_string_byte_order_stability() { - // First byte should land in the least-significant byte of - // the payload. This invariant is relied on by any future - // SIMD-style decoder that bulk-reads the payload. - let v = JSValue::try_short_string(b"abcde").unwrap(); - let payload = v.bits() & SHORT_STRING_DATA_MASK; - assert_eq!((payload & 0xFF) as u8, b'a'); - assert_eq!(((payload >> 8) & 0xFF) as u8, b'b'); - assert_eq!(((payload >> 16) & 0xFF) as u8, b'c'); - assert_eq!(((payload >> 24) & 0xFF) as u8, b'd'); - assert_eq!(((payload >> 32) & 0xFF) as u8, b'e'); - } - - #[test] - fn test_jsvalue_equals_numbers() { - // Same numbers - assert_eq!(js_jsvalue_equals(42.0, 42.0), 1); - assert_eq!(js_jsvalue_equals(0.0, 0.0), 1); - // -0 === 0 is true in JS - assert_eq!(js_jsvalue_equals(-0.0, 0.0), 1); - // Different numbers - assert_eq!(js_jsvalue_equals(1.0, 2.0), 0); - // null/undefined - let null = f64::from_bits(TAG_NULL); - let undef = f64::from_bits(TAG_UNDEFINED); - assert_eq!(js_jsvalue_equals(null, null), 1); - assert_eq!(js_jsvalue_equals(undef, undef), 1); - assert_eq!(js_jsvalue_equals(null, undef), 0); // strict: null !== undefined - assert_eq!(js_jsvalue_equals(null, 0.0), 0); - } -} diff --git a/crates/perry-runtime/src/.value.parked/nanbox.rs b/crates/perry-runtime/src/.value.parked/nanbox.rs deleted file mode 100644 index 0b10c4219e..0000000000 --- a/crates/perry-runtime/src/.value.parked/nanbox.rs +++ /dev/null @@ -1,237 +0,0 @@ -//! NaN-box pack / unpack FFI helpers. -//! -//! These are the smallest building blocks called from generated LLVM IR -//! and from other native code: pointer/string/bigint boxing constructors, -//! the inverse `is_*` / `get_*` predicates, plus debug printers and the -//! unified string-pointer extractor. - -use super::*; - -// FFI functions for creating NaN-boxed values from raw pointers - -/// Create a NaN-boxed pointer value from an i64 raw pointer. -/// Returns the value as f64 for storage in union-typed variables. -/// If the value already has a NaN-box tag (JS_HANDLE, STRING, POINTER, etc.), -/// it is preserved as-is to prevent tag corruption. -#[no_mangle] -pub extern "C" fn js_nanbox_pointer(ptr: i64) -> f64 { - // Guard: null pointer (ptr == 0) must NOT produce null POINTER_TAG (0x7FFD_0000_0000_0000). - // Null POINTER_TAG causes crashes when code tries to dereference it as a real object pointer. - if ptr == 0 { - return f64::from_bits(TAG_NULL); - } - let bits = ptr as u64; - // If value already has a NaN-box tag (top bits in NaN range), preserve it - if bits & 0xFFF0_0000_0000_0000 >= 0x7FF0_0000_0000_0000 { - return f64::from_bits(bits); - } - let jsval = JSValue::pointer(ptr as *const u8); - f64::from_bits(jsval.bits()) -} - -/// Create a NaN-boxed string pointer value from an i64 raw pointer. -/// Returns the value as f64 for storage in union-typed variables. -/// This uses STRING_TAG (0x7FFF) to distinguish from object pointers. -/// If ptr is null, returns a NaN-boxed empty string to prevent null -/// dereference when callers access .length on the result. -#[no_mangle] -pub extern "C" fn js_nanbox_string(ptr: i64) -> f64 { - let actual_ptr = if ptr == 0 { - // Allocate an empty string instead of boxing null - crate::string::js_string_from_bytes(b"".as_ptr(), 0) as i64 - } else { - ptr - }; - let jsval = JSValue::string_ptr(actual_ptr as *mut crate::string::StringHeader); - f64::from_bits(jsval.bits()) -} - -/// Debug checkpoint function: prints checkpoint number to stderr. -/// Used to narrow down crash locations in generated code. -#[no_mangle] -pub extern "C" fn js_checkpoint(n: i32) { - use std::io::Write; - let mut stderr = std::io::stderr(); - let _ = writeln!(stderr, "[CHECKPOINT] {}", n); - let _ = stderr.flush(); -} - -/// Debug: print a value's raw bits to stderr (for diagnosing NaN-boxing issues) -#[no_mangle] -pub extern "C" fn js_debug_val(label: i32, val: f64) { - use std::io::Write; - let bits = val.to_bits(); - let _ = writeln!( - std::io::stderr(), - "[DEBUG_VAL] label={} bits=0x{:016X} f64={}", - label, - bits, - val - ); - let _ = std::io::stderr().flush(); -} - -/// Create a NaN-boxed BigInt pointer value from an i64 raw pointer. -/// Returns the value as f64 for storage in union-typed variables. -/// This uses BIGINT_TAG (0x7FFA) to distinguish from other pointer types. -#[no_mangle] -pub extern "C" fn js_nanbox_bigint(ptr: i64) -> f64 { - let jsval = JSValue::bigint_ptr(ptr as *mut crate::bigint::BigIntHeader); - f64::from_bits(jsval.bits()) -} - -/// Check if an f64 value (interpreted as NaN-boxed) represents a BigInt. -#[no_mangle] -pub extern "C" fn js_nanbox_is_bigint(value: f64) -> i32 { - let jsval = JSValue::from_bits(value.to_bits()); - if jsval.is_bigint() { - 1 - } else { - 0 - } -} - -/// Extract a BigInt pointer from a NaN-boxed f64 value. -/// Returns the pointer as i64. -#[no_mangle] -pub extern "C" fn js_nanbox_get_bigint(value: f64) -> i64 { - let bits = value.to_bits(); - let jsval = JSValue::from_bits(bits); - if jsval.is_bigint() { - return jsval.as_bigint_ptr() as i64; - } - if value.is_nan() { - return 0; - } - bits as i64 -} - -/// Check if an f64 value (interpreted as NaN-boxed) represents a pointer. -#[no_mangle] -pub extern "C" fn js_nanbox_is_pointer(value: f64) -> i32 { - let jsval = JSValue::from_bits(value.to_bits()); - if jsval.is_pointer() { - 1 - } else { - 0 - } -} - -/// Extract a pointer from a NaN-boxed f64 value. -/// Also handles raw pointer bits (bitcast from i64) for backward compatibility. -/// Handles POINTER_TAG, STRING_TAG, BIGINT_TAG, and JS_HANDLE_TAG. -/// Returns the pointer as i64. -#[no_mangle] -pub extern "C" fn js_nanbox_get_pointer(value: f64) -> i64 { - let bits = value.to_bits(); - let jsval = JSValue::from_bits(bits); - - if jsval.is_pointer() { - return jsval.as_pointer::() as i64; - } - - if jsval.is_string() { - return jsval.as_string_ptr() as i64; - } - - if jsval.is_bigint() { - return jsval.as_bigint_ptr() as i64; - } - - // JS_HANDLE_TAG (0x7FFB): used for V8 handles and Perry UI widget handles - // when values pass through inline_nanbox_pointer's "already tagged" path. - if (bits & TAG_MASK) == JS_HANDLE_TAG { - return (bits & POINTER_MASK) as i64; - } - - if bits != 0 && bits <= POINTER_MASK { - let upper = bits >> 48; - if upper == 0 || (upper > 0 && upper < 0x7FF0) { - return bits as i64; - } - } - - 0 -} - -/// Returns the pointer as i64. -#[no_mangle] -pub extern "C" fn js_nanbox_get_string_pointer(value: f64) -> i64 { - let jsval = JSValue::from_bits(value.to_bits()); - if jsval.is_string() { - jsval.as_string_ptr() as i64 - } else { - 0 - } -} - -/// Extract a string pointer from an f64 value that may be either: -/// 1. A properly NaN-boxed string (with STRING_TAG) -/// 2. A raw pointer bitcast to f64 (for locally-created strings) -/// This unified function handles both cases for function parameters. -#[no_mangle] -pub extern "C" fn js_get_string_pointer_unified(value: f64) -> i64 { - let bits = value.to_bits(); - let jsval = JSValue::from_bits(bits); - - // Check if it's a properly NaN-boxed string (STRING_TAG = 0x7FFF) - if jsval.is_string() { - return jsval.as_string_ptr() as i64; - } - - // SSO inline value (SHORT_STRING_TAG = 0x7FF9) — caller wants a - // `*const StringHeader`, so materialize the inline bytes onto the - // heap. Pre-fix this fell through every branch (SSO bits are NaN - // so the raw-pointer / number-to-string fallbacks rejected it), - // returned 0, and any consumer that did - // `js_string_equals(handle_a, handle_b)` saw "one side is null - // → not equal" — which is why `JSON.parse(...).foo === "perry"` - // returned false (SSO === heap string mixed compare). Materialize - // here defeats the SSO win for the comparison path but is the - // smallest-blast-radius correctness fix; future codegen sites can - // avoid the alloc by routing through `js_jsvalue_equals` directly. - if jsval.is_short_string() { - return crate::string::js_string_materialize_to_heap(value) as i64; - } - - // Check if it's a POINTER_TAG (0x7FFD) NaN-boxed pointer (used for cross-module returns) - if jsval.is_pointer() { - return (bits & 0x0000_FFFF_FFFF_FFFF) as i64; - } - - // Raw pointer fallback: only accept values that look like valid heap pointers. - // Must be non-NaN, non-zero, within 48-bit address space, AND at least 4-byte aligned. - // The alignment check prevents subnormal f64 numbers like 2.16e-314 (bits=0x1100000003) - // from being misidentified as pointers. - if !value.is_nan() && bits != 0 && bits < 0x0001_0000_0000_0000 { - // Must be at least 4-byte aligned (StringHeader starts with u32 length) - // and above minimum heap address - if (bits & 0x3) == 0 && bits >= 0x10000 { - return bits as i64; - } - } - - // For numeric values used as property keys (e.g., obj[pool.id], obj[Direction.Up]), - // convert the number to a string representation. - // Note: 0.0 (bits == 0) is a valid number that should produce "0", so we must - // NOT skip it. The bits != 0 guard above is only for the raw-pointer fallback. - if !value.is_nan() { - let s = crate::string::js_number_to_string(value); - if !s.is_null() { - return s as i64; - } - } - - 0 -} - -/// Check if a NaN-boxed f64 value represents a string. -#[no_mangle] -pub extern "C" fn js_nanbox_is_string(value: f64) -> i32 { - let jsval = JSValue::from_bits(value.to_bits()); - if jsval.is_string() { - 1 - } else { - 0 - } -} diff --git a/crates/perry-runtime/src/.value.parked/tags.rs b/crates/perry-runtime/src/.value.parked/tags.rs deleted file mode 100644 index ebbd0d900d..0000000000 --- a/crates/perry-runtime/src/.value.parked/tags.rs +++ /dev/null @@ -1,122 +0,0 @@ -//! NaN-boxing tag constants and shared type aliases. -//! -//! Every tag value here is part of the wire-level contract between the -//! Rust runtime, the LLVM IR emitter in `perry-codegen`, and any external -//! tooling that inspects compiled binaries. Renumbering any constant in -//! this file is an ABI break — keep them verbatim. - -use std::sync::atomic::AtomicPtr; - -/// Tag-marker for the singleton specials (undefined / null / true / false / -/// hole). 0x7FFC chosen so the first two mantissa bits are `11`: that keeps -/// it inside the qNaN encoding space (mantissa bit 51 set) while staying -/// distinct from the canonical qNaN 0x7FF8 the FPU produces from arithmetic -/// like `0/0` — code that wants to tell "Perry tagged" from "real NaN" can -/// gate on `top16 >= 0x7FFC` (see `JSValue::is_number` below). -/// -/// #854: part of the NaN-boxing tag contract documented in CLAUDE.md. -/// Kept as a named constant even when no Rust code consults it directly — -/// codegen, doc references, and external tooling all match against the -/// numeric value. -#[allow(dead_code)] -pub(crate) const TAG_MARKER: u64 = 0x7FFC_0000_0000_0000; - -/// Special singleton values -pub(crate) const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; -pub(crate) const TAG_NULL: u64 = 0x7FFC_0000_0000_0002; -pub(crate) const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003; -pub(crate) const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; - -/// Issue #323: hole sentinel for sparse arrays. Slots in `new Array(n)` are -/// initialized to this value; reads through `js_array_get_f64` translate it -/// back to TAG_UNDEFINED so user code never observes the raw bits, while -/// `Object.keys` and the `in` operator inspect slots directly to distinguish a -/// hole from an explicit `undefined` write. Bits chosen in the same 0x7FFC -/// singleton namespace, distinct from UNDEFINED/NULL/FALSE/TRUE so a NaN-box -/// payload can never be mistaken for a hole. -pub(crate) const TAG_HOLE: u64 = 0x7FFC_0000_0000_0010; - -/// Pointer tag: 0x7FFD_XXXX_XXXX_XXXX (48 bits for pointer) - objects/arrays -pub(crate) const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000; -pub(crate) const POINTER_MASK: u64 = 0x0000_FFFF_FFFF_FFFF; - -/// Int32 tag: 0x7FFE_0000_XXXX_XXXX (32 bits for i32) -pub(crate) const INT32_TAG: u64 = 0x7FFE_0000_0000_0000; -pub(crate) const INT32_MASK: u64 = 0x0000_0000_FFFF_FFFF; - -/// String pointer tag: 0x7FFF_XXXX_XXXX_XXXX (48 bits for string pointer) -pub(crate) const STRING_TAG: u64 = 0x7FFF_0000_0000_0000; - -/// Small String Optimization (SSO) — tier 1 #2 per -/// `docs/memory-perf-roadmap.md`. A string of length 0..=5 bytes -/// encodes inline in the 48-bit NaN-box payload instead of -/// allocating a `StringHeader`. Layout: -/// -/// ```text -/// bits 63........48 47.....40 39...32 31..24 23..16 15..8 7..0 -/// 0x7FF9 tag length byte0 byte1 byte2 byte3 byte4 -/// ``` -/// -/// Length in bits 40..=47 (0..=5 — 6 valid values, 3 bits would -/// suffice but we use a full byte for alignment). Data in bits -/// 0..=39 (5 bytes, little-endian by byte index — `byte0` is the -/// first character). -/// -/// Why 5 bytes not 6: 6 bytes × 8 bits = 48 bits would fill the -/// entire payload leaving no room for length, forcing us to use 3 -/// different tag values for length buckets or a null-terminator -/// convention (which breaks strings containing U+0000). Staying at -/// 5 bytes with one tag keeps decode simple: tag check + 40-bit -/// extract. Covers "id", "name", "age", "true", "false", "null", -/// single-byte ASCII, etc. — a large fraction of real-world JSON -/// keys and short values. -/// -/// Strings with length > 5 fall through to the standard heap -/// `StringHeader` path; callers read-side use `is_string()` (which -/// accepts BOTH tags) + `string_bytes()` (which decodes either -/// form to a (ptr, len) slice view). -pub(crate) const SHORT_STRING_TAG: u64 = 0x7FF9_0000_0000_0000; -pub(crate) const SHORT_STRING_LEN_SHIFT: u64 = 40; -// Length byte at bits 40..=47 (byte index 5 from LSB). Not -// 0x00FF_0000_0000_0000 — that would be byte 6, overlapping the -// tag. -pub(crate) const SHORT_STRING_LEN_MASK: u64 = 0x0000_FF00_0000_0000; -// Data bytes at bits 0..=39 (5 bytes, byte indices 0..=4 from LSB). -pub(crate) const SHORT_STRING_DATA_MASK: u64 = 0x0000_00FF_FFFF_FFFF; -pub const SHORT_STRING_MAX_LEN: usize = 5; - -/// BigInt pointer tag: 0x7FFA_XXXX_XXXX_XXXX (48 bits for bigint pointer) -pub(crate) const BIGINT_TAG: u64 = 0x7FFA_0000_0000_0000; - -/// JS Handle tag: 0x7FFB_XXXX_XXXX_XXXX (48 bits for handle ID) -/// This is used by perry-jsruntime to reference V8 objects -pub(crate) const JS_HANDLE_TAG: u64 = 0x7FFB_0000_0000_0000; -pub(crate) const TAG_MASK: u64 = 0xFFFF_0000_0000_0000; - -// ----- JS handle function-pointer types (used by handle.rs FFI setters) ----- - -pub(crate) type JsHandleArrayGetFn = extern "C" fn(f64, i32) -> f64; -pub(crate) type JsHandleArrayLengthFn = extern "C" fn(f64) -> i32; -pub(crate) type JsHandleObjectGetPropertyFn = extern "C" fn(f64, *const i8, usize) -> f64; -pub(crate) type JsHandleToStringFn = extern "C" fn(f64) -> *mut crate::string::StringHeader; -pub(crate) type JsHandleCallMethodFn = - unsafe extern "C" fn(f64, *const i8, usize, *const f64, usize) -> f64; -pub(crate) type JsNativeModuleJsLoaderFn = - unsafe extern "C" fn(*const u8, usize, *const u8, usize) -> f64; -pub(crate) type JsNewFromHandleV8Fn = unsafe extern "C" fn(f64, *const f64, usize) -> f64; -/// Returns the JS spec `typeof` string discriminator for a V8 handle: -/// 1 = "function" (V8 callable), 0 = "object" (everything else — including arrays). -/// Negative values reserved for future use ("symbol" = 2 if V8 ever exposes it that way). -pub(crate) type JsHandleTypeofFn = unsafe extern "C" fn(f64) -> i32; - -// ----- JS handle dispatch atomics (shared between handle.rs and consumers) ----- - -pub(crate) static JS_HANDLE_ARRAY_GET: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); -pub(crate) static JS_HANDLE_ARRAY_LENGTH: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); -pub(crate) static JS_HANDLE_OBJECT_GET_PROPERTY: AtomicPtr<()> = - AtomicPtr::new(std::ptr::null_mut()); -pub(crate) static JS_HANDLE_TO_STRING: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); -pub static JS_HANDLE_CALL_METHOD: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); -pub static JS_NATIVE_MODULE_JS_LOADER: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); -pub static JS_NEW_FROM_HANDLE_V8: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); -pub static JS_HANDLE_TYPEOF: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut()); diff --git a/crates/perry-runtime/src/.value.parked/to_string.rs b/crates/perry-runtime/src/.value.parked/to_string.rs deleted file mode 100644 index 0847089494..0000000000 --- a/crates/perry-runtime/src/.value.parked/to_string.rs +++ /dev/null @@ -1,239 +0,0 @@ -//! NaN-boxed value to-string conversion helpers. - -use super::*; -use std::sync::atomic::Ordering; - -/// Convert a NaN-boxed f64 value to a string pointer. -/// Handles all value types: strings (extract pointer), numbers (convert), JS handles, etc. -#[no_mangle] -pub extern "C" fn js_jsvalue_to_string(value: f64) -> *mut crate::string::StringHeader { - // Check for JS handle first - these come from the JS runtime (e.g., process.env values) - if is_js_handle(value) { - let func_ptr = JS_HANDLE_TO_STRING.load(Ordering::SeqCst); - if !func_ptr.is_null() { - let func: JsHandleToStringFn = unsafe { std::mem::transmute(func_ptr) }; - return func(value); - } - // Fallback if no handler registered - return crate::string::js_string_from_bytes(b"[JS Handle]".as_ptr(), 11); - } - - let jsval = JSValue::from_bits(value.to_bits()); - - if jsval.is_string() { - // Already a heap string — return the pointer directly. - jsval.as_string_ptr() as *mut crate::string::StringHeader - } else if jsval.is_short_string() { - // Inline SSO — materialize into a heap StringHeader so the - // caller gets a uniform `*mut StringHeader`. This defeats - // the SSO benefit for this particular conversion, but it's - // a correctness-preserving compatibility shim for the many - // call sites that currently expect a heap pointer. - crate::string::js_string_materialize_to_heap(value) - } else if jsval.is_undefined() { - crate::string::js_string_from_bytes(b"undefined".as_ptr(), 9) - } else if jsval.is_null() { - crate::string::js_string_from_bytes(b"null".as_ptr(), 4) - } else if jsval.is_bool() { - if jsval.as_bool() { - crate::string::js_string_from_bytes(b"true".as_ptr(), 4) - } else { - crate::string::js_string_from_bytes(b"false".as_ptr(), 5) - } - } else if jsval.is_int32() { - // Convert int32 to string - let n = jsval.as_int32(); - let s = n.to_string(); - crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32) - } else if jsval.is_bigint() { - // BigInt - convert to decimal string - let ptr = jsval.as_bigint_ptr(); - crate::bigint::js_bigint_to_string(ptr) - } else if jsval.is_pointer() { - // Pointer: could be an array, object, or other heap type. Arrays - // stringify via `Array.prototype.join(",")` per JS semantics; other - // objects fall back to "[object Object]". - let ptr: *const u8 = jsval.as_pointer(); - if !ptr.is_null() && (ptr as usize) >= 0x10000 { - // Symbols: detect via the side-table before any GC header read. - if crate::symbol::is_registered_symbol(ptr as usize) { - return unsafe { - crate::symbol::js_symbol_to_string(value) as *mut crate::string::StringHeader - }; - } - // Consult `[Symbol.toPrimitive]("string")` if the object has a - // custom toPrimitive method registered in the symbol side-table. - // A changed result means the user-defined method produced a - // string-hint primitive — recurse so strings pass through as-is - // and numbers get js_number_to_string. - let primitive = unsafe { crate::symbol::js_to_primitive(value, 2) }; - if primitive.to_bits() != value.to_bits() { - return js_jsvalue_to_string(primitive); - } - // Buffers: BufferHeader has no GC header, so we must detect via - // BUFFER_REGISTRY before computing gc_header (which would read - // garbage one word before the buffer). `Buffer.toString()` with - // no arg defaults to UTF-8 — Node prints the raw bytes. - if crate::buffer::is_registered_buffer(ptr as usize) { - return crate::buffer::js_buffer_to_string( - ptr as *const crate::buffer::BufferHeader, - 0, - ); - } - unsafe { - let gc_header = ptr.sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - if (*gc_header).obj_type == crate::gc::GC_TYPE_ARRAY { - // Use js_array_join with a "," separator to match Array.prototype.toString. - let sep = crate::string::js_string_from_bytes(b",".as_ptr(), 1); - return crate::array::js_array_join( - ptr as *const crate::array::ArrayHeader, - sep as *const crate::string::StringHeader, - ); - } - } - } - crate::string::js_string_from_bytes(b"[object Object]".as_ptr(), 15) - } else { - // Regular number - use js_number_to_string - crate::string::js_number_to_string(value) - } -} - -/// Convert a NaN-boxed f64 value to a string with the given radix. -/// Handles BigInt (uses bigint_to_string_radix), numbers, strings, etc. -#[no_mangle] -pub extern "C" fn js_jsvalue_to_string_radix( - value: f64, - radix: i32, -) -> *mut crate::string::StringHeader { - let jsval = JSValue::from_bits(value.to_bits()); - - if jsval.is_bigint() { - let ptr = jsval.as_bigint_ptr(); - crate::bigint::js_bigint_to_string_radix(ptr, radix) - } else if jsval.is_string() { - jsval.as_string_ptr() as *mut crate::string::StringHeader - } else if jsval.is_int32() { - let n = jsval.as_int32(); - let s = if radix == 16 { - format!("{:x}", n) - } else if radix == 10 || radix == 0 { - n.to_string() - } else { - // General radix conversion - let mut result = String::new(); - let mut val = if n < 0 { -(n as i64) as u64 } else { n as u64 }; - let r = radix as u64; - if val == 0 { - return crate::string::js_string_from_bytes(b"0".as_ptr(), 1); - } - while val > 0 { - let digit = (val % r) as u8; - result.push(if digit < 10 { - (b'0' + digit) as char - } else { - (b'a' + digit - 10) as char - }); - val /= r; - } - if n < 0 { - result.push('-'); - } - let s: String = result.chars().rev().collect(); - return crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); - }; - crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32) - } else { - // Regular f64 number - let n = value; - if n.is_nan() { - return crate::string::js_string_from_bytes(b"NaN".as_ptr(), 3); - } - if n.is_infinite() { - if n > 0.0 { - return crate::string::js_string_from_bytes(b"Infinity".as_ptr(), 8); - } else { - return crate::string::js_string_from_bytes(b"-Infinity".as_ptr(), 9); - } - } - if radix == 10 || radix == 0 { - return crate::string::js_number_to_string(value); - } - // For hex and other radixes, convert via integer - let n_i64 = n as i64; - let s = if radix == 16 { - if n_i64 < 0 { - format!("-{:x}", -n_i64) - } else { - format!("{:x}", n_i64) - } - } else { - let mut result = String::new(); - let mut val = if n_i64 < 0 { - (-n_i64) as u64 - } else { - n_i64 as u64 - }; - let r = radix as u64; - if val == 0 { - return crate::string::js_string_from_bytes(b"0".as_ptr(), 1); - } - while val > 0 { - let digit = (val % r) as u8; - result.push(if digit < 10 { - (b'0' + digit) as char - } else { - (b'a' + digit - 10) as char - }); - val /= r; - } - if n_i64 < 0 { - result.push('-'); - } - result.chars().rev().collect() - }; - crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32) - } -} - -/// Ensure a value is a native string pointer. -/// This is specifically for fetch headers where we need to handle: -/// 1. Raw string pointers (literal strings - f64 bits ARE the pointer) -/// 2. NaN-boxed strings (STRING_TAG) -/// 3. JS handle strings (from process.env) -/// Returns the string pointer as i64. -#[no_mangle] -pub extern "C" fn js_ensure_string_ptr(value: f64) -> i64 { - let bits = value.to_bits(); - - // Check for JS handle first - these need conversion - if is_js_handle(value) { - let func_ptr = JS_HANDLE_TO_STRING.load(Ordering::SeqCst); - if !func_ptr.is_null() { - let func: JsHandleToStringFn = unsafe { std::mem::transmute(func_ptr) }; - return func(value) as i64; - } - // Fallback - create a placeholder string - return crate::string::js_string_from_bytes(b"[JS Handle]".as_ptr(), 11) as i64; - } - - // Check for NaN-boxed string (STRING_TAG) - if (bits & TAG_MASK) == STRING_TAG { - let ptr = (bits & POINTER_MASK) as i64; - if ptr != 0 { - let str_header = ptr as *const crate::string::StringHeader; - unsafe { - let length = (*str_header).byte_len; - // Make a copy of the string to ensure we have a Perry-allocated string - let data_ptr = (str_header as *const u8) - .add(std::mem::size_of::()); - let copy = crate::string::js_string_from_bytes(data_ptr, length); - return copy as i64; - } - } - return ptr; - } - - // Otherwise, treat the f64 bits directly as a pointer (raw string literal) - bits as i64 -} diff --git a/crates/perry-runtime/src/object/native_module_registry.rs b/crates/perry-runtime/src/object/native_module_registry.rs index 9e67911745..0c8120b828 100644 --- a/crates/perry-runtime/src/object/native_module_registry.rs +++ b/crates/perry-runtime/src/object/native_module_registry.rs @@ -1,5 +1,5 @@ //! Per-module native-module method-dispatch registry (devirtualization). -//! GENERATED scaffolding — see NM_DEVIRT_PLAN.md. Each `js_nm_install_()` is +//! GENERATED scaffolding (native-module devirtualization, #5256). Each `js_nm_install_()` is //! the SOLE static reference to its `nm_dispatch_` bucket fn; codegen emits a //! call per statically-imported native module so the linker dead-strips the rest. //! NOTHING here names all buckets together (that would re-pin everything). diff --git a/examples/issue_552_demo.ts b/examples/issue_552_demo.ts deleted file mode 100644 index b34936f26c..0000000000 --- a/examples/issue_552_demo.ts +++ /dev/null @@ -1,103 +0,0 @@ -// Issue #552 acceptance demo: location + photo-library picker + image compression + upload. -// -// Build: -// cargo run --release -- examples/issue_552_demo.ts -o demo --target ios -// cargo run --release -- examples/issue_552_demo.ts -o demo --target android -// -// Manifest entries required at app-bundle time (see types/perry/system/index.d.ts -// for full text): -// iOS: NSLocationWhenInUseUsageDescription in Info.plist -// Android: ACCESS_FINE_LOCATION (and optionally ACCESS_COARSE_LOCATION) in -// AndroidManifest.xml. ACTION_PICK_IMAGES (Photo Picker, API 33+) -// and ext-sharp need no extra permissions. - -import { App, VStack, Button, Text, setText, state } from "perry/ui" -import { - geolocationGetCurrent, - geolocationRequestPermission, - imagePickerPick, -} from "perry/system" -import sharp from "sharp" -import * as fs from "fs" - -const UPLOAD_URL = "https://example.com/upload" -const COMPRESSED_DIR = "/tmp" -const TARGET_BYTES = 500 * 1024 - -// Quality search: start at 80, halve until under TARGET_BYTES or quality ≤ 10. -function compressUnderLimit(srcPath: string, dstPath: string): number { - let quality = 80 - let outBytes = Number.MAX_SAFE_INTEGER - while (outBytes > TARGET_BYTES && quality >= 10) { - sharp(srcPath).resize(1600, 1600).jpeg(quality).toFile(dstPath) - const stat = fs.statSync(dstPath) - outBytes = stat.size - quality = Math.floor(quality / 2) - } - return outBytes -} - -async function uploadOne(path: string, idx: number): Promise { - const dst = `${COMPRESSED_DIR}/issue552_${idx}.jpg` - const finalBytes = compressUnderLimit(path, dst) - setText("status", `compressed photo ${idx + 1}: ${finalBytes} bytes`) - const body = fs.readFileSync(dst) - const res = await fetch(UPLOAD_URL, { - method: "POST", - headers: { "Content-Type": "image/jpeg" }, - body, - }) - if (!res.ok) { - throw new Error(`upload ${idx} failed: HTTP ${res.status}`) - } -} - -function onLocate(): void { - geolocationRequestPermission((status: string) => { - if (status !== "granted") { - setText("status", `location permission: ${status}`) - return - } - geolocationGetCurrent( - (lat: number, lng: number, accuracy: number, _ts: number) => { - setText( - "status", - `location: ${lat.toFixed(5)}, ${lng.toFixed(5)} (±${accuracy}m)`, - ) - }, - (err: string) => { - setText("status", `location error: ${err}`) - }, - ) - }) -} - -function onPickAndUpload(): void { - imagePickerPick(2, true, (paths: string[]) => { - if (paths.length === 0) { - setText("status", "picker cancelled") - return - } - setText("status", `picked ${paths.length} photo(s); compressing…`) - ;(async () => { - try { - for (let i = 0; i < paths.length; i++) { - await uploadOne(paths[i], i) - } - setText("status", `uploaded ${paths.length} photo(s)`) - } catch (e: any) { - setText("status", `failed: ${e?.message ?? e}`) - } - })() - }) -} - -App({ - title: "Issue #552 demo", - body: VStack(16, [ - Text("Tap to test location + photo upload"), - Text("(idle)", "status"), - Button("Get current location", onLocate), - Button("Pick 2 photos & upload", onPickAndUpload), - ]), -}) diff --git a/examples/issue_582_demo.ts b/examples/issue_582_demo.ts deleted file mode 100644 index 63b44d227a..0000000000 --- a/examples/issue_582_demo.ts +++ /dev/null @@ -1,48 +0,0 @@ -// Issue #582 demo — network reachability stdlib (online/offline + change events). -// -// Acceptance flow from the issue: -// 1. Logs current connection status at startup. -// 2. Subscribes to network change events. -// 3. On the device: toggle Airplane Mode. The program prints the -// transition within 1 second on each change. -// -// Compile + run on macOS host: -// cargo run --release -- examples/issue_582_demo.ts -o /tmp/net_demo && /tmp/net_demo -// -// HEADLESS HOST CAVEAT: NWPathMonitor delivers events on the main dispatch -// queue, which only spins inside a UIApplication / NSApplication run loop. -// A bare CLI binary on macOS never pumps that queue, so `getStatus` reports -// `connected=false / kind="unknown"` (the pre-monitor seed) and `onChange` -// never fires. On a real iPhone / Android phone the platform's run loop is -// always active, so transitions print within milliseconds — toggle Airplane -// Mode and the lines `[change #1]…[change #2]…` flow through. - -import { - networkGetStatus, - networkOnChange, - networkStopOnChange, -} from "perry/system"; - -console.log("[startup] reading initial network state..."); - -networkGetStatus((connected, kind) => { - console.log(`[startup] connected=${connected} type=${kind}`); -}); - -console.log("[startup] subscribing to change events; toggle Airplane Mode now..."); - -let count = 0; -const id = networkOnChange((connected, kind) => { - count++; - console.log(`[change #${count}] connected=${connected} type=${kind}`); -}); - -console.log(`[startup] subscription id=${id}`); - -// Keep the program alive long enough on host to observe a few transitions. -// Real iOS / Android apps would just leave the subscription live for the -// app's lifetime; the runtime drives microtasks via the platform run loop. -setTimeout(() => { - networkStopOnChange(id); - console.log(`[shutdown] unsubscribed after ${count} change(s)`); -}, 30_000); diff --git a/examples/issue_583_demo.ts b/examples/issue_583_demo.ts deleted file mode 100644 index af08f73516..0000000000 --- a/examples/issue_583_demo.ts +++ /dev/null @@ -1,54 +0,0 @@ -// Issue #583 demo — deep links (Universal Links / App Links / URL schemes). -// -// Acceptance flow from the issue: -// 1. Configures a custom scheme (`perryapp://…`) and a Universal Link -// domain (replace `example.com` with your real HTTPS endpoint that -// serves /.well-known/apple-app-site-association + assetlinks.json). -// 2. The app receives a deep link in three states (cold start / from -// background / while running) and prints the URL each time. -// -// To exercise on a real device: -// -// 1. Add to package.json (see this file's matching package.json snippet -// below): -// "perry": { -// "deepLinks": { -// "schemes": ["perryapp"], -// "universalLinks": { -// "ios": ["example.com"], -// "android": ["example.com"] -// } -// } -// } -// -// 2. Host two server-side files (Perry doesn't host these — it's the -// app developer's job, per the issue): -// https://example.com/.well-known/apple-app-site-association -// https://example.com/.well-known/assetlinks.json -// -// 3. iOS: build + run via `perry run --target ios`, sign with the -// generated app.entitlements file. Tap a link in Mail / Messages. -// Android: `perry run --target android`. Tap a link in Gmail. -// -// HEADLESS HOST CAVEAT: a bare CLI binary on macOS doesn't pump the -// AppKit run loop, so the AppDelegate's `application(_:open:)` and the -// kAEGetURL handler never fire. Run inside `App({ body: … })` (or with -// `perry run --target ios-simulator` etc.) for the URL pipeline to be -// active. - -import { appOnOpenUrl, appGetLaunchUrl } from "perry/system"; - -console.log(`[startup] launchUrl=${JSON.stringify(appGetLaunchUrl())}`); - -let count = 0; -appOnOpenUrl((url, source) => { - count++; - console.log(`[deeplink #${count}] source=${source} url=${url}`); - - // Real apps would route to the relevant screen here. Example: - // const u = new URL(url); - // if (u.pathname.startsWith("/chat/")) navigateTo("chat", u.pathname); - // else if (u.pathname.startsWith("/item/")) navigateTo("item", u.pathname); -}); - -console.log("[startup] handler installed; tap a deep link to test..."); diff --git a/examples/wasm_ui_demo.html b/examples/wasm_ui_demo.html deleted file mode 100644 index dffdfb2026..0000000000 --- a/examples/wasm_ui_demo.html +++ /dev/null @@ -1,2940 +0,0 @@ - - - - - - wasm_ui_demo - - - -
- - - - \ No newline at end of file diff --git a/isoA b/isoA deleted file mode 100755 index 8721139e47..0000000000 Binary files a/isoA and /dev/null differ diff --git a/m b/m deleted file mode 100755 index 769a9aa99e..0000000000 Binary files a/m and /dev/null differ diff --git a/m5 b/m5 deleted file mode 100755 index 417696094a..0000000000 Binary files a/m5 and /dev/null differ diff --git a/packages/perry-styling/examples/dist/showcase b/packages/perry-styling/examples/dist/showcase deleted file mode 100755 index d9a06a6fc2..0000000000 Binary files a/packages/perry-styling/examples/dist/showcase and /dev/null differ diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml deleted file mode 100644 index e614505eac..0000000000 --- a/res/values-de/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - #808080 - R: 128 G: 128 B: 128 - - + - diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml deleted file mode 100644 index 5451776a06..0000000000 --- a/res/values-es/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - #808080 - R: 128 V: 128 A: 128 - - + - diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml deleted file mode 100644 index 79e475aeec..0000000000 --- a/res/values-fr/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - #808080 - R: 128 V: 128 B: 128 - - + - diff --git a/res/values/strings.xml b/res/values/strings.xml deleted file mode 100644 index e614505eac..0000000000 --- a/res/values/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - #808080 - R: 128 G: 128 B: 128 - - + - diff --git a/run_llvm_sweep.sh b/run_llvm_sweep.sh deleted file mode 100755 index 9647ee6bbe..0000000000 --- a/run_llvm_sweep.sh +++ /dev/null @@ -1,178 +0,0 @@ -#!/usr/bin/env bash -# Perry Parity Sweep -# Compiles all test-files/test_*.ts, diffs output against Node.js, -# and reports MATCH/DIFF/CRASH/COMPILE_FAIL counts. -# -# Usage: -# ./run_llvm_sweep.sh # Run all tests -# ./run_llvm_sweep.sh test_array # Run only matching tests -# PERRY_TIMEOUT=30 ./run_llvm_sweep.sh # Custom timeout (default: 10s) - -set -u - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PERRY="${SCRIPT_DIR}/target/release/perry" -OUT_DIR="${PERRY_SWEEP_DIR:-/tmp/llvm_sweep_out}" -TIMEOUT_SEC="${PERRY_TIMEOUT:-10}" -FILTER="${1:-}" - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -NC='\033[0m' - -# Find timeout command -if command -v timeout &>/dev/null; then - TIMEOUT_CMD="timeout" -elif command -v gtimeout &>/dev/null; then - TIMEOUT_CMD="gtimeout" -else - TIMEOUT_CMD="" -fi - -run_with_timeout() { - local secs=$1; shift - if [[ -n "$TIMEOUT_CMD" ]]; then - $TIMEOUT_CMD "$secs" "$@" - else - "$@" - fi -} - -# Ensure binary exists -if [[ ! -x "$PERRY" ]]; then - echo "Building Perry (release)..." - cargo build --release -p perry --quiet 2>/dev/null || { - echo -e "${RED}Build failed${NC}" - exit 1 - } -fi - -mkdir -p "$OUT_DIR" -rm -f "$OUT_DIR"/*.diff "$OUT_DIR"/*.compile.log "$OUT_DIR"/summary.txt - -# Counters -COMPILE_PASS=0 -COMPILE_FAIL=0 -RUN_MATCH=0 -RUN_DIFF=0 -RUN_CRASH=0 -RUN_TIMEOUT=0 -NODE_FAIL=0 -TOTAL=0 - -# Track results for summary -declare -a MATCHES=() -declare -a DIFFS=() -declare -a CRASHES=() -declare -a COMPILE_FAILS=() - -echo "========================================" -echo " Perry LLVM Backend Sweep" -echo "========================================" -echo "" - -for f in "$SCRIPT_DIR"/test-files/test_*.ts; do - [[ -d "$f" ]] && continue - name=$(basename "$f" .ts) - - # Optional filter - if [[ -n "$FILTER" && "$name" != *"$FILTER"* ]]; then - continue - fi - - TOTAL=$((TOTAL + 1)) - bin="$OUT_DIR/$name.bin" - - # Compile (LLVM is the only backend post-cutover) - if ! "$PERRY" compile "$f" -o "$bin" >"$OUT_DIR/$name.compile.log" 2>&1; then - COMPILE_FAIL=$((COMPILE_FAIL + 1)) - COMPILE_FAILS+=("$name") - echo -e "${RED}COMPILE_FAIL${NC} $name" - echo "$name COMPILE_FAIL" >>"$OUT_DIR/summary.txt" - continue - fi - COMPILE_PASS=$((COMPILE_PASS + 1)) - - # Run LLVM binary - llvm_out=$(run_with_timeout "$TIMEOUT_SEC" "$bin" 2>&1) - llvm_exit=$? - - if [[ $llvm_exit -eq 124 ]]; then - RUN_TIMEOUT=$((RUN_TIMEOUT + 1)) - echo -e "${YELLOW}TIMEOUT${NC} $name" - echo "$name TIMEOUT" >>"$OUT_DIR/summary.txt" - rm -f "$bin" - continue - fi - - # Run with Node.js (filter stderr warnings about --experimental-strip-types) - node_out=$(run_with_timeout "$TIMEOUT_SEC" node --experimental-strip-types "$f" 2>/dev/null) - node_exit=$? - - if [[ $node_exit -ne 0 && $node_exit -ne 124 ]]; then - NODE_FAIL=$((NODE_FAIL + 1)) - echo -e "${YELLOW}NODE_FAIL${NC} $name" - echo "$name NODE_FAIL" >>"$OUT_DIR/summary.txt" - rm -f "$bin" - continue - fi - - # Compare - if [[ "$llvm_out" == "$node_out" ]]; then - RUN_MATCH=$((RUN_MATCH + 1)) - MATCHES+=("$name") - if [[ $llvm_exit -ne 0 ]]; then - echo -e "${GREEN}MATCH${NC} $name (exit=$llvm_exit)" - else - echo -e "${GREEN}MATCH${NC} $name" - fi - echo "$name MATCH" >>"$OUT_DIR/summary.txt" - elif [[ $llvm_exit -ne 0 ]]; then - RUN_CRASH=$((RUN_CRASH + 1)) - CRASHES+=("$name") - echo -e "${RED}CRASH${NC} $name (exit=$llvm_exit)" - echo "$name CRASH (exit=$llvm_exit)" >>"$OUT_DIR/summary.txt" - diff <(echo "$llvm_out") <(echo "$node_out") >"$OUT_DIR/$name.diff" 2>&1 - else - RUN_DIFF=$((RUN_DIFF + 1)) - DIFFS+=("$name") - # Count diff lines for severity indicator - diff_lines=$(diff <(echo "$llvm_out") <(echo "$node_out") | grep -c '^[<>]') - echo -e "${YELLOW}DIFF${NC} $name ($diff_lines lines differ)" - echo "$name DIFF ($diff_lines lines)" >>"$OUT_DIR/summary.txt" - diff <(echo "$llvm_out") <(echo "$node_out") >"$OUT_DIR/$name.diff" 2>&1 - fi - - rm -f "$bin" -done - -# Summary -RUNTIME_TESTED=$((RUN_MATCH + RUN_DIFF + RUN_CRASH + RUN_TIMEOUT)) -if [[ $RUNTIME_TESTED -gt 0 ]]; then - MATCH_PCT=$(echo "scale=1; $RUN_MATCH * 100 / $RUNTIME_TESTED" | bc) -else - MATCH_PCT="0.0" -fi - -echo "" -echo "========================================" -echo " LLVM Sweep Summary" -echo "========================================" -echo -e "Total tests: $TOTAL" -echo -e "${GREEN}Compile pass:${NC} $COMPILE_PASS" -echo -e "${RED}Compile fail:${NC} $COMPILE_FAIL" -echo "" -echo -e "${GREEN}MATCH Node:${NC} $RUN_MATCH" -echo -e "${YELLOW}DIFF Node:${NC} $RUN_DIFF" -echo -e "${RED}CRASH:${NC} $RUN_CRASH" -echo -e "${YELLOW}TIMEOUT:${NC} $RUN_TIMEOUT" -echo -e "${YELLOW}Node fail:${NC} $NODE_FAIL" -echo "" -echo -e "${CYAN}Match rate:${NC} ${MATCH_PCT}% ($RUN_MATCH/$RUNTIME_TESTED)" -echo "" -echo "Detailed diffs: $OUT_DIR/*.diff" -echo "Compile logs: $OUT_DIR/*.compile.log" -echo "Full summary: $OUT_DIR/summary.txt" diff --git a/run_tests.sh b/run_tests.sh deleted file mode 100755 index ee81d803fe..0000000000 --- a/run_tests.sh +++ /dev/null @@ -1,141 +0,0 @@ -#!/bin/bash -# Perry Test Runner -# Runs all test files in test-files/ directory - -# Don't exit on first error - we want to run all tests - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -TEST_DIR="$SCRIPT_DIR/test-files" -OUTPUT_DIR="/tmp/perry_tests" - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Counters -PASSED=0 -FAILED=0 -SKIPPED=0 - -# Create output directory -mkdir -p "$OUTPUT_DIR" - -# Tests to skip (known issues or special handling needed) -SKIP_TESTS=( - # Add any tests that need special handling here -) - -# Function to check if test should be skipped -should_skip() { - local test_name=$1 - for skip in "${SKIP_TESTS[@]}"; do - if [[ "$test_name" == "$skip" ]]; then - return 0 - fi - done - return 1 -} - -echo "========================================" -echo " Perry Test Runner" -echo "========================================" -echo "" - -# Build the compiler first -echo "Building compiler..." -cargo build --quiet 2>/dev/null || { - echo -e "${RED}Failed to build compiler${NC}" - exit 1 -} -echo -e "${GREEN}Compiler built successfully${NC}" -echo "" - -# Track failed tests for summary -declare -a FAILED_TESTS=() - -# Run each test -for test_file in "$TEST_DIR"/*.ts; do - test_name=$(basename "$test_file" .ts) - output_file="$OUTPUT_DIR/$test_name" - - # Check if test should be skipped - if should_skip "$test_name"; then - echo -e "${YELLOW}SKIP${NC} $test_name" - ((SKIPPED++)) - continue - fi - - # Compile the test (suppress warnings) - if ! cargo run --quiet --bin perry -- "$test_file" -o "$output_file" 2>/dev/null; then - # Try again to get error message - compile_output=$(cargo run --quiet --bin perry -- "$test_file" -o "$output_file" 2>&1 | grep -i "error" | head -3) - echo -e "${RED}FAIL${NC} $test_name (compile error)" - if [[ -n "$compile_output" ]]; then - echo " $compile_output" - fi - ((FAILED++)) - FAILED_TESTS+=("$test_name (compile)") - continue - fi - - # Run the test - run_output=$("$output_file" 2>&1) - run_status=$? - - if [[ $run_status -ne 0 ]]; then - echo -e "${RED}FAIL${NC} $test_name (runtime error: $run_status)" - echo " Output: $run_output" | head -3 - ((FAILED++)) - FAILED_TESTS+=("$test_name (runtime)") - else - echo -e "${GREEN}PASS${NC} $test_name" - ((PASSED++)) - fi -done - -# Summary -echo "" -echo "========================================" -echo " Test Summary" -echo "========================================" -echo -e "${GREEN}Passed:${NC} $PASSED" -echo -e "${RED}Failed:${NC} $FAILED" -echo -e "${YELLOW}Skipped:${NC} $SKIPPED" -echo "Total: $((PASSED + FAILED + SKIPPED))" -echo "" - -# List failed tests -if [[ ${#FAILED_TESTS[@]} -gt 0 ]]; then - echo "Failed tests:" - for failed in "${FAILED_TESTS[@]}"; do - echo " - $failed" - done -fi - -# Run regression tests from tests/ directory -echo "" -echo "========================================" -echo " Regression Tests" -echo "========================================" -for test_script in "$SCRIPT_DIR"/tests/test_*.sh; do - [ -f "$test_script" ] || continue - test_name=$(basename "$test_script" .sh) - script_output=$(bash "$test_script" 2>&1) - script_status=$? - if [[ $script_status -eq 0 ]]; then - echo -e "${GREEN}PASS${NC} $test_name" - ((PASSED++)) - else - echo -e "${RED}FAIL${NC} $test_name" - echo " $script_output" | head -3 - ((FAILED++)) - FAILED_TESTS+=("$test_name (regression)") - fi -done - -# Exit with error if any tests failed -if [[ $FAILED -gt 0 ]]; then - exit 1 -fi diff --git a/scripts/bisect_1114.sh b/scripts/bisect_1114.sh deleted file mode 100755 index f8691c8c47..0000000000 --- a/scripts/bisect_1114.sh +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env bash -# #1114 bisect helper. Run from the workspace root against the user's -# actual reproducer (the synthetic in /tmp/repro1114_real *did not* -# trigger the wedge even with a live MySQL — shop-admin's -# ~68-server-file shape is required). -# -# Usage: -# PERRY_REPRO_CMD="/path/to/shop-admin/dist/server-binary" -# PERRY_REPRO_CPU_LIMIT=80 # consider wedge when ./repro CPU >80% for 5s -# PERRY_REPRO_TIMEOUT=15 # kill after 15s -# bash scripts/bisect_1114.sh -# -# The script: -# 1. Walks first-parent commits between v0.5.1008 (0a908394) and -# v0.5.1009 (c71c780b) on `main`. -# 2. For each commit: cargo build --release -p perry-runtime -# -p perry-stdlib -p perry, then rebuild the user's binary -# ($PERRY_REPRO_CMD presumed to be a script that rebuilds it), -# run it, sample CPU. -# 3. Reports the first commit at which CPU exceeded the limit. -# -# IMPORTANT: this rebuilds perry-runtime + perry-stdlib + perry per -# step (~2 min each). With 8 commits in the candidate range, expect -# ~15 minutes. Pin `PERRY_NO_AUTO_OPTIMIZE=1` if your repro doesn't -# rely on the auto-optimize flip — saves a per-step rebuild of the -# auto-opt cache. -set -euo pipefail - -CMD="${PERRY_REPRO_CMD:-}" -if [ -z "$CMD" ]; then - echo "PERRY_REPRO_CMD must be set to the binary (or wrapper script) you want to run." - exit 2 -fi -CPU_LIMIT="${PERRY_REPRO_CPU_LIMIT:-80}" -TIMEOUT="${PERRY_REPRO_TIMEOUT:-15}" - -# Candidate commit range (oldest first), from `git log --first-parent -# 0a908394..c71c780b`. Update when the upstream lineage changes. -COMMITS=( - aa6a2cd2 # fix(security): #999 — validate explicit bundle IDs at read time - 3856caad # fix(transform): #1047 — async early return followed by an unreached await - 91bb8b5a # fix(wasm): #1049 instances 2+3 — wrapForI64 BigInt-coerce non-i64 returns - 1a51a2f0 # test(async): #1013 pin Promise.all + array destructure - d16d832a # fix(perry-jsruntime): #1022 — v8 proxies for sqlite Database/Statement - 52847008 # fix(jsruntime): #1021 — break V8-fallback CJS require cycles + process.exit - fcb097c9 # test(async): extend #1013 coverage to cross-module property-return shape - 7e3bd5a4 # fix(codegen): #321 — Effect.succeed via named-import-of-namespace-reexport - 634e1f58 # fix(fastify): non-blocking listen() + main-thread pump - c71c780b # fix(object): skip misaligned non-object pointers (v0.5.1009 release commit) -) - -probe_cpu() { - local pid="$1" - ps -p "$pid" -o %cpu= 2>/dev/null | tr -d ' ' -} - -verdict_at_head() { - local pid - "$CMD" >/dev/null 2>&1 & - pid=$! - sleep 5 - local cpu1; cpu1=$(probe_cpu "$pid"); cpu1=${cpu1:-0} - sleep 5 - local cpu2; cpu2=$(probe_cpu "$pid"); cpu2=${cpu2:-0} - kill "$pid" 2>/dev/null || true - wait "$pid" 2>/dev/null || true - echo "cpu_after_5s=$cpu1 cpu_after_10s=$cpu2" - # awk-style comparison for floats - awk -v c="$cpu2" -v l="$CPU_LIMIT" 'BEGIN { exit !(c > l) }' -} - -FIRST_BAD="" -for sha in "${COMMITS[@]}"; do - echo "==> checkout $sha" - git checkout -q "$sha" - cargo build --release -p perry-runtime -p perry-stdlib -p perry-runtime-static -p perry-stdlib-static -p perry --quiet 2>&1 \ - | tail -3 || { echo "build failed at $sha — skipping"; continue; } - echo "==> probing CPU at $sha" - if timeout "$TIMEOUT" bash -c "$(declare -f probe_cpu verdict_at_head); verdict_at_head"; then - echo "==> $sha : WEDGED (CPU > $CPU_LIMIT)" - FIRST_BAD="$sha" - break - else - echo "==> $sha : clean" - fi -done - -git checkout -q main -if [ -n "$FIRST_BAD" ]; then - echo "" - echo "First wedge observed at: $FIRST_BAD" -else - echo "" - echo "No commit in the candidate range exceeded the CPU limit. The" - echo "regression may be earlier (pre-v0.5.1008), in an interaction" - echo "with auto-optimize feature flags, or in a build artifact not" - echo "covered by this script." -fi diff --git a/scripts/gc_ffi_root_sources_gate.py b/scripts/gc_ffi_root_sources_gate.py deleted file mode 100644 index d4fb3a0ff4..0000000000 --- a/scripts/gc_ffi_root_sources_gate.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python3 -import re -import sys -from pathlib import Path - - -REPO_ROOT = Path(__file__).resolve().parents[1] -EXT_SRC_ROOTS = sorted((REPO_ROOT / "crates").glob("perry-ext-*/src")) -ANONYMOUS_MUTABLE_REGISTRATION = re.compile( - r"(? int: - anonymous_calls = [] - named_calls = [] - - for src_root in EXT_SRC_ROOTS: - crate_name = src_root.parent.name - for path in sorted(src_root.rglob("*.rs")): - text = path.read_text(encoding="utf-8") - if ANONYMOUS_MUTABLE_REGISTRATION.search(text): - anonymous_calls.append(path.relative_to(REPO_ROOT).as_posix()) - for match in NAMED_MUTABLE_REGISTRATION.finditer(text): - named_calls.append( - ( - crate_name, - path.relative_to(REPO_ROOT).as_posix(), - match.group(1), - ) - ) - - if anonymous_calls: - print( - "perry-ext mutable GC scanners must use " - f"gc_register_mutable_root_scanner_named: {anonymous_calls}", - file=sys.stderr, - ) - return 1 - - if not named_calls: - print("expected at least one named perry-ext GC scanner", file=sys.stderr) - return 1 - - mismatched_sources = [ - (path, source, crate_name) - for crate_name, path, source in named_calls - if source != crate_name - ] - if mismatched_sources: - print( - "perry-ext GC scanner source must match the crate name: " - f"{mismatched_sources}", - file=sys.stderr, - ) - return 1 - - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/gc_mutable_root_contract_gate.sh b/scripts/gc_mutable_root_contract_gate.sh deleted file mode 100755 index 4caf06a4d1..0000000000 --- a/scripts/gc_mutable_root_contract_gate.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT="$(cd "$(dirname "$0")/.." && pwd)" -cd "$ROOT" - -cargo test -p perry-runtime --release gc_mutable_root_contract -- --nocapture -cargo test -p perry-codegen --test shadow_slot_hygiene -python3 scripts/gc_ffi_root_sources_gate.py -python3 -m unittest tests/test_gc_1090_evidence_report.py -scripts/run_memory_stability_tests.sh diff --git a/scripts/node_builtin_manifest_radar.py b/scripts/node_builtin_manifest_radar.py deleted file mode 100755 index 9db7aec355..0000000000 --- a/scripts/node_builtin_manifest_radar.py +++ /dev/null @@ -1,153 +0,0 @@ -#!/usr/bin/env python3 -"""Check Node builtin modules against Perry's manifest and parity skiplist. - -The radar answers one narrow question: every builtin module reported by -Node must either be claimed by Perry's API manifest or explicitly skiplisted -with a reason. The parity suite directory map is included in the report so -suite-only drift is visible while triaging manifest gaps. -""" - -from __future__ import annotations - -import argparse -import json -import subprocess -import sys -from pathlib import Path - -try: - import tomllib -except ImportError: - import tomli as tomllib # type: ignore - - -REPO_ROOT = Path(__file__).resolve().parent.parent -SKIPLIST = REPO_ROOT / "scripts" / "parity-skiplist.toml" -SUITE_DIR = REPO_ROOT / "test-parity" / "node-suite" - -SUITE_ALIASES = { - "fs-promises": "fs/promises", - "inspector-promises": "inspector/promises", -} - - -def normalize_module(name: str) -> str: - if name.startswith("node:"): - return name.removeprefix("node:") - return name - - -def load_skip_modules(path: Path) -> set[str]: - with path.open("rb") as f: - data = tomllib.load(f) - return {normalize_module(name) for name in data.get("skip-modules", {})} - - -def load_manifest_modules(manifest_json: str) -> set[str]: - manifest = json.loads(manifest_json) - entries = manifest.get("entries", []) - return {normalize_module(entry["module"]) for entry in entries if "module" in entry} - - -def load_manifest_from_perry(perry: str) -> str: - proc = subprocess.run( - [perry, "--print-api-manifest=json"], - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - return proc.stdout - - -def load_node_builtins(node: str) -> set[str]: - script = ( - "console.log(JSON.stringify(" - "require('module').builtinModules.map((m) => m.replace(/^node:/, '')).sort()" - "))" - ) - proc = subprocess.run( - [node, "-e", script], - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - return {normalize_module(name) for name in json.loads(proc.stdout)} - - -def load_suite_modules(path: Path) -> set[str]: - if not path.exists(): - return set() - - modules: set[str] = set() - for child in path.iterdir(): - if not child.is_dir(): - continue - name = SUITE_ALIASES.get(child.name, child.name) - modules.add(name) - for subdir in child.iterdir(): - if not subdir.is_dir(): - continue - candidate = f"{child.name}/{subdir.name}" - modules.add(SUITE_ALIASES.get(candidate, candidate)) - return modules - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--node", default="node", help="Node executable to query") - parser.add_argument( - "--perry", - default=str(REPO_ROOT / "target" / "release" / "perry"), - help="Perry binary used to emit --print-api-manifest=json", - ) - parser.add_argument( - "--manifest-json", - type=Path, - help="Read an existing Perry manifest JSON file instead of running --perry", - ) - parser.add_argument("--skiplist", type=Path, default=SKIPLIST) - parser.add_argument("--suite-dir", type=Path, default=SUITE_DIR) - args = parser.parse_args() - - node_builtins = load_node_builtins(args.node) - if args.manifest_json: - manifest_json = args.manifest_json.read_text() - else: - manifest_json = load_manifest_from_perry(args.perry) - manifest_modules = load_manifest_modules(manifest_json) - skip_modules = load_skip_modules(args.skiplist) - suite_modules = load_suite_modules(args.suite_dir) - - unclassified = sorted(node_builtins - manifest_modules - skip_modules) - suite_only = sorted((node_builtins & suite_modules) - manifest_modules - skip_modules) - - if unclassified: - print("Unclassified Node builtin modules:", file=sys.stderr) - for module in unclassified: - suite_note = " suite=yes" if module in suite_modules else " suite=no" - print(f" - {module}{suite_note}", file=sys.stderr) - print( - "\nAdd a Perry API manifest entry or a scripts/parity-skiplist.toml " - "skip-modules reason for each module above.", - file=sys.stderr, - ) - return 1 - - print( - "Node builtin manifest radar clean: " - f"{len(node_builtins)} Node builtins, " - f"{len(manifest_modules)} manifest modules, " - f"{len(skip_modules)} skiplisted modules." - ) - if suite_only: - print( - "Suite-only builtins with no manifest/skiplist classification: " - + ", ".join(suite_only) - ) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/run_cache_tests.sh b/scripts/run_cache_tests.sh deleted file mode 100755 index 23e1dfdb25..0000000000 --- a/scripts/run_cache_tests.sh +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/env bash -# Integration smoke test for the V2.2 + #686 object cache -# (see `crates/perry/src/commands/compile/object_cache.rs :: ObjectCache` -# and `crates/perry-hir/src/stable_hash.rs :: hash_module`). -# -# Exercises: -# 1. `--no-cache` baseline: record expected runtime output. -# 2. Cold cache build (no .perry-cache/): every module is a miss + store, -# runtime output matches baseline. -# 3. Warm cache build: every module is a hit, no compile_module invocations, -# runtime output still matches baseline. -# 4. Source-change partial invalidation (behavior change): touch one -# module's source so its HIR changes, confirm N-1 hits / 1 miss and -# that runtime output reflects the edit. -# 5. Restore source and confirm full-hit re-warm. -# 6. Cosmetic-only edit (#686): touch comments / whitespace so the source -# bytes change but the post-transform HIR does NOT — confirm ALL hits -# and runtime output unchanged. This is the gate for the HIR-addressable -# cache: a regression that re-keyed on source bytes would surface here -# as "1 miss" instead of "0 miss". -# 7. Behavior-changing edit (#686): change a literal so the HIR differs — -# confirm 1 miss and runtime output reflects the new value. -# 8. Topological order regression (v0.5.127-128 class of bug): same -# `registry.ts` / `register-defaults.ts` / `oids.ts` project used as -# a smoke gate — if the cache key ever drops `non_entry_module_prefixes` -# ordering, a reordered init chain would hit a stale entry module -# and `count=N` would silently drift. -# -# The cache key itself is unit-tested in `object_cache_tests::...`. -# This script is the end-to-end gate: the whole pipeline (collect_modules → -# rayon codegen → cache lookup/store → linker) stays byte-accurate across -# cache states. - -set -euo pipefail - -PERRY="${PERRY:-$(pwd)/target/release/perry}" -if [ ! -x "$PERRY" ]; then - echo "error: $PERRY not found or not executable; run 'cargo build --release -p perry' first" >&2 - exit 1 -fi - -TEST_DIR="$(pwd)/test-files/module-init-order" -if [ ! -d "$TEST_DIR" ]; then - echo "error: $TEST_DIR not found" >&2 - exit 1 -fi - -WORK="$(mktemp -d)" -trap 'rm -rf "$WORK"' EXIT - -cp -R "$TEST_DIR"/* "$WORK/" -cd "$WORK" - -MAIN_ENTRY="main.ts" -BIN="./prog" - -run_and_capture() { - local label="$1" - shift - local logfile="$WORK/${label}.log" - echo "=== $label ===" >&2 - PERRY_DEV_VERBOSE=1 "$PERRY" compile "$MAIN_ENTRY" -o "$BIN" "$@" >"$logfile" 2>&1 \ - || { echo "compile failed ($label):"; cat "$logfile"; exit 1; } - "$BIN" > "$WORK/${label}.out" - cat "$logfile" | grep -E "• codegen cache" || echo " (no cache line)" -} - -# 1. Baseline with --no-cache. -rm -rf .perry-cache -run_and_capture baseline --no-cache -BASELINE_OUT="$(cat "$WORK/baseline.out")" -echo " baseline output: $BASELINE_OUT" - -# 2. Cold cache: every module should be a miss+store. -rm -rf .perry-cache -run_and_capture cold -COLD_OUT="$(cat "$WORK/cold.out")" -[ "$COLD_OUT" = "$BASELINE_OUT" ] || { echo "FAIL: cold output differs from baseline" >&2; exit 1; } -grep -E "• codegen cache: 0/[0-9]+ hit" "$WORK/cold.log" >/dev/null \ - || { echo "FAIL: cold build should have 0 hits" >&2; cat "$WORK/cold.log" | grep cache >&2; exit 1; } - -# 3. Warm cache: every module should be a hit. -run_and_capture warm -WARM_OUT="$(cat "$WORK/warm.out")" -[ "$WARM_OUT" = "$BASELINE_OUT" ] || { echo "FAIL: warm output differs from baseline" >&2; exit 1; } -# Expect zero misses: "N/N hit (0 miss)". "All hits" == miss count is 0 — -# use plain `grep -E` without a backreference so the test stays portable to -# BSD grep on macOS (GNU grep supports backrefs in -E as an extension, BSD -# grep does not). -if ! grep -E "• codegen cache: [0-9]+/[0-9]+ hit \(0 miss\)" "$WORK/warm.log" >/dev/null; then - echo "FAIL: warm build should be all hits" >&2 - cat "$WORK/warm.log" | grep cache >&2 - exit 1 -fi - -# 4. Edit one module; rebuild; that module should be a miss, the others hits. -# Using `cp` (not shell var capture) to preserve the original exactly — -# command substitution strips trailing newlines, which would flip the -# source hash on restore. -cp registry.ts registry.ts.orig -sed -i.bak 's/MISSING/NOTFOUND/' registry.ts -rm -f registry.ts.bak -run_and_capture partial -if ! grep -E "• codegen cache: [0-9]+/[0-9]+ hit \(1 miss\)" "$WORK/partial.log" >/dev/null; then - echo "FAIL: partial rebuild should be 1 miss" >&2 - cat "$WORK/partial.log" | grep cache >&2 - exit 1 -fi -# Output must reflect the edit — this is the key anti-staleness check: -# a cache bug that returned the OLD .o bytes would still print "MISSING". -grep -q "999=NOTFOUND" "$WORK/partial.out" \ - || { echo "FAIL: partial output did not reflect source edit" >&2; cat "$WORK/partial.out" >&2; exit 1; } - -# 5. Restore source and confirm the cache correctly roundtrips back to a -# full-hit state for the original sources (no lingering stale state). -cp registry.ts.orig registry.ts -rm -f registry.ts.orig -run_and_capture rewarm -REWARM_OUT="$(cat "$WORK/rewarm.out")" -[ "$REWARM_OUT" = "$BASELINE_OUT" ] || { echo "FAIL: post-restore output differs from baseline" >&2; exit 1; } -if ! grep -E "• codegen cache: [0-9]+/[0-9]+ hit \(0 miss\)" "$WORK/rewarm.log" >/dev/null; then - echo "FAIL: after restoring source, rebuild should be all hits" >&2 - cat "$WORK/rewarm.log" | grep cache >&2 - exit 1 -fi - -# 6. Cosmetic-only edit (#686): add a trailing comment to a module without -# changing any code. The HIR fingerprint must be identical, so the cache -# must hit on every module. Pre-#686 (when the key folded source bytes) -# this would have shown 1 miss; the all-hits assertion is the regression -# gate for HIR-addressable caching. -echo "// cosmetic comment (no behavior change)" >> registry.ts -run_and_capture cosmetic -COSMETIC_OUT="$(cat "$WORK/cosmetic.out")" -[ "$COSMETIC_OUT" = "$BASELINE_OUT" ] || { - echo "FAIL: cosmetic edit changed runtime output (HIR walk has a bug?)" >&2 - diff <(echo "$BASELINE_OUT") <(echo "$COSMETIC_OUT") >&2 - exit 1 -} -if ! grep -E "• codegen cache: [0-9]+/[0-9]+ hit \(0 miss\)" "$WORK/cosmetic.log" >/dev/null; then - echo "FAIL: cosmetic edit should produce all hits (HIR-addressable cache)" >&2 - cat "$WORK/cosmetic.log" | grep cache >&2 - exit 1 -fi - -# 7. Behavior-changing edit (#686): mutate a literal so the HIR differs. -# Use the same `registry.ts` we just touched cosmetically (which still -# contains the original "MISSING" literal — the partial step's NOTFOUND -# edit was reverted by step 5's rewarm). Expect 1 miss and runtime -# output reflecting the change. -sed -i.bak 's/MISSING/CHANGED/' registry.ts -rm -f registry.ts.bak -run_and_capture behavior -if ! grep -E "• codegen cache: [0-9]+/[0-9]+ hit \(1 miss\)" "$WORK/behavior.log" >/dev/null; then - echo "FAIL: behavior edit should be exactly 1 miss" >&2 - cat "$WORK/behavior.log" | grep cache >&2 - exit 1 -fi -grep -q "999=CHANGED" "$WORK/behavior.out" \ - || { echo "FAIL: behavior output did not reflect source edit" >&2; cat "$WORK/behavior.out" >&2; exit 1; } - -# 8. `perry cache info` and `perry cache clean` smoke-test. -"$PERRY" cache info >"$WORK/info.log" 2>&1 -grep -q ".perry-cache" "$WORK/info.log" || { echo "FAIL: cache info should mention .perry-cache" >&2; exit 1; } -"$PERRY" cache clean >"$WORK/clean.log" 2>&1 -grep -qE "Removed.*\\.perry-cache" "$WORK/clean.log" || { echo "FAIL: cache clean should report removal" >&2; exit 1; } -[ ! -d ".perry-cache" ] || { echo "FAIL: .perry-cache still present after clean" >&2; exit 1; } - -echo "PASS: V2.2 + #686 object cache end-to-end smoke test" diff --git a/scripts/run_tty_pty_smoke.sh b/scripts/run_tty_pty_smoke.sh deleted file mode 100755 index db5854f0d8..0000000000 --- a/scripts/run_tty_pty_smoke.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT="$(cd "$(dirname "$0")/.." && pwd)" -FIXTURE="$ROOT/test-parity/fixtures/tty-pty-smoke.ts" -OUT_DIR="$ROOT/target/tmp/tty-pty-smoke" -PERRY_BIN="$ROOT/target/release/perry" -PERRY_OUT="$OUT_DIR/perry-tty-pty-smoke" -BUILD_LOG="$OUT_DIR/build.log" -COMPILE_LOG="$OUT_DIR/compile.log" -NODE_BIN="${NODE_BIN:-/home/github-runner/actions-runner/externals/node24/bin/node}" - -if [[ ! -x "$NODE_BIN" ]]; then - NODE_BIN="$(command -v node)" -fi - -mkdir -p "$OUT_DIR" - -cargo build --release --quiet -p perry -p perry-runtime -p perry-stdlib -p perry-runtime-static -p perry-stdlib-static >"$BUILD_LOG" 2>&1 || { - cat "$BUILD_LOG" - exit 1 -} - -PERRY_ALLOW_UNIMPLEMENTED=1 "$PERRY_BIN" "$FIXTURE" -o "$PERRY_OUT" >"$COMPILE_LOG" 2>&1 || { - cat "$COMPILE_LOG" - exit 1 -} - -run_under_pty() { - local output_file="$1" - local command="$2" - script -q -e -c "stty rows 24 cols 80 < /dev/tty; (sleep 0.45; stty rows 31 cols 100 < /dev/tty; kill -WINCH \$\$ 2>/dev/null || true) & exec $command" /dev/null >"$output_file" 2>&1 -} - -run_under_pty "$OUT_DIR/node.raw" "'$NODE_BIN' --experimental-strip-types '$FIXTURE'" -run_under_pty "$OUT_DIR/perry.raw" "'$PERRY_OUT'" - -normalize() { - python3 - "$1" <<'PY' -import re -import sys -path = sys.argv[1] -text = open(path, "rb").read().decode("utf-8", "replace") -text = text.replace("\r", "") -text = re.sub(r"\x1b\[[0-9;?]*[ -/]*[@-~]", "", text) -text = "\n".join(line.rstrip() for line in text.splitlines() if line.strip()) -print(text) -PY -} - -normalize "$OUT_DIR/node.raw" >"$OUT_DIR/node.txt" -normalize "$OUT_DIR/perry.raw" >"$OUT_DIR/perry.txt" - -if ! diff -u "$OUT_DIR/node.txt" "$OUT_DIR/perry.txt"; then - echo "TTY PTY smoke mismatch; raw output kept in $OUT_DIR" >&2 - exit 1 -fi - -cat "$OUT_DIR/perry.txt" diff --git a/src/core/wit/perry-container.wit b/src/core/wit/perry-container.wit deleted file mode 100644 index 0acbead628..0000000000 --- a/src/core/wit/perry-container.wit +++ /dev/null @@ -1,41 +0,0 @@ -interface container { - use types.{container-spec, container-handle, container-info, container-logs, image-info, backend-info}; - - run: func(spec: container-spec) -> result; - create: func(spec: container-spec) -> result; - start: func(id: string) -> result<_, string>; - stop: func(id: string, timeout: option) -> result<_, string>; - remove: func(id: string, force: bool) -> result<_, string>; - list: func(all: bool) -> result, string>; - inspect: func(id: string) -> result; - logs: func(id: string, tail: option) -> result; - exec: func(id: string, cmd: list, env: option>>, workdir: option) -> result; - pull-image: func(reference: string) -> result<_, string>; - list-images: func() -> result, string>; - remove-image: func(reference: string, force: bool) -> result<_, string>; - get-backend: func() -> string; - detect-backend: func() -> result, string>; - compose-up: func(spec: string) -> result; -} - -interface compose { - use types.{container-info, container-logs}; - - down: func(handle-id: u64, volumes: bool) -> result<_, string>; - ps: func(handle-id: u64) -> result, string>; - logs: func(handle-id: u64, service: option, tail: option) -> result; - exec: func(handle-id: u64, service: string, cmd: list) -> result; -} - -interface workloads { - use types.{workload-graph, workload-node, run-graph-options, graph-status, node-info, container-logs}; - - run-graph: func(graph: workload-graph, opts: option) -> result; - inspect-graph: func(graph: workload-graph) -> result; - handle-down: func(handle-id: u64, opts: string) -> result<_, string>; - handle-status: func(handle-id: u64) -> result; - handle-graph: func(handle-id: u64) -> workload-graph; - handle-logs: func(handle-id: u64, node: option, tail: option) -> result; - handle-exec: func(handle-id: u64, node: string, cmd: list) -> result; - handle-ps: func(handle-id: u64) -> result, string>; -} diff --git a/tests/modules/main b/tests/modules/main deleted file mode 100755 index aaceb42aac..0000000000 Binary files a/tests/modules/main and /dev/null differ diff --git a/wasm_ui_demo.html b/wasm_ui_demo.html deleted file mode 100644 index 5b0f09c9a1..0000000000 --- a/wasm_ui_demo.html +++ /dev/null @@ -1,2257 +0,0 @@ - - - - - - wasm_ui_demo - - - -
- - - - \ No newline at end of file