diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 87a069068f..52414ae72e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -458,6 +458,9 @@ jobs: - name: Run harness unit tests run: python3 -m unittest tests.test_compiler_output_regression + - name: Run native ABI evidence report unit tests + run: python3 -m unittest tests.test_native_abi_evidence_report + - name: Gate native-region proof compiler output run: | python3 scripts/compiler_output_regression.py suite \ @@ -466,6 +469,7 @@ jobs: --benchmark-mode smoke \ --runs 1 \ --perf-counters off \ + --gate \ --print-summary - name: Gate native-ABI proof compiler output @@ -476,6 +480,7 @@ jobs: --benchmark-mode smoke \ --runs 1 \ --perf-counters off \ + --gate \ --print-summary - name: Gate typed feedback runtime evidence @@ -538,6 +543,70 @@ jobs: name: compiler-output-regression path: target/compiler-output-regression/ + # --------------------------------------------------------------------------- + # Native ABI evidence packet + # + # Full material-performance packet for the type-lowering gate. This is heavier + # than the per-PR compiler-output smoke because it runs the native-ABI proof + # packet with timing-quality samples, runtime checks, and release/LTO symbol + # freshness. Gate tag pushes and opt-in PR/manual runs; ordinary PRs rely on + # the lighter report/unit and compiler-output structural gates above. + # --------------------------------------------------------------------------- + native-abi-evidence-packet: + if: >- + github.event_name == 'push' || + (github.event_name == 'workflow_dispatch' && inputs.run_extended_tests) || + (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'run-extended-tests')) + runs-on: ubuntu-latest + timeout-minutes: 90 + env: + RUSTC_WRAPPER: sccache + SCCACHE_GHA_ENABLED: "false" + SCCACHE_DIR: ${{ github.workspace }}/.sccache + SCCACHE_CACHE_SIZE: "12G" + CARGO_INCREMENTAL: "0" + steps: + - uses: actions/checkout@v6 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Install sccache + uses: mozilla-actions/sccache-action@v0.0.10 + + - name: Cache sccache objects + uses: actions/cache@v4 + with: + path: ${{ github.workspace }}/.sccache + key: sccache-${{ runner.os }}-perry-native-abi-evidence-${{ github.run_id }} + restore-keys: | + sccache-${{ runner.os }}-perry- + + - uses: Swatinem/rust-cache@v2 + with: + shared-key: "${{ runner.os }}-perry" + save-if: ${{ github.ref == 'refs/heads/main' }} + + - name: Install clang + run: | + sudo apt-get update + sudo apt-get install -y clang + + - name: Gate native ABI evidence packet + env: + RUSTC_WRAPPER: "" + RUSTFLAGS: -Awarnings + run: | + PYTHON=python3 bash tests/test_native_abi_evidence_packet_smoke.sh \ + target/native-abi-evidence-packet + + - name: Upload native ABI evidence packet + if: always() + uses: actions/upload-artifact@v7 + with: + name: native-abi-evidence-packet + path: target/native-abi-evidence-packet/ + # --------------------------------------------------------------------------- # Parity tests (Perry output vs Node.js) # --------------------------------------------------------------------------- diff --git a/TYPE_LOWERING.md b/TYPE_LOWERING.md index 33ba02e03a..fcc24d34eb 100644 --- a/TYPE_LOWERING.md +++ b/TYPE_LOWERING.md @@ -11,22 +11,63 @@ Status legend: the architecture item is not complete. - `[ ]` not complete for this branch yet. +### 2026-06-19 Integration / Material Gate Status + +All six parallel worker tracks are integrated on this branch: + +- typed ABI/clones: typed-f64 clones now admit proven raw `Int32` locals; +- arrays/effects: first packed-i32 store loop slice with versioned side exits; +- class/scalar: scalar method summaries now inline straight-line immutable + local temporaries; +- strings/collections: `Set` selected paths consume raw `StringRef` + values through the native lowering dispatcher; +- async/BigInt: small BigInt literals lower as region-local `i128` and box only + at JS-visible boundaries; +- verification/observability: packet freshness, release sentinel counts, and + material-accounting contracts are reported and gated. + +End-to-end smoke evidence: +`PYTHON=python3 RUSTC_WRAPPER= RUSTFLAGS=-Awarnings bash +tests/test_native_abi_evidence_packet_smoke.sh +tmp/native-abi-evidence-smoke-20260619T-integrated-fix2` passed. + +The material packet proves a quantitative improvement rather than only local IR +shape: + +- boxed Number allocations: control `3` -> typed `0` (100% reduction); +- Buffer slow-path helpers: control `6` -> typed `0` (100% reduction); +- array slow-path helpers: control `6` -> typed `0` (100% reduction); +- static runtime calls: control `324` -> typed `73` (77.5% reduction); +- traced allocations: control `640` -> typed `0` (100% reduction); +- static write-barrier helpers: control `11` -> typed `2` (81.8% reduction); +- traced write barriers: control `38580` -> typed `0` (100% reduction); +- median wall time: control `181.62ms` -> typed `8.11ms` (22.4x speedup); +- p95 wall time: control `184.29ms` -> typed `9.01ms` (20.4x speedup); +- release/LTO sentinel guard: `101/101` rooted symbols present, with runtime + archive/source fingerprints recorded. + +The gate matrix is fully passing for native ABI correctness, native-region +artifacts, explain-lowering accounting, runtime safety, and release/LTO symbol +guarding. This still does not claim a general typed function/method/closure ABI; +the proof is for the selected native/region-local lowering packet and the +tracked production slices above. + | Status | Architecture requirement | Current evidence / remaining work | |---|---|---| | `[~]` | Lower HIR values into typed SSA/native reps first | Region-local native reps exist for `i32`/`u32`, `i1`, `f64`, buffer views, packed numeric arrays, raw numeric fields, and selected `JsValueBits` consumers. A narrow value-first ordinary-expression path now keeps simple numeric literals, locals, local assignment, and numeric binary ops as `f64`, and simple boolean literals/locals/assignment/comparison/`!` as `i1`, until return/runtime materialization. Broad ordinary expression lowering is still predominantly generic `double` unless a local proof applies. Evidence: `representation_first_numeric_locals_stay_f64_until_abi` and `representation_first_boolean_locals_stay_i1_until_abi`. | -| `[~]` | Keep `JSValue` as ABI/fallback, not optimizer default | Public ABI remains `double`/NaN-box. First ordinary-function, own-instance-method, and local-closure typed-f64/typed-i1 candidates now keep raw `double`/`i1` clones behind public JSValue wrappers that guard arguments, call the typed clone on success, box/materialize at the ABI edge, and fall back to an internal generic body. Own-instance methods now also have a narrow `Int32... -> Int32` bitwise-safe clone: the public method symbol stays a JSValue wrapper/vtable target, exact direct calls guard receiver/method identity and Int32 args, call the internal `i32(...) -> i32` clone, and box only at the method ABI boundary. Ordinary functions also have a first string passthrough clone shape: the internal clone passes raw `StringHeader*` handles as `i64`, the public wrapper guards/unboxes JS strings, boxes the raw result with `js_nanbox_string`, and falls back to the generic body; same-module direct `FuncRef` calls with proven string args can call that raw clone directly after guards. Local typed string closures now use the same closure-aware raw string ABI (`i64 %this_closure, i64 string args... -> i64 string`) behind a public JSValue wrapper and guarded direct local call path, including immutable string captures guarded at wrapper/direct-call boundaries. Local typed closure clones now use a closure-aware internal ABI (`i64 %this_closure, typed args...`) and accept immutable typed captures for the conservative numeric/boolean/string slices. Ordinary functions now also cover mixed native predicate shapes: `number... -> boolean` emits an internal `i1(double, ...)` clone after `f64` guards, `Int32... -> boolean` emits an internal `i1(i32, ...)` clone after finite/in-range integer guards, and straight-line `Int32... -> Int32` bitwise bodies emit an internal `i32(...) -> i32` clone that boxes only at public/direct-call ABI edges. Same-module direct `FuncRef` calls carry typed parameter reps and can call those clones directly after the matching guards. Async, string methods/operators, dynamic string calls, escaping/unknown closures, mutable/boxed/`this`/`new.target` captures, inherited/dynamic method bodies beyond public wrapper dispatch, typed-i32 closure returns, and most functions/methods still use generic ABI. | +| `[~]` | Keep `JSValue` as ABI/fallback, not optimizer default | Public ABI remains `double`/NaN-box. First ordinary-function, own-instance-method, and local-closure typed-f64/typed-i1 candidates now keep raw `double`/`i1` clones behind public JSValue wrappers that guard arguments, call the typed clone on success, box/materialize at the ABI edge, and fall back to an internal generic body. Own-instance methods now also have a narrow `Int32... -> Int32` bitwise-safe clone: the public method symbol stays a JSValue wrapper/vtable target, exact direct calls guard receiver/method identity and Int32 args, call the internal `i32(...) -> i32` clone, and box only at the method ABI boundary. Ordinary functions also have a first string passthrough clone shape: the internal clone passes raw `StringHeader*` handles as `i64`, the public wrapper guards/unboxes JS strings, boxes the raw result with `js_nanbox_string`, and falls back to the generic body; same-module direct `FuncRef` calls with proven string args can call that raw clone directly after guards. Own-instance string passthrough methods now use the same raw `i64` string clone shape behind the public JSValue method wrapper, and exact direct method calls guard receiver/method identity plus string args before boxing only at the call boundary. Local typed string closures now use the same closure-aware raw string ABI (`i64 %this_closure, i64 string args... -> i64 string`) behind a public JSValue wrapper and guarded direct local call path, including immutable string captures guarded at wrapper/direct-call boundaries. Local typed closure clones now use a closure-aware internal ABI (`i64 %this_closure, typed args...`) and accept immutable typed captures for the conservative numeric/i32/boolean/string slices. Ordinary functions now also cover mixed native predicate shapes: `number... -> boolean` emits an internal `i1(double, ...)` clone after `f64` guards, `Int32... -> boolean` emits an internal `i1(i32, ...)` clone after finite/in-range integer guards, and straight-line `Int32... -> Int32` bitwise bodies emit an internal `i32(...) -> i32` clone that boxes only at public/direct-call ABI edges. Same-module direct `FuncRef` calls carry typed parameter reps and can call those clones directly after the matching guards. Async, built-in string methods/operators, dynamic string calls, escaping/unknown closures, mutable/boxed/`this`/`new.target` captures, inherited/dynamic method bodies beyond public wrapper dispatch, unsupported typed-string/i32 closure shapes, and most functions/methods still use generic ABI. | | `[~]` | Use `i64 JSValueBits` internally for boxed values | `JsValueBits` records and selected production consumers exist, including write-barrier child selection, boxed local/parameter/PreallocateBoxes storage as raw `i64` box pointers, compiler-emitted closure capture slots for boxed/generic JSValue traffic as raw `i64` bits, `array.push` slot/runtime-helper value selection, and dynamic property/index-set RHS selection before boxing at the store/helper edge, including array runtime-key index setters. `ExpectedNativeRep::JsValueBits` now tries value-first lowering for ordinary native expressions and direct `f64`/proven-`i1`/integer/native-handle/promise-boundary materialization to boxed bits before falling back through `JSValue`. Public boolean parameters in generic bodies still enter as JSValue ABI locals unless a typed clone owns the call path. Hand-written runtime closure users keep the compatibility `f64` helper API, dynamic property/index helper edges beyond the covered store paths, and many generic expression paths still materialize through `double`. Evidence: `accepts_js_value_bits_materialization_transitions`, `artifact_records_direct_f64_to_js_value_bits_for_write_barrier`, `artifact_records_direct_i1_to_js_value_bits_for_write_barrier`, `artifact_records_write_barrier_child_js_value_bits`, `boxed_local_slot_uses_i64_js_value_bits_until_helper_edges`, `boxed_param_slot_uses_i64_js_value_bits_until_helper_edges`, `boxed_jsvalue_storage_uses_bits_helpers_for_strings_objects_and_tags`, `artifact_records_boxed_local_slot_as_js_value_bits`, `box_bits_roundtrips_non_number_tags_exactly`, `test_closure_capture_bits_roundtrip_tagged_values`, `artifact_records_array_push_value_bits_before_slot_store`, `artifact_records_dynamic_property_set_value_bits_before_helper`, `artifact_records_dynamic_index_set_value_bits_before_helper`, and `artifact_records_array_runtime_key_index_set_value_bits_before_helper`. | | `[~]` | Rich TypeFacts/effect/range/escape lattice | Array-kind, array-stability, noalias, effect, unknown-call, alias, aggregate identity exposure, materialization-hazard facts, and a first async/microtask escape fact now feed packed-f64 and cached-length proofs. Loop array-length consumers now emit accepted/rejected effect-fact artifacts, including explicit async/microtask rejection records when an `await` would make cached length or bounded-index lowering unsafe. Object facts, field-sensitive escape/range facts, broader async/microtask summaries, and wider consumer coverage remain incomplete. Evidence: `async_microtask_escape_is_tracked_as_effect_fact`, `loop_length_effect_artifact_records_consumed_preservation_fact`, `async_microtask_effect_blocks_length_and_bounds_proofs_with_artifact_reason`, `aggregate_array_identity_exposure_marks_materialization_hazard`, `indirect_array_alias_from_container_blocks_length_and_bounds_proofs`, `loop_local_array_alias_push_blocks_packed_f64_loop_and_artifacts`, `hir_facts` unit tests, and invalidation regressions in `crates/perry-codegen/tests/native_proof_regressions/invalidation.rs`. | | `[~]` | Late boxing only at true dynamic boundaries | Native fast paths reduce boxing in verified regions; straight-line numeric and boolean ordinary-expression slices now materialize `f64`/`i1` only at return/runtime compatibility boundaries. Ordinary bodies still frequently lower to JSValue/`double` early outside those proven slices. | -| `[~]` | Treat async/generator lowering as allocation lowering | Compiler-private async/generator control locals now avoid generic JSValue boxes for the narrow closure-shared control state: `__gen_state` / `__gen_pending_type` use typed `i32` heap cells, and `__gen_done` / `__gen_executing` use typed boolean heap cells. This preserves closure lifetime/sharing semantics while keeping control reads, writes, and `__gen_state === const` dispatch comparisons in native `i32`/`i1`. The compiler-private iter-result scratch slot now has a raw-`f64` handoff for numeric payloads: proven numeric payloads store raw, annotation-only numeric payloads coerce through `js_number_coerce` before raw storage, numeric consumers read through `js_iter_result_get_value_f64`, and the runtime side flag prevents GC from scanning raw numeric bits as roots. Public await/PROMISE resolution values, `__gen_sent`, pending values, async captures, and externally visible async boundaries remain JSValue/generic. Evidence: `compiler_private_async_control_cells_use_primitive_heap_boxes`, `artifact_records_compiler_private_async_control_cells`, `compiler_private_async_iter_result_f64_slot_uses_typed_handoff`, `compiler_private_async_iter_result_annotated_numeric_payload_is_coerced_before_raw_slot`, `artifact_records_compiler_private_async_iter_result_f64_handoff`, `test_promise_iter_result_raw_f64_slot_is_not_scanned_as_root`, `primitive_control_boxes_round_trip_and_reject_foreign_pointers`, `representation_lowering_helpers_have_lto_keepalive_anchors`, and `test_runtime_symbol_guard_roots_async_control_box_helpers`. | -| `[~]` | Typed internal function/method/closure paths plus generic trampolines | Ordinary functions now have conservative typed-f64 clones for straight-line numeric return bodies, a bounded typed-i1 clone path for fixed-arity boolean-only functions with straight-line boolean return bodies, a first numeric-predicate typed-i1 function shape whose internal clone takes `double` params and returns `i1`, a first `Int32` predicate typed-i1 function shape whose internal clone takes raw `i32` params and emits signed integer comparisons, a first straight-line `Int32... -> Int32` bitwise return clone whose internal clone takes and returns raw `i32`, and a first fixed-arity typed-string passthrough clone whose internal clone takes and returns raw string handles as `i64`. Eligible ordinary functions expose the original public symbol as a JSValue trampoline and move the generic implementation to an internal `__generic` body; same-module direct calls can target f64/i32/i1/string clones when their arguments are proven and guarded. Exact own instance methods now use the same public-symbol wrapper shape for the narrower method-eligible boolean/numeric slices: runtime vtables register the public JSValue trampoline, typed clones stay internal, numeric-predicate method clones use `i1(double, ...)` or typed-param-rep internal signatures, straight-line `Int32... -> Int32` bitwise method clones use `i32(...) -> i32`, and guarded direct compiled calls jump to the internal generic method body on typed-argument guard failure. Eligible local closures expose the original closure function pointer as a JSValue trampoline, keep the generic closure body under `__generic`, and keep typed clones internal; numeric-predicate closure clones use `i1(i64 closure, typed args...)` internal signatures, and string passthrough closure clones use `i64(i64 closure, i64 string...)` internal signatures with `js_nanbox_string` only at wrapper/direct-call boundaries. Typed closure clones now always receive `i64 %this_closure`; immutable f64/i32/i1/string capture slots are loaded through that handle and converted to native reps before body lowering, with string capture guards emitted before raw clone entry. String methods, string operators, dynamic string call sites, mutable captures, boxed captures, `this`/`new.target` captures, dynamic closure values, typed-i32 closure returns, and escaping/async closure shapes remain generic. Evidence: `typed_i32_return_function_uses_i32_params_return_and_public_wrapper`, `artifact_records_typed_i32_function_clone_selection`, `typed_i32_return_function_rejects_annotation_only_or_unsafe_shapes`, `typed_i32_method_clone_emits_internal_clone_and_guarded_direct_call`, `typed_i32_method_public_trampoline_dispatches_before_generic_body`, `artifact_records_typed_i32_method_clone_selection`, `typed_i32_method_clone_rejects_number_param_number_return_and_unsafe_add`, `typed_string_function_clone_emits_internal_clone_and_guarded_wrapper`, `artifact_records_typed_string_direct_call_selection`, `typed_string_function_clone_rejects_unsupported_string_shapes`, `typed_string_closure_clone_emits_internal_clone_and_guarded_direct_call`, `typed_string_closure_clone_accepts_immutable_string_capture`, `artifact_records_typed_string_closure_clone_selection`, `typed_string_closure_clone_rejects_any_and_mutable_capture`, `typed_string_closure_clone_rejects_dynamic_callee_call_site`, `typed_i1_numeric_predicate_function_uses_f64_params_and_public_wrapper`, `typed_i1_i32_predicate_function_uses_i32_params_and_public_wrapper`, `typed_i1_numeric_predicate_method_uses_f64_params_and_guarded_direct_call`, `typed_i1_numeric_predicate_closure_uses_f64_params_and_guarded_direct_call`, `typed_f64_public_trampoline_dispatches_before_generic_body`, `typed_i1_public_trampoline_dispatches_before_generic_body`, `typed_f64_method_public_trampoline_dispatches_before_generic_body`, `typed_i1_method_public_trampoline_dispatches_before_generic_body`, `typed_f64_function_clone_*`, `typed_i1_function_clone_*`, `typed_f64_method_clone_*`, `typed_i1_method_clone_*`, `typed_f64_closure_clone_*`, and `typed_i1_closure_clone_*` tests in `crates/perry-codegen/tests/native_proof_regressions.rs`. | -| `[~]` | Packed numeric array lowering/versioning with safe fallback | Guarded packed-f64 loop versioning and typed-feedback/runtime layout gates exist. A first store-bearing shape, `arr[i] = arr[i] + number` / safe numeric RHS, now side-exits to the slow clone on store-guard failure instead of rejoining after boxed fallback. Release symbol guard coverage now roots/asserts the generated typed-feedback array helpers (`packed_f64_array_loop_guard`, numeric get/set guards, boxed fallbacks, numeric push, and companion array feedback helpers) so stale LTO/static archives fail before link. Dynamic fractional index fallback evidence now covers preserving the original runtime key for get/set and not truncating typed-array fractional numeric keys. Local alias mutation, length writes, unknown calls, materialization hazards, and unsafe store-then-read shapes still invalidate or reject the relevant cached-length/bounds/packed-f64 proofs. Broader effect summaries remain incomplete. Evidence: `packed_f64_loop_store_update_versions_with_side_exit`, packed-f64 invalidation regressions, `test_runtime_symbol_guard_roots_typed_feedback_array_helpers`, `typed_feedback_boxed_fallback_preserves_fractional_keys_for_array_like_receivers`, `typed_feedback_boxed_set_fallback_does_not_truncate_fractional_array_like_keys`, `dynamic_fractional_array_index`, and `scripts/check_runtime_symbols.sh target/release/libperry_runtime.a`. | +| `[~]` | Treat async/generator lowering as allocation lowering | Compiler-private async/generator control locals now avoid generic JSValue boxes for the narrow closure-shared control state: `__gen_state` / `__gen_pending_type` use typed `i32` heap cells, and `__gen_done` / `__gen_executing` use typed boolean heap cells. This preserves closure lifetime/sharing semantics while keeping control reads, writes, and `__gen_state === const` dispatch comparisons in native `i32`/`i1`. The compiler-private iter-result scratch slot now has raw-`f64`, raw-`i32`, and raw-`i1` handoffs for proven numeric, Int32, and boolean payloads: proven numeric payloads store raw, annotation-only numeric payloads coerce through `js_number_coerce` before raw storage, proven Int32 payloads store raw `i32` while annotation-only Int32 values stay off the raw-i32 slot, proven boolean payloads store raw `i1`, annotation-only boolean payloads stay generic, numeric consumers read through `js_iter_result_get_value_f64`, Int32 consumers read through `js_iter_result_get_value_i32`, boolean consumers read through `js_iter_result_get_value_i1`, and runtime side flags prevent GC from scanning raw primitive bits as roots. Public await/PROMISE resolution values, `__gen_sent`, pending values, async captures, and externally visible async boundaries remain JSValue/generic. Evidence: `compiler_private_async_control_cells_use_primitive_heap_boxes`, `artifact_records_compiler_private_async_control_cells`, `compiler_private_async_iter_result_f64_slot_uses_typed_handoff`, `compiler_private_async_iter_result_annotated_numeric_payload_is_coerced_before_raw_slot`, `artifact_records_compiler_private_async_iter_result_f64_handoff`, `compiler_private_async_iter_result_i32_slot_uses_typed_handoff`, `compiler_private_async_iter_result_annotated_i32_payload_stays_off_raw_i32_slot`, `artifact_records_compiler_private_async_iter_result_i32_handoff`, `compiler_private_async_iter_result_i1_slot_uses_typed_handoff`, `compiler_private_async_iter_result_annotated_boolean_payload_stays_generic`, `artifact_records_compiler_private_async_iter_result_i1_handoff`, `test_promise_iter_result_raw_f64_slot_is_not_scanned_as_root`, `test_promise_iter_result_raw_i32_slot_is_not_scanned_as_root`, `test_promise_iter_result_raw_i1_slot_is_not_scanned_as_root`, `primitive_control_boxes_round_trip_and_reject_foreign_pointers`, `representation_lowering_helpers_have_lto_keepalive_anchors`, and `test_runtime_symbol_guard_roots_async_control_box_helpers`. | +| `[~]` | Typed internal function/method/closure paths plus generic trampolines | Ordinary functions now have conservative typed-f64 clones for straight-line numeric return bodies, a bounded typed-i1 clone path for fixed-arity boolean-only functions with straight-line boolean return bodies, a first numeric-predicate typed-i1 function shape whose internal clone takes `double` params and returns `i1`, a first `Int32` predicate typed-i1 function shape whose internal clone takes raw `i32` params and emits signed integer comparisons, a first straight-line `Int32... -> Int32` bitwise return clone whose internal clone takes and returns raw `i32`, and a first fixed-arity typed-string passthrough clone whose internal clone takes and returns raw string handles as `i64`. Eligible ordinary functions expose the original public symbol as a JSValue trampoline and move the generic implementation to an internal `__generic` body; same-module direct calls can target f64/i32/i1/string clones when their arguments are proven and guarded. Exact own instance methods now use the same public-symbol wrapper shape for the narrower method-eligible boolean/numeric/string slices: runtime vtables register the public JSValue trampoline, typed clones stay internal, numeric-predicate method clones use `i1(double, ...)` or typed-param-rep internal signatures, straight-line `Int32... -> Int32` bitwise method clones use `i32(...) -> i32`, string passthrough method clones use `i64(string...) -> i64 string`, and guarded direct compiled calls jump to the internal generic method body on typed-argument guard failure. Eligible local closures expose the original closure function pointer as a JSValue trampoline, keep the generic closure body under `__generic`, and keep typed clones internal; numeric-predicate closure clones use `i1(i64 closure, typed args...)` internal signatures, and string passthrough closure clones use `i64(i64 closure, i64 string...)` internal signatures with `js_nanbox_string` only at wrapper/direct-call boundaries, and straight-line `Int32... -> Int32` bitwise closure clones use `i32(i64 closure, i32...)` internal signatures with JSValue boxing only at wrapper/direct-call boundaries. Typed closure clones now always receive `i64 %this_closure`; immutable f64/i32/i1/string capture slots are loaded through that handle and converted to native reps before body lowering, with string capture guards emitted before raw clone entry. Built-in string methods/operators, dynamic string call sites, unsupported string method bodies, mutable captures, boxed captures, `this`/`new.target` captures, dynamic closure values, unsupported typed-i32 closure shapes and escaping/async closure shapes remain generic. Evidence: `typed_i32_return_function_uses_i32_params_return_and_public_wrapper`, `artifact_records_typed_i32_function_clone_selection`, `typed_i32_return_function_rejects_annotation_only_or_unsafe_shapes`, `typed_i32_method_clone_emits_internal_clone_and_guarded_direct_call`, `typed_i32_method_public_trampoline_dispatches_before_generic_body`, `artifact_records_typed_i32_method_clone_selection`, `typed_i32_method_clone_rejects_number_param_number_return_and_unsafe_add`, `typed_string_function_clone_emits_internal_clone_and_guarded_wrapper`, `artifact_records_typed_string_direct_call_selection`, `typed_string_function_clone_rejects_unsupported_string_shapes`, `typed_string_method_clone_emits_internal_clone_and_guarded_direct_call`, `artifact_records_typed_string_method_clone_selection`, `typed_string_method_clone_rejects_unsupported_string_shapes`, `artifact_records_typed_string_method_clone_rejection_reason`, `typed_string_method_clone_rejects_dynamic_receiver_direct_call_site`, `typed_string_closure_clone_emits_internal_clone_and_guarded_direct_call`, `typed_string_closure_clone_accepts_immutable_string_capture`, `artifact_records_typed_string_closure_clone_selection`, `typed_string_closure_clone_rejects_any_and_mutable_capture`, `typed_string_closure_clone_rejects_dynamic_callee_call_site`, `typed_i1_numeric_predicate_function_uses_f64_params_and_public_wrapper`, `typed_i1_i32_predicate_function_uses_i32_params_and_public_wrapper`, `typed_i1_numeric_predicate_method_uses_f64_params_and_guarded_direct_call`, `typed_i1_numeric_predicate_closure_uses_f64_params_and_guarded_direct_call`, `typed_f64_public_trampoline_dispatches_before_generic_body`, `typed_i1_public_trampoline_dispatches_before_generic_body`, `typed_f64_method_public_trampoline_dispatches_before_generic_body`, `typed_i1_method_public_trampoline_dispatches_before_generic_body`, `typed_f64_function_clone_*`, `typed_i1_function_clone_*`, `typed_f64_method_clone_*`, `typed_i1_method_clone_*`, `typed_f64_closure_clone_*`, `typed_i1_closure_clone_*`, and `typed_i32_closure_clone_*` tests in `crates/perry-codegen/tests/native_proof_regressions.rs`. | +| `[~]` | Packed numeric array lowering/versioning with safe fallback | Guarded packed-f64 loop versioning and typed-feedback/runtime layout gates exist. Store-bearing shapes such as `arr[i] = arr[i] + number` and `arr[i] = Math.abs(arr[i])` now side-exit to the slow clone on store-guard failure instead of rejoining after boxed fallback; the unary math shape lowers the fast RHS to native `llvm.fabs.f64` only when the operand is the proven packed element, while coercive operands stay generic. `Int32[]` loops now have a packed-i32 versioning slice: read loops use the i32-specific layout guard, label fast/slow clones and artifacts as `packed_i32`, and materialize `arr[i]` as native `i32` inside the fast clone from the guarded raw numeric slot; a first store-bearing shape, `arr[i] = (arr[i] + i32_const) | 0`, keeps the RHS in the i32 lane, stores the exact f64 raw numeric slot after the packed-i32 store guard, and side-exits to the slow clone on guard failure. Release symbol guard coverage now roots/asserts the generated typed-feedback array helpers (`packed_f64_array_loop_guard`, `packed_i32_array_loop_guard`, numeric get/set guards, boxed fallbacks, numeric push, and companion array feedback helpers) so stale LTO/static archives fail before link. Dynamic fractional index fallback evidence now covers preserving the original runtime key for get/set and not truncating typed-array fractional numeric keys. Local alias mutation, length writes, unknown calls, materialization hazards, unsafe f64 stores to Int32 arrays, and unsafe store-then-read shapes still invalidate or reject the relevant cached-length/bounds/packed numeric proofs. Broader effect summaries remain incomplete. Evidence: `packed_f64_loop_store_update_versions_with_side_exit`, `packed_f64_loop_unary_math_store_versions_with_side_exit`, `packed_f64_loop_rejects_coercive_unary_math_store_rhs`, `packed_i32_loop_read_materializes_integer_native_load_with_fallback`, `packed_i32_loop_store_update_versions_with_side_exit`, `packed_i32_loop_store_rejects_fractional_number_rhs`, `loop_local_array_alias_push_blocks_packed_i32_loop_and_artifacts`, packed-f64 invalidation regressions, `test_runtime_symbol_guard_roots_typed_feedback_array_helpers`, `typed_feedback_boxed_fallback_preserves_fractional_keys_for_array_like_receivers`, `typed_feedback_boxed_set_fallback_does_not_truncate_fractional_array_like_keys`, `dynamic_fractional_array_index`, and `scripts/check_runtime_symbols.sh target/release/libperry_runtime.a`. | | `[~]` | Fixed/unboxed class field layout and direct typed field access | Raw numeric class-field fast paths exist for proven fields. Numeric consumers now use a raw-f64 class-field get path that keeps the guarded fast load as native `f64` and coerces only the boxed runtime fallback before the numeric merge. Raw numeric class-field get/set artifacts now carry explicit exact-declared-receiver, guarded class-id/keys, raw-f64 slot-array, and pointer-free bitmap notes; raw numeric stores also emit `WriteBarrierElided` evidence because the slot is proven non-pointer. Unknown receivers and computed/dynamic-shape class bodies do not claim raw slot access in their source function. General fixed mixed layouts and runtime pointer bitmaps are not complete. Evidence: `typed_feedback_guards_direct_class_field_specialization`, `artifact_records_raw_numeric_class_field_f64_fast_paths_and_fallback_reasons`, and `raw_numeric_class_field_rejects_unknown_or_dynamic_shape_receiver`. | -| `[~]` | Method/effect summaries for scalar replacement across simple method calls | Exact-receiver summaries exist for scalar-replaced class instances whose own method is fixed-arity and synchronous with either a numeric `return` over public numeric `this.field` reads/numeric params/arithmetic, or a boolean comparison predicate over that same numeric subset. This lets `new Point(...).sum()` and `new Point(...).isAbove(n)`-style calls inline against scalar field slots without heap allocation or method dispatch when arguments are proven in the current expression. Public `number`/`Int32` local arguments now use a guarded fast path: the fast branch checks `js_typed_f64_arg_guard`, unboxes with `js_typed_f64_arg_to_raw`, and the fallback materializes the scalar receiver before generic by-ID method dispatch. Unproven `any` arguments stay generic. Mutation/effect summaries, inherited/dynamic methods, field writes, `this` escape, accessors, dynamic property reads, nested/unknown calls, and broader non-numeric methods remain open. Evidence: `scalar_replaced_simple_method_call_inlines_summary_without_dispatch`, `artifact_records_scalar_replaced_method_summary_inline`, `scalar_replaced_boolean_method_predicate_inlines_without_dispatch_or_allocation`, `artifact_records_scalar_replaced_boolean_method_predicate_inline`, `scalar_method_boolean_predicate_rejects_mutation_call_accessor_and_dynamic_property`, `scalar_method_boolean_predicate_rejects_unproven_numeric_arguments`, and `scalar_method_boolean_predicate_guards_public_numeric_arguments`. | +| `[~]` | Method/effect summaries for scalar replacement across simple method calls | Exact-receiver summaries exist for scalar-replaced class instances whose own method is fixed-arity and synchronous with either a numeric `return` over public numeric `this.field` reads/numeric params/arithmetic, a boolean comparison predicate over that same numeric subset, or a signed Int32 bitwise return over public Int32 fields/params/in-range literals. This lets `new Point(...).sum()` / `isAbove(n)` and narrow `new Flags(...).mix(i32)`-style calls inline against scalar field slots without heap allocation or method dispatch when arguments are proven in the current expression. Public `number`/`Int32` local arguments, plus arithmetic expressions over guarded numeric locals and literals, now use guarded fast paths: f64 summaries check `js_typed_f64_arg_guard`, Int32 bitwise summaries check `js_typed_i32_arg_guard`, and fallbacks materialize the scalar receiver before generic by-ID method dispatch. Schema-v15 artifacts give `scalar_method_summary` facts a structured `detail` field, so inline records distinguish `exact_receiver_summary` versus `guarded_numeric_args_fast_path`, while materialized fallbacks distinguish `generic_argument` versus `guarded_numeric_args_fallback`. Unproven `any` arguments/expressions, unsigned shifts, non-Int32 fields for Int32 summaries, and broader method shapes stay generic. Mutation/effect summaries, inherited/dynamic methods, field writes, `this` escape, accessors, dynamic property reads, nested/unknown calls, and broader non-numeric methods remain open. Evidence: `scalar_replaced_simple_method_call_inlines_summary_without_dispatch`, `artifact_records_scalar_replaced_method_summary_inline`, `scalar_replaced_boolean_method_predicate_inlines_without_dispatch_or_allocation`, `artifact_records_scalar_replaced_boolean_method_predicate_inline`, `scalar_replaced_int32_bitwise_method_inlines_without_dispatch_or_allocation`, `scalar_method_int32_bitwise_guards_public_int32_argument_and_preserves_fallback`, `scalar_method_int32_bitwise_rejects_unproven_or_unsigned_shapes`, `scalar_method_boolean_predicate_rejects_mutation_call_accessor_and_dynamic_property`, `scalar_method_boolean_predicate_rejects_unproven_numeric_arguments`, `scalar_method_boolean_predicate_rejects_unproven_numeric_argument_expressions`, `scalar_method_boolean_predicate_guards_public_numeric_arguments`, and `scalar_method_boolean_predicate_guards_public_numeric_argument_expressions`. | | `[~]` | Interned property/method ID dispatch for hot static names | A first compatibility ID layer routes selected generated static-name property get/set, method fallback/apply, typed-feedback method-call, and class-method bind callsites through `*_by_property_id` / `*_by_id` wrappers. The current ID representation is the interned heap `StringHeader` pointer emitted by the StringPool, preserving existing semantics while removing raw byte-pointer/length plumbing from those callsites. Full global numeric IDs, vtable/property maps keyed directly by IDs, dynamic/computed keys, JS bridge calls, and broad specialized paths remain open. Evidence: `static_property_access_on_computed_class_uses_property_id_wrappers`, `static_name_method_fallback_uses_method_id_wrapper`, `static_name_spread_method_fallback_uses_method_id_wrapper`, and `static_name_class_method_value_uses_method_id_bind_wrapper`. | -| `[~]` | Unified safe string-like lowering | A first `PerryStringRef` resolver normalizes raw interned `StringHeader*` IDs, boxed heap-string IDs, and boxed SSO short-string IDs for the by-ID property/method wrappers. The typed-string function and local-closure ABIs add a non-throwing string-only guard/unbox pair for JS string arguments and immutable string captures, and materialize SSO strings only after the guard. These still use raw `StringHeader*` handles for the internal clone, not a full end-to-end `PerryStringRef` value representation; string methods, string operators, mutable string captures, and dynamic/computed lowering sites remain generic. Evidence: `dispatch_id_resolver_accepts_raw_heap_and_sso_string_forms`, `typed_string_arg_guard_is_non_throwing_and_string_only`, `typed_string_function_clone_emits_internal_clone_and_guarded_wrapper`, `artifact_records_typed_string_direct_call_selection`, `typed_string_closure_clone_emits_internal_clone_and_guarded_direct_call`, `typed_string_closure_clone_accepts_immutable_string_capture`, and `artifact_records_typed_string_closure_clone_selection`. | -| `[~]` | Key-specialized Map/Set lowering | Runtime Map/Set side tables already index numeric and string-content keys. Codegen now has a first static string-key collection slice: `Map.set/has` lowers through `js_map_set_string_number` / `js_map_has_string_key`, `Map.get` lowers through `js_map_get_string_key` while preserving boxed `JSValue`/`undefined` miss semantics, and `Set.add/has/delete` lowers through `js_set_add_string` / `js_set_has_string` / `js_set_delete_string` when the receiver type arguments and key/value expression are proven string. The generated-call helpers are rooted for release/LTO and covered by the runtime symbol guard. Numeric/int32 key specialization, unboxed stored values beyond the f64 map-set helper boundary, dynamic receivers, and broader `Record`/dictionary lowering remain generic. Evidence: `map_string_number_set_has_use_string_key_specialization`, `set_string_add_has_delete_use_string_specialization`, `string_number_specialized_helpers_use_string_content_keys`, `test_set_string_specialized_helpers_use_content_keys`, `representation_lowering_helpers_have_lto_keepalive_anchors`, and `test_runtime_symbol_guard_roots_map_set_string_lowering_helpers`. | -| `[~]` | User-facing `--explain-lowering` report | `perry build/compile --explain-lowering` emits a fresh `.perry-trace/lowering/.../explain-lowering.json` report and text summary from native-rep artifacts. The report now includes explicit reason maps and evidence rows for typed-clone selected/rejected/not-recorded decisions, generic fallbacks, dynamic boundaries, boxes, unboxes/coercions, runtime property gets, direct field loads, bounds kept/eliminated, and barriers emitted/eliminated. Explain-lowering mode requests comprehensive typed-clone rejection records from codegen, including broad clone-family mismatches that default native-rep artifact runs suppress for noise control. A bounded non-clone completeness slice now derives concrete categories for scalar-replaced raw-f64 direct field loads, generic write-barrier child-bit emissions, and checked-native bounds records that lack an explicit `bounds_state`. Other absent non-clone proof is still reported as `not_recorded`. Evidence: `cargo test -p perry lowering_report`, `report_derives_non_clone_reasons_without_explicit_reason_notes`, and `explain_lowering_mode_records_broad_typed_clone_rejection_reasons`. | +| `[~]` | Unified safe string-like lowering | A first `PerryStringRef` resolver normalizes raw interned `StringHeader*` IDs, boxed heap-string IDs, and boxed SSO short-string IDs for the by-ID property/method wrappers. The typed-string function, own-instance-method, and local-closure ABIs add a non-throwing string-only guard/unbox pair for JS string arguments and immutable string captures, and materialize SSO strings only after the guard. These still use raw `StringHeader*` handles for the internal clone, not a full end-to-end `PerryStringRef` value representation; built-in string methods/operators, mutable string captures, dynamic receivers, unsupported string method bodies, and dynamic/computed lowering sites remain generic. Evidence: `dispatch_id_resolver_accepts_raw_heap_and_sso_string_forms`, `typed_string_arg_guard_is_non_throwing_and_string_only`, `typed_string_function_clone_emits_internal_clone_and_guarded_wrapper`, `artifact_records_typed_string_direct_call_selection`, `typed_string_method_clone_emits_internal_clone_and_guarded_direct_call`, `artifact_records_typed_string_method_clone_selection`, `typed_string_closure_clone_emits_internal_clone_and_guarded_direct_call`, `typed_string_closure_clone_accepts_immutable_string_capture`, and `artifact_records_typed_string_closure_clone_selection`. | +| `[~]` | Key-specialized Map/Set lowering | Runtime Map/Set side tables index numeric and string-content keys. Codegen now lowers proven `Map.set/get/has/delete` and `Set.add/has/delete` through string-key helpers, with typed Map value helpers for proven `number`/`Int32`/`PerryU32`/`PerryF32`/`boolean`/`string` set values and typed Set value helpers for proven `Int32`/`PerryU32`/`PerryF32`/`boolean`. It also lowers proven `Map.set/get/has/delete` through guarded raw-f64 key helpers (`js_map_*_number_key`) and proven `Set.add/has/delete` through guarded raw-f64 value helpers (`js_set_*_number`); guard failure and unproven values branch to the generic JSValue helpers. Helpers are rooted for release/LTO and covered by the runtime symbol guard. Native-rep artifacts record selected collection lanes with consumed type facts (`string_ref`, `f64`, `i32`, `u32`, `f32`, or `i1`) and rejected/generic lanes with rejected collection facts. Annotation-only typed values remain generic. Unboxed stored values beyond helper boundaries, dynamic receivers, and broader `Record`/dictionary lowering remain incomplete. Evidence: `map_number_key_set_get_has_delete_use_guarded_number_key_specialization`, `map_unproven_number_key_keeps_generic_fallback`, `artifact_records_map_number_key_helper_selection_and_rejection`, `set_number_add_has_delete_use_guarded_number_specialization`, `set_number_specialization_rejects_unproven_value`, `artifact_records_set_number_value_helper_selection_and_rejection`, `number_key_specialized_helpers_preserve_numeric_keys_and_fallback`, `test_set_number_specialized_helpers_preserve_numeric_values_and_fallback`, existing string/typed-value Map/Set artifact tests, `representation_lowering_helpers_have_lto_keepalive_anchors`, and `test_runtime_symbol_guard_roots_map_set_string_lowering_helpers`. | +| `[~]` | User-facing `--explain-lowering` report | `perry build/compile --explain-lowering` emits a fresh `.perry-trace/lowering/.../explain-lowering.json` report and text summary from native-rep artifacts. The report now includes explicit reason maps and evidence rows for typed-path selected/fallback/rejected decisions, typed-clone selected/rejected/not-recorded decisions, generic fallbacks, dynamic boundaries, boxes, unboxes/coercions, runtime property gets, direct field loads, scalar replacement selected/fallback/rejected decisions, bounds kept/eliminated, barriers emitted/eliminated, and selected/rejected collection helper lanes, including string-key Map/Set helpers, numeric Map/Set helpers, and typed-value Map/Set helpers. Evidence rows now carry consumed/rejected fact labels so selected and rejected typed paths are artifact-backed instead of note-only. Native-rep artifact summaries include consumed/rejected fact-kind counts and typed-path decision counts, and the verifier rejects malformed fact-use rows. Explain-lowering mode requests comprehensive typed-clone rejection records from codegen, including broad clone-family mismatches that default native-rep artifact runs suppress for noise control. A bounded non-clone completeness slice now derives concrete categories for scalar-replaced raw-f64 direct field loads, structured scalar-method summary details, generic write-barrier child-bit emissions, checked-native bounds records that lack an explicit `bounds_state`, collection helper selected/rejected decisions from artifact notes such as `selected_helper`, `generic_helper`, and `typed_collection_rejected`, and collection typed-value selected/rejected decisions from consumed/rejected `map.*_value_helper` and `set.*_value_helper` facts. Material evidence now reports/gates boxed-number allocations, buffer slow-path helpers, array slow-path helpers, traced allocations, write barriers, and wall-time speedups in the native ABI packet. Other absent non-clone proof is still reported as `not_recorded`. Evidence: `RUSTC_WRAPPER= RUSTFLAGS=-Awarnings cargo test -p perry lowering_report`, `RUSTC_WRAPPER= RUSTFLAGS=-Awarnings cargo test -p perry-codegen native_value::verify --lib`, `python3 -m unittest tests.test_compiler_output_regression tests.test_native_abi_evidence_report`, `report_classifies_scalar_method_inline_and_materialized_fallback_facts`, `report_counts_collection_helper_selection_and_rejection_reasons`, `report_derives_non_clone_reasons_without_explicit_reason_notes`, `report_counts_typed_clone_fallback_and_native_reps`, `verifier_accepts_structured_consumed_and_rejected_facts`, and `verifier_rejects_malformed_fact_uses`. | ## 0.1 Landed Scope for This Branch @@ -78,7 +119,15 @@ Compiler evidence for this branch covers: proof records; - packed-`f64` array loop versioning guarded by typed-feedback/runtime layout checks, including the first safe store-update path whose store-guard failure - side-exits/restarts in the slow clone instead of rejoining the raw fast clone; + side-exits/restarts in the slow clone instead of rejoining the raw fast clone. + A narrow unary-math store RHS, `Math.abs(arr[i])`, also stays native in the + fast clone as `llvm.fabs.f64` when the operand is the proven packed element, + while coercive unary math operands remain on the generic ToNumber-preserving + slow path. `Int32[]` loop slices now distinguish `packed_i32` array facts, + emit `for.packed_i32_fast`/slow clones, materialize `arr[i]` as native `i32` + inside read fast clones, and cover the first i32-preserving store update + shape with slow-clone side exits on store-guard failure while preserving + generic fallback and alias invalidation evidence; - a narrow representation-first ordinary-expression path for simple numeric literals, locals, local assignment, and numeric binary ops, plus simple boolean literals, locals, local assignment, numeric/boolean comparisons, and @@ -88,7 +137,8 @@ Compiler evidence for this branch covers: `representation_first_boolean_locals_stay_i1_until_abi`; - array-kind, noalias, length-stability, local-alias mutation, aggregate array-identity exposure, unknown-call, and materialization hazard facts - consumed by packed-array and cached-length proofs; + consumed by packed-array and cached-length proofs, including distinct + `packed_i32` versus `packed_f64` array-kind facts for guarded loop lowering; - raw numeric class-field get/set paths guarded by layout and field facts, including a numeric-consumer get variant that keeps the fast raw `f64` load native and moves `js_number_coerce` into the boxed fallback block before the @@ -98,15 +148,39 @@ Compiler evidence for this branch covers: `write_barrier.elided_raw_f64_class_field`; unknown receivers and computed/dynamic-shape class bodies are covered by negative evidence that they do not claim raw slot access in the source function; -- a first key-specialized collection lowering slice for statically proven - string-key collections. `Map.set/has`, `Map.get`, and +- a key-specialized collection lowering slice for statically proven string and + numeric collections. `Map.set/get/has/delete` and `Set.add/has/delete` lower through string-key runtime helpers when - receiver type arguments and key/value expressions are proven string. These - helpers preserve content equality across distinct heap-string pointers and - are rooted in the release/LTO symbol guard. `Map.get` still returns boxed - `JSValue` so missing entries remain `undefined`; numeric/int32 key - specialization, dynamic receivers, and broader Map/Set/Record typed storage - remain generic; + receiver type arguments and key/value expressions are proven string. Typed + `Map.set` value helpers cover proven `number`, `Int32`, + `PerryU32`, `PerryF32`, `boolean`, and `string` values, boxing only at the + map slot boundary. Typed `Set` value helpers + pass raw native values when the receiver type and value expression proof + match; annotation-only values remain generic. + `Map.set/get/has/delete` now guards the boxed key with + `js_typed_f64_arg_guard`, unboxes it with `js_typed_f64_arg_to_raw`, and calls + `js_map_set_number_key`, `js_map_get_number_key`, `js_map_has_number_key`, or + `js_map_delete_number_key`; guard failure and unproven keys call the generic + JSValue helpers. `Set.add/has/delete` follows the same guarded raw-f64 + pattern through `js_set_add_number`, `js_set_has_number`, and + `js_set_delete_number`. `Map.get` still returns boxed `JSValue` so missing + entries remain `undefined`. + Selected collection-helper lanes emit native-rep records with consumed + collection facts (`string_ref`, `f64`, `i32`, `u32`, `f32`, or `i1`); + rejected/generic lanes emit rejected facts and guard-failure reasons for + explain-lowering. The runtime helpers preserve numeric zero normalization and + string content equality, are rooted in the release/LTO symbol guard, and are + covered by runtime symbol sentinels. Dynamic receivers, unboxed stored values + beyond helper boundaries, and broader Map/Set/Record typed storage remain + generic/incomplete. Evidence: + `map_number_key_set_get_has_delete_use_guarded_number_key_specialization`, + `map_unproven_number_key_keeps_generic_fallback`, + `artifact_records_map_number_key_helper_selection_and_rejection`, + `set_number_add_has_delete_use_guarded_number_specialization`, + `set_number_specialization_rejects_unproven_value`, + `artifact_records_set_number_value_helper_selection_and_rejection`, + `number_key_specialized_helpers_preserve_numeric_keys_and_fallback`, and + `test_set_number_specialized_helpers_preserve_numeric_values_and_fallback`; - selected native binding descriptors such as scalar numbers, `buffer+len`, POD records/views, native handles, and promise boundaries; - `JsValueBits` as an internal bit-pattern representation with boxed local, @@ -120,21 +194,36 @@ Compiler evidence for this branch covers: runtime-key index sets now do the same for their RHS before calling runtime setter helpers. Unsupported/generic values still fall back through explicit `JSValue` bitcast transitions at compatibility boundaries; -- compiler-private async/generator scratch lowering for the first numeric - payload boundary. `IterResultSet` stores numeric payloads through - `js_iter_result_set_f64`; literals and prior raw iter-result values stay raw, - while annotation-only numeric payloads are coerced with `js_number_coerce` - before the raw slot side flag is set. Numeric consumers use - `js_iter_result_get_value_f64`, which returns raw slots directly and coerces - generic slots only on the cold fallback. The runtime GC scanner skips the - iter-result value slot only while the raw-f64 side flag is set, so - pointer-looking numeric bits are not rewritten as roots. Promise resolution - values, externally visible async boundaries, `__gen_sent`, pending values, - and async captures remain generic JSValue paths. Evidence: +- compiler-private async/generator scratch lowering for the first numeric, + Int32, and boolean payload boundaries. `IterResultSet` stores proven numeric payloads + through `js_iter_result_set_f64`; literals and prior raw iter-result values + stay raw, while annotation-only numeric payloads are coerced with + `js_number_coerce` before the raw slot side flag is set. It stores proven + Int32 payloads through `js_iter_result_set_i32` and serves Int32 consumers + through `js_iter_result_get_value_i32`, while annotation-only Int32 payloads + stay off the raw-i32 slot. It stores proven boolean payloads through + `js_iter_result_set_i1`; annotation-only boolean payloads stay generic. + Numeric consumers use `js_iter_result_get_value_f64`, which returns raw slots + directly and coerces generic slots only on the cold fallback, while boolean + consumers use `js_iter_result_get_value_i1`, which returns raw `i1` directly + and falls back to JS truthiness for generic slots. The runtime GC scanner + skips the iter-result value slot while a raw primitive side flag is set, so + pointer-looking numeric/int32 or stale JSValue bits are not rewritten as roots. + Promise resolution values, externally visible async + boundaries, `__gen_sent`, pending values, and async captures remain generic + JSValue paths. Evidence: `compiler_private_async_iter_result_f64_slot_uses_typed_handoff`, `compiler_private_async_iter_result_annotated_numeric_payload_is_coerced_before_raw_slot`, - `artifact_records_compiler_private_async_iter_result_f64_handoff`, and - `test_promise_iter_result_raw_f64_slot_is_not_scanned_as_root`; + `artifact_records_compiler_private_async_iter_result_f64_handoff`, + `compiler_private_async_iter_result_i32_slot_uses_typed_handoff`, + `compiler_private_async_iter_result_annotated_i32_payload_stays_off_raw_i32_slot`, + `artifact_records_compiler_private_async_iter_result_i32_handoff`, + `compiler_private_async_iter_result_i1_slot_uses_typed_handoff`, + `compiler_private_async_iter_result_annotated_boolean_payload_stays_generic`, + `artifact_records_compiler_private_async_iter_result_i1_handoff`, + `test_promise_iter_result_raw_f64_slot_is_not_scanned_as_root`, and + `test_promise_iter_result_raw_i32_slot_is_not_scanned_as_root`, and + `test_promise_iter_result_raw_i1_slot_is_not_scanned_as_root`; - a first ordinary-function typed-f64 clone path for conservative straight-line numeric functions. Eligible public symbols now guard JSValue args, unbox to raw `double`, call the typed clone, and fall back to an internal generic body @@ -167,6 +256,21 @@ Compiler evidence for this branch covers: `typed_string_arg_guard_is_non_throwing_and_string_only`, `typed_string_function_clone_emits_internal_clone_and_guarded_wrapper`, and `artifact_records_typed_string_direct_call_selection`. +- a first own-instance-method typed-string clone path for fixed-arity string + params and safe string passthrough returns. The public method symbol remains + the JSValue vtable target and wrapper; the internal clone takes raw + `StringHeader*` handles as `i64`, exact direct calls guard receiver/method + identity and string args, guard failures target the internal generic method + body, and results box with `js_nanbox_string` only at the public/direct-call + boundary. Unsupported string bodies, dynamic receivers, `any` params, + defaults/rest/`arguments`, and non-string params/returns stay generic. The + path reuses the already-rooted typed string guard/unbox and nanbox helpers; + no new runtime helper symbols are introduced. Evidence: + `typed_string_method_clone_emits_internal_clone_and_guarded_direct_call`, + `artifact_records_typed_string_method_clone_selection`, + `typed_string_method_clone_rejects_unsupported_string_shapes`, + `artifact_records_typed_string_method_clone_rejection_reason`, and + `typed_string_method_clone_rejects_dynamic_receiver_direct_call_site`. - a first own-instance-method typed-f64 clone path. It accepts only fixed-arity numeric params and numeric returns with a single simple numeric return expression; it rejects `this`, @@ -229,15 +333,36 @@ Compiler evidence for this branch covers: `artifact_records_typed_string_closure_clone_selection`, and `typed_string_closure_clone_rejects_any_and_mutable_capture`. - scalar-replaced method summary paths for exact local receivers and simple - numeric `return this.field` arithmetic or boolean comparisons over public - numeric `this.field` reads and numeric params, avoiding heap allocation and - runtime method dispatch when call arguments are proven numeric in the current - expression. Public `number`/`Int32` local arguments now get a guarded scalar - inline branch using `js_typed_f64_arg_guard` / `js_typed_f64_arg_to_raw`; guard - failure materializes the scalar receiver and dispatches through the generic - by-ID method path. Unproven `any` arguments stay generic rather than trusting - TypeScript annotations as runtime truth. Evidence: - `scalar_method_boolean_predicate_guards_public_numeric_arguments`. + numeric `return this.field` arithmetic, boolean comparisons over public + numeric `this.field` reads and numeric params, or signed Int32 bitwise returns + over public Int32 fields/params/in-range literals, avoiding heap allocation + and runtime method dispatch when call arguments are proven in the current + expression. Public `number`/`Int32` local arguments and supported arithmetic + expressions over guarded numeric locals and literals now get a guarded scalar + inline branch using either `js_typed_f64_arg_guard` / + `js_typed_f64_arg_to_raw` or `js_typed_i32_arg_guard` / + `js_typed_i32_arg_to_raw`; the f64 fast branch rebuilds `+`, `-`, `*`, `/`, + `%`, unary `+`, and unary `-` as raw `f64`, while the Int32 fast branch keeps + signed bitwise operators as native `i32` and boxes only at the scalar-call + boundary. Guard failure materializes the scalar receiver and dispatches + through the generic by-ID method path. Inline artifacts now consume a + `scalar_method_summary` fact, while unproven `any` arguments/expressions, + unsupported unsigned Int32 shifts, and guarded fallback branches reject that + fact with `generic_arg` or `arg_guard_failed` and record + `dynamic_fallback`/`runtime_api` materialization evidence rather than trusting + TypeScript annotations as runtime truth. Native-rep artifact schema v15 adds + a structured `detail` field to fact records; scalar-method summary details now + distinguish exact inline selection, guarded fast-path selection, generic-arg + fallback, and guard-failure fallback for explain-lowering. Evidence: + `artifact_records_scalar_replaced_method_summary_inline`, + `artifact_records_scalar_replaced_boolean_method_predicate_inline`, + `scalar_replaced_int32_bitwise_method_inlines_without_dispatch_or_allocation`, + `scalar_method_int32_bitwise_guards_public_int32_argument_and_preserves_fallback`, + `scalar_method_int32_bitwise_rejects_unproven_or_unsigned_shapes`, + `scalar_method_boolean_predicate_rejects_unproven_numeric_arguments`, + `scalar_method_boolean_predicate_rejects_unproven_numeric_argument_expressions`, + `scalar_method_boolean_predicate_guards_public_numeric_arguments`, and + `scalar_method_boolean_predicate_guards_public_numeric_argument_expressions`. - static write-barrier elision now leaves native-representation evidence for primitive array-store children and pointer-free raw numeric class-field stores, so reports can distinguish barriers skipped by proof from barriers @@ -255,25 +380,67 @@ Compiler evidence for this branch covers: classifies artifact-backed reasons for typed-f64 clone selection, generic fallback emission, dynamic fallbacks, boxing/unboxing/coercions, runtime property gets, direct field loads, bounds kept/eliminated, and write-barrier - emitted/eliminated decisions. Explain-lowering mode asks codegen to include + emitted/eliminated decisions. It also records scalar replacement selected, + fallback, and rejected decision counts from structured fact details, plus + collection helper selected and + rejected/generic decisions for string-key and typed-value Map/Set lanes, + grouping them by helper family and artifact-backed rejection reason. Selected + and rejected typed-value lanes are also summarized directly from consumed or + rejected `map.*_value_helper` / `set.*_value_helper` facts, so + `Map.set` value-helper + selections and `Map` / `Set` guarded raw-f64 helper + selections are visible even when the runtime helper is also a key-specialized + helper. + Explain-lowering mode asks codegen to include comprehensive typed-clone rejection reasons, while default native-rep artifact runs continue to suppress high-volume clone-family mismatch records. The current bounded report-completeness slice derives concrete reason categories from existing artifact shape for scalar-replaced raw-f64 field - loads, generic write-barrier child-bit emissions, and checked-native bounds - records without explicit `bounds_state`. Other non-clone records with no + loads, scalar-method summary inline/fallback facts with schema-v15 `detail` + reasons, generic write-barrier + child-bit emissions, checked-native bounds records without explicit + `bounds_state`, collection helper notes such as `selected_helper`, + `generic_helper`, and `typed_collection_rejected`, and collection typed-value + facts. Other non-clone records with no artifact-backed proof still use `not_recorded` rather than inventing proof. +- the native ABI evidence packet now hard-gates the material delta contract: + typed/control packet evidence must include explicit checksum rows proving the + same semantic output, plus `>=95%` traced allocation and traced write-barrier + reductions, `0` typed boxed-number allocations, `0` typed buffer slow paths, + `>=2x` median wall-time speedup, and `>=1.5x` p95 wall-time speedup from + timing-quality benchmark samples. It also checks per-workload packet + contracts: typed and control manifests must match the expected source/kind, + typed packets must carry artifact-backed unchecked `buffer_view` and `u8` + native records with proven/guarded bounds plus zero static boxed-number and + buffer slow-path counters, and control packets must keep positive boxed/slow + static baselines. The packet now requests GC trace support at + compile/link time for trace-budget workloads so auto-optimized runtimes keep + diagnostics enabled for evidence runs, while the boxed/control packet uses a + diagnostics-enabled absolute write-barrier envelope and the typed-vs-control + material delta remains the acceptance gate. CI now runs the + native-region/native-ABI compiler-output proof suites with `--gate`, and the + suite runner propagates that gate into each workload capture. CI also has a tag/extended + `native-abi-evidence-packet` job that runs the full gated packet and uploads + the retained evidence. The native ABI evidence packet also runs + `scripts/check_runtime_symbols.sh` against the resolved runtime archive and + fails gate mode unless the log proves the archive defines all sentinel + release/LTO helper symbols. The packet runner scrubs `RUSTC_WRAPPER` by + default, forces `RUSTFLAGS=-Awarnings` for verification commands unless an + explicit evidence override is supplied, and records the effective wrapper and + flags in packet metadata so release evidence can distinguish fresh local + builds from stale cached/LTO artifacts. Still follow-up unless separately implemented: - broad typed function/method/closure clone generation beyond the current - conservative typed-f64, typed-i1, ordinary-function typed-i32 return, - ordinary-function typed-string, and immutable-capture local-closure - typed-string slices; + conservative typed-f64, typed-i1, ordinary-function and local-closure typed-i32 return, + ordinary-function/own-instance-method typed-string, and immutable-capture + local-closure typed-string slices; - public generic trampolines beyond the current conservative ordinary-function, own-instance-method, and local-closure typed-f64/typed-i1 candidates, plus - the ordinary-function typed-i32 return and ordinary-function/immutable-capture - local-closure typed-string passthrough candidates; + the ordinary-function and local-closure typed-i32 return slices and + ordinary-function/own-instance-method/immutable-capture local-closure + typed-string passthrough candidates; - broader closure capture/call ABI coverage for mutable/boxed captures, escaping, dynamic, async, `this`/`new.target`, non-numeric, and mixed closure shapes, including non-passthrough string closure bodies; @@ -282,17 +449,20 @@ Still follow-up unless separately implemented: - broader typed method clones for inherited/dynamic receivers, static methods, receiver-sensitive bodies, non-numeric shapes, and broad effect summaries that allow mutation-safe method inlining beyond the current exact scalar receiver - numeric-return/boolean-predicate shapes and guarded public numeric-argument - scalar fast path. + numeric-return/boolean-predicate/Int32-bitwise shapes and guarded public + numeric-argument or simple numeric-expression scalar fast path, plus the + current exact own-instance string-passthrough method clone. - full `PerryStringRef` value lowering beyond raw `StringHeader*` typed-string - function/closure passthroughs, direct same-module string function calls, - immutable string closure captures, and static dispatch-ID resolution. + function/method/closure passthroughs, direct same-module string function and + exact own-method calls, immutable string closure captures, and static + dispatch-ID resolution. - direct runtime maps keyed by property/method IDs and migration of remaining static-name specialized paths away from raw bytes where semantics permit. - broader codegen-side reason emission for non-clone lowering failures that - currently leave no artifact record. The report has a `not_recorded` bucket for - these cases, but complete observability still needs eligibility failure facts - at more lowering decision sites. + currently leave no artifact record beyond the covered scalar-method, + collection-helper, bounds, field-load, and barrier decision sites. The report + has a `not_recorded` bucket for these cases, but complete observability still + needs eligibility failure facts at more lowering decision sites. ## 1. Type Lowering Pipeline @@ -400,11 +570,23 @@ can bypass part of the generic NaN-boxing overhead: - `MapHeader` + `SetHeader` with side-table indices: `MAP_INDEX` (numeric keys), `MAP_STRING_INDEX` (FNV-1a content hashes for GC-safe string lookup), `SET_INDEX`. - O(1) average lookup; content-based equality for strings. -- First compiler lowering slice: statically proven `Map.set/has`, - `Map.get`, and `Set.add/has/delete` call string-key helpers - directly instead of generic JSValue-key helpers. `Map.get` remains a boxed - value boundary for miss semantics; this is not yet a numeric-key, - typed-value-table, or `Record` specialization. +- Current compiler lowering slice: statically proven + `Map.set/get/has/delete` and `Set.add/has/delete` call + string-key helpers directly instead of generic JSValue-key helpers. Typed + `Map.set` value helpers take raw `i32`, `u32`, `float`, `i1`, or + string handles when the value expression has the matching native proof; other + `Map.set` cases keep stored values as JSValue unless a narrower typed-value + helper exists. `Set.add/has/delete` pass raw + native values into typed helpers when receiver type and value proof match. + `Map.set/get/has/delete` and `Set.add/has/delete` now use + guarded raw-f64 helpers for proven numeric keys/values and generic JSValue + helpers on guard failure or unproven inputs. `Map.get` remains a boxed value + boundary for miss semantics. Annotation-only typed values remain generic. + This is not yet a broad typed-value-table or `Record` + specialization. Native-rep artifacts describe selected string-key lanes as + `string_ref`, selected numeric lanes as `f64`, selected typed Map/Set value + lanes as `i32`, `u32`, `f32`, `i1`, or `string_ref`, and generic helper lanes + with rejected helper facts. ### Buffer (`BufferHeader`) @@ -582,10 +764,10 @@ narrow typed-f64 internal clone slices for ordinary functions, exact own instance methods, and local closures when the body is a single simple numeric return expression, plus typed-i1 slices for ordinary functions, exact own instance methods, and local closures when the body is a single simple boolean -return expression. Ordinary functions also include a first typed-i32 return -slice for fixed-arity `Int32` params and straight-line bitwise-preserving +return expression. Ordinary functions and local closures also include first typed-i32 return +slices for fixed-arity `Int32` params and straight-line bitwise-preserving `Int32` bodies. The local closure slices also accept immutable typed captures -in the current f64/i1 body subset. Eligible ordinary functions now get a public +in the current f64/i32/i1 body subset. Eligible ordinary functions now get a public `double`/NaN-box wrapper under the original symbol plus an internal generic body fallback. Eligible own instance methods and local closures now use the same public wrapper plus internal `__generic` body split. Ineligible method and diff --git a/benchmarks/compiler_output/README.md b/benchmarks/compiler_output/README.md index e05227167d..4feca7e288 100644 --- a/benchmarks/compiler_output/README.md +++ b/benchmarks/compiler_output/README.md @@ -64,6 +64,33 @@ The harness also captures best-effort explanation counters: the TOML spec. - Hardware counters from `perf stat` when available on Linux. +## Native ABI Evidence Packet Matrix + +`scripts/native_abi_evidence_packet.sh --gate` aggregates the +`native-abi-proof` compiler-output suite into +`native-abi-evidence.json` and `native-abi-evidence.md`. The packet is the +representative material type-lowering gate for PRs and release sweeps. + +| Gate row | Evidence | Required proof | +|---|---|---| +| Native ABI correctness | `tests/test_native_abi_contract.sh` and `tests/test_c_layout_pod_records.sh` retained native-rep artifacts | runtime output passes and required ABI/materialization tokens are present | +| Native-region artifact chain | retained HIR, LLVM before opt, LLVM after opt analysis, object disassembly, compile plans, and native-rep JSON | artifacts exist, structural safety checks pass, semantic checksum checks pass | +| Explain-lowering accounting | native-rep rows summarized into boxes, unboxes/coercions, dynamic fallbacks, barrier decisions, typed native records, and runtime counter summaries | typed/control material accounting rows pass | +| Runtime safety | `perry-runtime native_async` tests | required native async/rooting test names pass and appear in logs | +| Release/LTO symbols | `scripts/check_runtime_symbols.sh` over the runtime archive | runtime archive defines all sentinel symbols | + +The material accounting rows compare +`native_abi_packet_typed.ts` against `native_abi_packet_control.ts`. +The gate requires 100% elimination of boxed-number allocations, Buffer +slow-path helpers, and typed-array/Uint8Array slow-path helpers in the typed +packet; at least 95% fewer traced allocations and traced write barriers; at +least 75% fewer static write-barrier helper sites; at least 25% fewer static +runtime helper call sites; at least 2.0x median wall-time speedup; and at least +1.5x p95 wall-time speedup. The control packet must keep positive boxed, +helper, barrier, allocation, and runtime-call baselines, and both packets must +produce matching semantic checksums so the optimized path and fallback path are +comparing the same work. + The `hir_fact_rewrite` fixture is the rewrite-insensitivity gate for the HIR fact layer. It keeps `const j = helper(i); dst[j] = ...` on the same direct buffer path as an inline index expression: inbounds GEPs, bounds assumptions, diff --git a/benchmarks/compiler_output/workloads.toml b/benchmarks/compiler_output/workloads.toml index 0ea01d2771..8d427f0bbd 100644 --- a/benchmarks/compiler_output/workloads.toml +++ b/benchmarks/compiler_output/workloads.toml @@ -404,7 +404,8 @@ allowed_missed_reason_kinds = [ [workloads.h1_native_rep_equivalence.runtime_budgets] allocations_traced = 0 gc_collections_traced = 0 -write_barriers_static = 0 +# Includes root barrier setup now counted by the static analyzer. +write_barriers_static = 1 write_barriers_traced = 0 boxed_number_allocations_static = 0 buffer_slow_path_accesses_static = 0 @@ -547,7 +548,8 @@ allowed_missed_reason_kinds = [ [workloads.h1_buffer_alias_negative.runtime_budgets] allocations_traced = 0 gc_collections_traced = 0 -write_barriers_static = 0 +# Negative alias cases emit root barrier setup outside the hot proof surface. +write_barriers_static = 3 write_barriers_traced = 0 boxed_number_allocations_static = 0 buffer_slow_path_accesses_static = 100 @@ -1560,7 +1562,8 @@ allowed_missed_reason_kinds = [ [workloads.native_owned_typed_views.runtime_budgets] allocations_traced = 0 gc_collections_traced = 0 -write_barriers_static = 0 +# One root barrier remains in setup; traced hot-path barriers stay zero. +write_barriers_static = 1 write_barriers_traced = 0 boxed_number_allocations_static = 0 buffer_slow_path_accesses_static = 16 @@ -1718,7 +1721,8 @@ allowed_missed_reason_kinds = [ [workloads.native_pod_layout_constants.runtime_budgets] allocations_traced = 0 gc_collections_traced = 0 -write_barriers_static = 0 +# Native memory setup roots are counted as static barriers. +write_barriers_static = 2 write_barriers_traced = 0 boxed_number_allocations_static = 0 buffer_slow_path_accesses_static = 0 @@ -1750,7 +1754,8 @@ allowed_missed_reason_kinds = [ [workloads.native_memory_bulk_fill.runtime_budgets] allocations_traced = 0 gc_collections_traced = 0 -write_barriers_static = 0 +# Native memory setup roots are counted as static barriers. +write_barriers_static = 2 write_barriers_traced = 0 boxed_number_allocations_static = 0 buffer_slow_path_accesses_static = 0 @@ -1812,7 +1817,8 @@ allowed_missed_reason_kinds = [ [workloads.native_memory_fixture.runtime_budgets] allocations_traced = 0 gc_collections_traced = 0 -write_barriers_static = 0 +# Native memory setup roots are counted as static barriers. +write_barriers_static = 2 write_barriers_traced = 0 boxed_number_allocations_static = 0 buffer_slow_path_accesses_static = 0 @@ -1968,7 +1974,7 @@ allowed_missed_reason_kinds = [ allocations_traced = 640 gc_collections_traced = 5 write_barriers_static = 64 -write_barriers_traced = 360 +write_barriers_traced = 50000 boxed_number_allocations_static = 64 buffer_slow_path_accesses_static = 128 diff --git a/crates/perry-codegen/src/codegen/artifacts.rs b/crates/perry-codegen/src/codegen/artifacts.rs index 49b6584a2d..4d8ca2c204 100644 --- a/crates/perry-codegen/src/codegen/artifacts.rs +++ b/crates/perry-codegen/src/codegen/artifacts.rs @@ -19,13 +19,14 @@ use crate::types::{LlvmType, DOUBLE, I64, VOID}; use super::closure::{ compile_closure, compile_typed_f64_closure, compile_typed_i1_closure, - compile_typed_string_closure, + compile_typed_i32_closure, compile_typed_string_closure, }; use super::entry::compile_module_entry; use super::helpers::{function_body_returns_generator_object, sanitize, scoped_fn_name}; use super::method::{ compile_method, compile_static_method, compile_typed_f64_method, compile_typed_f64_receiver_method, compile_typed_i1_method, compile_typed_i32_method, + compile_typed_string_method, }; use super::opts::CrossModuleCtx; use super::spec_function_length; @@ -163,6 +164,16 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { ) .with_context(|| format!("lowering typed-i1 closure clone func_id={}", func_id))?; } + if cross_module.typed_i32_closures.contains(func_id) { + compile_typed_i32_closure( + llmod, + *func_id, + closure_expr, + module_prefix, + module_local_types, + ) + .with_context(|| format!("lowering typed-i32 closure clone func_id={}", func_id))?; + } if cross_module.typed_string_closures.contains(func_id) { compile_typed_string_closure( llmod, @@ -218,6 +229,11 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { .contains(&(class.name.clone(), method.name.clone())) { Some(TypedFunctionTrampolineKind::I1) + } else if cross_module + .typed_string_methods + .contains(&(class.name.clone(), method.name.clone())) + { + Some(TypedFunctionTrampolineKind::StringRef) } else { None }; @@ -270,6 +286,19 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> { ) })?; } + if cross_module + .typed_string_methods + .contains(&(class.name.clone(), method.name.clone())) + { + compile_typed_string_method(llmod, class, method, method_names).with_context( + || { + format!( + "lowering typed-string method clone '{}::{}'", + class.name, method.name + ) + }, + )?; + } compile_method( llmod, class, diff --git a/crates/perry-codegen/src/codegen/closure.rs b/crates/perry-codegen/src/codegen/closure.rs index 3f57748b15..028720191a 100644 --- a/crates/perry-codegen/src/codegen/closure.rs +++ b/crates/perry-codegen/src/codegen/closure.rs @@ -16,9 +16,10 @@ use crate::types::{LlvmType, DOUBLE, I1, I32, I64}; use super::opts::CrossModuleCtx; use super::typed_abi::{ emit_typed_arg_guard, emit_typed_arg_to_raw, generic_closure_body_name, - lower_typed_f64_body_with_seed_locals, lower_typed_i1_body_with_seed_locals, - lower_typed_string_body_with_seed_locals, typed_f64_closure_capture_reps, - typed_f64_closure_name, typed_i1_closure_capture_reps, typed_i1_closure_name, + lower_typed_f64_body_with_seed_locals_and_reps, lower_typed_i1_body_with_seed_locals, + lower_typed_i32_body_with_seed_locals, lower_typed_string_body_with_seed_locals, + typed_f64_closure_capture_reps, typed_f64_closure_name, typed_i1_closure_capture_reps, + typed_i1_closure_name, typed_i32_closure_capture_reps, typed_i32_closure_name, typed_param_reps_for_params, typed_string_closure_capture_reps, typed_string_closure_name, TypedFunctionTrampolineKind, TypedParamRep, }; @@ -32,21 +33,37 @@ fn emit_typed_closure_trampoline_fast_value( ) -> String { match kind { TypedFunctionTrampolineKind::F64 => { - let mut raw_args = Vec::with_capacity(arg_names.len()); - for arg in arg_names { - raw_args.push(blk.call( - DOUBLE, - "js_typed_f64_arg_to_raw", - &[(DOUBLE, arg.as_str())], - )); - } + let raw_args: Vec = arg_names + .iter() + .zip(arg_reps.iter()) + .map(|(arg, rep)| emit_typed_arg_to_raw(blk, *rep, arg)) + .collect(); let mut typed_args: Vec<(LlvmType, &str)> = Vec::with_capacity(raw_args.len() + 1); typed_args.push((I64, "%this_closure")); - typed_args.extend(raw_args.iter().map(|arg| (DOUBLE, arg.as_str()))); + typed_args.extend( + raw_args + .iter() + .zip(arg_reps.iter()) + .map(|(arg, rep)| (rep.llvm_ty(), arg.as_str())), + ); blk.call(DOUBLE, typed_name, &typed_args) } TypedFunctionTrampolineKind::I32 => { - unreachable!("typed-i32 closure trampolines are not emitted") + let raw_args: Vec = arg_names + .iter() + .zip(arg_reps.iter()) + .map(|(arg, rep)| emit_typed_arg_to_raw(blk, *rep, arg)) + .collect(); + let mut typed_args: Vec<(LlvmType, &str)> = Vec::with_capacity(raw_args.len() + 1); + typed_args.push((I64, "%this_closure")); + typed_args.extend( + raw_args + .iter() + .zip(arg_reps.iter()) + .map(|(arg, rep)| (rep.llvm_ty(), arg.as_str())), + ); + let raw_i32 = blk.call(I32, typed_name, &typed_args); + crate::expr::i32_to_nanbox(blk, &raw_i32) } TypedFunctionTrampolineKind::I1 => { let raw_args: Vec = arg_names @@ -74,7 +91,12 @@ fn emit_typed_closure_trampoline_fast_value( .collect(); let mut typed_args: Vec<(LlvmType, &str)> = Vec::with_capacity(raw_args.len() + 1); typed_args.push((I64, "%this_closure")); - typed_args.extend(raw_args.iter().map(|arg| (I64, arg.as_str()))); + typed_args.extend( + raw_args + .iter() + .zip(arg_reps.iter()) + .map(|(arg, rep)| (rep.llvm_ty(), arg.as_str())), + ); let raw_string = blk.call(I64, typed_name, &typed_args); blk.call(DOUBLE, "js_nanbox_string", &[(I64, &raw_string)]) } @@ -101,20 +123,19 @@ fn emit_public_typed_closure_trampoline( let public_name = format!("perry_closure_{}__{}", module_prefix, func_id); let typed_name = match kind { TypedFunctionTrampolineKind::F64 => typed_f64_closure_name(&public_name), - TypedFunctionTrampolineKind::I32 => { - unreachable!("typed-i32 closure trampolines are not emitted") - } + TypedFunctionTrampolineKind::I32 => typed_i32_closure_name(&public_name), TypedFunctionTrampolineKind::I1 => typed_i1_closure_name(&public_name), TypedFunctionTrampolineKind::StringRef => typed_string_closure_name(&public_name), }; let arg_reps = match kind { - TypedFunctionTrampolineKind::F64 => vec![TypedParamRep::F64; params.len()], - TypedFunctionTrampolineKind::I32 => { - unreachable!("typed-i32 closure trampolines are not emitted") - } + TypedFunctionTrampolineKind::F64 => typed_param_reps_for_params(params) + .unwrap_or_else(|| vec![TypedParamRep::F64; params.len()]), + TypedFunctionTrampolineKind::I32 => typed_param_reps_for_params(params) + .unwrap_or_else(|| vec![TypedParamRep::I32; params.len()]), TypedFunctionTrampolineKind::I1 => typed_param_reps_for_params(params) .unwrap_or_else(|| vec![TypedParamRep::I1; params.len()]), - TypedFunctionTrampolineKind::StringRef => vec![TypedParamRep::StringRef; params.len()], + TypedFunctionTrampolineKind::StringRef => typed_param_reps_for_params(params) + .unwrap_or_else(|| vec![TypedParamRep::StringRef; params.len()]), }; let mut llvm_params: Vec<(LlvmType, String)> = Vec::with_capacity(params.len() + 1); llvm_params.push((I64, "%this_closure".to_string())); @@ -281,7 +302,18 @@ pub(super) fn compile_typed_string_closure( let llvm_name = typed_string_closure_name(&generic_name); let mut llvm_params: Vec<(LlvmType, String)> = Vec::with_capacity(params.len() + 1); llvm_params.push((I64, "%this_closure".to_string())); - llvm_params.extend(params.iter().map(|p| (I64, format!("%arg{}", p.id)))); + let param_reps = typed_param_reps_for_params(params).ok_or_else(|| { + anyhow!( + "typed-string closure '{}' has unsupported parameter", + func_id + ) + })?; + llvm_params.extend( + params + .iter() + .zip(param_reps.iter()) + .map(|(p, rep)| (rep.llvm_ty(), format!("%arg{}", p.id))), + ); let lf = llmod.define_function(&llvm_name, I64, llvm_params); lf.linkage = "internal".to_string(); lf.force_inline = true; @@ -318,7 +350,14 @@ pub(super) fn compile_typed_f64_closure( let llvm_name = typed_f64_closure_name(&generic_name); let mut llvm_params: Vec<(LlvmType, String)> = Vec::with_capacity(params.len() + 1); llvm_params.push((I64, "%this_closure".to_string())); - llvm_params.extend(params.iter().map(|p| (DOUBLE, format!("%arg{}", p.id)))); + let param_reps = typed_param_reps_for_params(params) + .ok_or_else(|| anyhow!("typed-f64 closure '{}' has unsupported parameter", func_id))?; + llvm_params.extend( + params + .iter() + .zip(param_reps.iter()) + .map(|(p, rep)| (rep.llvm_ty(), format!("%arg{}", p.id))), + ); let lf = llmod.define_function(&llvm_name, DOUBLE, llvm_params); lf.linkage = "internal".to_string(); lf.force_inline = true; @@ -327,12 +366,14 @@ pub(super) fn compile_typed_f64_closure( let value = { let blk = lf.block_mut(0).unwrap(); let mut seed_locals = HashMap::new(); + let mut seed_reps = HashMap::new(); if let Some(captures) = typed_f64_closure_capture_reps(closure_expr, module_local_types) { for (idx, (id, rep)) in captures.iter().enumerate() { seed_locals.insert(*id, load_typed_capture(blk, idx, *rep)); + seed_reps.insert(*id, *rep); } } - lower_typed_f64_body_with_seed_locals(blk, params, body, seed_locals)? + lower_typed_f64_body_with_seed_locals_and_reps(blk, params, body, seed_locals, seed_reps)? }; lf.block_mut(0).unwrap().ret(DOUBLE, &value); Ok(()) @@ -383,6 +424,49 @@ pub(super) fn compile_typed_i1_closure( Ok(()) } +pub(super) fn compile_typed_i32_closure( + llmod: &mut LlModule, + func_id: perry_types::FuncId, + closure_expr: &perry_hir::Expr, + module_prefix: &str, + module_local_types: &HashMap, +) -> Result<()> { + let (params, body) = match closure_expr { + perry_hir::Expr::Closure { params, body, .. } => (params, body), + _ => return Err(anyhow!("compile_typed_i32_closure: expected Expr::Closure")), + }; + + let generic_name = format!("perry_closure_{}__{}", module_prefix, func_id); + let llvm_name = typed_i32_closure_name(&generic_name); + let mut llvm_params: Vec<(LlvmType, String)> = Vec::with_capacity(params.len() + 1); + llvm_params.push((I64, "%this_closure".to_string())); + let param_reps = typed_param_reps_for_params(params) + .ok_or_else(|| anyhow!("typed-i32 closure '{}' has unsupported parameter", func_id))?; + llvm_params.extend( + params + .iter() + .zip(param_reps.iter()) + .map(|(p, rep)| (rep.llvm_ty(), format!("%arg{}", p.id))), + ); + let lf = llmod.define_function(&llvm_name, I32, llvm_params); + lf.linkage = "internal".to_string(); + lf.force_inline = true; + let _ = lf.create_block("entry"); + + let value = { + let blk = lf.block_mut(0).unwrap(); + let mut seed_locals = HashMap::new(); + if let Some(captures) = typed_i32_closure_capture_reps(closure_expr, module_local_types) { + for (idx, (id, rep)) in captures.iter().enumerate() { + seed_locals.insert(*id, load_typed_capture(blk, idx, *rep)); + } + } + lower_typed_i32_body_with_seed_locals(blk, params, body, seed_locals)? + }; + lf.block_mut(0).unwrap().ret(I32, &value); + Ok(()) +} + /// Compile a closure body as a top-level LLVM function. /// /// Signature: `double perry_closure___(i64 this_closure, @@ -456,6 +540,8 @@ pub(super) fn compile_closure( let public_llvm_name = format!("perry_closure_{}__{}", module_prefix, func_id); let typed_public_trampoline = if cross_module.typed_f64_closures.contains(&func_id) { Some(TypedFunctionTrampolineKind::F64) + } else if cross_module.typed_i32_closures.contains(&func_id) { + Some(TypedFunctionTrampolineKind::I32) } else if cross_module.typed_i1_closures.contains(&func_id) { Some(TypedFunctionTrampolineKind::I1) } else if cross_module.typed_string_closures.contains(&func_id) { @@ -754,8 +840,10 @@ pub(super) fn compile_closure( typed_f64_methods: &cross_module.typed_f64_methods, typed_i32_methods: &cross_module.typed_i32_methods, typed_i1_methods: &cross_module.typed_i1_methods, + typed_string_methods: &cross_module.typed_string_methods, typed_i1_method_param_reps: &cross_module.typed_i1_method_param_reps, typed_f64_closures: &cross_module.typed_f64_closures, + typed_i32_closures: &cross_module.typed_i32_closures, typed_i1_closures: &cross_module.typed_i1_closures, typed_i1_closure_param_reps: &cross_module.typed_i1_closure_param_reps, typed_string_closures: &cross_module.typed_string_closures, diff --git a/crates/perry-codegen/src/codegen/entry.rs b/crates/perry-codegen/src/codegen/entry.rs index 801039523b..6738d6c6bc 100644 --- a/crates/perry-codegen/src/codegen/entry.rs +++ b/crates/perry-codegen/src/codegen/entry.rs @@ -468,8 +468,10 @@ pub(super) fn compile_module_entry( typed_f64_methods: &cross_module.typed_f64_methods, typed_i32_methods: &cross_module.typed_i32_methods, typed_i1_methods: &cross_module.typed_i1_methods, + typed_string_methods: &cross_module.typed_string_methods, typed_i1_method_param_reps: &cross_module.typed_i1_method_param_reps, typed_f64_closures: &cross_module.typed_f64_closures, + typed_i32_closures: &cross_module.typed_i32_closures, typed_i1_closures: &cross_module.typed_i1_closures, typed_i1_closure_param_reps: &cross_module.typed_i1_closure_param_reps, typed_string_closures: &cross_module.typed_string_closures, @@ -928,8 +930,10 @@ pub(super) fn compile_module_entry( typed_f64_methods: &cross_module.typed_f64_methods, typed_i32_methods: &cross_module.typed_i32_methods, typed_i1_methods: &cross_module.typed_i1_methods, + typed_string_methods: &cross_module.typed_string_methods, typed_i1_method_param_reps: &cross_module.typed_i1_method_param_reps, typed_f64_closures: &cross_module.typed_f64_closures, + typed_i32_closures: &cross_module.typed_i32_closures, typed_i1_closures: &cross_module.typed_i1_closures, typed_i1_closure_param_reps: &cross_module.typed_i1_closure_param_reps, typed_string_closures: &cross_module.typed_string_closures, diff --git a/crates/perry-codegen/src/codegen/function.rs b/crates/perry-codegen/src/codegen/function.rs index 4c81816494..882dde967e 100644 --- a/crates/perry-codegen/src/codegen/function.rs +++ b/crates/perry-codegen/src/codegen/function.rs @@ -37,10 +37,13 @@ pub(super) fn compile_typed_f64_function( .cloned() .ok_or_else(|| anyhow!("function name not resolved for {}", f.name))?; let llvm_name = typed_f64_function_name(&generic_name); + let param_reps = typed_param_reps_for_params(&f.params) + .ok_or_else(|| anyhow!("typed-f64 function '{}' has unsupported parameter", f.name))?; let params: Vec<(LlvmType, String)> = f .params .iter() - .map(|p| (DOUBLE, format!("%arg{}", p.id))) + .zip(param_reps.iter()) + .map(|(p, rep)| (rep.llvm_ty(), format!("%arg{}", p.id))) .collect(); let lf = llmod.define_function(&llvm_name, DOUBLE, params); lf.linkage = "internal".to_string(); @@ -68,10 +71,13 @@ pub(super) fn compile_typed_i32_function( .cloned() .ok_or_else(|| anyhow!("function name not resolved for {}", f.name))?; let llvm_name = typed_i32_function_name(&generic_name); + let param_reps = typed_param_reps_for_params(&f.params) + .ok_or_else(|| anyhow!("typed-i32 function '{}' has unsupported parameter", f.name))?; let params: Vec<(LlvmType, String)> = f .params .iter() - .map(|p| (I32, format!("%arg{}", p.id))) + .zip(param_reps.iter()) + .map(|(p, rep)| (rep.llvm_ty(), format!("%arg{}", p.id))) .collect(); let lf = llmod.define_function(&llvm_name, I32, params); lf.linkage = "internal".to_string(); @@ -134,10 +140,17 @@ pub(super) fn compile_typed_string_function( .cloned() .ok_or_else(|| anyhow!("function name not resolved for {}", f.name))?; let llvm_name = typed_string_function_name(&generic_name); + let param_reps = typed_param_reps_for_params(&f.params).ok_or_else(|| { + anyhow!( + "typed-string function '{}' has unsupported parameter", + f.name + ) + })?; let params: Vec<(LlvmType, String)> = f .params .iter() - .map(|p| (I64, format!("%arg{}", p.id))) + .zip(param_reps.iter()) + .map(|(p, rep)| (rep.llvm_ty(), format!("%arg{}", p.id))) .collect(); let lf = llmod.define_function(&llvm_name, I64, params); lf.linkage = "internal".to_string(); @@ -161,25 +174,29 @@ fn emit_typed_public_trampoline_fast_value( ) -> String { match kind { TypedFunctionTrampolineKind::F64 => { - let mut raw_args = Vec::with_capacity(arg_names.len()); - for arg in arg_names { - raw_args.push(blk.call( - DOUBLE, - "js_typed_f64_arg_to_raw", - &[(DOUBLE, arg.as_str())], - )); - } - let typed_args: Vec<(LlvmType, &str)> = - raw_args.iter().map(|arg| (DOUBLE, arg.as_str())).collect(); + let raw_args: Vec = arg_names + .iter() + .zip(arg_reps.iter()) + .map(|(arg, rep)| emit_typed_arg_to_raw(blk, *rep, arg)) + .collect(); + let typed_args: Vec<(LlvmType, &str)> = raw_args + .iter() + .zip(arg_reps.iter()) + .map(|(arg, rep)| (rep.llvm_ty(), arg.as_str())) + .collect(); blk.call(DOUBLE, typed_name, &typed_args) } TypedFunctionTrampolineKind::I32 => { let raw_args: Vec = arg_names .iter() - .map(|arg| blk.call(I32, "js_typed_i32_arg_to_raw", &[(DOUBLE, arg.as_str())])) + .zip(arg_reps.iter()) + .map(|(arg, rep)| emit_typed_arg_to_raw(blk, *rep, arg)) + .collect(); + let typed_args: Vec<(LlvmType, &str)> = raw_args + .iter() + .zip(arg_reps.iter()) + .map(|(arg, rep)| (rep.llvm_ty(), arg.as_str())) .collect(); - let typed_args: Vec<(LlvmType, &str)> = - raw_args.iter().map(|arg| (I32, arg.as_str())).collect(); let raw_i32 = blk.call(I32, typed_name, &typed_args); crate::expr::i32_to_nanbox(blk, &raw_i32) } @@ -204,8 +221,11 @@ fn emit_typed_public_trampoline_fast_value( .zip(arg_reps.iter()) .map(|(arg, rep)| emit_typed_arg_to_raw(blk, *rep, arg)) .collect(); - let typed_args: Vec<(LlvmType, &str)> = - raw_args.iter().map(|arg| (I64, arg.as_str())).collect(); + let typed_args: Vec<(LlvmType, &str)> = raw_args + .iter() + .zip(arg_reps.iter()) + .map(|(arg, rep)| (rep.llvm_ty(), arg.as_str())) + .collect(); let raw_string = blk.call(I64, typed_name, &typed_args); blk.call(DOUBLE, "js_nanbox_string", &[(I64, &raw_string)]) } @@ -226,11 +246,14 @@ fn emit_public_typed_function_trampoline( TypedFunctionTrampolineKind::StringRef => typed_string_function_name(public_name), }; let arg_reps = match kind { - TypedFunctionTrampolineKind::F64 => vec![TypedParamRep::F64; f.params.len()], - TypedFunctionTrampolineKind::I32 => vec![TypedParamRep::I32; f.params.len()], + TypedFunctionTrampolineKind::F64 => typed_param_reps_for_params(&f.params) + .unwrap_or_else(|| vec![TypedParamRep::F64; f.params.len()]), + TypedFunctionTrampolineKind::I32 => typed_param_reps_for_params(&f.params) + .unwrap_or_else(|| vec![TypedParamRep::I32; f.params.len()]), TypedFunctionTrampolineKind::I1 => typed_param_reps_for_params(&f.params) .unwrap_or_else(|| vec![TypedParamRep::I1; f.params.len()]), - TypedFunctionTrampolineKind::StringRef => vec![TypedParamRep::StringRef; f.params.len()], + TypedFunctionTrampolineKind::StringRef => typed_param_reps_for_params(&f.params) + .unwrap_or_else(|| vec![TypedParamRep::StringRef; f.params.len()]), }; let params: Vec<(LlvmType, String)> = f .params @@ -553,8 +576,10 @@ pub(super) fn compile_function( typed_f64_methods: &cross_module.typed_f64_methods, typed_i32_methods: &cross_module.typed_i32_methods, typed_i1_methods: &cross_module.typed_i1_methods, + typed_string_methods: &cross_module.typed_string_methods, typed_i1_method_param_reps: &cross_module.typed_i1_method_param_reps, typed_f64_closures: &cross_module.typed_f64_closures, + typed_i32_closures: &cross_module.typed_i32_closures, typed_i1_closures: &cross_module.typed_i1_closures, typed_i1_closure_param_reps: &cross_module.typed_i1_closure_param_reps, typed_string_closures: &cross_module.typed_string_closures, diff --git a/crates/perry-codegen/src/codegen/method.rs b/crates/perry-codegen/src/codegen/method.rs index 6f28dfc45b..d8b963769c 100644 --- a/crates/perry-codegen/src/codegen/method.rs +++ b/crates/perry-codegen/src/codegen/method.rs @@ -17,9 +17,9 @@ use super::opts::CrossModuleCtx; use super::typed_abi::{ emit_typed_arg_guard, emit_typed_arg_to_raw, generic_method_body_name, lower_typed_f64_body, lower_typed_f64_receiver_body, lower_typed_i1_body, lower_typed_i32_body, - typed_f64_method_name, typed_f64_receiver_method_name, typed_i1_method_name, - typed_i32_method_name, typed_param_reps_for_params, TypedFunctionTrampolineKind, TypedParamRep, - TypedReceiverMethodInfo, + lower_typed_string_body, typed_f64_method_name, typed_f64_receiver_method_name, + typed_i1_method_name, typed_i32_method_name, typed_param_reps_for_params, + typed_string_method_name, TypedFunctionTrampolineKind, TypedParamRep, TypedReceiverMethodInfo, }; fn emit_typed_method_trampoline_fast_value( @@ -31,25 +31,29 @@ fn emit_typed_method_trampoline_fast_value( ) -> String { match kind { TypedFunctionTrampolineKind::F64 => { - let mut raw_args = Vec::with_capacity(arg_names.len()); - for arg in arg_names { - raw_args.push(blk.call( - DOUBLE, - "js_typed_f64_arg_to_raw", - &[(DOUBLE, arg.as_str())], - )); - } - let typed_args: Vec<(LlvmType, &str)> = - raw_args.iter().map(|arg| (DOUBLE, arg.as_str())).collect(); + let raw_args: Vec = arg_names + .iter() + .zip(arg_reps.iter()) + .map(|(arg, rep)| emit_typed_arg_to_raw(blk, *rep, arg)) + .collect(); + let typed_args: Vec<(LlvmType, &str)> = raw_args + .iter() + .zip(arg_reps.iter()) + .map(|(arg, rep)| (rep.llvm_ty(), arg.as_str())) + .collect(); blk.call(DOUBLE, typed_name, &typed_args) } TypedFunctionTrampolineKind::I32 => { - let mut raw_args = Vec::with_capacity(arg_names.len()); - for arg in arg_names { - raw_args.push(blk.call(I32, "js_typed_i32_arg_to_raw", &[(DOUBLE, arg.as_str())])); - } - let typed_args: Vec<(LlvmType, &str)> = - raw_args.iter().map(|arg| (I32, arg.as_str())).collect(); + let raw_args: Vec = arg_names + .iter() + .zip(arg_reps.iter()) + .map(|(arg, rep)| emit_typed_arg_to_raw(blk, *rep, arg)) + .collect(); + let typed_args: Vec<(LlvmType, &str)> = raw_args + .iter() + .zip(arg_reps.iter()) + .map(|(arg, rep)| (rep.llvm_ty(), arg.as_str())) + .collect(); let raw_i32 = blk.call(I32, typed_name, &typed_args); crate::expr::i32_to_nanbox(blk, &raw_i32) } @@ -69,7 +73,18 @@ fn emit_typed_method_trampoline_fast_value( crate::expr::i32_bool_to_nanbox(blk, &typed_i32) } TypedFunctionTrampolineKind::StringRef => { - unreachable!("typed-string method trampolines are not emitted") + let raw_args: Vec = arg_names + .iter() + .zip(arg_reps.iter()) + .map(|(arg, rep)| emit_typed_arg_to_raw(blk, *rep, arg)) + .collect(); + let typed_args: Vec<(LlvmType, &str)> = raw_args + .iter() + .zip(arg_reps.iter()) + .map(|(arg, rep)| (rep.llvm_ty(), arg.as_str())) + .collect(); + let raw_string = blk.call(I64, typed_name, &typed_args); + blk.call(DOUBLE, "js_nanbox_string", &[(I64, &raw_string)]) } } } @@ -85,18 +100,17 @@ fn emit_public_typed_method_trampoline( TypedFunctionTrampolineKind::F64 => typed_f64_method_name(public_name), TypedFunctionTrampolineKind::I32 => typed_i32_method_name(public_name), TypedFunctionTrampolineKind::I1 => typed_i1_method_name(public_name), - TypedFunctionTrampolineKind::StringRef => { - unreachable!("typed-string method trampolines are not emitted") - } + TypedFunctionTrampolineKind::StringRef => typed_string_method_name(public_name), }; let arg_reps = match kind { - TypedFunctionTrampolineKind::F64 => vec![TypedParamRep::F64; method.params.len()], - TypedFunctionTrampolineKind::I32 => vec![TypedParamRep::I32; method.params.len()], + TypedFunctionTrampolineKind::F64 => typed_param_reps_for_params(&method.params) + .unwrap_or_else(|| vec![TypedParamRep::F64; method.params.len()]), + TypedFunctionTrampolineKind::I32 => typed_param_reps_for_params(&method.params) + .unwrap_or_else(|| vec![TypedParamRep::I32; method.params.len()]), TypedFunctionTrampolineKind::I1 => typed_param_reps_for_params(&method.params) .unwrap_or_else(|| vec![TypedParamRep::I1; method.params.len()]), - TypedFunctionTrampolineKind::StringRef => { - unreachable!("typed-string method trampolines are not emitted") - } + TypedFunctionTrampolineKind::StringRef => typed_param_reps_for_params(&method.params) + .unwrap_or_else(|| vec![TypedParamRep::StringRef; method.params.len()]), }; let mut params: Vec<(LlvmType, String)> = Vec::with_capacity(method.params.len() + 1); params.push((DOUBLE, "%this_arg".to_string())); @@ -446,8 +460,10 @@ pub(super) fn compile_method( typed_f64_methods: &cross_module.typed_f64_methods, typed_i32_methods: &cross_module.typed_i32_methods, typed_i1_methods: &cross_module.typed_i1_methods, + typed_string_methods: &cross_module.typed_string_methods, typed_i1_method_param_reps: &cross_module.typed_i1_method_param_reps, typed_f64_closures: &cross_module.typed_f64_closures, + typed_i32_closures: &cross_module.typed_i32_closures, typed_i1_closures: &cross_module.typed_i1_closures, typed_i1_closure_param_reps: &cross_module.typed_i1_closure_param_reps, typed_string_closures: &cross_module.typed_string_closures, @@ -778,10 +794,18 @@ pub(super) fn compile_typed_f64_method( ) })?; let llvm_name = typed_f64_method_name(&generic_name); + let param_reps = typed_param_reps_for_params(&method.params).ok_or_else(|| { + anyhow!( + "typed-f64 method '{}::{}' has unsupported parameter", + class.name, + method.name + ) + })?; let params: Vec<(LlvmType, String)> = method .params .iter() - .map(|p| (DOUBLE, format!("%arg{}", p.id))) + .zip(param_reps.iter()) + .map(|(p, rep)| (rep.llvm_ty(), format!("%arg{}", p.id))) .collect(); let lf = llmod.define_function(&llvm_name, DOUBLE, params); lf.linkage = "internal".to_string(); @@ -902,10 +926,18 @@ pub(super) fn compile_typed_i32_method( ) })?; let llvm_name = typed_i32_method_name(&generic_name); + let param_reps = typed_param_reps_for_params(&method.params).ok_or_else(|| { + anyhow!( + "typed-i32 method '{}::{}' has unsupported parameter", + class.name, + method.name + ) + })?; let params: Vec<(LlvmType, String)> = method .params .iter() - .map(|p| (I32, format!("%arg{}", p.id))) + .zip(param_reps.iter()) + .map(|(p, rep)| (rep.llvm_ty(), format!("%arg{}", p.id))) .collect(); let lf = llmod.define_function(&llvm_name, I32, params); lf.linkage = "internal".to_string(); @@ -920,6 +952,52 @@ pub(super) fn compile_typed_i32_method( Ok(()) } +/// Compile the internal typed-string clone for a conservatively eligible +/// instance method. The clone passes raw `StringHeader*` handles as i64; the +/// public method symbol remains a JSValue trampoline registered in vtables. +pub(super) fn compile_typed_string_method( + llmod: &mut LlModule, + class: &perry_hir::Class, + method: &Function, + methods: &HashMap<(String, String), String>, +) -> Result<()> { + let generic_name = methods + .get(&(class.name.clone(), method.name.clone())) + .cloned() + .ok_or_else(|| { + anyhow!( + "method '{}::{}' missing from registry", + class.name, + method.name + ) + })?; + let llvm_name = typed_string_method_name(&generic_name); + let param_reps = typed_param_reps_for_params(&method.params).ok_or_else(|| { + anyhow!( + "typed-string method '{}::{}' has unsupported parameter", + class.name, + method.name + ) + })?; + let params: Vec<(LlvmType, String)> = method + .params + .iter() + .zip(param_reps.iter()) + .map(|(p, rep)| (rep.llvm_ty(), format!("%arg{}", p.id))) + .collect(); + let lf = llmod.define_function(&llvm_name, I64, params); + lf.linkage = "internal".to_string(); + lf.force_inline = true; + let _ = lf.create_block("entry"); + + let value = { + let blk = lf.block_mut(0).unwrap(); + lower_typed_string_body(blk, &method.params, &method.body)? + }; + lf.block_mut(0).unwrap().ret(I64, &value); + Ok(()) +} + /// Compile a static class method as a top-level LLVM function with /// no `this` parameter. Mostly identical to `compile_function` but /// the LLVM symbol name is scoped by module, class id, class name, and @@ -1145,8 +1223,10 @@ pub(super) fn compile_static_method( typed_f64_methods: &cross_module.typed_f64_methods, typed_i32_methods: &cross_module.typed_i32_methods, typed_i1_methods: &cross_module.typed_i1_methods, + typed_string_methods: &cross_module.typed_string_methods, typed_i1_method_param_reps: &cross_module.typed_i1_method_param_reps, typed_f64_closures: &cross_module.typed_f64_closures, + typed_i32_closures: &cross_module.typed_i32_closures, typed_i1_closures: &cross_module.typed_i1_closures, typed_i1_closure_param_reps: &cross_module.typed_i1_closure_param_reps, typed_string_closures: &cross_module.typed_string_closures, diff --git a/crates/perry-codegen/src/codegen/mod.rs b/crates/perry-codegen/src/codegen/mod.rs index e7ec1fb0e7..2751aef3df 100644 --- a/crates/perry-codegen/src/codegen/mod.rs +++ b/crates/perry-codegen/src/codegen/mod.rs @@ -58,11 +58,13 @@ pub use opts::{ }; pub(crate) use opts::{CrossModuleCtx, ImportedCtor}; pub(crate) use typed_abi::{ - generic_closure_body_name, generic_function_body_name, generic_method_body_name, - typed_f64_closure_name, typed_f64_function_name, typed_f64_method_name, - typed_f64_receiver_method_info, typed_f64_receiver_method_name, typed_i1_closure_name, - typed_i1_function_name, typed_i1_method_name, typed_i32_function_name, typed_i32_method_name, - typed_string_closure_name, typed_string_function_name, TypedParamRep, TypedReceiverMethodInfo, + emit_typed_arg_guard, emit_typed_arg_to_raw, generic_closure_body_name, + generic_function_body_name, generic_method_body_name, typed_f64_closure_name, + typed_f64_function_name, typed_f64_method_name, typed_f64_receiver_method_info, + typed_f64_receiver_method_name, typed_i1_closure_name, typed_i1_function_name, + typed_i1_method_name, typed_i32_closure_name, typed_i32_function_name, typed_i32_method_name, + typed_param_reps_match_args, typed_string_closure_name, typed_string_function_name, + typed_string_method_name, TypedParamRep, TypedReceiverMethodInfo, }; use artifacts::{emit_module_artifacts, ModuleArtifactsCtx}; @@ -1179,6 +1181,9 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> match typed_abi::typed_f64_function_rejection_reason(f) { None => { typed_f64_functions.insert(f.id); + if let Some(reps) = typed_abi::typed_param_reps_for_params(&f.params) { + typed_i1_function_param_reps.insert(f.id, reps); + } } Some(reason) => record_typed_clone_rejection( &mut typed_clone_rejection_records, @@ -1195,6 +1200,9 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> match typed_abi::typed_i32_function_rejection_reason(f) { None => { typed_i32_functions.insert(f.id); + if let Some(reps) = typed_abi::typed_param_reps_for_params(&f.params) { + typed_i1_function_param_reps.insert(f.id, reps); + } } Some(reason) => record_typed_clone_rejection( &mut typed_clone_rejection_records, @@ -1230,6 +1238,9 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> match typed_abi::typed_string_function_rejection_reason(f) { None => { typed_string_functions.insert(f.id); + if let Some(reps) = typed_abi::typed_param_reps_for_params(&f.params) { + typed_i1_function_param_reps.insert(f.id, reps); + } } Some(reason) => record_typed_clone_rejection( &mut typed_clone_rejection_records, @@ -1247,6 +1258,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> let mut typed_f64_methods = std::collections::HashSet::new(); let mut typed_i32_methods = std::collections::HashSet::new(); let mut typed_i1_methods = std::collections::HashSet::new(); + let mut typed_string_methods = std::collections::HashSet::new(); let mut typed_i1_method_param_reps = std::collections::HashMap::new(); let mut typed_f64_receiver_methods = std::collections::HashMap::new(); for class in &hir.classes { @@ -1254,7 +1266,11 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> let source_function = format!("{}::{}", class.name, method.name); match typed_abi::typed_f64_method_rejection_reason(method) { None => { - typed_f64_methods.insert((class.name.clone(), method.name.clone())); + let key = (class.name.clone(), method.name.clone()); + typed_f64_methods.insert(key.clone()); + if let Some(reps) = typed_abi::typed_param_reps_for_params(&method.params) { + typed_i1_method_param_reps.insert(key, reps); + } } Some(reason) => record_typed_clone_rejection( &mut typed_clone_rejection_records, @@ -1316,7 +1332,11 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> } match typed_abi::typed_i32_method_rejection_reason(method) { None => { - typed_i32_methods.insert((class.name.clone(), method.name.clone())); + let key = (class.name.clone(), method.name.clone()); + typed_i32_methods.insert(key.clone()); + if let Some(reps) = typed_abi::typed_param_reps_for_params(&method.params) { + typed_i1_method_param_reps.insert(key, reps); + } } Some(reason) => record_typed_clone_rejection( &mut typed_clone_rejection_records, @@ -1331,6 +1351,27 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> ], ), } + match typed_abi::typed_string_method_rejection_reason(method) { + None => { + let key = (class.name.clone(), method.name.clone()); + typed_string_methods.insert(key.clone()); + if let Some(reps) = typed_abi::typed_param_reps_for_params(&method.params) { + typed_i1_method_param_reps.insert(key, reps); + } + } + Some(reason) => record_typed_clone_rejection( + &mut typed_clone_rejection_records, + source_function.clone(), + "typed_string_method_clone_decision", + reason, + vec![ + "typed_clone_kind=typed_string_method".to_string(), + format!("class={}", class.name), + format!("method={}", method.name), + format!("function_id={}", method.id), + ], + ), + } } } let mut compiler_private_async_i32_control_locals = std::collections::HashSet::new(); @@ -1491,9 +1532,11 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> typed_f64_methods, typed_i32_methods, typed_i1_methods, + typed_string_methods, typed_i1_method_param_reps, typed_f64_receiver_methods, typed_f64_closures: std::collections::HashSet::new(), + typed_i32_closures: std::collections::HashSet::new(), typed_i1_closures: std::collections::HashSet::new(), typed_string_closures: std::collections::HashSet::new(), typed_string_closure_capture_counts: std::collections::HashMap::new(), @@ -2449,6 +2492,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> collect_closures_in_stmts(&hir.init, &mut seen, &mut closures); } cross_module.typed_f64_closures.clear(); + cross_module.typed_i32_closures.clear(); cross_module.typed_i1_closures.clear(); cross_module.typed_string_closures.clear(); cross_module.typed_string_closure_capture_counts.clear(); @@ -2457,6 +2501,13 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> match typed_abi::typed_f64_closure_rejection_reason_with_types(expr, &module_local_types) { None => { cross_module.typed_f64_closures.insert(*func_id); + if let perry_hir::Expr::Closure { params, .. } = expr { + if let Some(reps) = typed_abi::typed_param_reps_for_params(params) { + cross_module + .typed_i1_closure_param_reps + .insert(*func_id, reps); + } + } } Some(reason) => record_typed_clone_rejection( &mut typed_clone_rejection_records, @@ -2505,10 +2556,46 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> ], ), } + match typed_abi::typed_i32_closure_rejection_reason_with_types(expr, &module_local_types) { + None => { + cross_module.typed_i32_closures.insert(*func_id); + if let perry_hir::Expr::Closure { params, .. } = expr { + if let Some(reps) = typed_abi::typed_param_reps_for_params(params) { + cross_module + .typed_i1_closure_param_reps + .insert(*func_id, reps); + } + } + } + Some(reason) => record_typed_clone_rejection( + &mut typed_clone_rejection_records, + format!("closure#{func_id}"), + "typed_i32_closure_clone_decision", + reason, + vec![ + "typed_clone_kind=typed_i32_closure".to_string(), + format!("closure_func_id={func_id}"), + format!( + "symbol={}", + typed_i32_closure_name(&format!( + "perry_closure_{}__{}", + module_prefix, func_id + )) + ), + ], + ), + } match typed_abi::typed_string_closure_rejection_reason_with_types(expr, &module_local_types) { None => { cross_module.typed_string_closures.insert(*func_id); + if let perry_hir::Expr::Closure { params, .. } = expr { + if let Some(reps) = typed_abi::typed_param_reps_for_params(params) { + cross_module + .typed_i1_closure_param_reps + .insert(*func_id, reps); + } + } let capture_count = typed_abi::typed_string_closure_capture_reps(expr, &module_local_types) .map(|captures| captures.len()) diff --git a/crates/perry-codegen/src/codegen/opts.rs b/crates/perry-codegen/src/codegen/opts.rs index fdb48cb478..83a2cf6f63 100644 --- a/crates/perry-codegen/src/codegen/opts.rs +++ b/crates/perry-codegen/src/codegen/opts.rs @@ -727,6 +727,11 @@ pub(crate) struct CrossModuleCtx { /// own-method direct calls may select these clones after receiver/method /// and per-representation typed argument guards pass. pub typed_i1_methods: std::collections::HashSet<(String, String)>, + /// Own instance methods that have a generated internal typed-string clone. + /// Public method symbols remain JSValue trampolines; exact own-method + /// direct calls may select these clones after receiver/method and string + /// argument guards pass. + pub typed_string_methods: std::collections::HashSet<(String, String)>, /// Per-method typed-i1 clone parameter reps. This lets exact same-module /// method calls target mixed native predicate clones such as /// `i1(double, double)` without routing through the public JSValue wrapper. @@ -742,6 +747,10 @@ pub(crate) struct CrossModuleCtx { /// Only statically-known local closure calls may select these clones after /// closure identity/arity and numeric argument guards pass. pub typed_f64_closures: std::collections::HashSet, + /// Inline closure bodies that have a generated internal typed-i32 clone. + /// Only statically-known local closure calls may select these clones after + /// closure identity/arity and Int32 argument guards pass. + pub typed_i32_closures: std::collections::HashSet, /// Inline closure bodies that have a generated internal typed-i1 clone. /// Only statically-known local closure calls may select these clones after /// closure identity/arity and per-representation argument guards pass. diff --git a/crates/perry-codegen/src/codegen/typed_abi.rs b/crates/perry-codegen/src/codegen/typed_abi.rs index e885627f31..361a96e15c 100644 --- a/crates/perry-codegen/src/codegen/typed_abi.rs +++ b/crates/perry-codegen/src/codegen/typed_abi.rs @@ -71,6 +71,8 @@ pub(crate) fn typed_param_rep_for_type(ty: &Type) -> Option { Some(TypedParamRep::F64) } else if matches!(ty, Type::Boolean) { Some(TypedParamRep::I1) + } else if is_string_type(ty) { + Some(TypedParamRep::StringRef) } else { None } @@ -89,23 +91,19 @@ pub(crate) fn typed_f64_closure_capture_reps( expr: &Expr, module_local_types: &HashMap, ) -> Option> { - let Expr::Closure { captures, .. } = expr else { - return None; - }; - let mut reps = Vec::with_capacity(captures.len()); - for id in captures { - let ty = module_local_types.get(id)?; - if !is_f64_type(ty) { - return None; - } - reps.push((*id, TypedParamRep::F64)); - } - Some(reps) + typed_closure_capture_reps(expr, module_local_types) } pub(crate) fn typed_i1_closure_capture_reps( expr: &Expr, module_local_types: &HashMap, +) -> Option> { + typed_closure_capture_reps(expr, module_local_types) +} + +fn typed_closure_capture_reps( + expr: &Expr, + module_local_types: &HashMap, ) -> Option> { let Expr::Closure { captures, .. } = expr else { return None; @@ -119,22 +117,18 @@ pub(crate) fn typed_i1_closure_capture_reps( Some(reps) } +pub(crate) fn typed_i32_closure_capture_reps( + expr: &Expr, + module_local_types: &HashMap, +) -> Option> { + typed_closure_capture_reps(expr, module_local_types) +} + pub(crate) fn typed_string_closure_capture_reps( expr: &Expr, module_local_types: &HashMap, ) -> Option> { - let Expr::Closure { captures, .. } = expr else { - return None; - }; - let mut reps = Vec::with_capacity(captures.len()); - for id in captures { - let ty = module_local_types.get(id)?; - if !is_string_type(ty) { - return None; - } - reps.push((*id, TypedParamRep::StringRef)); - } - Some(reps) + typed_closure_capture_reps(expr, module_local_types) } pub(crate) fn emit_typed_arg_guard( @@ -182,6 +176,29 @@ pub(crate) fn emit_typed_arg_to_raw( } } +pub(crate) fn typed_param_reps_match_args( + ctx: &crate::expr::FnCtx<'_>, + reps: &[TypedParamRep], + args: &[Expr], +) -> bool { + reps.len() == args.len() + && args.iter().zip(reps.iter()).all(|(arg, rep)| match rep { + TypedParamRep::F64 => crate::type_analysis::is_numeric_expr(ctx, arg), + TypedParamRep::I32 => { + matches!( + crate::type_analysis::static_type_of(ctx, arg), + Some(Type::Int32) + ) || matches!( + arg, + Expr::Integer(n) + if (i64::from(i32::MIN)..=i64::from(i32::MAX)).contains(n) + ) + } + TypedParamRep::I1 => crate::type_analysis::is_bool_expr(ctx, arg), + TypedParamRep::StringRef => crate::type_analysis::is_definitely_string_expr(ctx, arg), + }) +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum TypedCloneRejectionReason { NotClosure, @@ -319,6 +336,10 @@ pub(crate) fn typed_i32_method_name(generic_name: &str) -> String { format!("{generic_name}__typed_i32") } +pub(crate) fn typed_string_method_name(generic_name: &str) -> String { + format!("{generic_name}__typed_string") +} + pub(crate) fn typed_f64_closure_name(generic_name: &str) -> String { format!("{generic_name}__typed_f64") } @@ -327,6 +348,10 @@ pub(crate) fn typed_i1_closure_name(generic_name: &str) -> String { format!("{generic_name}__typed_i1") } +pub(crate) fn typed_i32_closure_name(generic_name: &str) -> String { + format!("{generic_name}__typed_i32") +} + pub(crate) fn typed_string_closure_name(generic_name: &str) -> String { format!("{generic_name}__typed_string") } @@ -361,6 +386,11 @@ pub(crate) fn is_typed_i1_method_candidate(method: &Function) -> bool { typed_i1_function_rejection_reason_impl(method).is_none() } +#[allow(dead_code)] +pub(crate) fn is_typed_string_method_candidate(method: &Function) -> bool { + typed_string_method_rejection_reason(method).is_none() +} + pub(crate) fn typed_f64_function_rejection_reason( function: &Function, ) -> Option { @@ -403,10 +433,12 @@ pub(crate) fn typed_string_function_rejection_reason( if param.arguments_object.is_some() { return Some(TypedCloneRejectionReason::ArgumentsObject); } - if !is_string_type(¶m.ty) { + let Some(rep) = typed_param_rep_for_type(¶m.ty) else { return Some(TypedCloneRejectionReason::ParamNotString); + }; + if matches!(rep, TypedParamRep::StringRef) { + locals.insert(param.id); } - locals.insert(param.id); } typed_string_body_rejection_reason(&function.body, locals) @@ -444,6 +476,12 @@ pub(crate) fn typed_i32_method_rejection_reason( typed_i32_function_rejection_reason_impl(method) } +pub(crate) fn typed_string_method_rejection_reason( + method: &Function, +) -> Option { + typed_string_function_rejection_reason(method) +} + #[allow(dead_code)] pub(crate) fn is_typed_f64_closure_candidate(expr: &Expr) -> bool { typed_f64_closure_rejection_reason(expr).is_none() @@ -484,7 +522,7 @@ pub(crate) fn typed_f64_closure_rejection_reason_with_types( return Some(TypedCloneRejectionReason::Captures); } - let mut numeric_params = HashSet::new(); + let mut numeric_params = HashMap::new(); for param in params { if param.default.is_some() { return Some(TypedCloneRejectionReason::ParamDefault); @@ -495,16 +533,16 @@ pub(crate) fn typed_f64_closure_rejection_reason_with_types( if param.arguments_object.is_some() { return Some(TypedCloneRejectionReason::ArgumentsObject); } - if !is_f64_type(¶m.ty) { + let Some(rep) = typed_param_rep_for_type(¶m.ty) else { return Some(TypedCloneRejectionReason::ParamNotF64); - } - numeric_params.insert(param.id); + }; + numeric_params.insert(param.id, rep); } let Some(capture_reps) = typed_f64_closure_capture_reps(expr, module_local_types) else { return Some(TypedCloneRejectionReason::Captures); }; - for (capture_id, _) in capture_reps { - numeric_params.insert(capture_id); + for (capture_id, rep) in capture_reps { + numeric_params.insert(capture_id, rep); } typed_f64_body_rejection_reason(body, numeric_params) @@ -576,6 +614,71 @@ pub(crate) fn typed_i1_closure_rejection_reason_with_types( typed_i1_body_rejection_reason(body, locals) } +pub(crate) fn typed_i32_closure_rejection_reason(expr: &Expr) -> Option { + typed_i32_closure_rejection_reason_with_types(expr, &HashMap::new()) +} + +pub(crate) fn typed_i32_closure_rejection_reason_with_types( + expr: &Expr, + module_local_types: &HashMap, +) -> Option { + let Expr::Closure { + params, + return_type, + body, + captures, + mutable_captures, + captures_this, + captures_new_target, + is_async, + is_generator, + .. + } = expr + else { + return Some(TypedCloneRejectionReason::NotClosure); + }; + if *is_async || *is_generator { + return Some(TypedCloneRejectionReason::AsyncOrGenerator); + } + if *captures_this { + return Some(TypedCloneRejectionReason::CapturesThis); + } + if *captures_new_target { + return Some(TypedCloneRejectionReason::CapturesNewTarget); + } + if !mutable_captures.is_empty() || captures.iter().any(|id| mutable_captures.contains(id)) { + return Some(TypedCloneRejectionReason::Captures); + } + if !matches!(return_type, Type::Int32) { + return Some(TypedCloneRejectionReason::ReturnTypeNotI32); + } + + let mut locals = HashMap::new(); + for param in params { + if param.default.is_some() { + return Some(TypedCloneRejectionReason::ParamDefault); + } + if param.is_rest { + return Some(TypedCloneRejectionReason::RestParam); + } + if param.arguments_object.is_some() { + return Some(TypedCloneRejectionReason::ArgumentsObject); + } + let Some(rep) = typed_param_rep_for_type(¶m.ty) else { + return Some(TypedCloneRejectionReason::ParamNotI32); + }; + locals.insert(param.id, rep); + } + let Some(capture_reps) = typed_i32_closure_capture_reps(expr, module_local_types) else { + return Some(TypedCloneRejectionReason::Captures); + }; + for (capture_id, rep) in capture_reps { + locals.insert(capture_id, rep); + } + + typed_i32_body_rejection_reason(body, locals) +} + #[allow(dead_code)] pub(crate) fn is_typed_string_closure_candidate(expr: &Expr) -> bool { typed_string_closure_rejection_reason(expr).is_none() @@ -629,16 +732,20 @@ pub(crate) fn typed_string_closure_rejection_reason_with_types( if param.arguments_object.is_some() { return Some(TypedCloneRejectionReason::ArgumentsObject); } - if !is_string_type(¶m.ty) { + let Some(rep) = typed_param_rep_for_type(¶m.ty) else { return Some(TypedCloneRejectionReason::ParamNotString); + }; + if matches!(rep, TypedParamRep::StringRef) { + locals.insert(param.id); } - locals.insert(param.id); } let Some(capture_reps) = typed_string_closure_capture_reps(expr, module_local_types) else { return Some(TypedCloneRejectionReason::Captures); }; - for (capture_id, _) in capture_reps { - locals.insert(capture_id); + for (capture_id, rep) in capture_reps { + if matches!(rep, TypedParamRep::StringRef) { + locals.insert(capture_id); + } } typed_string_body_rejection_reason(body, locals) @@ -701,10 +808,10 @@ fn typed_i32_function_rejection_reason_impl( if param.arguments_object.is_some() { return Some(TypedCloneRejectionReason::ArgumentsObject); } - if !matches!(param.ty, Type::Int32) { + let Some(rep) = typed_param_rep_for_type(¶m.ty) else { return Some(TypedCloneRejectionReason::ParamNotI32); - } - locals.insert(param.id, TypedParamRep::I32); + }; + locals.insert(param.id, rep); } typed_i32_body_rejection_reason(&function.body, locals) @@ -721,7 +828,7 @@ fn typed_f64_callable_rejection_reason(function: &Function) -> Option Option, + mut locals: HashMap, ) -> Option { - let mut locals: HashMap = numeric_locals - .into_iter() - .map(|id| (id, TypedParamRep::F64)) - .collect(); let Some((last, prefix)) = body.split_last() else { return Some(TypedCloneRejectionReason::BodyNotSingleReturn); }; @@ -965,6 +1068,15 @@ fn typed_f64_body_rejection_reason( } if is_f64_type(ty) && expr_is_typed_f64_safe(expr, &locals) => { locals.insert(*id, TypedParamRep::F64); } + Stmt::Let { + id, + ty: Type::Int32, + mutable: false, + init: Some(expr), + .. + } if expr_is_typed_i32_safe(expr, &locals) => { + locals.insert(*id, TypedParamRep::I32); + } _ => return Some(TypedCloneRejectionReason::BodyNotStraightLineTyped), } } @@ -1341,15 +1453,26 @@ fn lower_typed_string_expr_with_env( } pub(crate) fn lower_typed_f64_body_with_seed_locals( + blk: &mut crate::block::LlBlock, + params: &[perry_hir::Param], + body: &[Stmt], + locals: HashMap, +) -> anyhow::Result { + lower_typed_f64_body_with_seed_locals_and_reps(blk, params, body, locals, HashMap::new()) +} + +pub(crate) fn lower_typed_f64_body_with_seed_locals_and_reps( blk: &mut crate::block::LlBlock, params: &[perry_hir::Param], body: &[Stmt], mut locals: HashMap, + mut reps: HashMap, ) -> anyhow::Result { - let mut reps = HashMap::new(); for param in params { locals.insert(param.id, format!("%arg{}", param.id)); - reps.insert(param.id, TypedParamRep::F64); + if let Some(rep) = typed_param_rep_for_type(¶m.ty) { + reps.insert(param.id, rep); + } } let Some((last, prefix)) = body.split_last() else { anyhow::bail!("typed-f64 clone cannot lower empty body"); @@ -1367,6 +1490,17 @@ pub(crate) fn lower_typed_f64_body_with_seed_locals( locals.insert(*id, value); reps.insert(*id, TypedParamRep::F64); } + Stmt::Let { + id, + ty: Type::Int32, + mutable: false, + init: Some(expr), + .. + } => { + let value = lower_typed_i32_expr_with_env(blk, expr, &locals)?; + locals.insert(*id, value); + reps.insert(*id, TypedParamRep::I32); + } _ => anyhow::bail!("typed-f64 clone cannot lower non-straight-line statement"), } } @@ -1389,7 +1523,15 @@ pub(crate) fn lower_typed_i32_body( params: &[perry_hir::Param], body: &[Stmt], ) -> anyhow::Result { - let mut locals = HashMap::new(); + lower_typed_i32_body_with_seed_locals(blk, params, body, HashMap::new()) +} + +pub(crate) fn lower_typed_i32_body_with_seed_locals( + blk: &mut crate::block::LlBlock, + params: &[perry_hir::Param], + body: &[Stmt], + mut locals: HashMap, +) -> anyhow::Result { for param in params { locals.insert(param.id, format!("%arg{}", param.id)); } @@ -1624,3 +1766,168 @@ pub(crate) fn lower_typed_i1_body( ) -> anyhow::Result { lower_typed_i1_body_with_seed_locals(blk, params, body, HashMap::new(), HashMap::new()) } + +#[cfg(test)] +mod tests { + use super::*; + use perry_hir::Param; + + fn param(id: u32, name: &str, ty: Type) -> Param { + Param { + id, + name: name.to_string(), + ty, + default: None, + decorators: Vec::new(), + is_rest: false, + arguments_object: None, + } + } + + fn function(return_type: Type, params: Vec, body: Vec) -> Function { + Function { + id: 1, + name: "mixed".to_string(), + type_params: Vec::new(), + params, + return_type, + body, + is_async: false, + is_generator: false, + is_strict: false, + is_exported: false, + captures: Vec::new(), + decorators: Vec::new(), + was_plain_async: false, + was_unrolled: false, + } + } + + fn ret(expr: Expr) -> Vec { + vec![Stmt::Return(Some(expr))] + } + + #[test] + fn f64_clone_accepts_mixed_raw_params_when_return_expr_is_numeric_safe() { + let f = function( + Type::Number, + vec![ + param(10, "n", Type::Number), + param(11, "i", Type::Int32), + param(12, "flag", Type::Boolean), + ], + ret(Expr::Binary { + op: BinaryOp::Add, + left: Box::new(Expr::LocalGet(10)), + right: Box::new(Expr::LocalGet(11)), + }), + ); + + assert_eq!(typed_f64_function_rejection_reason(&f), None); + assert_eq!( + typed_param_reps_for_params(&f.params), + Some(vec![ + TypedParamRep::F64, + TypedParamRep::I32, + TypedParamRep::I1 + ]) + ); + } + + #[test] + fn f64_clone_accepts_raw_i32_locals_before_numeric_return() { + let f = function( + Type::Number, + vec![param(10, "n", Type::Number), param(11, "i", Type::Int32)], + vec![ + Stmt::Let { + id: 12, + name: "mask".to_string(), + ty: Type::Int32, + mutable: false, + init: Some(Expr::Binary { + op: BinaryOp::BitOr, + left: Box::new(Expr::LocalGet(11)), + right: Box::new(Expr::Integer(1)), + }), + }, + Stmt::Return(Some(Expr::Binary { + op: BinaryOp::Add, + left: Box::new(Expr::LocalGet(10)), + right: Box::new(Expr::LocalGet(12)), + })), + ], + ); + + assert_eq!(typed_f64_function_rejection_reason(&f), None); + } + + #[test] + fn f64_clone_rejects_unsafe_mixed_rep_use() { + let f = function( + Type::Number, + vec![ + param(10, "n", Type::Number), + param(11, "flag", Type::Boolean), + ], + ret(Expr::Binary { + op: BinaryOp::Add, + left: Box::new(Expr::LocalGet(10)), + right: Box::new(Expr::LocalGet(11)), + }), + ); + + assert_eq!( + typed_f64_function_rejection_reason(&f), + Some(TypedCloneRejectionReason::ReturnExprNotTypedF64Safe) + ); + } + + #[test] + fn string_clone_accepts_mixed_params_when_only_string_rep_flows_to_return() { + let f = function( + Type::String, + vec![ + param(10, "s", Type::String), + param(11, "i", Type::Int32), + param(12, "flag", Type::Boolean), + ], + ret(Expr::LocalGet(10)), + ); + + assert_eq!(typed_string_function_rejection_reason(&f), None); + } + + #[test] + fn closure_clone_accepts_mixed_immutable_captures_for_numeric_return() { + let expr = Expr::Closure { + func_id: 7, + params: vec![param(20, "scale", Type::Number)], + return_type: Type::Number, + body: ret(Expr::Binary { + op: BinaryOp::Add, + left: Box::new(Expr::LocalGet(20)), + right: Box::new(Expr::LocalGet(30)), + }), + captures: vec![30, 31], + mutable_captures: Vec::new(), + captures_this: false, + captures_new_target: false, + enclosing_class: None, + is_arrow: true, + is_async: false, + is_generator: false, + is_strict: false, + }; + let module_local_types = HashMap::from([(30, Type::Int32), (31, Type::Boolean)]); + + assert_eq!( + typed_f64_closure_rejection_reason_with_types(&expr, &module_local_types), + None + ); + assert_eq!( + typed_f64_closure_capture_reps(&expr, &module_local_types), + Some(vec![(30, TypedParamRep::I32), (31, TypedParamRep::I1)]) + ); + } +} diff --git a/crates/perry-codegen/src/collectors/hir_facts.rs b/crates/perry-codegen/src/collectors/hir_facts.rs index 38c4446a06..64c59d15f1 100644 --- a/crates/perry-codegen/src/collectors/hir_facts.rs +++ b/crates/perry-codegen/src/collectors/hir_facts.rs @@ -39,6 +39,8 @@ pub(crate) struct RepresentationFacts { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum ArrayKindFact { + PackedI32, + PackedU32, PackedF64, PackedValue, HoleyValue, @@ -134,6 +136,20 @@ impl TypeFacts { && !self.has_materialization_hazard(local_id) } + pub(crate) fn proves_packed_i32_array(&self, local_id: u32) -> bool { + self.array_kind(local_id) == ArrayKindFact::PackedI32 + && self.proves_noalias_array(local_id) + && self.proves_array_length_stable(local_id) + && !self.has_materialization_hazard(local_id) + } + + pub(crate) fn proves_packed_u32_array(&self, local_id: u32) -> bool { + self.array_kind(local_id) == ArrayKindFact::PackedU32 + && self.proves_noalias_array(local_id) + && self.proves_array_length_stable(local_id) + && !self.has_materialization_hazard(local_id) + } + pub(crate) fn proves_array_length_stable(&self, local_id: u32) -> bool { self.arrays.length_stable_locals.contains(&local_id) } @@ -505,12 +521,11 @@ impl ArrayFactCollector { Stmt::Let { id, ty, init, .. } => { let declared_kind = array_kind_from_declared_type(ty); if declared_kind != ArrayKindFact::Unknown { - let init_kind = init + let combined_kind = init .as_ref() - .map(array_kind_from_initializer) + .map(|expr| array_kind_from_declared_initializer(declared_kind, expr)) .unwrap_or(ArrayKindFact::Unknown); - self.local_kinds - .insert(*id, meet_array_kind(declared_kind, init_kind)); + self.local_kinds.insert(*id, combined_kind); } if let Some(init) = init { self.collect_expr(init); @@ -592,7 +607,9 @@ impl ArrayFactCollector { fn collect_expr(&mut self, expr: &Expr) { match expr { Expr::ArrayPush { array_id, value } => { - let value_kind = if expr_is_numeric_shaped(value) { + let value_kind = if expr_is_i32_shaped(value) { + ArrayKindFact::PackedI32 + } else if expr_is_numeric_shaped(value) { ArrayKindFact::PackedF64 } else { ArrayKindFact::PackedValue @@ -680,7 +697,9 @@ impl ArrayFactCollector { value, } => { if let Expr::LocalGet(id) = object.as_ref() { - let value_kind = if expr_is_numeric_shaped(value) { + let value_kind = if expr_is_i32_shaped(value) { + ArrayKindFact::PackedI32 + } else if expr_is_numeric_shaped(value) { ArrayKindFact::PackedF64 } else { ArrayKindFact::PackedValue @@ -1044,12 +1063,13 @@ impl ArrayFactCollector { fn array_kind_from_declared_type(ty: &perry_types::Type) -> ArrayKindFact { match ty { - perry_types::Type::Array(elem) - if matches!( - elem.as_ref(), - perry_types::Type::Number | perry_types::Type::Int32 - ) => - { + perry_types::Type::Array(elem) if matches!(elem.as_ref(), perry_types::Type::Int32) => { + ArrayKindFact::PackedI32 + } + perry_types::Type::Array(elem) if matches!(elem.as_ref(), perry_types::Type::Named(name) if name == "PerryU32") => { + ArrayKindFact::PackedU32 + } + perry_types::Type::Array(elem) if matches!(elem.as_ref(), perry_types::Type::Number) => { ArrayKindFact::PackedF64 } perry_types::Type::Array(_) => ArrayKindFact::PackedValue, @@ -1059,6 +1079,12 @@ fn array_kind_from_declared_type(ty: &perry_types::Type) -> ArrayKindFact { fn array_kind_from_initializer(expr: &Expr) -> ArrayKindFact { match expr { + Expr::Array(elements) if elements.iter().all(expr_is_literal_i32) => { + ArrayKindFact::PackedI32 + } + Expr::Array(elements) if elements.iter().all(expr_is_literal_u32) => { + ArrayKindFact::PackedU32 + } Expr::Array(elements) if elements.iter().all(expr_is_literal_number) => { ArrayKindFact::PackedF64 } @@ -1077,6 +1103,20 @@ fn array_kind_from_initializer(expr: &Expr) -> ArrayKindFact { } if saw_hole { ArrayKindFact::HoleyValue + } else if elements.iter().all(|element| { + matches!( + element, + perry_hir::ArrayElement::Expr(expr) if expr_is_literal_i32(expr) + ) + }) { + ArrayKindFact::PackedI32 + } else if elements.iter().all(|element| { + matches!( + element, + perry_hir::ArrayElement::Expr(expr) if expr_is_literal_u32(expr) + ) + }) { + ArrayKindFact::PackedU32 } else if all_numeric { ArrayKindFact::PackedF64 } else { @@ -1087,10 +1127,86 @@ fn array_kind_from_initializer(expr: &Expr) -> ArrayKindFact { } } +fn array_kind_from_declared_initializer(declared: ArrayKindFact, init: &Expr) -> ArrayKindFact { + if declared == ArrayKindFact::PackedU32 { + return if initializer_is_literal_u32_array(init) { + ArrayKindFact::PackedU32 + } else { + match array_kind_from_initializer(init) { + ArrayKindFact::Unknown => ArrayKindFact::Unknown, + ArrayKindFact::PackedValue => ArrayKindFact::PackedValue, + ArrayKindFact::HoleyValue => ArrayKindFact::HoleyValue, + ArrayKindFact::PackedI32 | ArrayKindFact::PackedU32 | ArrayKindFact::PackedF64 => { + ArrayKindFact::PackedF64 + } + } + }; + } + meet_declared_array_kind(declared, array_kind_from_initializer(init)) +} + +fn initializer_is_literal_u32_array(expr: &Expr) -> bool { + match expr { + Expr::Array(elements) => elements.iter().all(expr_is_literal_u32), + Expr::ArraySpread(elements) => elements.iter().all(|element| { + matches!( + element, + perry_hir::ArrayElement::Expr(expr) if expr_is_literal_u32(expr) + ) + }), + _ => false, + } +} + fn expr_is_literal_number(expr: &Expr) -> bool { matches!(expr, Expr::Integer(_) | Expr::Number(_)) } +fn expr_is_literal_i32(expr: &Expr) -> bool { + match expr { + Expr::Integer(n) => i32::try_from(*n).is_ok(), + Expr::Number(n) if n.is_finite() && n.fract() == 0.0 => { + let value = *n as i64; + i32::try_from(value).is_ok() && *n == value as f64 + } + _ => false, + } +} + +fn expr_is_literal_u32(expr: &Expr) -> bool { + match expr { + Expr::Integer(n) => u32::try_from(*n).is_ok(), + Expr::Number(n) if n.is_finite() && n.fract() == 0.0 => { + let value = *n as i64; + u32::try_from(value).is_ok() && *n == value as f64 + } + _ => false, + } +} + +fn expr_is_i32_shaped(expr: &Expr) -> bool { + match expr { + Expr::Integer(n) => i32::try_from(*n).is_ok(), + Expr::Binary { op, left, right } + if matches!( + op, + perry_hir::BinaryOp::BitAnd + | perry_hir::BinaryOp::BitOr + | perry_hir::BinaryOp::BitXor + | perry_hir::BinaryOp::Shl + | perry_hir::BinaryOp::Shr + | perry_hir::BinaryOp::UShr + ) => + { + expr_is_numeric_shaped(left) && expr_is_numeric_shaped(right) + } + Expr::MathImul(left, right) => { + expr_is_numeric_shaped(left) && expr_is_numeric_shaped(right) + } + _ => false, + } +} + fn expr_is_numeric_shaped(expr: &Expr) -> bool { match expr { Expr::Integer(_) | Expr::Number(_) | Expr::LocalGet(_) | Expr::IndexGet { .. } => true, @@ -1133,10 +1249,26 @@ fn meet_array_kind(left: ArrayKindFact, right: ArrayKindFact) -> ArrayKindFact { (Unknown, _) | (_, Unknown) => Unknown, (HoleyValue, _) | (_, HoleyValue) => HoleyValue, (PackedValue, _) | (_, PackedValue) => PackedValue, + (PackedI32, PackedI32) => PackedI32, + (PackedU32, PackedU32) => PackedU32, + (PackedI32, PackedF64) | (PackedF64, PackedI32) => PackedF64, + (PackedU32, PackedF64) | (PackedF64, PackedU32) => PackedF64, + (PackedI32, PackedU32) | (PackedU32, PackedI32) => PackedF64, (PackedF64, PackedF64) => PackedF64, } } +fn meet_declared_array_kind(declared: ArrayKindFact, init: ArrayKindFact) -> ArrayKindFact { + use ArrayKindFact::*; + match (declared, init) { + (PackedU32, PackedU32) => PackedU32, + (PackedU32, PackedI32) => PackedF64, + (PackedU32, PackedF64) => PackedF64, + (PackedI32, PackedU32) => PackedF64, + _ => meet_array_kind(declared, init), + } +} + #[cfg(test)] mod tests { use super::*; @@ -1179,6 +1311,30 @@ mod tests { } } + fn int32_array_let(id: u32, values: &[i64]) -> Stmt { + Stmt::Let { + id, + name: format!("a{}", id), + ty: Type::Array(Box::new(Type::Int32)), + mutable: true, + init: Some(Expr::Array( + values.iter().copied().map(Expr::Integer).collect(), + )), + } + } + + fn u32_array_let(id: u32, values: &[i64]) -> Stmt { + Stmt::Let { + id, + name: format!("a{}", id), + ty: Type::Array(Box::new(Type::Named("PerryU32".to_string()))), + mutable: true, + init: Some(Expr::Array( + values.iter().copied().map(Expr::Integer).collect(), + )), + } + } + fn alias_let(id: u32, source_id: u32) -> Stmt { Stmt::Let { id, @@ -1309,6 +1465,58 @@ mod tests { assert!(graph.proves_pure_helper(7)); } + #[test] + fn packed_i32_array_fact_requires_int32_array_with_i32_literal_initializer() { + let facts = collect_hir_facts( + &[ + int32_array_let(1, &[1, 2, 3]), + number_array_let(2, &[1, 2, 3]), + Stmt::Let { + id: 3, + name: "fractional".to_string(), + ty: Type::Array(Box::new(Type::Int32)), + mutable: true, + init: Some(Expr::Array(vec![Expr::Number(1.5)])), + }, + ], + &HashSet::new(), + &HashSet::new(), + ); + + assert_eq!(facts.array_kind(1), ArrayKindFact::PackedI32); + assert!(facts.proves_packed_i32_array(1)); + assert_eq!(facts.array_kind(2), ArrayKindFact::PackedF64); + assert!(!facts.proves_packed_i32_array(2)); + assert_eq!(facts.array_kind(3), ArrayKindFact::PackedF64); + assert!(!facts.proves_packed_i32_array(3)); + } + + #[test] + fn packed_u32_array_fact_requires_perry_u32_array_with_u32_literal_initializer() { + let facts = collect_hir_facts( + &[ + u32_array_let(1, &[0, 4_000_000_000]), + int32_array_let(2, &[0, 1]), + Stmt::Let { + id: 3, + name: "negative".to_string(), + ty: Type::Array(Box::new(Type::Named("PerryU32".to_string()))), + mutable: true, + init: Some(Expr::Array(vec![Expr::Integer(-1)])), + }, + ], + &HashSet::new(), + &HashSet::new(), + ); + + assert_eq!(facts.array_kind(1), ArrayKindFact::PackedU32); + assert!(facts.proves_packed_u32_array(1)); + assert_eq!(facts.array_kind(2), ArrayKindFact::PackedI32); + assert!(!facts.proves_packed_u32_array(2)); + assert_eq!(facts.array_kind(3), ArrayKindFact::PackedF64); + assert!(!facts.proves_packed_u32_array(3)); + } + #[test] fn native_fact_graph_collects_range_and_shape_escape_facts() { let stmts = vec![ diff --git a/crates/perry-codegen/src/collectors/scalar_methods.rs b/crates/perry-codegen/src/collectors/scalar_methods.rs index 1cce121b95..2ad6bd943d 100644 --- a/crates/perry-codegen/src/collectors/scalar_methods.rs +++ b/crates/perry-codegen/src/collectors/scalar_methods.rs @@ -5,6 +5,9 @@ //! body is either: //! - `return ` over numeric parameters, numeric literals, //! and direct `this.field` reads of public numeric fields; or +//! - `return ` over public Int32 fields/params/in-range +//! integer literals, signed bitwise binary operators, and immutable local +//! temporaries built from those expressions; or //! - `return ` for boolean //! predicates over the same safe numeric expression subset. @@ -16,6 +19,7 @@ use perry_types::Type; #[derive(Clone, Copy)] enum ScalarMethodReturnKind { Numeric, + Int32, Boolean, } @@ -55,27 +59,86 @@ pub(crate) fn is_simple_scalar_method( return false; } - let mut numeric_params = HashSet::new(); + let mut numeric_locals = HashSet::new(); for param in &method.params { + let param_type_is_safe = match return_kind { + ScalarMethodReturnKind::Int32 => is_int32_type(¶m.ty), + ScalarMethodReturnKind::Numeric | ScalarMethodReturnKind::Boolean => { + is_numeric_type(¶m.ty) + } + }; if param.default.is_some() || param.is_rest || param.arguments_object.is_some() || !param.decorators.is_empty() - || !is_numeric_type(¶m.ty) + || !param_type_is_safe { return false; } - numeric_params.insert(param.id); + numeric_locals.insert(param.id); } - let [Stmt::Return(Some(expr))] = method.body.as_slice() else { + let Some((return_expr, local_temps)) = scalar_method_straight_line_return(method, return_kind) + else { return false; }; - scalar_method_return_expr_is_safe(classes, class_name, expr, &numeric_params, return_kind) + for (id, init) in local_temps { + if !scalar_method_return_expr_is_safe( + classes, + class_name, + init, + &numeric_locals, + return_kind, + ) { + return false; + } + numeric_locals.insert(id); + } + scalar_method_return_expr_is_safe( + classes, + class_name, + return_expr, + &numeric_locals, + return_kind, + ) +} + +fn scalar_method_straight_line_return<'a>( + method: &'a Function, + return_kind: ScalarMethodReturnKind, +) -> Option<(&'a Expr, Vec<(u32, &'a Expr)>)> { + let mut local_temps = Vec::new(); + for (idx, stmt) in method.body.iter().enumerate() { + match stmt { + Stmt::Let { + id, + ty, + mutable, + init: Some(init), + .. + } if !*mutable && scalar_method_temp_type_is_safe(ty, return_kind) => { + local_temps.push((*id, init)); + } + Stmt::Return(Some(expr)) if idx + 1 == method.body.len() => { + return Some((expr, local_temps)); + } + _ => return None, + } + } + None +} + +fn scalar_method_temp_type_is_safe(ty: &Type, return_kind: ScalarMethodReturnKind) -> bool { + match return_kind { + ScalarMethodReturnKind::Int32 => is_int32_type(ty), + ScalarMethodReturnKind::Numeric | ScalarMethodReturnKind::Boolean => is_numeric_type(ty), + } } fn scalar_method_return_kind(ty: &Type) -> Option { - if is_numeric_type(ty) { + if matches!(ty, Type::Int32) { + Some(ScalarMethodReturnKind::Int32) + } else if matches!(ty, Type::Number) { Some(ScalarMethodReturnKind::Numeric) } else if matches!(ty, Type::Boolean) { Some(ScalarMethodReturnKind::Boolean) @@ -95,6 +158,9 @@ fn scalar_method_return_expr_is_safe( ScalarMethodReturnKind::Numeric => { numeric_scalar_method_expr_is_safe(classes, class_name, expr, numeric_params) } + ScalarMethodReturnKind::Int32 => { + int32_scalar_method_expr_is_safe(classes, class_name, expr, numeric_params) + } ScalarMethodReturnKind::Boolean => { boolean_scalar_method_expr_is_safe(classes, class_name, expr, numeric_params) } @@ -151,10 +217,41 @@ fn numeric_scalar_method_expr_is_safe( } } +fn int32_scalar_method_expr_is_safe( + classes: &HashMap, + class_name: &str, + expr: &Expr, + numeric_params: &HashSet, +) -> bool { + match expr { + Expr::Integer(value) => i32::try_from(*value).is_ok(), + Expr::LocalGet(id) => numeric_params.contains(id), + Expr::Binary { op, left, right } => { + matches!( + op, + BinaryOp::BitAnd + | BinaryOp::BitOr + | BinaryOp::BitXor + | BinaryOp::Shl + | BinaryOp::Shr + ) && int32_scalar_method_expr_is_safe(classes, class_name, left, numeric_params) + && int32_scalar_method_expr_is_safe(classes, class_name, right, numeric_params) + } + Expr::PropertyGet { object, property } if matches!(object.as_ref(), Expr::This) => { + public_int32_field(classes, class_name, property) + } + _ => false, + } +} + fn is_numeric_type(ty: &Type) -> bool { matches!(ty, Type::Number | Type::Int32) } +fn is_int32_type(ty: &Type) -> bool { + matches!(ty, Type::Int32) +} + fn public_numeric_field( classes: &HashMap, class_name: &str, @@ -189,6 +286,40 @@ fn public_numeric_field( false } +fn public_int32_field( + classes: &HashMap, + class_name: &str, + field_name: &str, +) -> bool { + let mut current = Some(class_name.to_string()); + let mut seen = HashSet::new(); + let mut depth = 0usize; + while let Some(name) = current { + depth += 1; + if depth > 64 || !seen.insert(name.clone()) { + return false; + } + let Some(class) = classes.get(&name).copied() else { + return false; + }; + if class.getters.iter().any(|(name, _)| name == field_name) + || class.setters.iter().any(|(name, _)| name == field_name) + { + return false; + } + if class.fields.iter().any(|field| { + field.key_expr.is_none() + && !field.is_private + && field.name == field_name + && is_int32_type(&field.ty) + }) { + return true; + } + current = class.extends_name.clone(); + } + false +} + fn class_declares_or_writes_own_property( classes: &HashMap, class_name: &str, diff --git a/crates/perry-codegen/src/expr/bigint_set.rs b/crates/perry-codegen/src/expr/bigint_set.rs index cacf9909a4..694d3984a3 100644 --- a/crates/perry-codegen/src/expr/bigint_set.rs +++ b/crates/perry-codegen/src/expr/bigint_set.rs @@ -28,7 +28,7 @@ use crate::type_analysis::{ receiver_class_name, set_static_type_args, }; #[allow(unused_imports)] -use crate::types::{DOUBLE, I1, I32, I64, I8, PTR}; +use crate::types::{DOUBLE, F32, I1, I32, I64, I8, PTR}; #[allow(unused_imports)] use super::{ @@ -38,13 +38,16 @@ use super::{ emit_write_barrier, emit_write_barrier_slot_on_block, expr_is_known_non_pointer_shadow_value, extract_array_of_object_shape, i32_bool_to_nanbox, import_origin_suffix, is_global_this_builtin_function_name, is_global_this_builtin_name, is_known_finite, - lower_array_literal, lower_channel_reduction, lower_expr, lower_expr_as_i32, + lower_array_literal, lower_channel_reduction, lower_expr, lower_expr_as_i32, lower_expr_native, lower_index_set_fast, lower_js_args_array, lower_object_literal, lower_stream_super_init, lower_url_string_getter, nanbox_bigint_inline, nanbox_pointer_inline, - nanbox_pointer_inline_pub, nanbox_string_inline, proxy_build_args_array, try_flat_const_2d_int, - try_lower_flat_const_index_get, try_match_channel_reduction, try_static_class_name, - unbox_str_handle, unbox_to_i64, variant_name, ChannelReduction, FlatConstInfo, FnCtx, - I18nLowerCtx, + nanbox_pointer_inline_pub, nanbox_string_inline, proxy_build_args_array, + record_collection_number_key_fallback, record_collection_number_key_selected, + record_collection_string_key_fallback, record_collection_string_key_selected, + record_collection_typed_value_fallback, record_collection_typed_value_selected, + try_flat_const_2d_int, try_lower_flat_const_index_get, try_match_channel_reduction, + try_static_class_name, unbox_str_handle, unbox_to_i64, variant_name, ChannelReduction, + FlatConstInfo, FnCtx, I18nLowerCtx, }; fn number_coerce_operand_is_already_primitive_number(ctx: &FnCtx<'_>, operand: &Expr) -> bool { @@ -87,6 +90,321 @@ fn is_static_string_set(ctx: &FnCtx<'_>, set: &Expr) -> bool { ) } +fn is_static_number_set(ctx: &FnCtx<'_>, set: &Expr) -> bool { + matches!(set_static_type_args(ctx, set), Some([HirType::Number])) +} + +fn guarded_set_number_add(ctx: &mut FnCtx<'_>, set_handle: &str, value_box: &str) -> String { + let guard_raw = ctx + .block() + .call(I32, "js_typed_f64_arg_guard", &[(DOUBLE, value_box)]); + let guard = ctx.block().icmp_ne(I32, &guard_raw, "0"); + let fast_idx = ctx.new_block("set_number.add.fast"); + let fallback_idx = ctx.new_block("set_number.add.fallback"); + let merge_idx = ctx.new_block("set_number.add.merge"); + let fast_label = ctx.block_label(fast_idx); + let fallback_label = ctx.block_label(fallback_idx); + let merge_label = ctx.block_label(merge_idx); + ctx.block().cond_br(&guard, &fast_label, &fallback_label); + + ctx.current_block = fast_idx; + let value_raw = ctx + .block() + .call(DOUBLE, "js_typed_f64_arg_to_raw", &[(DOUBLE, value_box)]); + let fast_value = ctx.block().call( + I64, + "js_set_add_number", + &[(I64, set_handle), (DOUBLE, &value_raw)], + ); + record_collection_number_key_selected( + ctx, + "SetAdd", + "collection_number_value.set_add", + &value_raw, + "set", + "number_value_helper", + "js_set_add_number", + "value", + ); + let after_fast = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&merge_label); + } + + ctx.current_block = fallback_idx; + let fallback_value = + ctx.block() + .call(I64, "js_set_add", &[(I64, set_handle), (DOUBLE, value_box)]); + record_collection_number_key_fallback( + ctx, + "SetAdd", + "collection_number_value.set_add_generic", + value_box, + "set", + "number_value_helper", + "js_set_add", + "runtime_value_guard_failed", + "value", + ); + let after_fallback = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&merge_label); + } + + ctx.current_block = merge_idx; + ctx.block().phi( + I64, + &[ + (fast_value.as_str(), after_fast.as_str()), + (fallback_value.as_str(), after_fallback.as_str()), + ], + ) +} + +fn guarded_set_number_has(ctx: &mut FnCtx<'_>, set_handle: &str, value_box: &str) -> String { + let guard_raw = ctx + .block() + .call(I32, "js_typed_f64_arg_guard", &[(DOUBLE, value_box)]); + let guard = ctx.block().icmp_ne(I32, &guard_raw, "0"); + let fast_idx = ctx.new_block("set_number.has.fast"); + let fallback_idx = ctx.new_block("set_number.has.fallback"); + let merge_idx = ctx.new_block("set_number.has.merge"); + let fast_label = ctx.block_label(fast_idx); + let fallback_label = ctx.block_label(fallback_idx); + let merge_label = ctx.block_label(merge_idx); + ctx.block().cond_br(&guard, &fast_label, &fallback_label); + + ctx.current_block = fast_idx; + let value_raw = ctx + .block() + .call(DOUBLE, "js_typed_f64_arg_to_raw", &[(DOUBLE, value_box)]); + let fast_value = ctx.block().call( + I32, + "js_set_has_number", + &[(I64, set_handle), (DOUBLE, &value_raw)], + ); + record_collection_number_key_selected( + ctx, + "SetHas", + "collection_number_value.set_has", + &value_raw, + "set", + "number_value_helper", + "js_set_has_number", + "value", + ); + let after_fast = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&merge_label); + } + + ctx.current_block = fallback_idx; + let fallback_value = + ctx.block() + .call(I32, "js_set_has", &[(I64, set_handle), (DOUBLE, value_box)]); + record_collection_number_key_fallback( + ctx, + "SetHas", + "collection_number_value.set_has_generic", + value_box, + "set", + "number_value_helper", + "js_set_has", + "runtime_value_guard_failed", + "value", + ); + let after_fallback = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&merge_label); + } + + ctx.current_block = merge_idx; + ctx.block().phi( + I32, + &[ + (fast_value.as_str(), after_fast.as_str()), + (fallback_value.as_str(), after_fallback.as_str()), + ], + ) +} + +fn guarded_set_number_delete(ctx: &mut FnCtx<'_>, set_handle: &str, value_box: &str) -> String { + let guard_raw = ctx + .block() + .call(I32, "js_typed_f64_arg_guard", &[(DOUBLE, value_box)]); + let guard = ctx.block().icmp_ne(I32, &guard_raw, "0"); + let fast_idx = ctx.new_block("set_number.delete.fast"); + let fallback_idx = ctx.new_block("set_number.delete.fallback"); + let merge_idx = ctx.new_block("set_number.delete.merge"); + let fast_label = ctx.block_label(fast_idx); + let fallback_label = ctx.block_label(fallback_idx); + let merge_label = ctx.block_label(merge_idx); + ctx.block().cond_br(&guard, &fast_label, &fallback_label); + + ctx.current_block = fast_idx; + let value_raw = ctx + .block() + .call(DOUBLE, "js_typed_f64_arg_to_raw", &[(DOUBLE, value_box)]); + let fast_value = ctx.block().call( + I32, + "js_set_delete_number", + &[(I64, set_handle), (DOUBLE, &value_raw)], + ); + record_collection_number_key_selected( + ctx, + "SetDelete", + "collection_number_value.set_delete", + &value_raw, + "set", + "number_value_helper", + "js_set_delete_number", + "value", + ); + let after_fast = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&merge_label); + } + + ctx.current_block = fallback_idx; + let fallback_value = ctx.block().call( + I32, + "js_set_delete", + &[(I64, set_handle), (DOUBLE, value_box)], + ); + record_collection_number_key_fallback( + ctx, + "SetDelete", + "collection_number_value.set_delete_generic", + value_box, + "set", + "number_value_helper", + "js_set_delete", + "runtime_value_guard_failed", + "value", + ); + let after_fallback = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&merge_label); + } + + ctx.current_block = merge_idx; + ctx.block().phi( + I32, + &[ + (fast_value.as_str(), after_fast.as_str()), + (fallback_value.as_str(), after_fallback.as_str()), + ], + ) +} + +fn is_static_i32_set(ctx: &FnCtx<'_>, set: &Expr) -> bool { + matches!(set_static_type_args(ctx, set), Some([HirType::Int32])) +} + +fn is_perry_u32_type(ctx: &FnCtx<'_>, ty: &HirType) -> bool { + match ty { + HirType::Named(name) if name == "PerryU32" => true, + HirType::Named(name) => ctx + .type_aliases + .get(name) + .is_some_and(|alias| is_perry_u32_type(ctx, alias)), + _ => false, + } +} + +fn is_static_u32_set(ctx: &FnCtx<'_>, set: &Expr) -> bool { + match set_static_type_args(ctx, set) { + Some([value_ty]) => is_perry_u32_type(ctx, value_ty), + _ => false, + } +} + +fn is_perry_f32_type(ctx: &FnCtx<'_>, ty: &HirType) -> bool { + match ty { + HirType::Named(name) if name == "PerryF32" => true, + HirType::Named(name) => ctx + .type_aliases + .get(name) + .is_some_and(|alias| is_perry_f32_type(ctx, alias)), + _ => false, + } +} + +fn is_static_f32_set(ctx: &FnCtx<'_>, set: &Expr) -> bool { + match set_static_type_args(ctx, set) { + Some([value_ty]) => is_perry_f32_type(ctx, value_ty), + _ => false, + } +} + +fn is_static_boolean_set(ctx: &FnCtx<'_>, set: &Expr) -> bool { + matches!(set_static_type_args(ctx, set), Some([HirType::Boolean])) +} + +fn can_lower_i32_for_collection_value(ctx: &FnCtx<'_>, value: &Expr) -> bool { + can_lower_expr_as_i32( + value, + &ctx.i32_counter_slots, + ctx.flat_const_arrays, + &ctx.array_row_aliases, + ctx.integer_locals, + ctx.clamp3_functions, + ctx.clamp_u8_functions, + ctx.integer_returning_functions, + ctx.i32_identity_functions, + ) +} + +fn can_lower_u32_for_collection_value(ctx: &FnCtx<'_>, value: &Expr) -> bool { + match value { + Expr::Integer(n) => *n >= 0 && u32::try_from(*n).is_ok(), + Expr::Binary { + op: BinaryOp::UShr, + left, + right, + } => { + can_lower_i32_for_collection_value(ctx, left) + && can_lower_i32_for_collection_value(ctx, right) + } + Expr::Uint8ArrayGet { .. } + | Expr::BufferIndexGet { .. } + | Expr::Uint8ArrayLength(_) + | Expr::BufferLength(_) => true, + Expr::LocalGet(id) => ctx.unsigned_i32_locals.contains(id), + _ => false, + } +} + +fn literal_f64(expr: &Expr) -> Option { + match expr { + Expr::Integer(n) => Some(*n as f64), + Expr::Number(n) => Some(*n), + _ => None, + } +} + +fn f32_roundtrips_exact(value: f64) -> bool { + let narrowed = value as f32; + (narrowed as f64).to_bits() == value.to_bits() +} + +fn can_lower_f32_for_collection_value(value: &Expr) -> bool { + literal_f64(value).is_some_and(f32_roundtrips_exact) +} + +fn can_lower_i1_for_collection_value(ctx: &FnCtx<'_>, value: &Expr) -> bool { + match value { + Expr::Bool(_) => true, + Expr::LocalGet(id) => { + ctx.i1_local_slots.contains_key(id) + && !ctx.closure_captures.contains_key(id) + && !ctx.boxed_vars.contains(id) + && !ctx.module_globals.contains_key(id) + } + _ => false, + } +} + pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { match expr { Expr::ObjectRest { @@ -284,22 +602,259 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // -------- set.add(value) — updates the local in place -------- Expr::SetAdd { set_id, value } => { let set_expr = Expr::LocalGet(*set_id); - let use_string_set = - is_static_string_set(ctx, &set_expr) && is_definitely_string_expr(ctx, value); - let v = lower_expr(ctx, value)?; - let set_box = lower_expr(ctx, &set_expr)?; - let blk = ctx.block(); - let set_handle = unbox_to_i64(blk, &set_box); - let new_handle = if use_string_set { - let value_handle = unbox_str_handle(blk, &v); - blk.call( - I64, - "js_set_add_string", - &[(I64, &set_handle), (I64, &value_handle)], - ) + let receiver_i32_set = is_static_i32_set(ctx, &set_expr); + let use_i32_set = receiver_i32_set && can_lower_i32_for_collection_value(ctx, value); + let receiver_u32_set = is_static_u32_set(ctx, &set_expr); + let use_u32_set = receiver_u32_set && can_lower_u32_for_collection_value(ctx, value); + let receiver_f32_set = is_static_f32_set(ctx, &set_expr); + let use_f32_set = receiver_f32_set && can_lower_f32_for_collection_value(value); + let receiver_boolean_set = is_static_boolean_set(ctx, &set_expr); + let use_boolean_set = + receiver_boolean_set && can_lower_i1_for_collection_value(ctx, value); + let receiver_number_set = is_static_number_set(ctx, &set_expr); + let use_number_set = receiver_number_set && is_numeric_expr(ctx, value); + let receiver_string_set = is_static_string_set(ctx, &set_expr); + let value_is_string = is_definitely_string_expr(ctx, value); + let use_string_set = receiver_string_set && value_is_string; + let new_handle = if use_i32_set { + let value_i32 = + lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::I32)?; + let set_box = lower_expr(ctx, &set_expr)?; + let set_handle = { + let blk = ctx.block(); + unbox_to_i64(blk, &set_box) + }; + let new_handle = { + let blk = ctx.block(); + blk.call( + I64, + "js_set_add_i32", + &[(I64, &set_handle), (I32, &value_i32.value)], + ) + }; + record_collection_typed_value_selected( + ctx, + "SetAdd", + "collection_typed_value.set_add_i32", + &value_i32, + "set", + "int32_value_helper", + "js_set_add_i32", + "set_slot", + ); + new_handle + } else if use_u32_set { + let value_u32 = + lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::U32)?; + let set_box = lower_expr(ctx, &set_expr)?; + let set_handle = { + let blk = ctx.block(); + unbox_to_i64(blk, &set_box) + }; + let new_handle = { + let blk = ctx.block(); + blk.call( + I64, + "js_set_add_u32", + &[(I64, &set_handle), (I32, &value_u32.value)], + ) + }; + record_collection_typed_value_selected( + ctx, + "SetAdd", + "collection_typed_value.set_add_u32", + &value_u32, + "set", + "uint32_value_helper", + "js_set_add_u32", + "set_slot", + ); + new_handle + } else if use_f32_set { + let value_f32 = + lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::F32)?; + let set_box = lower_expr(ctx, &set_expr)?; + let set_handle = { + let blk = ctx.block(); + unbox_to_i64(blk, &set_box) + }; + let new_handle = { + let blk = ctx.block(); + blk.call( + I64, + "js_set_add_f32", + &[(I64, &set_handle), (F32, &value_f32.value)], + ) + }; + record_collection_typed_value_selected( + ctx, + "SetAdd", + "collection_typed_value.set_add_f32", + &value_f32, + "set", + "float32_value_helper", + "js_set_add_f32", + "set_slot", + ); + new_handle + } else if use_boolean_set { + let value_i1 = + lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::I1)?; + let set_box = lower_expr(ctx, &set_expr)?; + let set_handle = { + let blk = ctx.block(); + unbox_to_i64(blk, &set_box) + }; + let new_handle = { + let blk = ctx.block(); + let value_i32 = blk.zext(I1, &value_i1.value, I32); + blk.call( + I64, + "js_set_add_bool", + &[(I64, &set_handle), (I32, &value_i32)], + ) + }; + record_collection_typed_value_selected( + ctx, + "SetAdd", + "collection_typed_value.set_add_bool", + &value_i1, + "set", + "boolean_value_helper", + "js_set_add_bool", + "set_slot", + ); + new_handle + } else if use_number_set { + let v = lower_expr(ctx, value)?; + let set_box = lower_expr(ctx, &set_expr)?; + let set_handle = { + let blk = ctx.block(); + unbox_to_i64(blk, &set_box) + }; + guarded_set_number_add(ctx, &set_handle, &v) } else { - blk.call(I64, "js_set_add", &[(I64, &set_handle), (DOUBLE, &v)]) + let set_box = lower_expr(ctx, &set_expr)?; + let set_handle = { + let blk = ctx.block(); + unbox_to_i64(blk, &set_box) + }; + if use_string_set { + let value_ref = lower_expr_native( + ctx, + value, + crate::native_value::ExpectedNativeRep::StringRef, + )?; + let new_handle = { + let blk = ctx.block(); + let new_handle = blk.call( + I64, + "js_set_add_string", + &[(I64, &set_handle), (I64, &value_ref.value)], + ); + new_handle + }; + record_collection_string_key_selected( + ctx, + "SetAdd", + "collection_string_key.set_add", + &value_ref.value, + "set", + "js_set_add_string", + ); + record_collection_typed_value_selected( + ctx, + "SetAdd", + "collection_typed_value.set_add_string", + &value_ref, + "set", + "string_value_helper", + "js_set_add_string", + "set_slot", + ); + new_handle + } else { + let v = lower_expr(ctx, value)?; + let new_handle = { + let blk = ctx.block(); + blk.call(I64, "js_set_add", &[(I64, &set_handle), (DOUBLE, &v)]) + }; + let reason = if receiver_string_set { + "value_expr_not_definitely_string" + } else { + "receiver_value_not_static_string" + }; + if receiver_i32_set { + record_collection_typed_value_fallback( + ctx, + "SetAdd", + "collection_typed_value.set_add_generic", + &v, + "set", + "int32_value_helper", + "js_set_add", + "value_expr_not_native_i32", + ); + } else if receiver_u32_set { + record_collection_typed_value_fallback( + ctx, + "SetAdd", + "collection_typed_value.set_add_generic", + &v, + "set", + "uint32_value_helper", + "js_set_add", + "value_expr_not_native_u32", + ); + } else if receiver_f32_set { + record_collection_typed_value_fallback( + ctx, + "SetAdd", + "collection_typed_value.set_add_generic", + &v, + "set", + "float32_value_helper", + "js_set_add", + "value_expr_not_native_f32", + ); + } else if receiver_boolean_set { + record_collection_typed_value_fallback( + ctx, + "SetAdd", + "collection_typed_value.set_add_generic", + &v, + "set", + "boolean_value_helper", + "js_set_add", + "value_expr_not_native_i1", + ); + } else if receiver_number_set { + record_collection_number_key_fallback( + ctx, + "SetAdd", + "collection_number_value.set_add_generic", + &v, + "set", + "number_value_helper", + "js_set_add", + "value_expr_not_numeric", + "value", + ); + } else { + record_collection_string_key_fallback( + ctx, + "SetAdd", + "collection_string_key.set_add_generic", + &v, + "set", + "js_set_add", + reason, + ); + } + new_handle + } }; + let blk = ctx.block(); let new_box = nanbox_pointer_inline(blk, &new_handle); // Write back to the storage so subsequent reads see the // possibly-realloc'd pointer. @@ -326,22 +881,228 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // -------- set.has(value) -> boolean -------- Expr::SetHas { set, value } => { + let receiver_i32_set = is_static_i32_set(ctx, set); + let use_i32_set = receiver_i32_set && can_lower_i32_for_collection_value(ctx, value); + let receiver_u32_set = is_static_u32_set(ctx, set); + let use_u32_set = receiver_u32_set && can_lower_u32_for_collection_value(ctx, value); + let receiver_f32_set = is_static_f32_set(ctx, set); + let use_f32_set = receiver_f32_set && can_lower_f32_for_collection_value(value); + let receiver_boolean_set = is_static_boolean_set(ctx, set); + let use_boolean_set = + receiver_boolean_set && can_lower_i1_for_collection_value(ctx, value); + let receiver_number_set = is_static_number_set(ctx, set); + let use_number_set = receiver_number_set && is_numeric_expr(ctx, value); let use_string_set = is_static_string_set(ctx, set) && is_definitely_string_expr(ctx, value); let s_box = lower_expr(ctx, set)?; - let v_box = lower_expr(ctx, value)?; - let blk = ctx.block(); - let s_handle = unbox_to_i64(blk, &s_box); - let i32_v = if use_string_set { - let value_handle = unbox_str_handle(blk, &v_box); - blk.call( - I32, - "js_set_has_string", - &[(I64, &s_handle), (I64, &value_handle)], - ) + let s_handle = { + let blk = ctx.block(); + unbox_to_i64(blk, &s_box) + }; + let i32_v = if use_i32_set { + let value_i32 = + lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::I32)?; + let i32_v = { + let blk = ctx.block(); + blk.call( + I32, + "js_set_has_i32", + &[(I64, &s_handle), (I32, &value_i32.value)], + ) + }; + record_collection_typed_value_selected( + ctx, + "SetHas", + "collection_typed_value.set_has_i32", + &value_i32, + "set", + "int32_value_helper", + "js_set_has_i32", + "set_slot", + ); + i32_v + } else if use_u32_set { + let value_u32 = + lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::U32)?; + let i32_v = { + let blk = ctx.block(); + blk.call( + I32, + "js_set_has_u32", + &[(I64, &s_handle), (I32, &value_u32.value)], + ) + }; + record_collection_typed_value_selected( + ctx, + "SetHas", + "collection_typed_value.set_has_u32", + &value_u32, + "set", + "uint32_value_helper", + "js_set_has_u32", + "set_slot", + ); + i32_v + } else if use_f32_set { + let value_f32 = + lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::F32)?; + let i32_v = { + let blk = ctx.block(); + blk.call( + I32, + "js_set_has_f32", + &[(I64, &s_handle), (F32, &value_f32.value)], + ) + }; + record_collection_typed_value_selected( + ctx, + "SetHas", + "collection_typed_value.set_has_f32", + &value_f32, + "set", + "float32_value_helper", + "js_set_has_f32", + "set_slot", + ); + i32_v + } else if use_boolean_set { + let value_i1 = + lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::I1)?; + let i32_v = { + let blk = ctx.block(); + let value_i32 = blk.zext(I1, &value_i1.value, I32); + blk.call( + I32, + "js_set_has_bool", + &[(I64, &s_handle), (I32, &value_i32)], + ) + }; + record_collection_typed_value_selected( + ctx, + "SetHas", + "collection_typed_value.set_has_bool", + &value_i1, + "set", + "boolean_value_helper", + "js_set_has_bool", + "set_slot", + ); + i32_v + } else if use_number_set { + let v_box = lower_expr(ctx, value)?; + guarded_set_number_has(ctx, &s_handle, &v_box) } else { - blk.call(I32, "js_set_has", &[(I64, &s_handle), (DOUBLE, &v_box)]) + if use_string_set { + let value_ref = lower_expr_native( + ctx, + value, + crate::native_value::ExpectedNativeRep::StringRef, + )?; + let i32_v = { + let blk = ctx.block(); + let i32_v = blk.call( + I32, + "js_set_has_string", + &[(I64, &s_handle), (I64, &value_ref.value)], + ); + i32_v + }; + record_collection_string_key_selected( + ctx, + "SetHas", + "collection_string_key.set_has", + &value_ref.value, + "set", + "js_set_has_string", + ); + record_collection_typed_value_selected( + ctx, + "SetHas", + "collection_typed_value.set_has_string", + &value_ref, + "set", + "string_value_helper", + "js_set_has_string", + "set_slot", + ); + i32_v + } else { + let v_box = lower_expr(ctx, value)?; + let i32_v = { + let blk = ctx.block(); + blk.call(I32, "js_set_has", &[(I64, &s_handle), (DOUBLE, &v_box)]) + }; + if receiver_i32_set { + record_collection_typed_value_fallback( + ctx, + "SetHas", + "collection_typed_value.set_has_generic", + &v_box, + "set", + "int32_value_helper", + "js_set_has", + "value_expr_not_native_i32", + ); + } else if receiver_u32_set { + record_collection_typed_value_fallback( + ctx, + "SetHas", + "collection_typed_value.set_has_generic", + &v_box, + "set", + "uint32_value_helper", + "js_set_has", + "value_expr_not_native_u32", + ); + } else if receiver_f32_set { + record_collection_typed_value_fallback( + ctx, + "SetHas", + "collection_typed_value.set_has_generic", + &v_box, + "set", + "float32_value_helper", + "js_set_has", + "value_expr_not_native_f32", + ); + } else if receiver_boolean_set { + record_collection_typed_value_fallback( + ctx, + "SetHas", + "collection_typed_value.set_has_generic", + &v_box, + "set", + "boolean_value_helper", + "js_set_has", + "value_expr_not_native_i1", + ); + } else if receiver_number_set { + record_collection_number_key_fallback( + ctx, + "SetHas", + "collection_number_value.set_has_generic", + &v_box, + "set", + "number_value_helper", + "js_set_has", + "value_expr_not_numeric", + "value", + ); + } else { + record_collection_string_key_fallback( + ctx, + "SetHas", + "collection_string_key.set_has_generic", + &v_box, + "set", + "js_set_has", + "receiver_or_value_not_static_string", + ); + } + i32_v + } }; + let blk = ctx.block(); let bit = blk.icmp_ne(I32, &i32_v, "0"); let tagged = blk.select( crate::types::I1, @@ -355,22 +1116,228 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // -------- set.delete(value) -> boolean -------- Expr::SetDelete { set, value } => { + let receiver_i32_set = is_static_i32_set(ctx, set); + let use_i32_set = receiver_i32_set && can_lower_i32_for_collection_value(ctx, value); + let receiver_u32_set = is_static_u32_set(ctx, set); + let use_u32_set = receiver_u32_set && can_lower_u32_for_collection_value(ctx, value); + let receiver_f32_set = is_static_f32_set(ctx, set); + let use_f32_set = receiver_f32_set && can_lower_f32_for_collection_value(value); + let receiver_boolean_set = is_static_boolean_set(ctx, set); + let use_boolean_set = + receiver_boolean_set && can_lower_i1_for_collection_value(ctx, value); + let receiver_number_set = is_static_number_set(ctx, set); + let use_number_set = receiver_number_set && is_numeric_expr(ctx, value); let use_string_set = is_static_string_set(ctx, set) && is_definitely_string_expr(ctx, value); let s_box = lower_expr(ctx, set)?; - let v_box = lower_expr(ctx, value)?; - let blk = ctx.block(); - let s_handle = unbox_to_i64(blk, &s_box); - let i32_v = if use_string_set { - let value_handle = unbox_str_handle(blk, &v_box); - blk.call( - I32, - "js_set_delete_string", - &[(I64, &s_handle), (I64, &value_handle)], - ) + let s_handle = { + let blk = ctx.block(); + unbox_to_i64(blk, &s_box) + }; + let i32_v = if use_i32_set { + let value_i32 = + lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::I32)?; + let i32_v = { + let blk = ctx.block(); + blk.call( + I32, + "js_set_delete_i32", + &[(I64, &s_handle), (I32, &value_i32.value)], + ) + }; + record_collection_typed_value_selected( + ctx, + "SetDelete", + "collection_typed_value.set_delete_i32", + &value_i32, + "set", + "int32_value_helper", + "js_set_delete_i32", + "set_slot", + ); + i32_v + } else if use_u32_set { + let value_u32 = + lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::U32)?; + let i32_v = { + let blk = ctx.block(); + blk.call( + I32, + "js_set_delete_u32", + &[(I64, &s_handle), (I32, &value_u32.value)], + ) + }; + record_collection_typed_value_selected( + ctx, + "SetDelete", + "collection_typed_value.set_delete_u32", + &value_u32, + "set", + "uint32_value_helper", + "js_set_delete_u32", + "set_slot", + ); + i32_v + } else if use_f32_set { + let value_f32 = + lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::F32)?; + let i32_v = { + let blk = ctx.block(); + blk.call( + I32, + "js_set_delete_f32", + &[(I64, &s_handle), (F32, &value_f32.value)], + ) + }; + record_collection_typed_value_selected( + ctx, + "SetDelete", + "collection_typed_value.set_delete_f32", + &value_f32, + "set", + "float32_value_helper", + "js_set_delete_f32", + "set_slot", + ); + i32_v + } else if use_boolean_set { + let value_i1 = + lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::I1)?; + let i32_v = { + let blk = ctx.block(); + let value_i32 = blk.zext(I1, &value_i1.value, I32); + blk.call( + I32, + "js_set_delete_bool", + &[(I64, &s_handle), (I32, &value_i32)], + ) + }; + record_collection_typed_value_selected( + ctx, + "SetDelete", + "collection_typed_value.set_delete_bool", + &value_i1, + "set", + "boolean_value_helper", + "js_set_delete_bool", + "set_slot", + ); + i32_v + } else if use_number_set { + let v_box = lower_expr(ctx, value)?; + guarded_set_number_delete(ctx, &s_handle, &v_box) } else { - blk.call(I32, "js_set_delete", &[(I64, &s_handle), (DOUBLE, &v_box)]) + if use_string_set { + let value_ref = lower_expr_native( + ctx, + value, + crate::native_value::ExpectedNativeRep::StringRef, + )?; + let i32_v = { + let blk = ctx.block(); + let i32_v = blk.call( + I32, + "js_set_delete_string", + &[(I64, &s_handle), (I64, &value_ref.value)], + ); + i32_v + }; + record_collection_string_key_selected( + ctx, + "SetDelete", + "collection_string_key.set_delete", + &value_ref.value, + "set", + "js_set_delete_string", + ); + record_collection_typed_value_selected( + ctx, + "SetDelete", + "collection_typed_value.set_delete_string", + &value_ref, + "set", + "string_value_helper", + "js_set_delete_string", + "set_slot", + ); + i32_v + } else { + let v_box = lower_expr(ctx, value)?; + let i32_v = { + let blk = ctx.block(); + blk.call(I32, "js_set_delete", &[(I64, &s_handle), (DOUBLE, &v_box)]) + }; + if receiver_i32_set { + record_collection_typed_value_fallback( + ctx, + "SetDelete", + "collection_typed_value.set_delete_generic", + &v_box, + "set", + "int32_value_helper", + "js_set_delete", + "value_expr_not_native_i32", + ); + } else if receiver_u32_set { + record_collection_typed_value_fallback( + ctx, + "SetDelete", + "collection_typed_value.set_delete_generic", + &v_box, + "set", + "uint32_value_helper", + "js_set_delete", + "value_expr_not_native_u32", + ); + } else if receiver_f32_set { + record_collection_typed_value_fallback( + ctx, + "SetDelete", + "collection_typed_value.set_delete_generic", + &v_box, + "set", + "float32_value_helper", + "js_set_delete", + "value_expr_not_native_f32", + ); + } else if receiver_boolean_set { + record_collection_typed_value_fallback( + ctx, + "SetDelete", + "collection_typed_value.set_delete_generic", + &v_box, + "set", + "boolean_value_helper", + "js_set_delete", + "value_expr_not_native_i1", + ); + } else if receiver_number_set { + record_collection_number_key_fallback( + ctx, + "SetDelete", + "collection_number_value.set_delete_generic", + &v_box, + "set", + "number_value_helper", + "js_set_delete", + "value_expr_not_numeric", + "value", + ); + } else { + record_collection_string_key_fallback( + ctx, + "SetDelete", + "collection_string_key.set_delete_generic", + &v_box, + "set", + "js_set_delete", + "receiver_or_value_not_static_string", + ); + } + i32_v + } }; + let blk = ctx.block(); let bit = blk.icmp_ne(I32, &i32_v, "0"); let tagged = blk.select( crate::types::I1, diff --git a/crates/perry-codegen/src/expr/binary.rs b/crates/perry-codegen/src/expr/binary.rs index 576f5dbfc0..7c6f2f630f 100644 --- a/crates/perry-codegen/src/expr/binary.rs +++ b/crates/perry-codegen/src/expr/binary.rs @@ -21,6 +21,10 @@ use crate::lower_string_method::{ }; #[allow(unused_imports)] use crate::nanbox::{double_literal, POINTER_MASK_I64}; +use crate::native_value::{ + materialize_small_bigint_pointer_to_js_value, BufferAccessMode, LoweredValue, + MaterializationReason, +}; #[allow(unused_imports)] use crate::type_analysis::{ add_operands_have_pod_materialization_hazard, compute_auto_captures, @@ -29,7 +33,7 @@ use crate::type_analysis::{ receiver_class_name, }; #[allow(unused_imports)] -use crate::types::{DOUBLE, I1, I32, I64, I8, PTR}; +use crate::types::{DOUBLE, I1, I128, I32, I64, I8, PTR}; #[allow(unused_imports)] use super::{ @@ -64,6 +68,156 @@ fn lower_arithmetic_operand(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<(String, Ok((lower_expr(ctx, expr)?, false)) } +fn small_bigint_literal_value(expr: &Expr) -> Option { + let Expr::BigInt(raw) = expr else { + return None; + }; + let normalized = raw.replace('_', ""); + let s = normalized.strip_suffix('n').unwrap_or(&normalized); + let (negative, digits) = match s.strip_prefix('-') { + Some(rest) => (true, rest), + None => (false, s.strip_prefix('+').unwrap_or(s)), + }; + if digits.is_empty() { + return None; + } + let (radix, digits) = if let Some(rest) = digits + .strip_prefix("0x") + .or_else(|| digits.strip_prefix("0X")) + { + (16, rest) + } else if let Some(rest) = digits + .strip_prefix("0o") + .or_else(|| digits.strip_prefix("0O")) + { + (8, rest) + } else if let Some(rest) = digits + .strip_prefix("0b") + .or_else(|| digits.strip_prefix("0B")) + { + (2, rest) + } else { + (10, digits) + }; + if digits.is_empty() { + return None; + } + let magnitude = i128::from_str_radix(digits, radix).ok()?; + let value = if negative { -magnitude } else { magnitude }; + i64::try_from(value).ok() +} + +fn small_bigint_native_op(op: BinaryOp) -> Option<(&'static str, &'static str)> { + match op { + BinaryOp::Add => Some(("add", "js_dynamic_add")), + BinaryOp::Sub => Some(("sub", "js_dynamic_sub")), + BinaryOp::Mul => Some(("mul", "js_dynamic_mul")), + _ => None, + } +} + +fn bigint_dynamic_helper(op: BinaryOp) -> &'static str { + match op { + BinaryOp::Add => "js_dynamic_add", + BinaryOp::Sub => "js_dynamic_sub", + BinaryOp::Mul => "js_dynamic_mul", + BinaryOp::Div => "js_dynamic_div", + BinaryOp::Mod => "js_dynamic_mod", + BinaryOp::BitAnd => "js_dynamic_bitand", + BinaryOp::BitOr => "js_dynamic_bitor", + BinaryOp::BitXor => "js_dynamic_bitxor", + BinaryOp::Shl => "js_dynamic_shl", + BinaryOp::Shr => "js_dynamic_shr", + BinaryOp::Pow => "js_dynamic_pow", + BinaryOp::UShr => "js_dynamic_ushr", + } +} + +fn record_small_bigint_rejection( + ctx: &mut FnCtx<'_>, + reason: &'static str, + fallback_helper: &'static str, +) { + let lowered = LoweredValue::js_value("0.0"); + ctx.record_lowered_value_with_access_mode( + "BigIntSmallBinaryRejected", + None, + "small_bigint.literal_binary_rejected", + &lowered, + None, + None, + Some(BufferAccessMode::DynamicFallback), + Some(MaterializationReason::RuntimeApi), + false, + false, + vec![ + format!("small_bigint_rejected={reason}"), + format!("fallback={fallback_helper}"), + "boxed_at=generic_bigint_dynamic_helper".to_string(), + ], + ); +} + +fn try_lower_small_bigint_literal_binary( + ctx: &mut FnCtx<'_>, + op: BinaryOp, + left: &Expr, + right: &Expr, +) -> Option { + let (native_op, fallback_helper) = small_bigint_native_op(op)?; + let Some(left_i64) = small_bigint_literal_value(left) else { + record_small_bigint_rejection(ctx, "requires_left_i64_literal", fallback_helper); + return None; + }; + let Some(right_i64) = small_bigint_literal_value(right) else { + record_small_bigint_rejection(ctx, "requires_right_i64_literal", fallback_helper); + return None; + }; + + let left_const = left_i64.to_string(); + let right_const = right_i64.to_string(); + let result_i128 = { + let blk = ctx.block(); + let left_wide = blk.sext(I64, &left_const, I128); + let right_wide = blk.sext(I64, &right_const, I128); + match op { + BinaryOp::Add => blk.add(I128, &left_wide, &right_wide), + BinaryOp::Sub => blk.sub(I128, &left_wide, &right_wide), + BinaryOp::Mul => blk.mul(I128, &left_wide, &right_wide), + _ => return None, + } + }; + let lowered = LoweredValue::small_bigint(result_i128.clone()); + ctx.record_lowered_value( + "BigIntSmallBinary", + None, + "small_bigint.literal_binary_i128", + &lowered, + None, + None, + None, + false, + false, + vec![ + "proof=both_operands_bigint_literals_fit_i64".to_string(), + format!("native_op=i128_{native_op}"), + "public_semantics=materialize_bigint_object_before_js_boundary".to_string(), + ], + ); + let ptr = { + let blk = ctx.block(); + let lo = blk.trunc(I128, &result_i128, I64); + let hi_wide = blk.ashr(I128, &result_i128, "64"); + let hi = blk.trunc(I128, &hi_wide, I64); + blk.call(I64, "js_bigint_from_i128_parts", &[(I64, &lo), (I64, &hi)]) + }; + Some(materialize_small_bigint_pointer_to_js_value( + ctx, + &ptr, + MaterializationReason::RuntimeApi, + )) +} + pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { match expr { Expr::Binary { op, left, right } => { @@ -118,6 +272,23 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { &[(DOUBLE, &l), (DOUBLE, &r)], )); } + if is_bigint_expr(ctx, left) && is_bigint_expr(ctx, right) { + if let Some(value) = try_lower_small_bigint_literal_binary( + ctx, + *op, + left.as_ref(), + right.as_ref(), + ) { + return Ok(value); + } + let l = lower_expr(ctx, left)?; + let r = lower_expr(ctx, right)?; + return Ok(ctx.block().call( + DOUBLE, + "js_dynamic_add", + &[(DOUBLE, &l), (DOUBLE, &r)], + )); + } // Refs #486: neither operand is statically known. Per JS // spec for `+`, if EITHER side is a string at runtime, the // result is string concatenation; otherwise numeric add @@ -159,41 +330,17 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // concat (the `is_definitely_string_expr` check above // already ruled out the string case). Closes GH #33. if is_bigint_expr(ctx, left) || is_bigint_expr(ctx, right) { - let helper = match op { - BinaryOp::Add => Some("js_dynamic_add"), - BinaryOp::Sub => Some("js_dynamic_sub"), - BinaryOp::Mul => Some("js_dynamic_mul"), - BinaryOp::Div => Some("js_dynamic_div"), - BinaryOp::Mod => Some("js_dynamic_mod"), - // Bitwise ops on bigints dispatch to the same - // unbox→bigint-op→rebox helpers used for arithmetic. - // Without this, `5n ^ 1n` fell through to the i32 - // ToInt32 path that interprets the NaN-boxed bigint - // bits as a double — `fptosi` on a NaN-payload f64 - // yielded a small signed integer (e.g. -6 for XOR of - // two 64-bit bigints) and masking with - // 0xFFFFFFFFFFFFFFFFn collapsed to 0 (closes #39). - BinaryOp::BitAnd => Some("js_dynamic_bitand"), - BinaryOp::BitOr => Some("js_dynamic_bitor"), - BinaryOp::BitXor => Some("js_dynamic_bitxor"), - BinaryOp::Shl => Some("js_dynamic_shl"), - BinaryOp::Shr => Some("js_dynamic_shr"), - // `bigint ** bigint` is a BigInt operation (RangeError on - // negative exponent); `>>>` on any BigInt is a TypeError. - // Both are routed through the dynamic helpers so the - // numeric fallback only fires when neither side is a - // BigInt at runtime (#2908). - BinaryOp::Pow => Some("js_dynamic_pow"), - BinaryOp::UShr => Some("js_dynamic_ushr"), - _ => None, - }; - if let Some(fname) = helper { - let l = lower_expr(ctx, left)?; - let r = lower_expr(ctx, right)?; - return Ok(ctx - .block() - .call(DOUBLE, fname, &[(DOUBLE, &l), (DOUBLE, &r)])); + let fname = bigint_dynamic_helper(*op); + if let Some(value) = + try_lower_small_bigint_literal_binary(ctx, *op, left.as_ref(), right.as_ref()) + { + return Ok(value); } + let l = lower_expr(ctx, left)?; + let r = lower_expr(ctx, right)?; + return Ok(ctx + .block() + .call(DOUBLE, fname, &[(DOUBLE, &l), (DOUBLE, &r)])); } // Fast path: ` % ` (the // factorial / `i % 1000` loop shape). `frem double` lowers diff --git a/crates/perry-codegen/src/expr/i32_fast_path.rs b/crates/perry-codegen/src/expr/i32_fast_path.rs index 00de326124..41dbe58fdb 100644 --- a/crates/perry-codegen/src/expr/i32_fast_path.rs +++ b/crates/perry-codegen/src/expr/i32_fast_path.rs @@ -1,14 +1,20 @@ //! i32-native expression fast path + flat-const 2D-table lowering //! (extracted from `expr.rs`, issue #1098). Pure move — no logic changes. -use anyhow::Result; +use anyhow::{bail, Result}; use perry_hir::{BinaryOp, Expr}; -use super::{lower_expr, unbox_to_i64, FlatConstInfo, FnCtx}; +use super::{ + array_kind_fact, lower_expr, raw_f64_layout_fact, unbox_str_handle, unbox_to_i64, + FlatConstInfo, FnCtx, PackedNumericLoopKind, +}; use crate::native_value::{ - materialize_js_value_bits, ExpectedNativeRep, LoweredValue, MaterializationReason, NativeRep, + materialize_js_value_bits, BoundsState, BufferAccessMode, ExpectedNativeRep, LoweredValue, + MaterializationReason, NativeRep, +}; +use crate::type_analysis::{ + expr_may_return_boxed_value_from_raw_f64_fallback, is_definitely_string_expr, is_numeric_expr, }; -use crate::type_analysis::{expr_may_return_boxed_value_from_raw_f64_fallback, is_numeric_expr}; use crate::types::{DOUBLE, F32, I32, I64}; /// Returns true if `e` is guaranteed to produce a finite double value @@ -352,6 +358,104 @@ pub(crate) fn can_lower_expr_as_i32( } } +fn packed_i32_loop_index_get_fact(ctx: &FnCtx<'_>, e: &Expr) -> Option { + let Expr::IndexGet { object, index } = e else { + return None; + }; + let (Expr::LocalGet(arr_id), Expr::LocalGet(idx_id)) = (object.as_ref(), index.as_ref()) else { + return None; + }; + ctx.packed_f64_loop_facts + .iter() + .find(|fact| { + fact.array_local_id == *arr_id + && fact.index_local_id == *idx_id + && fact.array_kind == PackedNumericLoopKind::I32 + }) + .cloned() +} + +fn packed_u32_loop_index_get_fact(ctx: &FnCtx<'_>, e: &Expr) -> Option { + let Expr::IndexGet { object, index } = e else { + return None; + }; + let (Expr::LocalGet(arr_id), Expr::LocalGet(idx_id)) = (object.as_ref(), index.as_ref()) else { + return None; + }; + ctx.packed_f64_loop_facts + .iter() + .find(|fact| { + fact.array_local_id == *arr_id + && fact.index_local_id == *idx_id + && fact.array_kind == PackedNumericLoopKind::U32 + }) + .cloned() +} + +pub(crate) fn can_lower_expr_as_i32_in_current_region(ctx: &FnCtx<'_>, e: &Expr) -> bool { + if matches!(e, Expr::IterResultGetValue) { + return true; + } + if can_lower_expr_as_i32( + e, + &ctx.i32_counter_slots, + ctx.flat_const_arrays, + &ctx.array_row_aliases, + ctx.native_facts.integer_locals(), + ctx.clamp3_functions, + ctx.clamp_u8_functions, + ctx.integer_returning_functions, + ctx.i32_identity_functions, + ) { + return true; + } + if packed_i32_loop_index_get_fact(ctx, e).is_some() { + return true; + } + match e { + Expr::MathImul(left, right) => { + can_lower_expr_as_i32_in_current_region(ctx, left) + && can_lower_expr_as_i32_in_current_region(ctx, right) + } + Expr::Binary { + op: BinaryOp::BitOr, + left, + right, + } if matches!(right.as_ref(), Expr::Integer(0)) => { + can_lower_expr_as_i32_in_current_region(ctx, left) + } + Expr::Binary { op, left, right } + if matches!( + op, + BinaryOp::Add + | BinaryOp::Sub + | BinaryOp::Mul + | BinaryOp::BitAnd + | BinaryOp::BitOr + | BinaryOp::BitXor + | BinaryOp::Shl + | BinaryOp::Shr + | BinaryOp::UShr + ) => + { + can_lower_expr_as_i32_in_current_region(ctx, left) + && can_lower_expr_as_i32_in_current_region(ctx, right) + } + Expr::Call { callee, args, .. } => { + let Expr::FuncRef(fid) = callee.as_ref() else { + return false; + }; + ((ctx.clamp3_functions.contains(fid) && args.len() == 3) + || (ctx.clamp_u8_functions.contains(fid) && args.len() == 1) + || ctx.i32_identity_functions.contains(fid)) + && args + .iter() + .all(|arg| can_lower_expr_as_i32_in_current_region(ctx, arg)) + } + _ => false, + } +} + /// Typed native-expression lowering entry point. It deliberately returns a /// `LoweredValue` so callers keep the JS semantic meaning separate from the /// LLVM representation chosen for the hot path. @@ -370,6 +474,7 @@ pub(crate) fn lower_expr_native( ExpectedNativeRep::I1 => lower_expr_native_i1(ctx, e), ExpectedNativeRep::F64 => lower_expr_native_f64(ctx, e), ExpectedNativeRep::F32 => lower_expr_native_f32(ctx, e), + ExpectedNativeRep::StringRef => lower_expr_native_string_ref(ctx, e), ExpectedNativeRep::BufferLen => lower_expr_native_buffer_len(ctx, e), ExpectedNativeRep::HandleId => lower_expr_native_handle_id(ctx, e), ExpectedNativeRep::NativeHandle => lower_expr_native_handle(ctx, e), @@ -415,6 +520,10 @@ fn f32_lowered(value: String) -> LoweredValue { LoweredValue::f32(value) } +fn string_ref_lowered(value: String) -> LoweredValue { + LoweredValue::string_ref(value) +} + fn buffer_len_lowered(value: String) -> LoweredValue { LoweredValue::buffer_len(value) } @@ -440,10 +549,20 @@ fn native_expr_kind(e: &Expr) -> &'static str { Expr::Call { .. } => "Call", Expr::Uint8ArrayGet { .. } => "Uint8ArrayGet", Expr::BufferIndexGet { .. } => "BufferIndexGet", + Expr::IndexGet { .. } => "IndexGet", _ => "Expr", } } +fn lower_expr_native_string_ref(ctx: &mut FnCtx<'_>, e: &Expr) -> Result { + if !is_definitely_string_expr(ctx, e) { + bail!("cannot lower expression as native StringRef without a string proof"); + } + let boxed = lower_expr(ctx, e)?; + let raw = unbox_str_handle(ctx.block(), &boxed); + Ok(string_ref_lowered(raw)) +} + fn try_lower_expr_native_i32_structural(ctx: &mut FnCtx<'_>, e: &Expr) -> Result> { let value = match e { Expr::Integer(n) => Some((*n as i32).to_string()), @@ -547,7 +666,148 @@ fn try_lower_expr_native_i32_structural(ctx: &mut FnCtx<'_>, e: &Expr) -> Result Ok(value) } +fn lower_packed_i32_loop_index_get(ctx: &mut FnCtx<'_>, e: &Expr) -> Result> { + let Expr::IndexGet { object, index } = e else { + return Ok(None); + }; + let (Expr::LocalGet(arr_id), Expr::LocalGet(idx_id)) = (object.as_ref(), index.as_ref()) else { + return Ok(None); + }; + let Some(fact) = packed_i32_loop_index_get_fact(ctx, e) else { + return Ok(None); + }; + let Some(i32_slot) = ctx.i32_counter_slots.get(idx_id).cloned() else { + return Ok(None); + }; + + let arr_box = lower_expr(ctx, object)?; + let idx_i32 = ctx.block().load(I32, &i32_slot); + let raw_f64 = { + let blk = ctx.block(); + let arr_bits = blk.bitcast_double_to_i64(&arr_box); + let arr_handle = blk.and(I64, &arr_bits, crate::nanbox::POINTER_MASK_I64); + let idx_i64 = blk.zext(I32, &idx_i32, I64); + let byte_offset = blk.shl(I64, &idx_i64, "3"); + let with_header = blk.add(I64, &byte_offset, "8"); + let element_addr = blk.add(I64, &arr_handle, &with_header); + let element_ptr = blk.inttoptr(I64, &element_addr); + blk.load(DOUBLE, &element_ptr) + }; + let value = ctx.block().fptosi(DOUBLE, &raw_f64, I32); + let lowered = LoweredValue::i32(value); + let guard_id = fact.guard_id.clone(); + ctx.record_lowered_value_with_access_mode_and_facts( + "PackedI32LoopLoad", + Some(*arr_id), + "packed_i32_loop_load", + &lowered, + Some(BoundsState::Guarded { + guard_id: guard_id.clone(), + }), + None, + Some(BufferAccessMode::CheckedNative), + None, + None, + None, + vec![ + array_kind_fact(Some(*arr_id), "consumed", "packed_i32", None), + raw_f64_layout_fact(Some(*arr_id), "consumed", &guard_id, None), + ], + Vec::new(), + false, + false, + vec![ + "index_range=nonnegative_i32".to_string(), + "length_range=guarded_i32".to_string(), + "storage_layout=raw_f64_numeric_slots".to_string(), + "integer_materialization=fptosi_guarded_packed_i32".to_string(), + ], + ); + Ok(Some(lowered)) +} + +pub(crate) fn lower_packed_u32_loop_index_get( + ctx: &mut FnCtx<'_>, + e: &Expr, +) -> Result> { + let Expr::IndexGet { object, index } = e else { + return Ok(None); + }; + let (Expr::LocalGet(arr_id), Expr::LocalGet(idx_id)) = (object.as_ref(), index.as_ref()) else { + return Ok(None); + }; + let Some(fact) = packed_u32_loop_index_get_fact(ctx, e) else { + return Ok(None); + }; + let Some(i32_slot) = ctx.i32_counter_slots.get(idx_id).cloned() else { + return Ok(None); + }; + + let arr_box = lower_expr(ctx, object)?; + let idx_i32 = ctx.block().load(I32, &i32_slot); + let raw_f64 = { + let blk = ctx.block(); + let arr_bits = blk.bitcast_double_to_i64(&arr_box); + let arr_handle = blk.and(I64, &arr_bits, crate::nanbox::POINTER_MASK_I64); + let idx_i64 = blk.zext(I32, &idx_i32, I64); + let byte_offset = blk.shl(I64, &idx_i64, "3"); + let with_header = blk.add(I64, &byte_offset, "8"); + let element_addr = blk.add(I64, &arr_handle, &with_header); + let element_ptr = blk.inttoptr(I64, &element_addr); + blk.load(DOUBLE, &element_ptr) + }; + let value = ctx.block().fptoui(DOUBLE, &raw_f64, I32); + let lowered = LoweredValue::u32(value); + let guard_id = fact.guard_id.clone(); + ctx.record_lowered_value_with_access_mode_and_facts( + "PackedU32LoopLoad", + Some(*arr_id), + "packed_u32_loop_load", + &lowered, + Some(BoundsState::Guarded { + guard_id: guard_id.clone(), + }), + None, + Some(BufferAccessMode::CheckedNative), + None, + None, + None, + vec![ + array_kind_fact(Some(*arr_id), "consumed", "packed_u32", None), + raw_f64_layout_fact(Some(*arr_id), "consumed", &guard_id, None), + ], + Vec::new(), + false, + false, + vec![ + "index_range=nonnegative_i32".to_string(), + "length_range=guarded_i32".to_string(), + "storage_layout=raw_f64_numeric_slots".to_string(), + "integer_materialization=fptoui_guarded_packed_u32".to_string(), + ], + ); + Ok(Some(lowered)) +} + fn lower_expr_native_i1(ctx: &mut FnCtx<'_>, e: &Expr) -> Result { + if matches!(e, Expr::IterResultGetValue) { + let value_i32 = ctx.block().call(I32, "js_iter_result_get_value_i1", &[]); + let value = ctx.block().icmp_ne(I32, &value_i32, "0"); + let lowered = i1_lowered(value); + ctx.record_lowered_value( + native_expr_kind(e), + None, + "compiler_private_async_iter_result_get_i1", + &lowered, + None, + None, + None, + false, + false, + vec!["slot_kind=raw_i1_or_truthy_jsvalue".to_string()], + ); + return Ok(lowered); + } if let Some(lowered) = crate::expr::lower_expr_value(ctx, e)? { if matches!(lowered.rep, NativeRep::I1) { ctx.record_lowered_value( @@ -584,17 +844,27 @@ fn lower_expr_native_i1(ctx: &mut FnCtx<'_>, e: &Expr) -> Result { } fn lower_expr_native_i32(ctx: &mut FnCtx<'_>, e: &Expr) -> Result { - if can_lower_expr_as_i32( - e, - &ctx.i32_counter_slots, - ctx.flat_const_arrays, - &ctx.array_row_aliases, - ctx.native_facts.integer_locals(), - ctx.clamp3_functions, - ctx.clamp_u8_functions, - ctx.integer_returning_functions, - ctx.i32_identity_functions, - ) { + if matches!(e, Expr::IterResultGetValue) { + let value = ctx.block().call(I32, "js_iter_result_get_value_i32", &[]); + let lowered = i32_lowered(value); + ctx.record_lowered_value( + native_expr_kind(e), + None, + "compiler_private_async_iter_result_get_i32", + &lowered, + None, + None, + None, + false, + false, + vec!["slot_kind=raw_i32_or_toint32_jsvalue".to_string()], + ); + return Ok(lowered); + } + if let Some(lowered) = lower_packed_i32_loop_index_get(ctx, e)? { + return Ok(lowered); + } + if can_lower_expr_as_i32_in_current_region(ctx, e) { if let Some(value) = try_lower_expr_native_i32_structural(ctx, e)? { let lowered = i32_lowered(value); ctx.record_lowered_value( @@ -825,6 +1095,9 @@ fn lower_expr_native_js_value_bits(ctx: &mut FnCtx<'_>, e: &Expr) -> Result, e: &Expr) -> Result { + if let Some(lowered) = lower_packed_u32_loop_index_get(ctx, e)? { + return Ok(lowered); + } if let Some(lowered) = crate::expr::lower_expr_value(ctx, e)? { let value = match lowered.rep { NativeRep::I32 | NativeRep::U32 | NativeRep::BufferLen => Some(lowered.value), diff --git a/crates/perry-codegen/src/expr/index_get.rs b/crates/perry-codegen/src/expr/index_get.rs index 5210d9ba2e..35b89f1315 100644 --- a/crates/perry-codegen/src/expr/index_get.rs +++ b/crates/perry-codegen/src/expr/index_get.rs @@ -49,7 +49,7 @@ use super::{ raw_f64_layout_fact, try_flat_const_2d_int, try_lower_flat_const_index_get, try_match_channel_reduction, try_static_class_name, unbox_str_handle, unbox_to_i64, variant_name, BufferAccessSpec, ChannelReduction, FlatConstInfo, FnCtx, I18nLowerCtx, - PackedF64LoopFact, TypedFeedbackContract, TypedFeedbackKind, + PackedF64LoopFact, PackedNumericLoopKind, TypedFeedbackContract, TypedFeedbackKind, }; fn is_width_tracked_typed_array_receiver(ctx: &FnCtx<'_>, object: &Expr) -> bool { @@ -445,6 +445,7 @@ fn lower_packed_f64_loop_index_get( arr_box: &str, idx_i32: &str, guard_id: &str, + array_kind: PackedNumericLoopKind, ) -> String { let value = { let blk = ctx.block(); @@ -464,9 +465,9 @@ fn lower_packed_f64_loop_index_get( value: value.clone(), }; ctx.record_lowered_value_with_access_mode_and_facts( - "PackedF64LoopLoad", + array_kind.load_expr_kind(), Some(arr_id), - "packed_f64_loop_load", + array_kind.load_consumer_f64(), &lowered, Some(BoundsState::Guarded { guard_id: guard_id.to_string(), @@ -477,7 +478,12 @@ fn lower_packed_f64_loop_index_get( None, None, vec![ - array_kind_fact(Some(arr_id), "consumed", "packed_f64", None), + array_kind_fact( + Some(arr_id), + "consumed", + array_kind.array_kind_label(), + None, + ), raw_f64_layout_fact(Some(arr_id), "consumed", guard_id, None), ], Vec::new(), @@ -486,6 +492,7 @@ fn lower_packed_f64_loop_index_get( vec![ "index_range=nonnegative_i32".to_string(), "length_range=guarded_i32".to_string(), + "storage_layout=raw_f64_numeric_slots".to_string(), ], ); value @@ -513,6 +520,7 @@ pub(crate) fn lower_numeric_index_get_for_number_context( &arr_box, &idx_i32, &fact.guard_id, + fact.array_kind, ))); } } @@ -1057,6 +1065,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { &arr_box, &idx_i32, &fact.guard_id, + fact.array_kind, )); } } diff --git a/crates/perry-codegen/src/expr/index_set.rs b/crates/perry-codegen/src/expr/index_set.rs index 4a51db5e56..f194e731f0 100644 --- a/crates/perry-codegen/src/expr/index_set.rs +++ b/crates/perry-codegen/src/expr/index_set.rs @@ -52,8 +52,8 @@ use super::{ nanbox_pointer_inline_pub, nanbox_string_inline, proxy_build_args_array, raw_f64_layout_fact, try_flat_const_2d_int, try_lower_flat_const_index_get, try_match_channel_reduction, try_static_class_name, unbox_str_handle, unbox_to_i64, variant_name, BufferAccessSpec, - ChannelReduction, FlatConstInfo, FnCtx, I18nLowerCtx, PackedF64LoopFact, TypedFeedbackContract, - TypedFeedbackKind, + ChannelReduction, FlatConstInfo, FnCtx, I18nLowerCtx, PackedF64LoopFact, PackedNumericLoopKind, + TypedFeedbackContract, TypedFeedbackKind, }; fn canonicalize_raw_f64_numeric_store_value( @@ -269,26 +269,76 @@ fn lower_array_index_set_via_runtime_key( Ok(val_double) } -fn lower_packed_f64_loop_index_set( +fn lower_packed_f64_loop_store_value( + ctx: &mut FnCtx<'_>, + arr_id: u32, + value: &Expr, +) -> Result<(String, Vec)> { + if let Expr::MathAbs(operand) = value { + if matches!( + operand.as_ref(), + Expr::IndexGet { object, .. } + if matches!(object.as_ref(), Expr::LocalGet(id) if *id == arr_id) + ) { + let raw = lower_expr(ctx, operand)?; + let abs = ctx.block().call(DOUBLE, "llvm.fabs.f64", &[(DOUBLE, &raw)]); + return Ok((abs, vec!["rhs_unary_math=llvm.fabs.f64".to_string()])); + } + } + Ok((lower_expr(ctx, value)?, Vec::new())) +} + +fn lower_packed_numeric_loop_store_value( + ctx: &mut FnCtx<'_>, + arr_id: u32, + value: &Expr, + array_kind: PackedNumericLoopKind, +) -> Result<(String, String, Vec)> { + match array_kind { + PackedNumericLoopKind::F64 => { + let (value, notes) = lower_packed_f64_loop_store_value(ctx, arr_id, value)?; + Ok((value.clone(), value, notes)) + } + PackedNumericLoopKind::I32 => { + let value_i32 = lower_expr_as_i32(ctx, value)?; + let value_double = ctx.block().sitofp(I32, &value_i32, DOUBLE); + Ok(( + value_double, + value_i32, + vec!["rhs_i32_store=sitofp_i32_to_raw_f64_slot".to_string()], + )) + } + PackedNumericLoopKind::U32 => bail!("packed-u32 loop stores are not implemented"), + } +} + +fn lower_packed_numeric_loop_index_set( ctx: &mut FnCtx<'_>, arr_id: u32, idx_i32: &str, value: &Expr, guard_id: &str, side_exit_label: &str, + array_kind: PackedNumericLoopKind, ) -> Result { - let val_double = lower_expr(ctx, value)?; + let (val_double, native_value, rhs_notes) = + lower_packed_numeric_loop_store_value(ctx, arr_id, value, array_kind)?; let arr_expr = Expr::LocalGet(arr_id); let arr_box = lower_expr(ctx, &arr_expr)?; let feedback_site_id = emit_typed_feedback_register_site( ctx, TypedFeedbackKind::ArrayElement, - "array[packed_f64_loop]=", + match array_kind { + PackedNumericLoopKind::F64 => "array[packed_f64_loop]=", + PackedNumericLoopKind::I32 => "array[packed_i32_loop]=", + PackedNumericLoopKind::U32 => "array[packed_u32_loop]=", + }, TypedFeedbackContract::bounded_numeric_array_set_index(), ); - let fast_idx = ctx.new_block("packed_f64_loop_store.fast"); - let fallback_idx = ctx.new_block("packed_f64_loop_store.fallback"); - let merge_idx = ctx.new_block("packed_f64_loop_store.merge"); + let loop_label = array_kind.loop_label(); + let fast_idx = ctx.new_block(&format!("{loop_label}_loop_store.fast")); + let fallback_idx = ctx.new_block(&format!("{loop_label}_loop_store.fallback")); + let merge_idx = ctx.new_block(&format!("{loop_label}_loop_store.merge")); let fast_label = ctx.block_label(fast_idx); let fallback_label = ctx.block_label(fallback_idx); let merge_label = ctx.block_label(merge_idx); @@ -320,9 +370,9 @@ fn lower_packed_f64_loop_index_set( value: arr_box.clone(), }; ctx.record_lowered_value_with_access_mode_and_facts( - "PackedF64LoopStore", + array_kind.store_expr_kind(), Some(arr_id), - "packed_f64_loop_store_side_exit", + array_kind.store_side_exit_consumer(), &fallback, Some(BoundsState::Unknown), None, @@ -332,10 +382,16 @@ fn lower_packed_f64_loop_index_set( None, Vec::new(), vec![ + array_kind_fact( + Some(arr_id), + "rejected", + array_kind.array_kind_label(), + Some(MaterializationReason::RuntimeApi), + ), raw_f64_layout_fact( Some(arr_id), "rejected", - "packed_f64_loop_store_guard", + array_kind.store_guard_detail(), Some(MaterializationReason::RuntimeApi), ), raw_f64_layout_fact( @@ -355,10 +411,16 @@ fn lower_packed_f64_loop_index_set( } ctx.current_block = fast_idx; - let numeric_value = { - let numeric_value = { - let blk = ctx.block(); - canonicalize_raw_f64_numeric_store_value(blk, &val_double) + { + let slot_value = { + match array_kind { + PackedNumericLoopKind::F64 => { + let blk = ctx.block(); + canonicalize_raw_f64_numeric_store_value(blk, &val_double) + } + PackedNumericLoopKind::I32 => val_double.clone(), + PackedNumericLoopKind::U32 => val_double.clone(), + } }; let fast_arr_box = lower_expr(ctx, &arr_expr)?; let blk = ctx.block(); @@ -369,20 +431,27 @@ fn lower_packed_f64_loop_index_set( let with_header = blk.add(I64, &byte_offset, "8"); let element_addr = blk.add(I64, &arr_handle, &with_header); let element_ptr = blk.inttoptr(I64, &element_addr); - blk.store(DOUBLE, &numeric_value, &element_ptr); + blk.store(DOUBLE, &slot_value, &element_ptr); blk.br(&merge_label); - numeric_value - }; + } let stored = LoweredValue { semantic: SemanticKind::JsNumber, - rep: NativeRep::F64, - llvm_ty: DOUBLE, - value: numeric_value, + rep: match array_kind { + PackedNumericLoopKind::F64 => NativeRep::F64, + PackedNumericLoopKind::I32 => NativeRep::I32, + PackedNumericLoopKind::U32 => NativeRep::U32, + }, + llvm_ty: match array_kind { + PackedNumericLoopKind::F64 => DOUBLE, + PackedNumericLoopKind::I32 => I32, + PackedNumericLoopKind::U32 => I32, + }, + value: native_value, }; ctx.record_lowered_value_with_access_mode_and_facts( - "PackedF64LoopStore", + array_kind.store_expr_kind(), Some(arr_id), - "packed_f64_loop_store", + array_kind.store_consumer(), &stored, Some(BoundsState::Guarded { guard_id: guard_id.to_string(), @@ -393,22 +462,34 @@ fn lower_packed_f64_loop_index_set( None, None, vec![ - array_kind_fact(Some(arr_id), "consumed", "packed_f64", None), + array_kind_fact( + Some(arr_id), + "consumed", + array_kind.array_kind_label(), + None, + ), raw_f64_layout_fact(Some(arr_id), "consumed", guard_id, None), ], Vec::new(), false, false, - vec![ - "rhs_numeric_guard=js_typed_feedback_numeric_array_index_set_guard".to_string(), - "raw_f64_canonicalized=js_array_numeric_value_to_raw_f64".to_string(), - "array_reloaded_after_rhs=1".to_string(), - "array_reloaded_after_store_guard=1".to_string(), - "array_reloaded_after_canonicalization=1".to_string(), - "store_guard_failure=side_exit_slow_restart".to_string(), - "index_range=nonnegative_i32".to_string(), - "length_range=guarded_i32".to_string(), - ], + { + let mut notes = vec![ + "rhs_numeric_guard=js_typed_feedback_numeric_array_index_set_guard".to_string(), + "array_reloaded_after_rhs=1".to_string(), + "array_reloaded_after_store_guard=1".to_string(), + "store_guard_failure=side_exit_slow_restart".to_string(), + "index_range=nonnegative_i32".to_string(), + "length_range=guarded_i32".to_string(), + format!("storage_layout={}", array_kind.array_kind_label()), + ]; + if matches!(array_kind, PackedNumericLoopKind::F64) { + notes.push("raw_f64_canonicalized=js_array_numeric_value_to_raw_f64".to_string()); + notes.push("array_reloaded_after_canonicalization=1".to_string()); + } + notes.extend(rhs_notes); + notes + }, ); ctx.current_block = merge_idx; Ok(val_double) @@ -679,13 +760,14 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { if let Some(fact) = packed_f64_loop_fact(ctx, *arr_id, *idx_id) { if let Some(i32_slot) = ctx.i32_counter_slots.get(idx_id).cloned() { let idx_i32 = ctx.block().load(I32, &i32_slot); - return lower_packed_f64_loop_index_set( + return lower_packed_numeric_loop_index_set( ctx, *arr_id, &idx_i32, value.as_ref(), &fact.guard_id, &fact.store_side_exit_label, + fact.array_kind, ); } } diff --git a/crates/perry-codegen/src/expr/literals_vars.rs b/crates/perry-codegen/src/expr/literals_vars.rs index 5262ae8031..140ac9c8b3 100644 --- a/crates/perry-codegen/src/expr/literals_vars.rs +++ b/crates/perry-codegen/src/expr/literals_vars.rs @@ -32,13 +32,13 @@ use crate::types::{DOUBLE, I1, I32, I64, I8, PTR}; #[allow(unused_imports)] use super::{ - buffer_alias_metadata_suffix, can_lower_expr_as_i32, emit_layout_note_slot_on_block, - emit_root_nanbox_store_on_block, emit_shadow_slot_clear, emit_shadow_slot_update_for_expr, - emit_string_literal_global, emit_v8_export_call, emit_v8_member_method_call, - emit_write_barrier, emit_write_barrier_slot_on_block, expr_is_known_non_pointer_shadow_value, - extract_array_of_object_shape, i32_bool_to_nanbox, import_origin_suffix, - is_global_this_builtin_function_name, is_global_this_builtin_name, is_known_finite, - lower_array_literal, lower_channel_reduction, lower_expr, lower_expr_as_i32, + buffer_alias_metadata_suffix, can_lower_expr_as_i32, can_lower_expr_as_i32_in_current_region, + emit_layout_note_slot_on_block, emit_root_nanbox_store_on_block, emit_shadow_slot_clear, + emit_shadow_slot_update_for_expr, emit_string_literal_global, emit_v8_export_call, + emit_v8_member_method_call, emit_write_barrier, emit_write_barrier_slot_on_block, + expr_is_known_non_pointer_shadow_value, extract_array_of_object_shape, i32_bool_to_nanbox, + import_origin_suffix, is_global_this_builtin_function_name, is_global_this_builtin_name, + is_known_finite, lower_array_literal, lower_channel_reduction, lower_expr, lower_expr_as_i32, lower_index_set_fast, lower_js_args_array, lower_object_literal, lower_pod_local_reassignment, lower_stream_super_init, lower_url_string_getter, materialize_pod_local, nanbox_bigint_inline, nanbox_pointer_inline, nanbox_pointer_inline_pub, nanbox_string_inline, proxy_build_args_array, @@ -540,17 +540,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { if let Some(i32_slot) = ctx.i32_counter_slots.get(id).cloned() { if !ctx.closure_captures.contains_key(id) && !(ctx.boxed_vars.contains(id) && !ctx.module_globals.contains_key(id)) - && can_lower_expr_as_i32( - value, - &ctx.i32_counter_slots, - ctx.flat_const_arrays, - &ctx.array_row_aliases, - ctx.integer_locals, - ctx.clamp3_functions, - ctx.clamp_u8_functions, - ctx.integer_returning_functions, - ctx.i32_identity_functions, - ) + && can_lower_expr_as_i32_in_current_region(ctx, value) { let v_i32 = lower_expr_as_i32(ctx, value)?; let unsigned_i32 = ctx.unsigned_i32_locals.contains(id); diff --git a/crates/perry-codegen/src/expr/logical_collections.rs b/crates/perry-codegen/src/expr/logical_collections.rs index 006cf5d849..b7ce6cbc1b 100644 --- a/crates/perry-codegen/src/expr/logical_collections.rs +++ b/crates/perry-codegen/src/expr/logical_collections.rs @@ -23,8 +23,9 @@ use crate::lower_string_method::{ use crate::nanbox::{double_literal, POINTER_MASK_I64, TAG_UNDEFINED}; #[allow(unused_imports)] use crate::type_analysis::{ - compute_auto_captures, is_array_expr, is_bigint_expr, is_bool_expr, is_map_expr, - is_numeric_expr, is_set_expr, is_string_expr, is_url_search_params_expr, receiver_class_name, + compute_auto_captures, is_array_expr, is_bigint_expr, is_bool_expr, is_definitely_string_expr, + is_map_expr, is_numeric_expr, is_set_expr, is_string_expr, is_url_search_params_expr, + map_static_type_args, receiver_class_name, }; #[allow(unused_imports)] use crate::types::{DOUBLE, I1, I32, I64, I8, PTR}; @@ -40,12 +41,97 @@ use super::{ lower_array_literal, lower_channel_reduction, lower_expr, lower_expr_as_i32, lower_index_set_fast, lower_js_args_array, lower_object_literal, lower_stream_super_init, lower_url_string_getter, nanbox_bigint_inline, nanbox_pointer_inline, - nanbox_pointer_inline_pub, nanbox_string_inline, proxy_build_args_array, try_flat_const_2d_int, - try_lower_flat_const_index_get, try_match_channel_reduction, try_static_class_name, - unbox_str_handle, unbox_to_i64, variant_name, ChannelReduction, FlatConstInfo, FnCtx, - I18nLowerCtx, + nanbox_pointer_inline_pub, nanbox_string_inline, proxy_build_args_array, + record_collection_number_key_fallback, record_collection_number_key_selected, + record_collection_string_key_fallback, record_collection_string_key_selected, + try_flat_const_2d_int, try_lower_flat_const_index_get, try_match_channel_reduction, + try_static_class_name, unbox_str_handle, unbox_to_i64, variant_name, ChannelReduction, + FlatConstInfo, FnCtx, I18nLowerCtx, }; +fn is_static_string_key_map(ctx: &FnCtx<'_>, map: &Expr) -> bool { + matches!( + map_static_type_args(ctx, map), + Some([HirType::String | HirType::StringLiteral(_), _]) + ) +} + +fn is_static_number_key_map(ctx: &FnCtx<'_>, map: &Expr) -> bool { + matches!( + map_static_type_args(ctx, map), + Some([HirType::Number | HirType::Int32, _]) + ) +} + +fn guarded_map_number_key_delete(ctx: &mut FnCtx<'_>, map_handle: &str, key_box: &str) -> String { + let guard_raw = ctx + .block() + .call(I32, "js_typed_f64_arg_guard", &[(DOUBLE, key_box)]); + let guard = ctx.block().icmp_ne(I32, &guard_raw, "0"); + let fast_idx = ctx.new_block("map_number_key.delete.fast"); + let fallback_idx = ctx.new_block("map_number_key.delete.fallback"); + let merge_idx = ctx.new_block("map_number_key.delete.merge"); + let fast_label = ctx.block_label(fast_idx); + let fallback_label = ctx.block_label(fallback_idx); + let merge_label = ctx.block_label(merge_idx); + ctx.block().cond_br(&guard, &fast_label, &fallback_label); + + ctx.current_block = fast_idx; + let key_raw = ctx + .block() + .call(DOUBLE, "js_typed_f64_arg_to_raw", &[(DOUBLE, key_box)]); + let fast_value = ctx.block().call( + I32, + "js_map_delete_number_key", + &[(I64, map_handle), (DOUBLE, &key_raw)], + ); + record_collection_number_key_selected( + ctx, + "MapDelete", + "collection_number_key.map_delete", + &key_raw, + "map", + "number_key_helper", + "js_map_delete_number_key", + "key", + ); + let after_fast = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&merge_label); + } + + ctx.current_block = fallback_idx; + let fallback_value = ctx.block().call( + I32, + "js_map_delete", + &[(I64, map_handle), (DOUBLE, key_box)], + ); + record_collection_number_key_fallback( + ctx, + "MapDelete", + "collection_number_key.map_delete_generic", + key_box, + "map", + "number_key_helper", + "js_map_delete", + "runtime_key_guard_failed", + "key", + ); + let after_fallback = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&merge_label); + } + + ctx.current_block = merge_idx; + ctx.block().phi( + I32, + &[ + (fast_value.as_str(), after_fast.as_str()), + (fallback_value.as_str(), after_fallback.as_str()), + ], + ) +} + pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { match expr { Expr::Logical { op, left, right } => lower_logical(ctx, *op, left, right), @@ -373,11 +459,56 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // -------- map.delete(key) -> boolean -------- Expr::MapDelete { map, key } => { + let use_string_key_map = + is_static_string_key_map(ctx, map) && is_definitely_string_expr(ctx, key); + let use_number_key_map = !use_string_key_map + && is_static_number_key_map(ctx, map) + && is_numeric_expr(ctx, key); let m_box = lower_expr(ctx, map)?; let k_box = lower_expr(ctx, key)?; + let m_handle = { + let blk = ctx.block(); + unbox_to_i64(blk, &m_box) + }; + let i32_v = if use_string_key_map { + let (k_handle, i32_v) = { + let blk = ctx.block(); + let k_handle = unbox_str_handle(blk, &k_box); + let i32_v = blk.call( + I32, + "js_map_delete_string_key", + &[(I64, &m_handle), (I64, &k_handle)], + ); + (k_handle, i32_v) + }; + record_collection_string_key_selected( + ctx, + "MapDelete", + "collection_string_key.map_delete", + &k_handle, + "map", + "js_map_delete_string_key", + ); + i32_v + } else if use_number_key_map { + guarded_map_number_key_delete(ctx, &m_handle, &k_box) + } else { + let i32_v = { + let blk = ctx.block(); + blk.call(I32, "js_map_delete", &[(I64, &m_handle), (DOUBLE, &k_box)]) + }; + record_collection_string_key_fallback( + ctx, + "MapDelete", + "collection_string_key.map_delete_generic", + &k_box, + "map", + "js_map_delete", + "receiver_or_key_not_static_string", + ); + i32_v + }; let blk = ctx.block(); - let m_handle = unbox_to_i64(blk, &m_box); - let i32_v = blk.call(I32, "js_map_delete", &[(I64, &m_handle), (DOUBLE, &k_box)]); let bit = blk.icmp_ne(I32, &i32_v, "0"); let tagged = blk.select( crate::types::I1, diff --git a/crates/perry-codegen/src/expr/math_simple.rs b/crates/perry-codegen/src/expr/math_simple.rs index 2c40832ad8..da13c525b1 100644 --- a/crates/perry-codegen/src/expr/math_simple.rs +++ b/crates/perry-codegen/src/expr/math_simple.rs @@ -28,7 +28,7 @@ use crate::type_analysis::{ map_static_type_args, receiver_class_name, }; #[allow(unused_imports)] -use crate::types::{DOUBLE, I1, I32, I64, I8, PTR}; +use crate::types::{DOUBLE, F32, I1, I32, I64, I8, PTR}; #[allow(unused_imports)] use super::{ @@ -38,13 +38,16 @@ use super::{ emit_write_barrier_slot_on_block, expr_is_known_non_pointer_shadow_value, extract_array_of_object_shape, i32_bool_to_nanbox, import_origin_suffix, is_global_this_builtin_function_name, is_global_this_builtin_name, is_known_finite, - lower_array_literal, lower_channel_reduction, lower_expr, lower_expr_as_i32, + lower_array_literal, lower_channel_reduction, lower_expr, lower_expr_as_i32, lower_expr_native, lower_index_set_fast, lower_js_args_array, lower_math_operand, lower_object_literal, lower_stream_super_init, lower_url_string_getter, nanbox_bigint_inline, nanbox_pointer_inline, - nanbox_pointer_inline_pub, nanbox_string_inline, proxy_build_args_array, try_flat_const_2d_int, - try_lower_flat_const_index_get, try_match_channel_reduction, try_static_class_name, - unbox_str_handle, unbox_to_i64, variant_name, ChannelReduction, FlatConstInfo, FnCtx, - I18nLowerCtx, + nanbox_pointer_inline_pub, nanbox_string_inline, proxy_build_args_array, + record_collection_number_key_fallback, record_collection_number_key_selected, + record_collection_string_key_fallback, record_collection_string_key_selected, + record_collection_string_key_value_selected, record_collection_typed_value_fallback, + record_collection_typed_value_selected, try_flat_const_2d_int, try_lower_flat_const_index_get, + try_match_channel_reduction, try_static_class_name, unbox_str_handle, unbox_to_i64, + variant_name, ChannelReduction, FlatConstInfo, FnCtx, I18nLowerCtx, }; fn is_static_string_number_map(ctx: &FnCtx<'_>, map: &Expr) -> bool { @@ -57,6 +60,141 @@ fn is_static_string_number_map(ctx: &FnCtx<'_>, map: &Expr) -> bool { ) } +fn is_static_string_i32_map(ctx: &FnCtx<'_>, map: &Expr) -> bool { + matches!( + map_static_type_args(ctx, map), + Some([HirType::String | HirType::StringLiteral(_), HirType::Int32]) + ) +} + +fn is_perry_u32_type(ctx: &FnCtx<'_>, ty: &HirType) -> bool { + match ty { + HirType::Named(name) if name == "PerryU32" => true, + HirType::Named(name) => ctx + .type_aliases + .get(name) + .is_some_and(|alias| is_perry_u32_type(ctx, alias)), + _ => false, + } +} + +fn is_static_string_u32_map(ctx: &FnCtx<'_>, map: &Expr) -> bool { + match map_static_type_args(ctx, map) { + Some([HirType::String | HirType::StringLiteral(_), value_ty]) => { + is_perry_u32_type(ctx, value_ty) + } + _ => false, + } +} + +fn is_perry_f32_type(ctx: &FnCtx<'_>, ty: &HirType) -> bool { + match ty { + HirType::Named(name) if name == "PerryF32" => true, + HirType::Named(name) => ctx + .type_aliases + .get(name) + .is_some_and(|alias| is_perry_f32_type(ctx, alias)), + _ => false, + } +} + +fn is_static_string_f32_map(ctx: &FnCtx<'_>, map: &Expr) -> bool { + match map_static_type_args(ctx, map) { + Some([HirType::String | HirType::StringLiteral(_), value_ty]) => { + is_perry_f32_type(ctx, value_ty) + } + _ => false, + } +} + +fn is_static_string_boolean_map(ctx: &FnCtx<'_>, map: &Expr) -> bool { + matches!( + map_static_type_args(ctx, map), + Some([ + HirType::String | HirType::StringLiteral(_), + HirType::Boolean + ]) + ) +} + +fn is_static_string_string_map(ctx: &FnCtx<'_>, map: &Expr) -> bool { + matches!( + map_static_type_args(ctx, map), + Some([ + HirType::String | HirType::StringLiteral(_), + HirType::String | HirType::StringLiteral(_) + ]) + ) +} + +fn can_lower_i32_for_collection_value(ctx: &FnCtx<'_>, value: &Expr) -> bool { + can_lower_expr_as_i32( + value, + &ctx.i32_counter_slots, + ctx.flat_const_arrays, + &ctx.array_row_aliases, + ctx.integer_locals, + ctx.clamp3_functions, + ctx.clamp_u8_functions, + ctx.integer_returning_functions, + ctx.i32_identity_functions, + ) +} + +fn can_use_string_i32_map_value(ctx: &FnCtx<'_>, value: &Expr) -> bool { + can_lower_i32_for_collection_value(ctx, value) +} + +fn can_use_string_u32_map_value(ctx: &FnCtx<'_>, value: &Expr) -> bool { + match value { + Expr::Integer(n) => *n >= 0 && u32::try_from(*n).is_ok(), + Expr::Binary { + op: BinaryOp::UShr, + left, + right, + } => { + can_lower_i32_for_collection_value(ctx, left) + && can_lower_i32_for_collection_value(ctx, right) + } + Expr::Uint8ArrayGet { .. } + | Expr::BufferIndexGet { .. } + | Expr::Uint8ArrayLength(_) + | Expr::BufferLength(_) => true, + Expr::LocalGet(id) => ctx.unsigned_i32_locals.contains(id), + _ => false, + } +} + +fn literal_f64(expr: &Expr) -> Option { + match expr { + Expr::Integer(n) => Some(*n as f64), + Expr::Number(n) => Some(*n), + _ => None, + } +} + +fn f32_roundtrips_exact(value: f64) -> bool { + let narrowed = value as f32; + (narrowed as f64).to_bits() == value.to_bits() +} + +fn can_use_string_f32_map_value(value: &Expr) -> bool { + literal_f64(value).is_some_and(f32_roundtrips_exact) +} + +fn can_use_string_boolean_map_value(ctx: &FnCtx<'_>, value: &Expr) -> bool { + match value { + Expr::Bool(_) => true, + Expr::LocalGet(id) => { + ctx.i1_local_slots.contains_key(id) + && !ctx.closure_captures.contains_key(id) + && !ctx.boxed_vars.contains(id) + && !ctx.module_globals.contains_key(id) + } + _ => false, + } +} + fn is_static_string_key_map(ctx: &FnCtx<'_>, map: &Expr) -> bool { matches!( map_static_type_args(ctx, map), @@ -64,6 +202,233 @@ fn is_static_string_key_map(ctx: &FnCtx<'_>, map: &Expr) -> bool { ) } +fn is_static_number_key_map(ctx: &FnCtx<'_>, map: &Expr) -> bool { + matches!( + map_static_type_args(ctx, map), + Some([HirType::Number | HirType::Int32, _]) + ) +} + +fn is_static_number_string_map(ctx: &FnCtx<'_>, map: &Expr) -> bool { + matches!( + map_static_type_args(ctx, map), + Some([ + HirType::Number | HirType::Int32, + HirType::String | HirType::StringLiteral(_) + ]) + ) +} + +fn guarded_map_number_key_set( + ctx: &mut FnCtx<'_>, + map_handle: &str, + key_box: &str, + value_box: &str, +) -> String { + let guard_raw = ctx + .block() + .call(I32, "js_typed_f64_arg_guard", &[(DOUBLE, key_box)]); + let guard = ctx.block().icmp_ne(I32, &guard_raw, "0"); + let fast_idx = ctx.new_block("map_number_key.set.fast"); + let fallback_idx = ctx.new_block("map_number_key.set.fallback"); + let merge_idx = ctx.new_block("map_number_key.set.merge"); + let fast_label = ctx.block_label(fast_idx); + let fallback_label = ctx.block_label(fallback_idx); + let merge_label = ctx.block_label(merge_idx); + ctx.block().cond_br(&guard, &fast_label, &fallback_label); + + ctx.current_block = fast_idx; + let key_raw = ctx + .block() + .call(DOUBLE, "js_typed_f64_arg_to_raw", &[(DOUBLE, key_box)]); + let fast_value = ctx.block().call( + I64, + "js_map_set_number_key", + &[(I64, map_handle), (DOUBLE, &key_raw), (DOUBLE, value_box)], + ); + record_collection_number_key_selected( + ctx, + "MapSet", + "collection_number_key.map_set", + &key_raw, + "map", + "number_key_helper", + "js_map_set_number_key", + "key", + ); + let after_fast = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&merge_label); + } + + ctx.current_block = fallback_idx; + let fallback_value = ctx.block().call( + I64, + "js_map_set", + &[(I64, map_handle), (DOUBLE, key_box), (DOUBLE, value_box)], + ); + record_collection_number_key_fallback( + ctx, + "MapSet", + "collection_number_key.map_set_generic", + key_box, + "map", + "number_key_helper", + "js_map_set", + "runtime_key_guard_failed", + "key", + ); + let after_fallback = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&merge_label); + } + + ctx.current_block = merge_idx; + ctx.block().phi( + I64, + &[ + (fast_value.as_str(), after_fast.as_str()), + (fallback_value.as_str(), after_fallback.as_str()), + ], + ) +} + +fn guarded_map_number_key_get(ctx: &mut FnCtx<'_>, map_handle: &str, key_box: &str) -> String { + let guard_raw = ctx + .block() + .call(I32, "js_typed_f64_arg_guard", &[(DOUBLE, key_box)]); + let guard = ctx.block().icmp_ne(I32, &guard_raw, "0"); + let fast_idx = ctx.new_block("map_number_key.get.fast"); + let fallback_idx = ctx.new_block("map_number_key.get.fallback"); + let merge_idx = ctx.new_block("map_number_key.get.merge"); + let fast_label = ctx.block_label(fast_idx); + let fallback_label = ctx.block_label(fallback_idx); + let merge_label = ctx.block_label(merge_idx); + ctx.block().cond_br(&guard, &fast_label, &fallback_label); + + ctx.current_block = fast_idx; + let key_raw = ctx + .block() + .call(DOUBLE, "js_typed_f64_arg_to_raw", &[(DOUBLE, key_box)]); + let fast_value = ctx.block().call( + DOUBLE, + "js_map_get_number_key", + &[(I64, map_handle), (DOUBLE, &key_raw)], + ); + record_collection_number_key_selected( + ctx, + "MapGet", + "collection_number_key.map_get", + &key_raw, + "map", + "number_key_helper", + "js_map_get_number_key", + "key", + ); + let after_fast = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&merge_label); + } + + ctx.current_block = fallback_idx; + let fallback_value = ctx.block().call( + DOUBLE, + "js_map_get", + &[(I64, map_handle), (DOUBLE, key_box)], + ); + record_collection_number_key_fallback( + ctx, + "MapGet", + "collection_number_key.map_get_generic", + key_box, + "map", + "number_key_helper", + "js_map_get", + "runtime_key_guard_failed", + "key", + ); + let after_fallback = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&merge_label); + } + + ctx.current_block = merge_idx; + ctx.block().phi( + DOUBLE, + &[ + (fast_value.as_str(), after_fast.as_str()), + (fallback_value.as_str(), after_fallback.as_str()), + ], + ) +} + +fn guarded_map_number_key_has(ctx: &mut FnCtx<'_>, map_handle: &str, key_box: &str) -> String { + let guard_raw = ctx + .block() + .call(I32, "js_typed_f64_arg_guard", &[(DOUBLE, key_box)]); + let guard = ctx.block().icmp_ne(I32, &guard_raw, "0"); + let fast_idx = ctx.new_block("map_number_key.has.fast"); + let fallback_idx = ctx.new_block("map_number_key.has.fallback"); + let merge_idx = ctx.new_block("map_number_key.has.merge"); + let fast_label = ctx.block_label(fast_idx); + let fallback_label = ctx.block_label(fallback_idx); + let merge_label = ctx.block_label(merge_idx); + ctx.block().cond_br(&guard, &fast_label, &fallback_label); + + ctx.current_block = fast_idx; + let key_raw = ctx + .block() + .call(DOUBLE, "js_typed_f64_arg_to_raw", &[(DOUBLE, key_box)]); + let fast_value = ctx.block().call( + I32, + "js_map_has_number_key", + &[(I64, map_handle), (DOUBLE, &key_raw)], + ); + record_collection_number_key_selected( + ctx, + "MapHas", + "collection_number_key.map_has", + &key_raw, + "map", + "number_key_helper", + "js_map_has_number_key", + "key", + ); + let after_fast = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&merge_label); + } + + ctx.current_block = fallback_idx; + let fallback_value = + ctx.block() + .call(I32, "js_map_has", &[(I64, map_handle), (DOUBLE, key_box)]); + record_collection_number_key_fallback( + ctx, + "MapHas", + "collection_number_key.map_has_generic", + key_box, + "map", + "number_key_helper", + "js_map_has", + "runtime_key_guard_failed", + "key", + ); + let after_fallback = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&merge_label); + } + + ctx.current_block = merge_idx; + ctx.block().phi( + I32, + &[ + (fast_value.as_str(), after_fast.as_str()), + (fallback_value.as_str(), after_fallback.as_str()), + ], + ) +} + pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { match expr { Expr::IsNaN(operand) => { @@ -146,69 +511,416 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // -------- map.set(key, value) / .get / .has -------- Expr::MapSet { map, key, value } => { + let has_string_key_map = + is_static_string_key_map(ctx, map) && is_definitely_string_expr(ctx, key); + let use_number_key_map = !has_string_key_map + && is_static_number_key_map(ctx, map) + && is_numeric_expr(ctx, key); + let static_number_string_map = + use_number_key_map && is_static_number_string_map(ctx, map); + let use_number_string_map = + static_number_string_map && is_definitely_string_expr(ctx, value); + let use_string_i32_map = is_static_string_i32_map(ctx, map) + && is_definitely_string_expr(ctx, key) + && can_use_string_i32_map_value(ctx, value); + let use_string_u32_map = is_static_string_u32_map(ctx, map) + && is_definitely_string_expr(ctx, key) + && can_use_string_u32_map_value(ctx, value); + let use_string_f32_map = is_static_string_f32_map(ctx, map) + && is_definitely_string_expr(ctx, key) + && can_use_string_f32_map_value(value); let use_string_number_map = is_static_string_number_map(ctx, map) && is_definitely_string_expr(ctx, key); + let static_string_boolean_map = + is_static_string_boolean_map(ctx, map) && is_definitely_string_expr(ctx, key); + let use_string_boolean_map = + static_string_boolean_map && can_use_string_boolean_map_value(ctx, value); + let use_string_string_map = is_static_string_string_map(ctx, map) + && is_definitely_string_expr(ctx, key) + && is_definitely_string_expr(ctx, value); let m_box = lower_expr(ctx, map)?; let k_box = lower_expr(ctx, key)?; - let v_box = lower_expr(ctx, value)?; - let blk = ctx.block(); - let m_handle = unbox_to_i64(blk, &m_box); - let new_handle = if use_string_number_map { - let k_handle = unbox_str_handle(blk, &k_box); - blk.call( - I64, + let m_handle = { + let blk = ctx.block(); + unbox_to_i64(blk, &m_box) + }; + let new_handle = if use_string_i32_map { + let value_i32 = + lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::I32)?; + let (k_handle, new_handle) = { + let blk = ctx.block(); + let k_handle = unbox_str_handle(blk, &k_box); + let new_handle = blk.call( + I64, + "js_map_set_string_i32", + &[(I64, &m_handle), (I64, &k_handle), (I32, &value_i32.value)], + ); + (k_handle, new_handle) + }; + record_collection_string_key_value_selected( + ctx, + "MapSet", + "collection_string_key.map_set_string_i32", + &value_i32, + "map", + "int32_value_helper", + "js_map_set_string_i32", + ); + record_collection_string_key_selected( + ctx, + "MapSet", + "collection_string_key.map_set_string_i32_key", + &k_handle, + "map", + "js_map_set_string_i32", + ); + new_handle + } else if use_string_u32_map { + let value_u32 = + lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::U32)?; + let (k_handle, new_handle) = { + let blk = ctx.block(); + let k_handle = unbox_str_handle(blk, &k_box); + let new_handle = blk.call( + I64, + "js_map_set_string_u32", + &[(I64, &m_handle), (I64, &k_handle), (I32, &value_u32.value)], + ); + (k_handle, new_handle) + }; + record_collection_string_key_value_selected( + ctx, + "MapSet", + "collection_string_key.map_set_string_u32", + &value_u32, + "map", + "uint32_value_helper", + "js_map_set_string_u32", + ); + record_collection_string_key_selected( + ctx, + "MapSet", + "collection_string_key.map_set_string_u32_key", + &k_handle, + "map", + "js_map_set_string_u32", + ); + new_handle + } else if use_string_f32_map { + let value_f32 = + lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::F32)?; + let (k_handle, new_handle) = { + let blk = ctx.block(); + let k_handle = unbox_str_handle(blk, &k_box); + let new_handle = blk.call( + I64, + "js_map_set_string_f32", + &[(I64, &m_handle), (I64, &k_handle), (F32, &value_f32.value)], + ); + (k_handle, new_handle) + }; + record_collection_string_key_value_selected( + ctx, + "MapSet", + "collection_string_key.map_set_string_f32", + &value_f32, + "map", + "float32_value_helper", + "js_map_set_string_f32", + ); + record_collection_string_key_selected( + ctx, + "MapSet", + "collection_string_key.map_set_string_f32_key", + &k_handle, + "map", + "js_map_set_string_f32", + ); + new_handle + } else if use_string_number_map { + let v_box = lower_expr(ctx, value)?; + let (k_handle, new_handle) = { + let blk = ctx.block(); + let k_handle = unbox_str_handle(blk, &k_box); + let new_handle = blk.call( + I64, + "js_map_set_string_number", + &[(I64, &m_handle), (I64, &k_handle), (DOUBLE, &v_box)], + ); + (k_handle, new_handle) + }; + record_collection_string_key_selected( + ctx, + "MapSet", + "collection_string_key.map_set_string_number", + &k_handle, + "map", "js_map_set_string_number", - &[(I64, &m_handle), (I64, &k_handle), (DOUBLE, &v_box)], - ) + ); + new_handle + } else if use_string_boolean_map { + let value_i1 = + lower_expr_native(ctx, value, crate::native_value::ExpectedNativeRep::I1)?; + let (k_handle, new_handle) = { + let blk = ctx.block(); + let k_handle = unbox_str_handle(blk, &k_box); + let value_i32 = blk.zext(I1, &value_i1.value, I32); + let new_handle = blk.call( + I64, + "js_map_set_string_bool", + &[(I64, &m_handle), (I64, &k_handle), (I32, &value_i32)], + ); + (k_handle, new_handle) + }; + record_collection_string_key_value_selected( + ctx, + "MapSet", + "collection_string_key.map_set_string_bool", + &value_i1, + "map", + "boolean_value_helper", + "js_map_set_string_bool", + ); + record_collection_string_key_selected( + ctx, + "MapSet", + "collection_string_key.map_set_string_bool_key", + &k_handle, + "map", + "js_map_set_string_bool", + ); + new_handle + } else if use_string_string_map { + let v_box = lower_expr(ctx, value)?; + let (k_handle, v_handle, new_handle) = { + let blk = ctx.block(); + let k_handle = unbox_str_handle(blk, &k_box); + let v_handle = unbox_str_handle(blk, &v_box); + let new_handle = blk.call( + I64, + "js_map_set_string_string", + &[(I64, &m_handle), (I64, &k_handle), (I64, &v_handle)], + ); + (k_handle, v_handle, new_handle) + }; + let lowered_value = crate::native_value::LoweredValue::string_ref(&v_handle); + record_collection_string_key_value_selected( + ctx, + "MapSet", + "collection_string_key.map_set_string_string", + &lowered_value, + "map", + "string_value_helper", + "js_map_set_string_string", + ); + record_collection_string_key_selected( + ctx, + "MapSet", + "collection_string_key.map_set_string_string_key", + &k_handle, + "map", + "js_map_set_string_string", + ); + new_handle + } else if has_string_key_map { + let v_box = lower_expr(ctx, value)?; + let (k_handle, new_handle) = { + let blk = ctx.block(); + let k_handle = unbox_str_handle(blk, &k_box); + let new_handle = blk.call( + I64, + "js_map_set_string_key", + &[(I64, &m_handle), (I64, &k_handle), (DOUBLE, &v_box)], + ); + (k_handle, new_handle) + }; + record_collection_string_key_selected( + ctx, + "MapSet", + "collection_string_key.map_set_string_key", + &k_handle, + "map", + "js_map_set_string_key", + ); + if static_string_boolean_map { + record_collection_typed_value_fallback( + ctx, + "MapSet", + "collection_typed_value.map_set_string_bool_generic", + &v_box, + "map", + "boolean_value_helper", + "js_map_set_string_key", + "value_expr_not_native_i1", + ); + } + new_handle + } else if use_number_string_map { + let v_box = lower_expr(ctx, value)?; + let (v_handle, v_slot_box) = { + let blk = ctx.block(); + let v_handle = unbox_str_handle(blk, &v_box); + let v_slot_box = nanbox_string_inline(blk, &v_handle); + (v_handle, v_slot_box) + }; + let lowered_value = crate::native_value::LoweredValue::string_ref(&v_handle); + record_collection_typed_value_selected( + ctx, + "MapSet", + "collection_typed_value.map_set_number_string", + &lowered_value, + "map", + "string_value_helper", + "js_map_set_number_key", + "map_slot", + ); + guarded_map_number_key_set(ctx, &m_handle, &k_box, &v_slot_box) + } else if use_number_key_map { + let v_box = lower_expr(ctx, value)?; + if static_number_string_map { + record_collection_typed_value_fallback( + ctx, + "MapSet", + "collection_typed_value.map_set_number_string_generic", + &v_box, + "map", + "string_value_helper", + "js_map_set_number_key", + "value_expr_not_definitely_string", + ); + } + guarded_map_number_key_set(ctx, &m_handle, &k_box, &v_box) } else { - blk.call( - I64, + let v_box = lower_expr(ctx, value)?; + let new_handle = { + let blk = ctx.block(); + blk.call( + I64, + "js_map_set", + &[(I64, &m_handle), (DOUBLE, &k_box), (DOUBLE, &v_box)], + ) + }; + record_collection_string_key_fallback( + ctx, + "MapSet", + "collection_string_key.map_set_generic", + &k_box, + "map", "js_map_set", - &[(I64, &m_handle), (DOUBLE, &k_box), (DOUBLE, &v_box)], - ) + "receiver_or_key_not_static_string", + ); + new_handle }; // map.set returns the (possibly-realloc'd) map. Re-NaN-box // and return. The caller may need to write this back to a // local; that's the caller's problem if Map is held in a // mutable variable that grows. + let blk = ctx.block(); Ok(nanbox_pointer_inline(blk, &new_handle)) } Expr::MapGet { map, key } => { let use_string_key_map = is_static_string_key_map(ctx, map) && is_definitely_string_expr(ctx, key); + let use_number_key_map = !use_string_key_map + && is_static_number_key_map(ctx, map) + && is_numeric_expr(ctx, key); let m_box = lower_expr(ctx, map)?; let k_box = lower_expr(ctx, key)?; - let blk = ctx.block(); - let m_handle = unbox_to_i64(blk, &m_box); + let m_handle = { + let blk = ctx.block(); + unbox_to_i64(blk, &m_box) + }; if use_string_key_map { - let k_handle = unbox_str_handle(blk, &k_box); - Ok(blk.call( - DOUBLE, + let (k_handle, value) = { + let blk = ctx.block(); + let k_handle = unbox_str_handle(blk, &k_box); + let value = blk.call( + DOUBLE, + "js_map_get_string_key", + &[(I64, &m_handle), (I64, &k_handle)], + ); + (k_handle, value) + }; + record_collection_string_key_selected( + ctx, + "MapGet", + "collection_string_key.map_get", + &k_handle, + "map", "js_map_get_string_key", - &[(I64, &m_handle), (I64, &k_handle)], - )) + ); + Ok(value) + } else if use_number_key_map { + Ok(guarded_map_number_key_get(ctx, &m_handle, &k_box)) } else { - Ok(blk.call(DOUBLE, "js_map_get", &[(I64, &m_handle), (DOUBLE, &k_box)])) + let value = { + let blk = ctx.block(); + blk.call(DOUBLE, "js_map_get", &[(I64, &m_handle), (DOUBLE, &k_box)]) + }; + record_collection_string_key_fallback( + ctx, + "MapGet", + "collection_string_key.map_get_generic", + &k_box, + "map", + "js_map_get", + "receiver_or_key_not_static_string", + ); + Ok(value) } } Expr::MapHas { map, key } => { - let use_string_number_map = - is_static_string_number_map(ctx, map) && is_definitely_string_expr(ctx, key); + let use_string_key_map = + is_static_string_key_map(ctx, map) && is_definitely_string_expr(ctx, key); + let use_number_key_map = !use_string_key_map + && is_static_number_key_map(ctx, map) + && is_numeric_expr(ctx, key); let m_box = lower_expr(ctx, map)?; let k_box = lower_expr(ctx, key)?; - let blk = ctx.block(); - let m_handle = unbox_to_i64(blk, &m_box); - let i32_v = if use_string_number_map { - let k_handle = unbox_str_handle(blk, &k_box); - blk.call( - I32, + let m_handle = { + let blk = ctx.block(); + unbox_to_i64(blk, &m_box) + }; + let i32_v = if use_string_key_map { + let (k_handle, i32_v) = { + let blk = ctx.block(); + let k_handle = unbox_str_handle(blk, &k_box); + let i32_v = blk.call( + I32, + "js_map_has_string_key", + &[(I64, &m_handle), (I64, &k_handle)], + ); + (k_handle, i32_v) + }; + record_collection_string_key_selected( + ctx, + "MapHas", + "collection_string_key.map_has", + &k_handle, + "map", "js_map_has_string_key", - &[(I64, &m_handle), (I64, &k_handle)], - ) + ); + i32_v + } else if use_number_key_map { + guarded_map_number_key_has(ctx, &m_handle, &k_box) } else { - blk.call(I32, "js_map_has", &[(I64, &m_handle), (DOUBLE, &k_box)]) + let i32_v = { + let blk = ctx.block(); + blk.call(I32, "js_map_has", &[(I64, &m_handle), (DOUBLE, &k_box)]) + }; + record_collection_string_key_fallback( + ctx, + "MapHas", + "collection_string_key.map_has_generic", + &k_box, + "map", + "js_map_has", + "receiver_or_key_not_static_string", + ); + i32_v }; // NaN-tagged boolean for "true"/"false" printing. + let blk = ctx.block(); let bit = blk.icmp_ne(I32, &i32_v, "0"); let tagged = blk.select( crate::types::I1, diff --git a/crates/perry-codegen/src/expr/misc_methods.rs b/crates/perry-codegen/src/expr/misc_methods.rs index 6f389c1aa0..247c164a8c 100644 --- a/crates/perry-codegen/src/expr/misc_methods.rs +++ b/crates/perry-codegen/src/expr/misc_methods.rs @@ -21,7 +21,7 @@ use crate::lower_string_method::{ }; #[allow(unused_imports)] use crate::nanbox::{double_literal, POINTER_MASK_I64}; -use crate::native_value::{LoweredValue, MaterializationReason, NativeRep}; +use crate::native_value::{ExpectedNativeRep, LoweredValue, MaterializationReason, NativeRep}; #[allow(unused_imports)] use crate::type_analysis::{ compute_auto_captures, is_array_expr, is_bigint_expr, is_bool_expr, is_map_expr, @@ -38,13 +38,13 @@ use super::{ emit_write_barrier_slot_on_block, expr_is_known_non_pointer_shadow_value, extract_array_of_object_shape, i32_bool_to_nanbox, import_origin_suffix, is_global_this_builtin_function_name, is_global_this_builtin_name, is_known_finite, - lower_array_literal, lower_channel_reduction, lower_expr, lower_expr_as_i32, lower_expr_value, - lower_index_set_fast, lower_js_args_array, lower_math_operand, lower_object_literal, - lower_stream_super_init, lower_url_string_getter, materialize_js_value, nanbox_bigint_inline, - nanbox_pointer_inline, nanbox_pointer_inline_pub, nanbox_string_inline, proxy_build_args_array, - try_flat_const_2d_int, try_lower_flat_const_index_get, try_match_channel_reduction, - try_static_class_name, unbox_str_handle, unbox_to_i64, variant_name, ChannelReduction, - FlatConstInfo, FnCtx, I18nLowerCtx, + lower_array_literal, lower_channel_reduction, lower_expr, lower_expr_as_i32, lower_expr_native, + lower_expr_value, lower_index_set_fast, lower_js_args_array, lower_math_operand, + lower_object_literal, lower_stream_super_init, lower_url_string_getter, materialize_js_value, + nanbox_bigint_inline, nanbox_pointer_inline, nanbox_pointer_inline_pub, nanbox_string_inline, + proxy_build_args_array, try_flat_const_2d_int, try_lower_flat_const_index_get, + try_match_channel_reduction, try_static_class_name, unbox_str_handle, unbox_to_i64, + variant_name, ChannelReduction, FlatConstInfo, FnCtx, I18nLowerCtx, }; fn lowered_value_to_iter_result_f64( @@ -105,6 +105,55 @@ fn lower_iter_result_f64_payload( } } +fn is_definite_bool_iter_result_payload(value: &Expr) -> bool { + matches!( + value, + Expr::Bool(_) + | Expr::Compare { .. } + | Expr::Unary { + op: UnaryOp::Not, + .. + } + | Expr::BooleanCoerce(_) + | Expr::IsFinite(_) + | Expr::IsNaN(_) + | Expr::NumberIsNaN(_) + | Expr::NumberIsFinite(_) + | Expr::NumberIsInteger(_) + | Expr::IsUndefinedOrBareNan(_) + | Expr::SetHas { .. } + | Expr::SetDelete { .. } + | Expr::MapHas { .. } + | Expr::MapDelete { .. } + | Expr::ArrayIncludes { .. } + ) +} + +fn lower_iter_result_i1_payload(ctx: &mut FnCtx<'_>, value: &Expr) -> Result> { + if matches!(value, Expr::LocalGet(_)) { + let Some(lowered) = lower_expr_value(ctx, value)? else { + return Ok(None); + }; + return Ok(matches!(lowered.rep, NativeRep::I1).then_some(lowered)); + } + if !is_definite_bool_iter_result_payload(value) { + return Ok(None); + } + let lowered = lower_expr_native(ctx, value, ExpectedNativeRep::I1)?; + Ok(matches!(lowered.rep, NativeRep::I1).then_some(lowered)) +} + +fn lower_iter_result_i32_payload( + ctx: &mut FnCtx<'_>, + value: &Expr, +) -> Result> { + if !super::can_lower_expr_as_i32_in_current_region(ctx, value) { + return Ok(None); + } + let lowered = lower_expr_native(ctx, value, ExpectedNativeRep::I32)?; + Ok(matches!(lowered.rep, NativeRep::I32).then_some(lowered)) +} + pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { match expr { Expr::MathFround(operand) => { @@ -780,7 +829,46 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // per-await `{value, done}` heap alloc on the hot path. Expr::IterResultSet(value, done) => { let done_str = if *done { "1" } else { "0" }; - if is_numeric_expr(ctx, value) { + if let Some(raw) = lower_iter_result_i1_payload(ctx, value)? { + let value_i32 = ctx.block().zext(I1, &raw.value, I32); + let result = ctx.block().call( + DOUBLE, + "js_iter_result_set_i1", + &[(I32, &value_i32), (I32, done_str)], + ); + ctx.record_lowered_value( + "IterResultSet", + None, + "compiler_private_async_iter_result_set_i1", + &raw, + None, + None, + None, + false, + false, + vec!["slot_kind=raw_i1_proven".to_string()], + ); + Ok(result) + } else if let Some(raw) = lower_iter_result_i32_payload(ctx, value)? { + let result = ctx.block().call( + DOUBLE, + "js_iter_result_set_i32", + &[(I32, raw.value.as_str()), (I32, done_str)], + ); + ctx.record_lowered_value( + "IterResultSet", + None, + "compiler_private_async_iter_result_set_i32", + &raw, + None, + None, + None, + false, + false, + vec!["slot_kind=raw_i32_proven".to_string()], + ); + Ok(result) + } else if is_numeric_expr(ctx, value) { let (raw, slot_note) = lower_iter_result_f64_payload(ctx, value)?; let result = ctx.block().call( DOUBLE, diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs index 67a12a7dcd..10658b5ebe 100644 --- a/crates/perry-codegen/src/expr/mod.rs +++ b/crates/perry-codegen/src/expr/mod.rs @@ -90,8 +90,9 @@ pub(crate) use helpers::{ type_has_numeric_pointer_free_array_layout, unbox_str_handle, unbox_to_i64, }; pub(crate) use i32_fast_path::{ - can_lower_expr_as_i32, is_known_finite, lower_expr_as_i32, lower_expr_native, - try_flat_const_2d_int, try_lower_flat_const_index_get, + can_lower_expr_as_i32, can_lower_expr_as_i32_in_current_region, is_known_finite, + lower_expr_as_i32, lower_expr_native, lower_packed_u32_loop_index_get, try_flat_const_2d_int, + try_lower_flat_const_index_get, }; pub(crate) use index::lower_index_set_fast; pub(crate) use nanbox_inline::{ @@ -858,9 +859,11 @@ pub(crate) struct FnCtx<'a> { pub typed_f64_methods: &'a std::collections::HashSet<(String, String)>, pub typed_i32_methods: &'a std::collections::HashSet<(String, String)>, pub typed_i1_methods: &'a std::collections::HashSet<(String, String)>, + pub typed_string_methods: &'a std::collections::HashSet<(String, String)>, pub typed_i1_method_param_reps: &'a std::collections::HashMap<(String, String), Vec>, pub typed_f64_closures: &'a std::collections::HashSet, + pub typed_i32_closures: &'a std::collections::HashSet, pub typed_i1_closures: &'a std::collections::HashSet, pub typed_i1_closure_param_reps: &'a std::collections::HashMap>, @@ -1083,6 +1086,103 @@ pub(crate) struct BoundedIndexPair { pub scope_id: u32, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum PackedNumericLoopKind { + F64, + I32, + U32, +} + +impl PackedNumericLoopKind { + pub(crate) fn array_kind_label(self) -> &'static str { + match self { + Self::F64 => "packed_f64", + Self::I32 => "packed_i32", + Self::U32 => "packed_u32", + } + } + + pub(crate) fn loop_label(self) -> &'static str { + match self { + Self::F64 => "packed_f64", + Self::I32 => "packed_i32", + Self::U32 => "packed_u32", + } + } + + pub(crate) fn guard_expr_kind(self) -> &'static str { + match self { + Self::F64 => "PackedF64LoopGuard", + Self::I32 => "PackedI32LoopGuard", + Self::U32 => "PackedU32LoopGuard", + } + } + + pub(crate) fn guard_consumer(self) -> &'static str { + match self { + Self::F64 => "packed_f64_loop_guard", + Self::I32 => "packed_i32_loop_guard", + Self::U32 => "packed_u32_loop_guard", + } + } + + pub(crate) fn fallback_consumer(self) -> &'static str { + match self { + Self::F64 => "packed_f64_loop_fallback", + Self::I32 => "packed_i32_loop_fallback", + Self::U32 => "packed_u32_loop_fallback", + } + } + + pub(crate) fn load_expr_kind(self) -> &'static str { + match self { + Self::F64 => "PackedF64LoopLoad", + Self::I32 => "PackedI32LoopLoad", + Self::U32 => "PackedU32LoopLoad", + } + } + + pub(crate) fn load_consumer_f64(self) -> &'static str { + match self { + Self::F64 => "packed_f64_loop_load", + Self::I32 => "packed_i32_loop_load_f64", + Self::U32 => "packed_u32_loop_load_f64", + } + } + + pub(crate) fn store_expr_kind(self) -> &'static str { + match self { + Self::F64 => "PackedF64LoopStore", + Self::I32 => "PackedI32LoopStore", + Self::U32 => "PackedU32LoopStore", + } + } + + pub(crate) fn store_consumer(self) -> &'static str { + match self { + Self::F64 => "packed_f64_loop_store", + Self::I32 => "packed_i32_loop_store", + Self::U32 => "packed_u32_loop_store", + } + } + + pub(crate) fn store_side_exit_consumer(self) -> &'static str { + match self { + Self::F64 => "packed_f64_loop_store_side_exit", + Self::I32 => "packed_i32_loop_store_side_exit", + Self::U32 => "packed_u32_loop_store_side_exit", + } + } + + pub(crate) fn store_guard_detail(self) -> &'static str { + match self { + Self::F64 => "packed_f64_loop_store_guard", + Self::I32 => "packed_i32_loop_store_guard", + Self::U32 => "packed_u32_loop_store_guard", + } + } +} + #[derive(Debug, Clone)] pub(crate) struct PackedF64LoopFact { pub index_local_id: u32, @@ -1090,6 +1190,7 @@ pub(crate) struct PackedF64LoopFact { pub scope_id: u32, pub guard_id: String, pub store_side_exit_label: String, + pub array_kind: PackedNumericLoopKind, } impl<'a> FnCtx<'a> { @@ -1511,6 +1612,268 @@ mod this_super_call; mod unary; mod url_main; +fn collection_fact( + receiver_kind: &str, + fact_suffix: &str, + state: &str, +) -> crate::native_value::NativeFactUse { + crate::native_value::NativeFactUse { + fact_id: format!("{receiver_kind}.{fact_suffix}"), + kind: "type_fact".to_string(), + local_id: None, + state: state.to_string(), + detail: fact_suffix.to_string(), + reason: None, + } +} + +pub(crate) fn record_collection_string_key_selected( + ctx: &mut FnCtx<'_>, + expr_kind: &'static str, + consumer: &'static str, + key_handle: &str, + receiver_kind: &'static str, + helper: &'static str, +) { + let lowered = LoweredValue::string_ref(key_handle); + ctx.record_lowered_value_with_access_mode_and_facts( + expr_kind, + None, + consumer, + &lowered, + None, + None, + None, + None, + None, + None, + vec![collection_fact( + receiver_kind, + "string_key_helper", + "consumed", + )], + Vec::new(), + false, + false, + vec![ + format!("selected_helper={helper}"), + "key_rep=string_ref".to_string(), + "boxed_key_avoided=true".to_string(), + ], + ); +} + +pub(crate) fn record_collection_string_key_value_selected( + ctx: &mut FnCtx<'_>, + expr_kind: &'static str, + consumer: &'static str, + lowered_value: &LoweredValue, + receiver_kind: &'static str, + value_fact_suffix: &'static str, + helper: &'static str, +) { + ctx.record_lowered_value_with_access_mode_and_facts( + expr_kind, + None, + consumer, + lowered_value, + None, + None, + None, + None, + None, + None, + vec![ + collection_fact(receiver_kind, "string_key_helper", "consumed"), + collection_fact(receiver_kind, value_fact_suffix, "consumed"), + ], + Vec::new(), + false, + false, + vec![ + format!("selected_helper={helper}"), + "key_rep=string_ref".to_string(), + format!("value_rep={}", lowered_value.rep.name()), + "boxed_key_avoided=true".to_string(), + "boxed_value_avoided_until_map_slot=true".to_string(), + ], + ); +} + +pub(crate) fn record_collection_string_key_fallback( + ctx: &mut FnCtx<'_>, + expr_kind: &'static str, + consumer: &'static str, + key_box: &str, + receiver_kind: &'static str, + helper: &'static str, + reason: &'static str, +) { + let lowered = LoweredValue::js_value(key_box); + ctx.record_lowered_value_with_access_mode_and_facts( + expr_kind, + None, + consumer, + &lowered, + None, + None, + None, + None, + None, + None, + Vec::new(), + vec![collection_fact( + receiver_kind, + "string_key_helper", + "rejected", + )], + false, + false, + vec![ + format!("generic_helper={helper}"), + format!("typed_collection_rejected={reason}"), + "key_rep=js_value".to_string(), + ], + ); +} + +pub(crate) fn record_collection_number_key_selected( + ctx: &mut FnCtx<'_>, + expr_kind: &'static str, + consumer: &'static str, + key_raw: &str, + receiver_kind: &'static str, + fact_suffix: &'static str, + helper: &'static str, + key_label: &'static str, +) { + let lowered = LoweredValue::f64(key_raw.to_string()); + ctx.record_lowered_value_with_access_mode_and_facts( + expr_kind, + None, + consumer, + &lowered, + None, + None, + None, + None, + None, + None, + vec![collection_fact(receiver_kind, fact_suffix, "consumed")], + Vec::new(), + false, + false, + vec![ + format!("selected_helper={helper}"), + format!("{key_label}_rep=raw_f64"), + format!("{key_label}_guard=js_typed_f64_arg_guard"), + "generic_helper_avoided=true".to_string(), + ], + ); +} + +pub(crate) fn record_collection_number_key_fallback( + ctx: &mut FnCtx<'_>, + expr_kind: &'static str, + consumer: &'static str, + key_box: &str, + receiver_kind: &'static str, + fact_suffix: &'static str, + helper: &'static str, + reason: &'static str, + key_label: &'static str, +) { + let lowered = LoweredValue::js_value(key_box); + ctx.record_lowered_value_with_access_mode_and_facts( + expr_kind, + None, + consumer, + &lowered, + None, + None, + None, + None, + None, + None, + Vec::new(), + vec![collection_fact(receiver_kind, fact_suffix, "rejected")], + false, + false, + vec![ + format!("generic_helper={helper}"), + format!("typed_collection_rejected={reason}"), + format!("{key_label}_rep=js_value"), + ], + ); +} + +pub(crate) fn record_collection_typed_value_selected( + ctx: &mut FnCtx<'_>, + expr_kind: &'static str, + consumer: &'static str, + lowered_value: &LoweredValue, + receiver_kind: &'static str, + fact_suffix: &'static str, + helper: &'static str, + slot_boundary: &'static str, +) { + ctx.record_lowered_value_with_access_mode_and_facts( + expr_kind, + None, + consumer, + lowered_value, + None, + None, + None, + None, + None, + None, + vec![collection_fact(receiver_kind, fact_suffix, "consumed")], + Vec::new(), + false, + false, + vec![ + format!("selected_helper={helper}"), + format!("value_rep={}", lowered_value.rep.name()), + format!("boxed_value_avoided_until_{slot_boundary}=true"), + ], + ); +} + +pub(crate) fn record_collection_typed_value_fallback( + ctx: &mut FnCtx<'_>, + expr_kind: &'static str, + consumer: &'static str, + value_box: &str, + receiver_kind: &'static str, + fact_suffix: &'static str, + helper: &'static str, + reason: &'static str, +) { + let lowered = LoweredValue::js_value(value_box); + ctx.record_lowered_value_with_access_mode_and_facts( + expr_kind, + None, + consumer, + &lowered, + None, + None, + None, + None, + None, + None, + Vec::new(), + vec![collection_fact(receiver_kind, fact_suffix, "rejected")], + false, + false, + vec![ + format!("generic_helper={helper}"), + format!("typed_collection_rejected={reason}"), + "value_rep=js_value".to_string(), + ], + ); +} + fn is_plain_f64_local(ctx: &FnCtx<'_>, id: u32) -> bool { !ctx.closure_captures.contains_key(&id) && !ctx.boxed_vars.contains(&id) @@ -1750,6 +2113,9 @@ fn lower_numeric_operand_value(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result, lowered: &LoweredValue) -> Option Option { + let normalized = raw.replace('_', ""); + let s = normalized.strip_suffix('n').unwrap_or(&normalized); + let (negative, digits) = match s.strip_prefix('-') { + Some(rest) => (true, rest), + None => (false, s.strip_prefix('+').unwrap_or(s)), + }; + if digits.is_empty() { + return None; + } + let (radix, digits) = if let Some(rest) = digits + .strip_prefix("0x") + .or_else(|| digits.strip_prefix("0X")) + { + (16, rest) + } else if let Some(rest) = digits + .strip_prefix("0o") + .or_else(|| digits.strip_prefix("0O")) + { + (8, rest) + } else if let Some(rest) = digits + .strip_prefix("0b") + .or_else(|| digits.strip_prefix("0B")) + { + (2, rest) + } else { + (10, digits) + }; + if digits.is_empty() { + return None; + } + let magnitude = i128::from_str_radix(digits, radix).ok()?; + if negative { + magnitude.checked_neg() + } else { + Some(magnitude) + } +} + fn lower_bitwise_operand_i32(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result> { if let Expr::Integer(value) = expr { return Ok(Some((*value as i32).to_string())); } + if matches!(expr, Expr::IterResultGetValue) { + return Ok(Some( + lower_expr_native(ctx, expr, ExpectedNativeRep::I32)?.value, + )); + } if let Expr::LocalGet(id) = expr { if let Some(slot) = ctx.i32_counter_slots.get(id).cloned() { return Ok(Some(ctx.block().load(I32, &slot))); @@ -2005,6 +2415,45 @@ pub(crate) fn lower_expr_value(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { + let Some(value) = small_bigint_literal_i128(raw) else { + let lowered = LoweredValue::js_value("0.0"); + ctx.record_lowered_value_with_access_mode( + "BigInt", + None, + "ordinary_expr_value.small_bigint_literal_rejected", + &lowered, + None, + None, + Some(BufferAccessMode::DynamicFallback), + Some(MaterializationReason::RuntimeApi), + false, + false, + vec![ + "small_bigint_rejected=literal_outside_i128_or_invalid".to_string(), + "fallback=js_bigint_from_string".to_string(), + ], + ); + return Ok(None); + }; + let lowered = LoweredValue::small_bigint(value.to_string()); + ctx.record_lowered_value( + "BigInt", + None, + "ordinary_expr_value.small_bigint_literal_i128", + &lowered, + None, + None, + None, + false, + false, + vec![ + "proof=bigint_literal_fits_i128".to_string(), + "public_semantics=materialize_bigint_object_before_js_boundary".to_string(), + ], + ); + Ok(Some(lowered)) + } Expr::IterResultGetValue => { let value = ctx .block() @@ -2260,6 +2709,24 @@ pub(crate) fn lower_expr_value(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { + let value_i32 = ctx.block().call(I32, "js_iter_result_get_value_i1", &[]); + let value = ctx.block().icmp_ne(I32, &value_i32, "0"); + let lowered = LoweredValue::i1(value); + ctx.record_lowered_value( + "IterResultGetValue", + None, + "compiler_private_async_iter_result_get_i1", + &lowered, + None, + None, + None, + false, + false, + vec!["slot_kind=raw_i1_or_truthy_jsvalue".to_string()], + ); + Ok(Some(lowered)) + } Expr::BooleanCoerce(operand) => { let Some(lowered_operand) = lower_expr_value(ctx, operand)? else { return Ok(None); diff --git a/crates/perry-codegen/src/expr/native_record.rs b/crates/perry-codegen/src/expr/native_record.rs index 179fec83f7..f1eac06300 100644 --- a/crates/perry-codegen/src/expr/native_record.rs +++ b/crates/perry-codegen/src/expr/native_record.rs @@ -49,6 +49,7 @@ fn native_fact_use( kind: kind.to_string(), local_id, state: state.to_string(), + detail: detail.to_string(), reason, } } @@ -99,6 +100,13 @@ pub(super) fn native_fact_uses_for_record( None, )), NativeRep::JsValue => {} + NativeRep::StringRef => consumed.push(native_fact_use( + "representation", + local_id, + "consumed", + "string_ref", + None, + )), NativeRep::I32 => consumed.push(native_fact_use( "representation", local_id, @@ -190,6 +198,13 @@ pub(super) fn native_fact_uses_for_record( "promise_boundary", None, )), + NativeRep::SmallBigInt => consumed.push(native_fact_use( + "representation", + local_id, + "consumed", + "small_bigint", + None, + )), NativeRep::BufferView(_) => consumed.push(native_fact_use( "representation", local_id, diff --git a/crates/perry-codegen/src/expr/typed_feedback.rs b/crates/perry-codegen/src/expr/typed_feedback.rs index bb8c05e68f..a84db9fda6 100644 --- a/crates/perry-codegen/src/expr/typed_feedback.rs +++ b/crates/perry-codegen/src/expr/typed_feedback.rs @@ -86,6 +86,14 @@ impl TypedFeedbackContract { Self::new("packed_f64_array_loop_guard", "generic_jsvalue_loop") } + pub(crate) const fn packed_i32_array_loop() -> Self { + Self::new("packed_i32_array_loop_guard", "generic_jsvalue_loop") + } + + pub(crate) const fn packed_u32_array_loop() -> Self { + Self::new("packed_u32_array_loop_guard", "generic_jsvalue_loop") + } + pub(crate) const fn array_set_index() -> Self { Self::new("plain_array_index_set_guard", "js_array_set_f64_extend") } diff --git a/crates/perry-codegen/src/lower_call/early_branches.rs b/crates/perry-codegen/src/lower_call/early_branches.rs index 7a5df4b6a1..142d2f1f5c 100644 --- a/crates/perry-codegen/src/lower_call/early_branches.rs +++ b/crates/perry-codegen/src/lower_call/early_branches.rs @@ -39,6 +39,23 @@ fn typed_string_closure_signature_note(arg_count: usize) -> String { } } +fn typed_closure_signature_note(ret: &str, reps: &[crate::codegen::TypedParamRep]) -> String { + let first = reps.first().map(|rep| rep.label()).unwrap_or("void"); + if reps.len() <= 1 { + format!("typed_signature={ret}(i64 closure, {first})->{ret}") + } else { + format!("typed_signature={ret}(i64 closure, {first}, ...)->{ret}") + } +} + +fn typed_i32_closure_signature_note(arg_count: usize) -> String { + if arg_count <= 1 { + "typed_signature=i32(i64 closure, i32)->i32".to_string() + } else { + "typed_signature=i32(i64 closure, i32, ...)->i32".to_string() + } +} + fn is_async_dispose_symbol_index(index: &Expr) -> bool { let Expr::SymbolFor(symbol_name) = index else { return false; @@ -337,14 +354,36 @@ pub fn try_lower_closure_typed_local_call( .cond_br(&guard_pass, &fast_label, &fallback_label); ctx.current_block = fast_idx; - let uses_typed_f64_clone = ctx.typed_f64_closures.contains(&func_id) - && args - .iter() - .all(|arg| crate::type_analysis::is_numeric_expr(ctx, arg)); - let uses_typed_string_clone = ctx.typed_string_closures.contains(&func_id) - && args - .iter() - .all(|arg| crate::type_analysis::is_definitely_string_expr(ctx, arg)); + let typed_f64_param_reps = if ctx.typed_f64_closures.contains(&func_id) { + ctx.typed_i1_closure_param_reps + .get(&func_id) + .filter(|reps| { + crate::codegen::typed_param_reps_match_args(ctx, reps, args) + }) + .cloned() + } else { + None + }; + let typed_i32_param_reps = if ctx.typed_i32_closures.contains(&func_id) { + ctx.typed_i1_closure_param_reps + .get(&func_id) + .filter(|reps| { + crate::codegen::typed_param_reps_match_args(ctx, reps, args) + }) + .cloned() + } else { + None + }; + let typed_string_param_reps = if ctx.typed_string_closures.contains(&func_id) { + ctx.typed_i1_closure_param_reps + .get(&func_id) + .filter(|reps| { + crate::codegen::typed_param_reps_match_args(ctx, reps, args) + }) + .cloned() + } else { + None + }; let typed_i1_param_reps = if ctx.typed_i1_closures.contains(&func_id) { if let Some(reps) = ctx.typed_i1_closure_param_reps.get(&func_id) { let matches_args = reps.len() == args.len() @@ -378,18 +417,13 @@ pub fn try_lower_closure_typed_local_call( } else { None }; - let fast_value = if uses_typed_f64_clone { + let fast_value = if let Some(typed_param_reps) = typed_f64_param_reps { let typed_fn = crate::codegen::typed_f64_closure_name(&closure_fn); let generic_closure_fn = crate::codegen::generic_closure_body_name(&closure_fn); let mut numeric_guard: Option = None; - for value in &lowered_args { - let raw = ctx.block().call( - I32, - "js_typed_f64_arg_guard", - &[(DOUBLE, value.as_str())], - ); - let ok = ctx.block().icmp_ne(I32, &raw, "0"); + for (value, rep) in lowered_args.iter().zip(typed_param_reps.iter()) { + let ok = crate::codegen::emit_typed_arg_guard(ctx.block(), *rep, value); numeric_guard = Some(match numeric_guard { Some(prev) => ctx.block().and(I1, &prev, &ok), None => ok, @@ -412,17 +446,22 @@ pub fn try_lower_closure_typed_local_call( ctx.current_block = typed_idx; let mut typed_args_storage: Vec = Vec::with_capacity(lowered_args.len()); - for value in &lowered_args { - typed_args_storage.push(ctx.block().call( - DOUBLE, - "js_typed_f64_arg_to_raw", - &[(DOUBLE, value.as_str())], + for (value, rep) in lowered_args.iter().zip(typed_param_reps.iter()) { + typed_args_storage.push(crate::codegen::emit_typed_arg_to_raw( + ctx.block(), + *rep, + value, )); } let mut typed_args: Vec<(crate::types::LlvmType, &str)> = Vec::with_capacity(typed_args_storage.len() + 1); typed_args.push((I64, &closure_handle)); - typed_args.extend(typed_args_storage.iter().map(|s| (DOUBLE, s.as_str()))); + typed_args.extend( + typed_args_storage + .iter() + .zip(typed_param_reps.iter()) + .map(|(s, rep)| (rep.llvm_ty(), s.as_str())), + ); let typed_value = ctx.block().call(DOUBLE, &typed_fn, &typed_args); let after_typed = ctx.block().label.clone(); if !ctx.block().is_terminated() { @@ -464,21 +503,109 @@ pub fn try_lower_closure_typed_local_call( format!("typed_clone={typed_fn}"), format!("generic_closure={generic_closure_fn}"), format!("closure_func_id={func_id}"), + typed_closure_signature_note("f64", &typed_param_reps), ], ); result - } else if uses_typed_string_clone { + } else if let Some(typed_param_reps) = typed_i32_param_reps { + let typed_fn = crate::codegen::typed_i32_closure_name(&closure_fn); + let generic_closure_fn = + crate::codegen::generic_closure_body_name(&closure_fn); + let mut typed_guard: Option = None; + for (value, rep) in lowered_args.iter().zip(typed_param_reps.iter()) { + let ok = crate::codegen::emit_typed_arg_guard(ctx.block(), *rep, value); + typed_guard = Some(match typed_guard { + Some(prev) => ctx.block().and(I1, &prev, &ok), + None => ok, + }); + } + + let typed_idx = ctx.new_block("closure_direct.typed_i32"); + let generic_idx = ctx.new_block("closure_direct.generic"); + let typed_merge_idx = ctx.new_block("closure_direct.typed_merge"); + let typed_label = ctx.block_label(typed_idx); + let generic_label = ctx.block_label(generic_idx); + let typed_merge_label = ctx.block_label(typed_merge_idx); + if let Some(typed_guard) = typed_guard { + ctx.block() + .cond_br(&typed_guard, &typed_label, &generic_label); + } else { + ctx.block().br(&typed_label); + } + + ctx.current_block = typed_idx; + let mut typed_args_storage: Vec = + Vec::with_capacity(lowered_args.len()); + for (value, rep) in lowered_args.iter().zip(typed_param_reps.iter()) { + typed_args_storage.push(crate::codegen::emit_typed_arg_to_raw( + ctx.block(), + *rep, + value, + )); + } + let mut typed_args: Vec<(crate::types::LlvmType, &str)> = + Vec::with_capacity(typed_args_storage.len() + 1); + typed_args.push((I64, &closure_handle)); + typed_args.extend( + typed_args_storage + .iter() + .zip(typed_param_reps.iter()) + .map(|(s, rep)| (rep.llvm_ty(), s.as_str())), + ); + let raw_i32 = ctx.block().call(I32, &typed_fn, &typed_args); + let typed_value = crate::expr::i32_to_nanbox(ctx.block(), &raw_i32); + let after_typed = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&typed_merge_label); + } + + ctx.current_block = generic_idx; + let mut generic_args: Vec<(crate::types::LlvmType, &str)> = + vec![(I64, &closure_handle)]; + for v in &lowered_args { + generic_args.push((DOUBLE, v.as_str())); + } + let generic_value = + ctx.block().call(DOUBLE, &generic_closure_fn, &generic_args); + let after_generic = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&typed_merge_label); + } + + ctx.current_block = typed_merge_idx; + let result = ctx.block().phi( + DOUBLE, + &[ + (typed_value.as_str(), after_typed.as_str()), + (generic_value.as_str(), after_generic.as_str()), + ], + ); + ctx.record_lowered_value( + "ClosureCall", + None, + "typed_i32_closure_direct_call", + &LoweredValue::js_value(result.clone()), + None, + None, + None, + false, + false, + vec![ + format!("typed_clone={typed_fn}"), + format!("generic_closure={generic_closure_fn}"), + format!("closure_func_id={func_id}"), + typed_closure_signature_note("i32", &typed_param_reps), + "boxed_result_at=direct_call_boundary".to_string(), + ], + ); + result + } else if let Some(typed_param_reps) = typed_string_param_reps { let typed_fn = crate::codegen::typed_string_closure_name(&closure_fn); let generic_closure_fn = crate::codegen::generic_closure_body_name(&closure_fn); let mut typed_guard: Option = None; - for value in &lowered_args { - let raw = ctx.block().call( - I32, - "js_typed_string_arg_guard", - &[(DOUBLE, value.as_str())], - ); - let ok = ctx.block().icmp_ne(I32, &raw, "0"); + for (value, rep) in lowered_args.iter().zip(typed_param_reps.iter()) { + let ok = crate::codegen::emit_typed_arg_guard(ctx.block(), *rep, value); typed_guard = Some(match typed_guard { Some(prev) => ctx.block().and(I1, &prev, &ok), None => ok, @@ -520,17 +647,22 @@ pub fn try_lower_closure_typed_local_call( ctx.current_block = typed_idx; let mut typed_args_storage: Vec = Vec::with_capacity(lowered_args.len()); - for value in &lowered_args { - typed_args_storage.push(ctx.block().call( - I64, - "js_typed_string_arg_to_raw", - &[(DOUBLE, value.as_str())], + for (value, rep) in lowered_args.iter().zip(typed_param_reps.iter()) { + typed_args_storage.push(crate::codegen::emit_typed_arg_to_raw( + ctx.block(), + *rep, + value, )); } let mut typed_args: Vec<(crate::types::LlvmType, &str)> = Vec::with_capacity(typed_args_storage.len() + 1); typed_args.push((I64, &closure_handle)); - typed_args.extend(typed_args_storage.iter().map(|s| (I64, s.as_str()))); + typed_args.extend( + typed_args_storage + .iter() + .zip(typed_param_reps.iter()) + .map(|(s, rep)| (rep.llvm_ty(), s.as_str())), + ); let raw_string = ctx.block().call(I64, &typed_fn, &typed_args); let typed_value = ctx.block() @@ -575,7 +707,7 @@ pub fn try_lower_closure_typed_local_call( format!("typed_clone={typed_fn}"), format!("generic_closure={generic_closure_fn}"), format!("closure_func_id={func_id}"), - typed_string_closure_signature_note(lowered_args.len()), + typed_closure_signature_note("string", &typed_param_reps), "boxed_result_at=direct_call_boundary".to_string(), ], ); diff --git a/crates/perry-codegen/src/lower_call/func_ref.rs b/crates/perry-codegen/src/lower_call/func_ref.rs index 456d0f0dd4..81744ec9d9 100644 --- a/crates/perry-codegen/src/lower_call/func_ref.rs +++ b/crates/perry-codegen/src/lower_call/func_ref.rs @@ -53,6 +53,22 @@ fn typed_i32_signature_note(arg_count: usize) -> String { } } +fn typed_signature_note( + ret: &str, + reps: &[crate::codegen::TypedParamRep], + closure_arg: bool, +) -> String { + let first = reps.first().map(|rep| rep.label()).unwrap_or("void"); + let first = if closure_arg { "i64 closure" } else { first }; + if reps.is_empty() { + format!("typed_signature={ret}({first})->{ret}") + } else if reps.len() == 1 && !closure_arg { + format!("typed_signature={ret}({first})->{ret}") + } else { + format!("typed_signature={ret}({first}, ...)->{ret}") + } +} + pub fn try_lower_func_ref_call( ctx: &mut FnCtx<'_>, callee: &Expr, @@ -266,28 +282,45 @@ pub fn try_lower_func_ref_call( } else { None }; - let uses_typed_f64_clone = !resets_this + let typed_f64_call_param_reps = if !resets_this && !has_rest && !ctx.func_synthetic_arguments.contains(fid) && ctx.typed_f64_functions.contains(fid) && declared_count == args.len() - && args - .iter() - .all(|arg| crate::type_analysis::is_numeric_expr(ctx, arg)); - let uses_typed_i32_clone = !resets_this + { + ctx.typed_i1_function_param_reps + .get(fid) + .filter(|reps| typed_i1_param_reps_match_args(ctx, reps, args)) + .cloned() + } else { + None + }; + let typed_i32_call_param_reps = if !resets_this && !has_rest && !ctx.func_synthetic_arguments.contains(fid) && ctx.typed_i32_functions.contains(fid) && declared_count == args.len() - && args.iter().all(|arg| is_i32_expr(ctx, arg)); - let uses_typed_string_clone = !resets_this + { + ctx.typed_i1_function_param_reps + .get(fid) + .filter(|reps| typed_i1_param_reps_match_args(ctx, reps, args)) + .cloned() + } else { + None + }; + let typed_string_call_param_reps = if !resets_this && !has_rest && !ctx.func_synthetic_arguments.contains(fid) && ctx.typed_string_functions.contains(fid) && declared_count == args.len() - && args - .iter() - .all(|arg| crate::type_analysis::is_definitely_string_expr(ctx, arg)); + { + ctx.typed_i1_function_param_reps + .get(fid) + .filter(|reps| typed_i1_param_reps_match_args(ctx, reps, args)) + .cloned() + } else { + None + }; let typed_i1_call_param_reps = if !resets_this && !has_rest && !ctx.func_synthetic_arguments.contains(fid) @@ -300,15 +333,12 @@ pub fn try_lower_func_ref_call( } else { None }; - let result = if uses_typed_f64_clone { + let result = if let Some(reps) = typed_f64_call_param_reps { let typed_name = crate::codegen::typed_f64_function_name(&fname); let generic_body_name = crate::codegen::generic_function_body_name(&fname); let mut guard: Option = None; - for value in &lowered { - let raw = ctx - .block() - .call(I32, "js_typed_f64_arg_guard", &[(DOUBLE, value.as_str())]); - let ok = ctx.block().icmp_ne(I32, &raw, "0"); + for (value, rep) in lowered.iter().zip(reps.iter()) { + let ok = crate::codegen::emit_typed_arg_guard(ctx.block(), *rep, value); guard = Some(match guard { Some(prev) => ctx.block().and(I1, &prev, &ok), None => ok, @@ -328,16 +358,17 @@ pub fn try_lower_func_ref_call( ctx.current_block = fast_idx; let mut typed_args_storage: Vec = Vec::with_capacity(lowered.len()); - for value in &lowered { - typed_args_storage.push(ctx.block().call( - DOUBLE, - "js_typed_f64_arg_to_raw", - &[(DOUBLE, value.as_str())], + for (value, rep) in lowered.iter().zip(reps.iter()) { + typed_args_storage.push(crate::codegen::emit_typed_arg_to_raw( + ctx.block(), + *rep, + value, )); } let typed_args: Vec<(crate::types::LlvmType, &str)> = typed_args_storage .iter() - .map(|s| (DOUBLE, s.as_str())) + .zip(reps.iter()) + .map(|(s, rep)| (rep.llvm_ty(), s.as_str())) .collect(); let fast_value = ctx.block().call(DOUBLE, &typed_name, &typed_args); let after_fast = ctx.block().label.clone(); @@ -370,20 +401,18 @@ pub fn try_lower_func_ref_call( None, false, false, - vec![format!( - "typed_clone={typed_name}; generic_body={generic_body_name}" - )], + vec![ + format!("typed_clone={typed_name}; generic_body={generic_body_name}"), + typed_signature_note("f64", &reps, false), + ], ); result - } else if uses_typed_i32_clone { + } else if let Some(reps) = typed_i32_call_param_reps { let typed_name = crate::codegen::typed_i32_function_name(&fname); let generic_body_name = crate::codegen::generic_function_body_name(&fname); let mut guard: Option = None; - for value in &lowered { - let raw = ctx - .block() - .call(I32, "js_typed_i32_arg_guard", &[(DOUBLE, value.as_str())]); - let ok = ctx.block().icmp_ne(I32, &raw, "0"); + for (value, rep) in lowered.iter().zip(reps.iter()) { + let ok = crate::codegen::emit_typed_arg_guard(ctx.block(), *rep, value); guard = Some(match guard { Some(prev) => ctx.block().and(I1, &prev, &ok), None => ok, @@ -403,16 +432,17 @@ pub fn try_lower_func_ref_call( ctx.current_block = fast_idx; let mut typed_args_storage: Vec = Vec::with_capacity(lowered.len()); - for value in &lowered { - typed_args_storage.push(ctx.block().call( - I32, - "js_typed_i32_arg_to_raw", - &[(DOUBLE, value.as_str())], + for (value, rep) in lowered.iter().zip(reps.iter()) { + typed_args_storage.push(crate::codegen::emit_typed_arg_to_raw( + ctx.block(), + *rep, + value, )); } let typed_args: Vec<(crate::types::LlvmType, &str)> = typed_args_storage .iter() - .map(|s| (I32, s.as_str())) + .zip(reps.iter()) + .map(|(s, rep)| (rep.llvm_ty(), s.as_str())) .collect(); let raw_i32 = ctx.block().call(I32, &typed_name, &typed_args); let fast_value = i32_to_nanbox(ctx.block(), &raw_i32); @@ -448,22 +478,17 @@ pub fn try_lower_func_ref_call( false, vec![ format!("typed_clone={typed_name}; generic_body={generic_body_name}"), - typed_i32_signature_note(lowered.len()), + typed_signature_note("i32", &reps, false), "boxed_result_at=direct_call_boundary".to_string(), ], ); result - } else if uses_typed_string_clone { + } else if let Some(reps) = typed_string_call_param_reps { let typed_name = crate::codegen::typed_string_function_name(&fname); let generic_body_name = crate::codegen::generic_function_body_name(&fname); let mut guard: Option = None; - for value in &lowered { - let raw = ctx.block().call( - I32, - "js_typed_string_arg_guard", - &[(DOUBLE, value.as_str())], - ); - let ok = ctx.block().icmp_ne(I32, &raw, "0"); + for (value, rep) in lowered.iter().zip(reps.iter()) { + let ok = crate::codegen::emit_typed_arg_guard(ctx.block(), *rep, value); guard = Some(match guard { Some(prev) => ctx.block().and(I1, &prev, &ok), None => ok, @@ -483,16 +508,17 @@ pub fn try_lower_func_ref_call( ctx.current_block = fast_idx; let mut typed_args_storage: Vec = Vec::with_capacity(lowered.len()); - for value in &lowered { - typed_args_storage.push(ctx.block().call( - I64, - "js_typed_string_arg_to_raw", - &[(DOUBLE, value.as_str())], + for (value, rep) in lowered.iter().zip(reps.iter()) { + typed_args_storage.push(crate::codegen::emit_typed_arg_to_raw( + ctx.block(), + *rep, + value, )); } let typed_args: Vec<(crate::types::LlvmType, &str)> = typed_args_storage .iter() - .map(|s| (I64, s.as_str())) + .zip(reps.iter()) + .map(|(s, rep)| (rep.llvm_ty(), s.as_str())) .collect(); let raw_string = ctx.block().call(I64, &typed_name, &typed_args); let fast_value = ctx diff --git a/crates/perry-codegen/src/lower_call/method_override.rs b/crates/perry-codegen/src/lower_call/method_override.rs index b9f228bef2..b1bd337831 100644 --- a/crates/perry-codegen/src/lower_call/method_override.rs +++ b/crates/perry-codegen/src/lower_call/method_override.rs @@ -30,6 +30,23 @@ fn typed_i32_method_signature_note(arg_count: usize) -> String { } } +fn typed_string_method_signature_note(arg_count: usize) -> String { + if arg_count <= 1 { + "typed_signature=string(string)->string".to_string() + } else { + "typed_signature=string(string, ...)->string".to_string() + } +} + +fn typed_method_signature_note(ret: &str, reps: &[crate::codegen::TypedParamRep]) -> String { + let first = reps.first().map(|rep| rep.label()).unwrap_or("void"); + if reps.len() <= 1 { + format!("typed_signature={ret}({first})->{ret}") + } else { + format!("typed_signature={ret}({first}, ...)->{ret}") + } +} + /// Issue #620: emit a runtime check before the static class-method dispatch. /// If the receiver has an own-property override at `property` (set via /// `this.method = X`), invoke the stored closure via `js_native_call_value`; @@ -158,10 +175,11 @@ pub(super) fn emit_guarded_direct_method_call( direct_fn: &str, direct_arg_slices: &[(crate::types::LlvmType, &str)], fallback_user_args: &[String], - typed_direct_fn: Option<(&str, usize)>, + typed_direct_fn: Option<(&str, Vec)>, typed_f64_receiver_direct_fn: Option<(&str, usize, &crate::codegen::TypedReceiverMethodInfo)>, - typed_i32_direct_fn: Option<(&str, usize)>, + typed_i32_direct_fn: Option<(&str, Vec)>, typed_i1_direct_fn: Option<(&str, Vec)>, + typed_string_direct_fn: Option<(&str, Vec)>, shape_only_guard: bool, ) -> Option { let expected_class_id = *ctx.class_ids.get(receiver_class_name)?; @@ -360,20 +378,17 @@ pub(super) fn emit_guarded_direct_method_call( ], ); result - } else if let Some((typed_fn, typed_formal_count)) = typed_direct_fn { + } else if let Some((typed_fn, typed_param_reps)) = typed_direct_fn { let generic_body_fn = crate::codegen::generic_method_body_name(direct_fn); let formal_args: Vec<&str> = direct_arg_slices .iter() .skip(1) - .take(typed_formal_count) + .take(typed_param_reps.len()) .map(|(_, value)| *value) .collect(); let mut guard: Option = None; - for value in &formal_args { - let raw = ctx - .block() - .call(I32, "js_typed_f64_arg_guard", &[(DOUBLE, *value)]); - let ok = ctx.block().icmp_ne(I32, &raw, "0"); + for (value, rep) in formal_args.iter().zip(typed_param_reps.iter()) { + let ok = crate::codegen::emit_typed_arg_guard(ctx.block(), *rep, value); guard = Some(match guard { Some(prev) => ctx.block().and(I1, &prev, &ok), None => ok, @@ -394,16 +409,17 @@ pub(super) fn emit_guarded_direct_method_call( ctx.current_block = typed_idx; let mut typed_args_storage: Vec = Vec::with_capacity(formal_args.len()); - for value in &formal_args { - typed_args_storage.push(ctx.block().call( - DOUBLE, - "js_typed_f64_arg_to_raw", - &[(DOUBLE, *value)], + for (value, rep) in formal_args.iter().zip(typed_param_reps.iter()) { + typed_args_storage.push(crate::codegen::emit_typed_arg_to_raw( + ctx.block(), + *rep, + value, )); } let typed_args: Vec<(crate::types::LlvmType, &str)> = typed_args_storage .iter() - .map(|value| (DOUBLE, value.as_str())) + .zip(typed_param_reps.iter()) + .map(|(value, rep)| (rep.llvm_ty(), value.as_str())) .collect(); let typed_value = ctx.block().call(DOUBLE, typed_fn, &typed_args); let after_typed = ctx.block().label.clone(); @@ -443,23 +459,21 @@ pub(super) fn emit_guarded_direct_method_call( format!("generic_method={generic_body_fn}"), format!("receiver_class={receiver_class_name}"), format!("method={property}"), + typed_method_signature_note("f64", &typed_param_reps), ], ); result - } else if let Some((typed_fn, typed_formal_count)) = typed_i32_direct_fn { + } else if let Some((typed_fn, typed_param_reps)) = typed_i32_direct_fn { let generic_body_fn = crate::codegen::generic_method_body_name(direct_fn); let formal_args: Vec<&str> = direct_arg_slices .iter() .skip(1) - .take(typed_formal_count) + .take(typed_param_reps.len()) .map(|(_, value)| *value) .collect(); let mut guard: Option = None; - for value in &formal_args { - let raw = ctx - .block() - .call(I32, "js_typed_i32_arg_guard", &[(DOUBLE, *value)]); - let ok = ctx.block().icmp_ne(I32, &raw, "0"); + for (value, rep) in formal_args.iter().zip(typed_param_reps.iter()) { + let ok = crate::codegen::emit_typed_arg_guard(ctx.block(), *rep, value); guard = Some(match guard { Some(prev) => ctx.block().and(I1, &prev, &ok), None => ok, @@ -480,16 +494,17 @@ pub(super) fn emit_guarded_direct_method_call( ctx.current_block = typed_idx; let mut typed_args_storage: Vec = Vec::with_capacity(formal_args.len()); - for value in &formal_args { - typed_args_storage.push(ctx.block().call( - I32, - "js_typed_i32_arg_to_raw", - &[(DOUBLE, *value)], + for (value, rep) in formal_args.iter().zip(typed_param_reps.iter()) { + typed_args_storage.push(crate::codegen::emit_typed_arg_to_raw( + ctx.block(), + *rep, + value, )); } let typed_args: Vec<(crate::types::LlvmType, &str)> = typed_args_storage .iter() - .map(|value| (I32, value.as_str())) + .zip(typed_param_reps.iter()) + .map(|(value, rep)| (rep.llvm_ty(), value.as_str())) .collect(); let raw_i32 = ctx.block().call(I32, typed_fn, &typed_args); let typed_value = i32_to_nanbox(ctx.block(), &raw_i32); @@ -530,7 +545,7 @@ pub(super) fn emit_guarded_direct_method_call( format!("generic_method={generic_body_fn}"), format!("receiver_class={receiver_class_name}"), format!("method={property}"), - typed_i32_method_signature_note(typed_formal_count), + typed_method_signature_note("i32", &typed_param_reps), "boxed_result_at=direct_call_boundary".to_string(), ], ); @@ -635,6 +650,95 @@ pub(super) fn emit_guarded_direct_method_call( ], ); result + } else if let Some((typed_fn, typed_param_reps)) = typed_string_direct_fn { + let generic_body_fn = crate::codegen::generic_method_body_name(direct_fn); + let formal_args: Vec<&str> = direct_arg_slices + .iter() + .skip(1) + .take(typed_param_reps.len()) + .map(|(_, value)| *value) + .collect(); + let mut guard: Option = None; + for (value, rep) in formal_args.iter().zip(typed_param_reps.iter()) { + let ok = crate::codegen::emit_typed_arg_guard(ctx.block(), *rep, value); + guard = Some(match guard { + Some(prev) => ctx.block().and(I1, &prev, &ok), + None => ok, + }); + } + + let typed_idx = ctx.new_block("typed_string_method.fast"); + let generic_idx = ctx.new_block("typed_string_method.generic"); + let typed_merge_idx = ctx.new_block("typed_string_method.merge"); + let typed_label = ctx.block_label(typed_idx); + let generic_label = ctx.block_label(generic_idx); + let typed_merge_label = ctx.block_label(typed_merge_idx); + if let Some(guard) = guard { + ctx.block().cond_br(&guard, &typed_label, &generic_label); + } else { + ctx.block().br(&typed_label); + } + + ctx.current_block = typed_idx; + let mut typed_args_storage: Vec = Vec::with_capacity(formal_args.len()); + for (value, rep) in formal_args.iter().zip(typed_param_reps.iter()) { + typed_args_storage.push(crate::codegen::emit_typed_arg_to_raw( + ctx.block(), + *rep, + value, + )); + } + let typed_args: Vec<(crate::types::LlvmType, &str)> = typed_args_storage + .iter() + .zip(typed_param_reps.iter()) + .map(|(value, rep)| (rep.llvm_ty(), value.as_str())) + .collect(); + let raw_string = ctx.block().call(I64, typed_fn, &typed_args); + let typed_value = ctx + .block() + .call(DOUBLE, "js_nanbox_string", &[(I64, &raw_string)]); + let after_typed = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&typed_merge_label); + } + + ctx.current_block = generic_idx; + let generic_value = ctx + .block() + .call(DOUBLE, &generic_body_fn, direct_arg_slices); + let after_generic = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&typed_merge_label); + } + + ctx.current_block = typed_merge_idx; + let result = ctx.block().phi( + DOUBLE, + &[ + (typed_value.as_str(), after_typed.as_str()), + (generic_value.as_str(), after_generic.as_str()), + ], + ); + ctx.record_lowered_value( + "MethodCall", + None, + "typed_string_method_direct_call", + &LoweredValue::js_value(result.clone()), + None, + None, + None, + false, + false, + vec![ + format!("typed_clone={typed_fn}"), + format!("generic_method={generic_body_fn}"), + format!("receiver_class={receiver_class_name}"), + format!("method={property}"), + typed_method_signature_note("string", &typed_param_reps), + "boxed_result_at=direct_call_boundary".to_string(), + ], + ); + result } else { ctx.block().call(DOUBLE, direct_fn, direct_arg_slices) } diff --git a/crates/perry-codegen/src/lower_call/property_get.rs b/crates/perry-codegen/src/lower_call/property_get.rs index 9c4c718c7e..64a1135c8b 100644 --- a/crates/perry-codegen/src/lower_call/property_get.rs +++ b/crates/perry-codegen/src/lower_call/property_get.rs @@ -2072,11 +2072,12 @@ pub fn try_lower_property_get_method_call( .methods .get(&typed_method_key) .is_some_and(|name| name == &fallback_fn) - && args.len() == typed_formal_count - && args - .iter() - .all(|arg| crate::type_analysis::is_numeric_expr(ctx, arg)) - { + && ctx + .typed_i1_method_param_reps + .get(&typed_method_key) + .is_some_and(|reps| { + crate::codegen::typed_param_reps_match_args(ctx, reps, args) + }) { Some(crate::codegen::typed_f64_method_name(&fallback_fn)) } else { None @@ -2086,17 +2087,12 @@ pub fn try_lower_property_get_method_call( .methods .get(&typed_method_key) .is_some_and(|name| name == &fallback_fn) - && args.len() == typed_formal_count - && args.iter().all(|arg| { - matches!( - crate::type_analysis::static_type_of(ctx, arg), - Some(perry_types::Type::Int32) - ) || matches!( - arg, - Expr::Integer(n) - if (i64::from(i32::MIN)..=i64::from(i32::MAX)).contains(n) - ) - }) { + && ctx + .typed_i1_method_param_reps + .get(&typed_method_key) + .is_some_and(|reps| { + crate::codegen::typed_param_reps_match_args(ctx, reps, args) + }) { Some(crate::codegen::typed_i32_method_name(&fallback_fn)) } else { None @@ -2110,38 +2106,35 @@ pub fn try_lower_property_get_method_call( .typed_i1_method_param_reps .get(&typed_method_key) .is_some_and(|reps| { - args.len() == reps.len() - && args.iter().zip(reps.iter()).all(|(arg, rep)| match rep { - crate::codegen::TypedParamRep::F64 => { - crate::type_analysis::is_numeric_expr(ctx, arg) - } - crate::codegen::TypedParamRep::I32 => { - matches!( - crate::type_analysis::static_type_of(ctx, arg), - Some(perry_types::Type::Int32) - ) || matches!( - arg, - Expr::Integer(n) - if (i64::from(i32::MIN) - ..=i64::from(i32::MAX)) - .contains(n) - ) - } - crate::codegen::TypedParamRep::I1 => { - crate::type_analysis::is_bool_expr(ctx, arg) - } - crate::codegen::TypedParamRep::StringRef => { - crate::type_analysis::is_definitely_string_expr(ctx, arg) - } - }) + crate::codegen::typed_param_reps_match_args(ctx, reps, args) }) { Some(crate::codegen::typed_i1_method_name(&fallback_fn)) } else { None }; - let typed_direct = typed_direct_name - .as_ref() - .map(|name| (name.as_str(), typed_formal_count)); + let typed_string_direct_name = if ctx + .typed_string_methods + .contains(&typed_method_key) + && ctx + .methods + .get(&typed_method_key) + .is_some_and(|name| name == &fallback_fn) + && ctx + .typed_i1_method_param_reps + .get(&typed_method_key) + .is_some_and(|reps| { + crate::codegen::typed_param_reps_match_args(ctx, reps, args) + }) { + Some(crate::codegen::typed_string_method_name(&fallback_fn)) + } else { + None + }; + let typed_direct = typed_direct_name.as_ref().and_then(|name| { + ctx.typed_i1_method_param_reps + .get(&typed_method_key) + .cloned() + .map(|reps| (name.as_str(), reps)) + }); let typed_receiver_direct = match ( typed_receiver_direct_name.as_ref(), typed_receiver_info.as_ref(), @@ -2149,15 +2142,24 @@ pub fn try_lower_property_get_method_call( (Some(name), Some(info)) => Some((name.as_str(), typed_formal_count, info)), _ => None, }; - let typed_i32_direct = typed_i32_direct_name - .as_ref() - .map(|name| (name.as_str(), typed_formal_count)); + let typed_i32_direct = typed_i32_direct_name.as_ref().and_then(|name| { + ctx.typed_i1_method_param_reps + .get(&typed_method_key) + .cloned() + .map(|reps| (name.as_str(), reps)) + }); let typed_i1_direct = typed_i1_direct_name.as_ref().and_then(|name| { ctx.typed_i1_method_param_reps .get(&typed_method_key) .cloned() .map(|reps| (name.as_str(), reps)) }); + let typed_string_direct = typed_string_direct_name.as_ref().and_then(|name| { + ctx.typed_i1_method_param_reps + .get(&typed_method_key) + .cloned() + .map(|reps| (name.as_str(), reps)) + }); if let Some(guarded) = emit_guarded_direct_method_call( ctx, &recv_box, @@ -2170,6 +2172,7 @@ pub fn try_lower_property_get_method_call( typed_receiver_direct, typed_i32_direct, typed_i1_direct, + typed_string_direct, shape_only_guard, ) { return Ok(Some(guarded)); diff --git a/crates/perry-codegen/src/lower_call/scalar_method.rs b/crates/perry-codegen/src/lower_call/scalar_method.rs index adf36a6435..babf8bc16d 100644 --- a/crates/perry-codegen/src/lower_call/scalar_method.rs +++ b/crates/perry-codegen/src/lower_call/scalar_method.rs @@ -1,56 +1,271 @@ //! Scalar-replaced receiver method summaries. use anyhow::{bail, Result}; -use perry_hir::Expr; +use std::collections::HashMap; + +use perry_hir::{BinaryOp, Expr, UnaryOp}; use perry_types::Type; -use crate::expr::{lower_expr, nanbox_pointer_inline, FnCtx}; -use crate::native_value::{LoweredValue, NativeRep, SemanticKind}; -use crate::types::{DOUBLE, I1, I32, I64, PTR}; +use crate::expr::{ + emit_jsvalue_slot_store_on_block, i32_to_nanbox, lower_expr, lower_expr_as_i32, + nanbox_pointer_inline, FnCtx, +}; +use crate::native_value::{ + BufferAccessMode, LoweredValue, MaterializationReason, NativeFactUse, NativeRep, SemanticKind, +}; +use crate::types::{DOUBLE, I1, I32, I64, I8, PTR}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum ScalarMethodArgKind { ProvenNumeric, - GuardedF64Local, + GuardedF64Expr, + GuardedI32Local, Generic, } -fn scalar_method_arg_is_proven_numeric(arg: &Expr) -> bool { +#[derive(Clone, Debug)] +struct ScalarMethodArgPlan { + kind: ScalarMethodArgKind, + guard_locals: Vec, + expression_guard: bool, +} + +fn push_guard_local_once(locals: &mut Vec, id: u32) { + if !locals.contains(&id) { + locals.push(id); + } +} + +fn collect_guarded_numeric_arg_locals(ctx: &FnCtx<'_>, arg: &Expr) -> Option> { + fn walk(ctx: &FnCtx<'_>, arg: &Expr, locals: &mut Vec) -> bool { + match arg { + Expr::Integer(_) | Expr::Number(_) => true, + Expr::LocalGet(id) => { + if ctx.closure_captures.contains_key(id) + || ctx.boxed_vars.contains(id) + || ctx.module_globals.contains_key(id) + || !ctx.locals.contains_key(id) + || !ctx + .local_types + .get(id) + .is_some_and(|ty| matches!(ty, Type::Number | Type::Int32)) + { + return false; + } + push_guard_local_once(locals, *id); + true + } + Expr::Unary { op, operand } => { + matches!(op, UnaryOp::Pos | UnaryOp::Neg) && walk(ctx, operand, locals) + } + Expr::Binary { op, left, right } => { + matches!( + op, + BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Mod + ) && walk(ctx, left, locals) + && walk(ctx, right, locals) + } + _ => false, + } + } + + let mut locals = Vec::new(); + if walk(ctx, arg, &mut locals) { + Some(locals) + } else { + None + } +} + +fn local_can_use_public_arg_guard(ctx: &FnCtx<'_>, id: u32, expected: Type) -> bool { + !ctx.closure_captures.contains_key(&id) + && !ctx.boxed_vars.contains(&id) + && !ctx.module_globals.contains_key(&id) + && ctx.locals.contains_key(&id) + && ctx.local_types.get(&id).is_some_and(|ty| *ty == expected) +} + +fn scalar_method_arg_plan(ctx: &FnCtx<'_>, arg: &Expr, param_ty: &Type) -> ScalarMethodArgPlan { + if matches!(param_ty, Type::Int32) { + return match arg { + Expr::Integer(value) if i32::try_from(*value).is_ok() => ScalarMethodArgPlan { + kind: ScalarMethodArgKind::ProvenNumeric, + guard_locals: Vec::new(), + expression_guard: false, + }, + Expr::LocalGet(id) if local_can_use_public_arg_guard(ctx, *id, Type::Int32) => { + ScalarMethodArgPlan { + kind: ScalarMethodArgKind::GuardedI32Local, + guard_locals: vec![*id], + expression_guard: false, + } + } + _ => ScalarMethodArgPlan { + kind: ScalarMethodArgKind::Generic, + guard_locals: Vec::new(), + expression_guard: false, + }, + }; + } + + match collect_guarded_numeric_arg_locals(ctx, arg) { + Some(guard_locals) if guard_locals.is_empty() => ScalarMethodArgPlan { + kind: ScalarMethodArgKind::ProvenNumeric, + guard_locals, + expression_guard: false, + }, + Some(guard_locals) => ScalarMethodArgPlan { + kind: ScalarMethodArgKind::GuardedF64Expr, + guard_locals, + expression_guard: !matches!(arg, Expr::LocalGet(_)), + }, + None => ScalarMethodArgPlan { + kind: ScalarMethodArgKind::Generic, + guard_locals: Vec::new(), + expression_guard: false, + }, + } +} + +fn lower_int32_scalar_arg_fast( + ctx: &mut FnCtx<'_>, + arg: &Expr, + raw_i32_locals: &HashMap, +) -> Result { + match arg { + Expr::Integer(value) => i32::try_from(*value) + .map(|value| value.to_string()) + .map_err(|_| anyhow::anyhow!("scalar Int32 method literal out of range: {value}")), + Expr::LocalGet(id) => raw_i32_locals + .get(id) + .cloned() + .ok_or_else(|| anyhow::anyhow!("missing guarded scalar Int32 method arg local {id}")), + _ => lower_expr_as_i32(ctx, arg), + } +} + +fn lower_guarded_numeric_arg_fast( + ctx: &mut FnCtx<'_>, + arg: &Expr, + raw_locals: &HashMap, +) -> Result { match arg { - Expr::Integer(_) | Expr::Number(_) => true, + Expr::Integer(_) | Expr::Number(_) => lower_expr(ctx, arg), + Expr::LocalGet(id) => raw_locals + .get(id) + .cloned() + .ok_or_else(|| anyhow::anyhow!("missing guarded scalar method arg local {id}")), Expr::Unary { op, operand } => { - matches!(op, perry_hir::UnaryOp::Pos | perry_hir::UnaryOp::Neg) - && scalar_method_arg_is_proven_numeric(operand) + let value = lower_guarded_numeric_arg_fast(ctx, operand, raw_locals)?; + Ok(match op { + UnaryOp::Pos => value, + UnaryOp::Neg => ctx.block().fneg(&value), + _ => bail!("unsupported guarded scalar method unary arg"), + }) } Expr::Binary { op, left, right } => { - matches!( - op, - perry_hir::BinaryOp::Add - | perry_hir::BinaryOp::Sub - | perry_hir::BinaryOp::Mul - | perry_hir::BinaryOp::Div - | perry_hir::BinaryOp::Mod - ) && scalar_method_arg_is_proven_numeric(left) - && scalar_method_arg_is_proven_numeric(right) + let left = lower_guarded_numeric_arg_fast(ctx, left, raw_locals)?; + let right = lower_guarded_numeric_arg_fast(ctx, right, raw_locals)?; + Ok(match op { + BinaryOp::Add => ctx.block().fadd(&left, &right), + BinaryOp::Sub => ctx.block().fsub(&left, &right), + BinaryOp::Mul => ctx.block().fmul(&left, &right), + BinaryOp::Div => ctx.block().fdiv(&left, &right), + BinaryOp::Mod => ctx.block().frem(&left, &right), + _ => bail!("unsupported guarded scalar method binary arg"), + }) } - _ => false, + _ => bail!( + "unsupported guarded scalar method arg expression kind {}", + crate::expr::variant_name(arg) + ), } } -fn scalar_method_arg_kind(ctx: &FnCtx<'_>, arg: &Expr) -> ScalarMethodArgKind { - if scalar_method_arg_is_proven_numeric(arg) { - return ScalarMethodArgKind::ProvenNumeric; +fn load_scalar_method_arg_guard_value(ctx: &mut FnCtx<'_>, id: u32) -> Result { + let slot = ctx + .locals + .get(&id) + .cloned() + .ok_or_else(|| anyhow::anyhow!("missing scalar method arg guard local {id}"))?; + Ok(ctx.block().load(DOUBLE, &slot)) +} + +fn collect_guard_local_values( + ctx: &mut FnCtx<'_>, + arg_plans: &[ScalarMethodArgPlan], +) -> Result> { + let mut values = Vec::new(); + let mut seen = Vec::new(); + for plan in arg_plans { + if !matches!(plan.kind, ScalarMethodArgKind::GuardedF64Expr) { + continue; + } + for id in &plan.guard_locals { + if seen.contains(id) { + continue; + } + seen.push(*id); + values.push((*id, load_scalar_method_arg_guard_value(ctx, *id)?)); + } } - if let Expr::LocalGet(id) = arg { - if ctx - .local_types - .get(id) - .is_some_and(|ty| matches!(ty, Type::Number | Type::Int32)) - { - return ScalarMethodArgKind::GuardedF64Local; + Ok(values) +} + +fn collect_i32_guard_local_values( + ctx: &mut FnCtx<'_>, + arg_plans: &[ScalarMethodArgPlan], +) -> Result> { + let mut values = Vec::new(); + let mut seen = Vec::new(); + for plan in arg_plans { + if !matches!(plan.kind, ScalarMethodArgKind::GuardedI32Local) { + continue; + } + for id in &plan.guard_locals { + if seen.contains(id) { + continue; + } + seen.push(*id); + values.push((*id, load_scalar_method_arg_guard_value(ctx, *id)?)); } } - ScalarMethodArgKind::Generic + Ok(values) +} + +fn scalar_method_summary_fact( + receiver_id: u32, + class_name: &str, + property: &str, + state: &'static str, + detail: &'static str, +) -> NativeFactUse { + NativeFactUse { + fact_id: format!( + "native_region.scalar_method_summary.{receiver_id}.{class_name}.{property}" + ), + kind: "scalar_method_summary".to_string(), + local_id: Some(receiver_id), + state: state.to_string(), + detail: detail.to_string(), + reason: None, + } +} + +fn scalar_method_notes(class_name: &str, property: &str) -> Vec { + vec![ + format!("class={class_name}"), + format!("method={property}"), + "receiver=scalar_replaced".to_string(), + ] +} + +fn scalar_method_return_note(method: &perry_hir::Function) -> &'static str { + match method.return_type { + Type::Int32 => "summary_return=int32", + Type::Boolean => "summary_return=boolean", + _ => "summary_return=number", + } } fn lower_scalar_method_inline_body( @@ -60,6 +275,8 @@ fn lower_scalar_method_inline_body( property: &str, method: &perry_hir::Function, arg_values: &[String], + fact_detail: &'static str, + extra_notes: Vec, ) -> Result { let saved_locals = ctx.locals.clone(); let saved_local_types = ctx.local_types.clone(); @@ -79,16 +296,132 @@ fn lower_scalar_method_inline_body( let dummy_this = ctx.func.alloca_entry(DOUBLE); ctx.this_stack.push(dummy_this); - let result = match method.body.as_slice() { - [perry_hir::Stmt::Return(Some(expr))] => lower_expr(ctx, expr)?, - _ => unreachable!("simple scalar method summary only accepts one return"), + let mut result = None; + for stmt in &method.body { + match stmt { + perry_hir::Stmt::Let { + id, + ty, + init: Some(init), + .. + } => { + let value = lower_expr(ctx, init)?; + let slot = ctx.func.alloca_entry(DOUBLE); + ctx.block().store(DOUBLE, &value, &slot); + ctx.locals.insert(*id, slot); + ctx.local_types.insert(*id, ty.clone()); + } + perry_hir::Stmt::Return(Some(expr)) => { + result = Some(lower_expr(ctx, expr)?); + break; + } + _ => unreachable!("simple scalar method summary only accepts lets and one return"), + } + } + let result = result.expect("simple scalar method summary must return a value"); + + ctx.this_stack.truncate(saved_this_len); + ctx.class_stack.truncate(saved_class_len); + ctx.scalar_ctor_target.truncate(saved_scalar_ctor_len); + ctx.locals = saved_locals; + ctx.local_types = saved_local_types; + + let lowered = LoweredValue { + semantic: SemanticKind::JsValue, + rep: NativeRep::JsValue, + llvm_ty: DOUBLE, + value: result.clone(), }; + let mut notes = scalar_method_notes(class_name, property); + notes.push(scalar_method_return_note(method).to_string()); + notes.extend(extra_notes); + ctx.record_lowered_value_with_access_mode_and_facts( + "ScalarMethodCall", + Some(receiver_id), + "scalar_method_summary_inline", + &lowered, + None, + None, + None, + None, + None, + None, + vec![scalar_method_summary_fact( + receiver_id, + class_name, + property, + "consumed", + fact_detail, + )], + Vec::new(), + false, + false, + notes, + ); + + Ok(result) +} + +fn lower_scalar_method_int32_inline_body( + ctx: &mut FnCtx<'_>, + receiver_id: u32, + class_name: &str, + property: &str, + method: &perry_hir::Function, + arg_values: &[String], + fact_detail: &'static str, + extra_notes: Vec, +) -> Result { + let saved_locals = ctx.locals.clone(); + let saved_local_types = ctx.local_types.clone(); + let saved_i32_slots = ctx.i32_counter_slots.clone(); + let saved_this_len = ctx.this_stack.len(); + let saved_class_len = ctx.class_stack.len(); + let saved_scalar_ctor_len = ctx.scalar_ctor_target.len(); + + for (param, value) in method.params.iter().zip(arg_values.iter()) { + let slot = ctx.func.alloca_entry(I32); + ctx.block().store(I32, value, &slot); + ctx.i32_counter_slots.insert(param.id, slot); + ctx.local_types.insert(param.id, param.ty.clone()); + } + + ctx.scalar_ctor_target.push(receiver_id); + ctx.class_stack.push(class_name.to_string()); + let dummy_this = ctx.func.alloca_entry(DOUBLE); + ctx.this_stack.push(dummy_this); + + let mut raw_i32 = None; + for stmt in &method.body { + match stmt { + perry_hir::Stmt::Let { + id, + ty, + init: Some(init), + .. + } => { + let value = lower_expr_as_i32(ctx, init)?; + let slot = ctx.func.alloca_entry(I32); + ctx.block().store(I32, &value, &slot); + ctx.i32_counter_slots.insert(*id, slot); + ctx.local_types.insert(*id, ty.clone()); + } + perry_hir::Stmt::Return(Some(expr)) => { + raw_i32 = Some(lower_expr_as_i32(ctx, expr)?); + break; + } + _ => unreachable!("simple scalar method summary only accepts lets and one return"), + } + } + let raw_i32 = raw_i32.expect("simple scalar method summary must return a value"); + let result = i32_to_nanbox(ctx.block(), &raw_i32); ctx.this_stack.truncate(saved_this_len); ctx.class_stack.truncate(saved_class_len); ctx.scalar_ctor_target.truncate(saved_scalar_ctor_len); ctx.locals = saved_locals; ctx.local_types = saved_local_types; + ctx.i32_counter_slots = saved_i32_slots; let lowered = LoweredValue { semantic: SemanticKind::JsValue, @@ -96,7 +429,10 @@ fn lower_scalar_method_inline_body( llvm_ty: DOUBLE, value: result.clone(), }; - ctx.record_lowered_value( + let mut notes = scalar_method_notes(class_name, property); + notes.push(scalar_method_return_note(method).to_string()); + notes.extend(extra_notes); + ctx.record_lowered_value_with_access_mode_and_facts( "ScalarMethodCall", Some(receiver_id), "scalar_method_summary_inline", @@ -104,18 +440,74 @@ fn lower_scalar_method_inline_body( None, None, None, + None, + None, + None, + vec![scalar_method_summary_fact( + receiver_id, + class_name, + property, + "consumed", + fact_detail, + )], + Vec::new(), false, false, - vec![ - format!("class={class_name}"), - format!("method={property}"), - "receiver=scalar_replaced".to_string(), - ], + notes, ); Ok(result) } +fn record_scalar_method_materialized_fallback( + ctx: &mut FnCtx<'_>, + receiver_id: u32, + class_name: &str, + property: &str, + value: &str, + fallback_state: &'static str, + guard_note: Option<&'static str>, +) { + let lowered = LoweredValue { + semantic: SemanticKind::JsValue, + rep: NativeRep::JsValue, + llvm_ty: DOUBLE, + value: value.to_string(), + }; + let mut notes = scalar_method_notes(class_name, property); + notes.push(format!("scalar_method_fallback={fallback_state}")); + if let Some(guard) = guard_note { + notes.push(format!("arg_guard={guard}")); + } + ctx.record_lowered_value_with_access_mode_and_facts( + "ScalarMethodCall", + Some(receiver_id), + "scalar_method_summary_materialized_fallback", + &lowered, + None, + None, + Some(BufferAccessMode::DynamicFallback), + Some(MaterializationReason::RuntimeApi), + None, + None, + Vec::new(), + vec![scalar_method_summary_fact( + receiver_id, + class_name, + property, + fallback_state, + match fallback_state { + "generic_arg" => "generic_argument", + "arg_guard_failed" => "guarded_numeric_args_fallback", + _ => fallback_state, + }, + )], + false, + false, + notes, + ); +} + fn materialize_scalar_receiver( ctx: &mut FnCtx<'_>, receiver_id: u32, @@ -146,30 +538,222 @@ fn materialize_scalar_receiver( .max(field_slots.len() as u32); let class_id_str = class_id.to_string(); let field_count_str = field_count.to_string(); - let obj_handle = ctx.block().call( - I64, - "js_object_alloc", - &[(I32, &class_id_str), (I32, &field_count_str)], - ); + let parent_class_id = ctx + .classes + .get(class_name) + .and_then(|class| class.extends_name.as_deref()) + .and_then(|parent| ctx.class_ids.get(parent).copied()) + .unwrap_or(0); + let parent_class_id_str = parent_class_id.to_string(); + let (obj_handle, has_stable_keys) = + if let Some(keys_global_name) = ctx.class_keys_globals.get(class_name).cloned() { + let keys_slot = if let Some(slot) = ctx.class_keys_slots.get(class_name).cloned() { + slot + } else { + let slot = ctx.func.entry_init_load_global(&keys_global_name, I64); + ctx.class_keys_slots + .insert(class_name.to_string(), slot.clone()); + slot + }; + let keys_ptr = ctx.block().load(I64, &keys_slot); + ctx.pending_declares.push(( + "js_object_alloc_class_inline_keys".to_string(), + I64, + vec![I32, I32, I32, I64], + )); + let obj_handle = ctx.block().call( + I64, + "js_object_alloc_class_inline_keys", + &[ + (I32, &class_id_str), + (I32, &parent_class_id_str), + (I32, &field_count_str), + (I64, &keys_ptr), + ], + ); + emit_materialized_scalar_receiver_typed_shape_init(ctx, class_name, &obj_handle); + (obj_handle, true) + } else { + ( + ctx.block().call( + I64, + "js_object_alloc", + &[(I32, &class_id_str), (I32, &field_count_str)], + ), + false, + ) + }; for (field, slot) in field_slots { let value = ctx.block().load(DOUBLE, &slot); - let key_idx = ctx.strings.intern(&field); - let key_handle_global = format!("@{}", ctx.strings.entry(key_idx).handle_global); - let key_box = ctx.block().load(DOUBLE, &key_handle_global); - let key_bits = ctx.block().bitcast_double_to_i64(&key_box); - let key_raw = ctx - .block() - .and(I64, &key_bits, crate::nanbox::POINTER_MASK_I64); - ctx.block().call_void( - "js_object_set_field_by_name", - &[(I64, &obj_handle), (I64, &key_raw), (DOUBLE, &value)], - ); + if let (true, Some(field_index)) = ( + has_stable_keys, + crate::type_analysis::class_field_global_index(ctx, class_name, &field), + ) { + emit_materialized_scalar_receiver_direct_field_store( + ctx, + receiver_id, + class_name, + &field, + field_index, + &obj_handle, + &value, + ); + } else { + let key_idx = ctx.strings.intern(&field); + let key_handle_global = format!("@{}", ctx.strings.entry(key_idx).handle_global); + let key_box = ctx.block().load(DOUBLE, &key_handle_global); + let key_bits = ctx.block().bitcast_double_to_i64(&key_box); + let key_raw = ctx + .block() + .and(I64, &key_bits, crate::nanbox::POINTER_MASK_I64); + ctx.block().call_void( + "js_object_set_field_by_name", + &[(I64, &obj_handle), (I64, &key_raw), (DOUBLE, &value)], + ); + } } Ok(nanbox_pointer_inline(ctx.block(), &obj_handle)) } +fn emit_materialized_scalar_receiver_typed_shape_init( + ctx: &mut FnCtx<'_>, + class_name: &str, + obj_handle: &str, +) { + let Some(keys_global_name) = ctx.class_keys_globals.get(class_name).cloned() else { + return; + }; + let typed_layout = crate::typed_shape::class_typed_layout(ctx.classes, class_name); + let slot_count_str = typed_layout.slot_count.to_string(); + let raw_mask_word_count_str = typed_layout.raw_f64_mask_words.len().to_string(); + let pointer_mask_word_count_str = typed_layout.pointer_mask_words.len().to_string(); + let raw_mask_ref = if typed_layout.raw_f64_mask_words.is_empty() { + "null".to_string() + } else { + format!( + "@{}", + crate::typed_shape::raw_f64_mask_global_name_from_keys_global(&keys_global_name) + ) + }; + let pointer_mask_ref = if typed_layout.pointer_mask_words.is_empty() { + "null".to_string() + } else { + format!( + "@{}", + crate::typed_shape::mask_global_name_from_keys_global(&keys_global_name) + ) + }; + ctx.block().call_void( + "js_gc_init_typed_shape_layout", + &[ + (I64, obj_handle), + (I32, &slot_count_str), + (PTR, &raw_mask_ref), + (I32, &raw_mask_word_count_str), + (PTR, &pointer_mask_ref), + (I32, &pointer_mask_word_count_str), + ], + ); +} + +fn emit_materialized_scalar_receiver_direct_field_store( + ctx: &mut FnCtx<'_>, + receiver_id: u32, + class_name: &str, + field: &str, + field_index: u32, + obj_handle: &str, + value: &str, +) { + let field_idx_str = field_index.to_string(); + let field_ptr = { + let blk = ctx.block(); + let obj_ptr = blk.inttoptr(I64, obj_handle); + let fields_base = blk.gep(I8, &obj_ptr, &[(I64, "24")]); + blk.gep(DOUBLE, &fields_base, &[(I64, &field_idx_str)]) + }; + let is_raw_f64 = crate::type_analysis::class_field_declared_type(ctx, class_name, field) + .as_ref() + .is_some_and(crate::typed_shape::type_is_raw_f64_candidate); + let stored = if is_raw_f64 { + let raw = ctx.block().call( + DOUBLE, + "js_array_numeric_value_to_raw_f64", + &[(DOUBLE, value)], + ); + ctx.block().store(DOUBLE, &raw, &field_ptr); + LoweredValue::f64(raw) + } else { + let field_addr = ctx.block().ptrtoint(&field_ptr, I64); + emit_jsvalue_slot_store_on_block( + ctx.block(), + &field_ptr, + value, + obj_handle, + &field_idx_str, + true, + obj_handle, + &field_addr, + true, + ); + LoweredValue { + semantic: SemanticKind::JsValue, + rep: NativeRep::JsValue, + llvm_ty: DOUBLE, + value: value.to_string(), + } + }; + let mut notes = scalar_method_notes(class_name, ""); + notes.push(format!("field={field}")); + notes.push(format!("field_index={field_idx_str}")); + notes.push("receiver_materialization=direct_slot".to_string()); + notes.push("field_layout=fixed_slot_array".to_string()); + notes.push(format!("raw_f64_field={}", is_raw_f64 as u8)); + if is_raw_f64 { + notes.push("pointer_bitmap=non_pointer".to_string()); + notes.push("write_barrier=elided_raw_f64".to_string()); + } else { + notes.push("write_barrier=emitted_conservative".to_string()); + } + ctx.record_lowered_value_with_access_mode( + "ScalarReceiverMaterializeField", + Some(receiver_id), + "scalar_receiver_materialize.direct_field_store", + &stored, + None, + None, + Some(BufferAccessMode::CheckedNative), + Some(MaterializationReason::RuntimeApi), + false, + false, + notes, + ); + if is_raw_f64 { + let mut barrier_notes = scalar_method_notes(class_name, ""); + barrier_notes.push("reason=scalar_receiver_raw_f64_field_pointer_free".to_string()); + barrier_notes.push(format!("field={field}")); + barrier_notes.push(format!("field_index={field_idx_str}")); + barrier_notes.push("receiver_materialization=direct_slot".to_string()); + barrier_notes.push("field_layout=raw_f64_slot_array".to_string()); + barrier_notes.push("pointer_bitmap=non_pointer".to_string()); + ctx.record_lowered_value_with_access_mode( + "WriteBarrierElided", + Some(receiver_id), + "write_barrier.elided_scalar_receiver_materialize_raw_f64", + &stored, + None, + None, + None, + Some(MaterializationReason::RuntimeApi), + false, + false, + barrier_notes, + ); + } +} + fn lower_materialized_receiver_dispatch( ctx: &mut FnCtx<'_>, receiver_id: u32, @@ -213,6 +797,164 @@ fn lower_materialized_receiver_dispatch( )) } +fn lower_scalar_replaced_int32_method_call( + ctx: &mut FnCtx<'_>, + receiver_id: u32, + class_name: &str, + property: &str, + method: &perry_hir::Function, + args: &[Expr], +) -> Result { + let arg_plans: Vec<_> = args + .iter() + .zip(method.params.iter()) + .map(|(arg, param)| scalar_method_arg_plan(ctx, arg, ¶m.ty)) + .collect(); + + if arg_plans + .iter() + .any(|plan| matches!(plan.kind, ScalarMethodArgKind::Generic)) + { + let mut lowered_args = Vec::with_capacity(args.len()); + for arg in args { + lowered_args.push(lower_expr(ctx, arg)?); + } + let fallback = lower_materialized_receiver_dispatch( + ctx, + receiver_id, + class_name, + property, + &lowered_args, + )?; + record_scalar_method_materialized_fallback( + ctx, + receiver_id, + class_name, + property, + &fallback, + "generic_arg", + None, + ); + return Ok(fallback); + } + + if !arg_plans + .iter() + .any(|plan| matches!(plan.kind, ScalarMethodArgKind::GuardedI32Local)) + { + let mut raw_args = Vec::with_capacity(args.len()); + let raw_i32_locals = HashMap::new(); + for arg in args { + raw_args.push(lower_int32_scalar_arg_fast(ctx, arg, &raw_i32_locals)?); + } + return lower_scalar_method_int32_inline_body( + ctx, + receiver_id, + class_name, + property, + method, + &raw_args, + "exact_receiver_summary", + vec!["arg_proof=proven_int32".to_string()], + ); + } + + let guard_values = collect_i32_guard_local_values(ctx, &arg_plans)?; + let mut guard: Option = None; + for (_, value) in &guard_values { + let raw = ctx + .block() + .call(I32, "js_typed_i32_arg_guard", &[(DOUBLE, value.as_str())]); + let ok = ctx.block().icmp_ne(I32, &raw, "0"); + guard = Some(match guard { + Some(prev) => ctx.block().and(I1, &prev, &ok), + None => ok, + }); + } + + let fast_idx = ctx.new_block("scalar_method_arg_guard.fast"); + let fallback_idx = ctx.new_block("scalar_method_arg_guard.fallback"); + let merge_idx = ctx.new_block("scalar_method_arg_guard.merge"); + let fast_label = ctx.block_label(fast_idx); + let fallback_label = ctx.block_label(fallback_idx); + let merge_label = ctx.block_label(merge_idx); + if let Some(guard) = guard { + ctx.block().cond_br(&guard, &fast_label, &fallback_label); + } else { + ctx.block().br(&fast_label); + } + + ctx.current_block = fast_idx; + let mut raw_i32_locals = HashMap::new(); + for (id, value) in &guard_values { + raw_i32_locals.insert( + *id, + ctx.block() + .call(I32, "js_typed_i32_arg_to_raw", &[(DOUBLE, value.as_str())]), + ); + } + let mut fast_args = Vec::with_capacity(args.len()); + for arg in args { + fast_args.push(lower_int32_scalar_arg_fast(ctx, arg, &raw_i32_locals)?); + } + let guarded_arg_count = arg_plans + .iter() + .filter(|plan| matches!(plan.kind, ScalarMethodArgKind::GuardedI32Local)) + .count(); + let fast_value = lower_scalar_method_int32_inline_body( + ctx, + receiver_id, + class_name, + property, + method, + &fast_args, + "guarded_numeric_args_fast_path", + vec![ + "arg_guard=js_typed_i32_arg_guard".to_string(), + format!("guarded_arg_count={guarded_arg_count}"), + ], + )?; + let after_fast = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&merge_label); + } + + ctx.current_block = fallback_idx; + let mut lowered_args = Vec::with_capacity(args.len()); + for arg in args { + lowered_args.push(lower_expr(ctx, arg)?); + } + let fallback_value = lower_materialized_receiver_dispatch( + ctx, + receiver_id, + class_name, + property, + &lowered_args, + )?; + record_scalar_method_materialized_fallback( + ctx, + receiver_id, + class_name, + property, + &fallback_value, + "arg_guard_failed", + Some("js_typed_i32_arg_guard"), + ); + let after_fallback = ctx.block().label.clone(); + if !ctx.block().is_terminated() { + ctx.block().br(&merge_label); + } + + ctx.current_block = merge_idx; + Ok(ctx.block().phi( + DOUBLE, + &[ + (fast_value.as_str(), after_fast.as_str()), + (fallback_value.as_str(), after_fallback.as_str()), + ], + )) +} + pub(super) fn try_lower_scalar_replaced_method_call( ctx: &mut FnCtx<'_>, callee: &Expr, @@ -239,33 +981,57 @@ pub(super) fn try_lower_scalar_replaced_method_call( .cloned() else { return Ok(None); }; - let arg_kinds: Vec<_> = args + if matches!(method.return_type, Type::Int32) { + return Ok(Some(lower_scalar_replaced_int32_method_call( + ctx, + *receiver_id, + &class_name, + property, + &method, + args, + )?)); + } + let arg_plans: Vec<_> = args .iter() - .map(|arg| scalar_method_arg_kind(ctx, arg)) + .zip(method.params.iter()) + .map(|(arg, param)| scalar_method_arg_plan(ctx, arg, ¶m.ty)) .collect(); - let mut lowered_args = Vec::with_capacity(args.len()); - for arg in args { - lowered_args.push(lower_expr(ctx, arg)?); - } - - if arg_kinds + if arg_plans .iter() - .any(|kind| matches!(kind, ScalarMethodArgKind::Generic)) + .any(|plan| matches!(plan.kind, ScalarMethodArgKind::Generic)) { - return Ok(Some(lower_materialized_receiver_dispatch( + let mut lowered_args = Vec::with_capacity(args.len()); + for arg in args { + lowered_args.push(lower_expr(ctx, arg)?); + } + let fallback = lower_materialized_receiver_dispatch( ctx, *receiver_id, &class_name, property, &lowered_args, - )?)); + )?; + record_scalar_method_materialized_fallback( + ctx, + *receiver_id, + &class_name, + property, + &fallback, + "generic_arg", + None, + ); + return Ok(Some(fallback)); } - if !arg_kinds + if !arg_plans .iter() - .any(|kind| matches!(kind, ScalarMethodArgKind::GuardedF64Local)) + .any(|plan| matches!(plan.kind, ScalarMethodArgKind::GuardedF64Expr)) { + let mut lowered_args = Vec::with_capacity(args.len()); + for arg in args { + lowered_args.push(lower_expr(ctx, arg)?); + } return Ok(Some(lower_scalar_method_inline_body( ctx, *receiver_id, @@ -273,14 +1039,14 @@ pub(super) fn try_lower_scalar_replaced_method_call( property, &method, &lowered_args, + "exact_receiver_summary", + vec!["arg_proof=proven_numeric".to_string()], )?)); } + let guard_values = collect_guard_local_values(ctx, &arg_plans)?; let mut guard: Option = None; - for (kind, value) in arg_kinds.iter().zip(lowered_args.iter()) { - if !matches!(kind, ScalarMethodArgKind::GuardedF64Local) { - continue; - } + for (_, value) in &guard_values { let raw = ctx .block() .call(I32, "js_typed_f64_arg_guard", &[(DOUBLE, value.as_str())]); @@ -304,18 +1070,30 @@ pub(super) fn try_lower_scalar_replaced_method_call( } ctx.current_block = fast_idx; - let mut fast_args = Vec::with_capacity(lowered_args.len()); - for (kind, value) in arg_kinds.iter().zip(lowered_args.iter()) { - if matches!(kind, ScalarMethodArgKind::GuardedF64Local) { - fast_args.push(ctx.block().call( + let mut raw_locals = HashMap::new(); + for (id, value) in &guard_values { + raw_locals.insert( + *id, + ctx.block().call( DOUBLE, "js_typed_f64_arg_to_raw", &[(DOUBLE, value.as_str())], - )); - } else { - fast_args.push(value.clone()); - } + ), + ); + } + let mut fast_args = Vec::with_capacity(args.len()); + for arg in args { + fast_args.push(lower_guarded_numeric_arg_fast(ctx, arg, &raw_locals)?); } + let guarded_arg_count = arg_plans + .iter() + .filter(|plan| matches!(plan.kind, ScalarMethodArgKind::GuardedF64Expr)) + .count(); + let guard_note = if arg_plans.iter().any(|plan| plan.expression_guard) { + "public_numeric_expr" + } else { + "js_typed_f64_arg_guard" + }; let fast_value = lower_scalar_method_inline_body( ctx, *receiver_id, @@ -323,6 +1101,11 @@ pub(super) fn try_lower_scalar_replaced_method_call( property, &method, &fast_args, + "guarded_numeric_args_fast_path", + vec![ + format!("arg_guard={guard_note}"), + format!("guarded_arg_count={guarded_arg_count}"), + ], )?; let after_fast = ctx.block().label.clone(); if !ctx.block().is_terminated() { @@ -330,6 +1113,10 @@ pub(super) fn try_lower_scalar_replaced_method_call( } ctx.current_block = fallback_idx; + let mut lowered_args = Vec::with_capacity(args.len()); + for arg in args { + lowered_args.push(lower_expr(ctx, arg)?); + } let fallback_value = lower_materialized_receiver_dispatch( ctx, *receiver_id, @@ -337,6 +1124,15 @@ pub(super) fn try_lower_scalar_replaced_method_call( property, &lowered_args, )?; + record_scalar_method_materialized_fallback( + ctx, + *receiver_id, + &class_name, + property, + &fallback_value, + "arg_guard_failed", + Some(guard_note), + ); let after_fallback = ctx.block().label.clone(); if !ctx.block().is_terminated() { ctx.block().br(&merge_label); diff --git a/crates/perry-codegen/src/native_value/artifact.rs b/crates/perry-codegen/src/native_value/artifact.rs index 437623798b..c5dfb07f80 100644 --- a/crates/perry-codegen/src/native_value/artifact.rs +++ b/crates/perry-codegen/src/native_value/artifact.rs @@ -21,6 +21,7 @@ pub(crate) struct NativeFactUse { pub kind: String, pub local_id: Option, pub state: String, + pub detail: String, pub reason: Option, } @@ -45,6 +46,8 @@ pub(crate) enum NativeAbiTransitionOp { NativeHandleBox, PromiseBox, BoolToJsValue, + #[serde(rename = "bigint_box")] + BigIntBox, } #[derive(Debug, Clone, Serialize, PartialEq, Eq)] @@ -350,6 +353,9 @@ struct NativeRepSummary { unsafe_unchecked_unknown_bounds_accesses: usize, consumed_fact_count: usize, rejected_fact_count: usize, + consumed_fact_kind_counts: BTreeMap, + rejected_fact_kind_counts: BTreeMap, + typed_path_decision_counts: BTreeMap, raw_f64_layout_fact_counts: BTreeMap, js_value_bits_count: usize, write_barrier_elided_count: usize, @@ -372,6 +378,9 @@ impl NativeRepSummary { let mut unsafe_unchecked_unknown_bounds_accesses = 0; let mut consumed_fact_count = 0; let mut rejected_fact_count = 0; + let mut consumed_fact_kind_counts = BTreeMap::new(); + let mut rejected_fact_kind_counts = BTreeMap::new(); + let mut typed_path_decision_counts = BTreeMap::new(); let mut raw_f64_layout_fact_counts = BTreeMap::from([ ("consumed".to_string(), 0), ("rejected".to_string(), 0), @@ -430,6 +439,7 @@ impl NativeRepSummary { NativeAbiTransitionOp::NativeHandleBox => "native_handle_box", NativeAbiTransitionOp::PromiseBox => "promise_box", NativeAbiTransitionOp::BoolToJsValue => "bool_to_js_value", + NativeAbiTransitionOp::BigIntBox => "bigint_box", }; *native_abi_transition_op_counts .entry(op_name.to_string()) @@ -470,6 +480,52 @@ impl NativeRepSummary { } consumed_fact_count += record.consumed_facts.len(); rejected_fact_count += record.rejected_facts.len(); + for fact in &record.consumed_facts { + *consumed_fact_kind_counts + .entry(fact.kind.clone()) + .or_insert(0) += 1; + } + for fact in &record.rejected_facts { + *rejected_fact_kind_counts + .entry(fact.kind.clone()) + .or_insert(0) += 1; + } + if record + .notes + .iter() + .any(|note| note.contains("typed_clone=")) + || record + .consumed_facts + .iter() + .any(|fact| fact.kind.starts_with("typed_") || fact.kind == "type_fact") + { + *typed_path_decision_counts + .entry("selected".to_string()) + .or_insert(0) += 1; + } + if record.notes.iter().any(|note| { + note.contains("generic_wrapper=") + || note.contains("generic_method=") + || note.contains("generic_closure=") + }) || record.fallback_reason.is_some() + { + *typed_path_decision_counts + .entry("fallback".to_string()) + .or_insert(0) += 1; + } + if record + .notes + .iter() + .any(|note| note.contains("typed_clone_rejected=")) + || record + .rejected_facts + .iter() + .any(|fact| fact.kind.starts_with("typed_") || fact.kind == "type_fact") + { + *typed_path_decision_counts + .entry("rejected".to_string()) + .or_insert(0) += 1; + } for fact in record .consumed_facts .iter() @@ -494,6 +550,9 @@ impl NativeRepSummary { unsafe_unchecked_unknown_bounds_accesses, consumed_fact_count, rejected_fact_count, + consumed_fact_kind_counts, + rejected_fact_kind_counts, + typed_path_decision_counts, raw_f64_layout_fact_counts, js_value_bits_count, write_barrier_elided_count, @@ -550,7 +609,7 @@ pub(crate) fn write_native_rep_artifact_if_enabled( pid, wall_nonce, counter )); let artifact = NativeRepArtifact { - schema_version: 14, + schema_version: 15, module, records, pod_layouts: collect_pod_layouts(records), diff --git a/crates/perry-codegen/src/native_value/materialize.rs b/crates/perry-codegen/src/native_value/materialize.rs index fc9baf6c37..a285ae835a 100644 --- a/crates/perry-codegen/src/native_value/materialize.rs +++ b/crates/perry-codegen/src/native_value/materialize.rs @@ -1,8 +1,8 @@ use serde::Serialize; use crate::expr::FnCtx; -use crate::nanbox::POINTER_TAG_I64; -use crate::types::{DOUBLE, F32, I1, I32, I64, I8}; +use crate::nanbox::{BIGINT_TAG_I64, POINTER_TAG_I64}; +use crate::types::{DOUBLE, F32, I1, I128, I32, I64, I8}; use super::artifact::{NativeAbiTransitionOp, NativeAbiTransitionRecord}; use super::rep::{LoweredValue, NativeRep, SemanticKind}; @@ -51,7 +51,8 @@ fn transition_lossy(rep: &NativeRep, op: &NativeAbiTransitionOp) -> bool { | NativeAbiTransitionOp::PointerBox | NativeAbiTransitionOp::NativeHandleBox | NativeAbiTransitionOp::PromiseBox - | NativeAbiTransitionOp::BoolToJsValue => false, + | NativeAbiTransitionOp::BoolToJsValue + | NativeAbiTransitionOp::BigIntBox => false, } } @@ -219,6 +220,43 @@ pub(crate) fn materialize_promise_boundary_to_js_value( ) } +pub(crate) fn materialize_small_bigint_pointer_to_js_value( + ctx: &mut FnCtx<'_>, + ptr_i64: &str, + reason: MaterializationReason, +) -> String { + let tagged = ctx.block().or(I64, ptr_i64, BIGINT_TAG_I64); + let value = ctx.block().bitcast_i64_to_double(&tagged); + let materialized = LoweredValue { + semantic: SemanticKind::JsValue, + rep: NativeRep::JsValue, + llvm_ty: DOUBLE, + value: value.clone(), + }; + record_materialized_transition( + ctx, + "materialize_js_value", + "materialize_small_bigint", + &materialized, + NativeRep::SmallBigInt.name().to_string(), + NativeAbiTransitionOp::BigIntBox, + reason, + false, + ); + value +} + +fn box_small_bigint_i128_to_js_value(ctx: &mut FnCtx<'_>, value_i128: &str) -> String { + let lo = ctx.block().trunc(I128, value_i128, I64); + let hi_wide = ctx.block().ashr(I128, value_i128, "64"); + let hi = ctx.block().trunc(I128, &hi_wide, I64); + let ptr = ctx + .block() + .call(I64, "js_bigint_from_i128_parts", &[(I64, &lo), (I64, &hi)]); + let tagged = ctx.block().or(I64, &ptr, BIGINT_TAG_I64); + ctx.block().bitcast_i64_to_double(&tagged) +} + pub(crate) fn materialize_js_value_bits( ctx: &mut FnCtx<'_>, lowered: LoweredValue, @@ -245,9 +283,30 @@ pub(crate) fn materialize_js_value_bits( "materialize_promise_boundary_bits", ); } + if matches!(lowered.rep, NativeRep::SmallBigInt) { + let from_native_rep = lowered.rep.name().to_string(); + let value = box_small_bigint_i128_to_js_value(ctx, &lowered.value); + let bits = ctx.block().bitcast_double_to_i64(&value); + let materialized = LoweredValue::js_value_bits(bits.clone()); + record_transition( + ctx, + "materialize_js_value_bits", + "materialize_small_bigint_bits", + &materialized, + from_native_rep, + NativeRep::JsValueBits.name().to_string(), + NativeAbiTransitionOp::BigIntBox, + reason, + false, + ); + return bits; + } if matches!( lowered.rep, - NativeRep::BufferView(_) | NativeRep::PodRecord { .. } | NativeRep::PodRecordView { .. } + NativeRep::StringRef + | NativeRep::BufferView(_) + | NativeRep::PodRecord { .. } + | NativeRep::PodRecordView { .. } ) { let js_value = materialize_js_value(ctx, lowered, reason.clone()); let bits = ctx.block().bitcast_double_to_i64(&js_value); @@ -281,9 +340,11 @@ pub(crate) fn materialize_js_value_bits( NativeRep::BufferView(_) | NativeRep::PodRecord { .. } | NativeRep::PodRecordView { .. } + | NativeRep::StringRef | NativeRep::JsValueBits | NativeRep::NativeHandle - | NativeRep::PromiseBoundary => NativeAbiTransitionOp::None, + | NativeRep::PromiseBoundary + | NativeRep::SmallBigInt => NativeAbiTransitionOp::None, }; let lossy = transition_lossy(&lowered.rep, &conversion_op); let bits = match &lowered.rep { @@ -328,9 +389,11 @@ pub(crate) fn materialize_js_value_bits( NativeRep::BufferView(_) | NativeRep::PodRecord { .. } | NativeRep::PodRecordView { .. } + | NativeRep::StringRef | NativeRep::JsValueBits | NativeRep::NativeHandle - | NativeRep::PromiseBoundary => { + | NativeRep::PromiseBoundary + | NativeRep::SmallBigInt => { unreachable!("handled before direct js_value_bits materialization") } }; @@ -387,6 +450,22 @@ pub(crate) fn materialize_js_value( if matches!(&lowered.rep, NativeRep::PromiseBoundary) { return materialize_promise_boundary_to_js_value(ctx, lowered, reason); } + if matches!(&lowered.rep, NativeRep::SmallBigInt) { + let from_native_rep = lowered.rep.name().to_string(); + let value = box_small_bigint_i128_to_js_value(ctx, &lowered.value); + let materialized = LoweredValue::js_value(value.clone()); + record_materialized_transition( + ctx, + "materialize_js_value", + "materialize_small_bigint", + &materialized, + from_native_rep, + NativeAbiTransitionOp::BigIntBox, + reason, + false, + ); + return value; + } let from_native_rep = lowered.rep.name().to_string(); let conversion_op = match &lowered.rep { NativeRep::I32 | NativeRep::I64 => NativeAbiTransitionOp::SignedIntToFloat, @@ -399,13 +478,15 @@ pub(crate) fn materialize_js_value( NativeRep::I1 => NativeAbiTransitionOp::BoolToJsValue, NativeRep::F32 => NativeAbiTransitionOp::FloatExtend, NativeRep::F64 => NativeAbiTransitionOp::None, + NativeRep::StringRef => NativeAbiTransitionOp::PointerBox, NativeRep::BufferView(_) | NativeRep::PodRecord { .. } | NativeRep::PodRecordView { .. } | NativeRep::JsValueBits | NativeRep::JsValue | NativeRep::NativeHandle - | NativeRep::PromiseBoundary => NativeAbiTransitionOp::None, + | NativeRep::PromiseBoundary + | NativeRep::SmallBigInt => NativeAbiTransitionOp::None, }; let lossy = transition_lossy(&lowered.rep, &conversion_op); let value = match &lowered.rep { @@ -431,6 +512,10 @@ pub(crate) fn materialize_js_value( } NativeRep::BufferLen => ctx.block().uitofp(I32, &lowered.value, DOUBLE), NativeRep::F32 => ctx.block().fpext(F32, &lowered.value, DOUBLE), + NativeRep::StringRef => { + ctx.block() + .call(DOUBLE, "js_nanbox_string", &[(I64, &lowered.value)]) + } NativeRep::BufferView(_) => lowered.value.clone(), NativeRep::PodRecord { .. } => lowered.value.clone(), NativeRep::PodRecordView { .. } => lowered.value.clone(), @@ -438,7 +523,8 @@ pub(crate) fn materialize_js_value( NativeRep::JsValue | NativeRep::F64 | NativeRep::NativeHandle - | NativeRep::PromiseBoundary => lowered.value.clone(), + | NativeRep::PromiseBoundary + | NativeRep::SmallBigInt => lowered.value.clone(), }; let materialized = LoweredValue { semantic: lowered.semantic, @@ -470,6 +556,10 @@ pub(crate) fn materialize_js_value_without_record( let tagged = ctx.block().or(I64, &lowered.value, POINTER_TAG_I64); ctx.block().bitcast_i64_to_double(&tagged) } + NativeRep::StringRef => { + ctx.block() + .call(DOUBLE, "js_nanbox_string", &[(I64, &lowered.value)]) + } NativeRep::I1 => { let bits = ctx.block().select( I1, @@ -495,5 +585,6 @@ pub(crate) fn materialize_js_value_without_record( NativeRep::BufferView(_) | NativeRep::PodRecord { .. } | NativeRep::PodRecordView { .. } => lowered.value.clone(), + NativeRep::SmallBigInt => box_small_bigint_i128_to_js_value(ctx, &lowered.value), } } diff --git a/crates/perry-codegen/src/native_value/mod.rs b/crates/perry-codegen/src/native_value/mod.rs index 57cb7627ec..88097b6669 100644 --- a/crates/perry-codegen/src/native_value/mod.rs +++ b/crates/perry-codegen/src/native_value/mod.rs @@ -18,7 +18,8 @@ pub(crate) use buffer::{ pub(crate) use materialize::{ materialize_js_value, materialize_js_value_bits, materialize_js_value_without_record, materialize_native_handle_to_js_value, materialize_promise_boundary_to_js_value, - record_runtime_native_handle_box_transition, MaterializationReason, + materialize_small_bigint_pointer_to_js_value, record_runtime_native_handle_box_transition, + MaterializationReason, }; pub(crate) use pod::{ collect_pod_init_fields, field_expected_rep, layout_decision_for_type, layout_for_manifest_pod, diff --git a/crates/perry-codegen/src/native_value/rep.rs b/crates/perry-codegen/src/native_value/rep.rs index 70e27cc0d6..cc3d254ded 100644 --- a/crates/perry-codegen/src/native_value/rep.rs +++ b/crates/perry-codegen/src/native_value/rep.rs @@ -1,6 +1,6 @@ use serde::Serialize; -use crate::types::{LlvmType, DOUBLE, F32, I1, I32, I64, I8, PTR}; +use crate::types::{LlvmType, DOUBLE, F32, I1, I128, I32, I64, I8, PTR}; use super::buffer::{AliasState, BoundsState, BufferElem, BufferIndexUnit, BufferViewRep}; @@ -9,6 +9,7 @@ use super::buffer::{AliasState, BoundsState, BufferElem, BufferIndexUnit, Buffer pub(crate) enum SemanticKind { JsNumber, JsValue, + BigInt, TypedArrayElement, BufferObject, PodRecord, @@ -23,6 +24,11 @@ pub(crate) enum NativeRep { /// boxed values where preserving payload bits matters. JsValueBits, JsValue, + /// Raw, proven string-like runtime reference carried as an integer + /// (`StringHeader*` after string guards/unboxing). Public ABI remains + /// `JsValue`; this rep is for region-local string-key/string-ABI helper + /// boundaries that consume a raw string handle. + StringRef, I32, /// Legacy signed 64-bit scalar. Kept for existing native-library /// manifests that declare `"i64"` and expect a JS-number bridge. @@ -56,6 +62,11 @@ pub(crate) enum NativeRep { /// Raw promise handle at an async/native boundary. Region-local unless /// boxed by a dedicated promise-boundary transition. PromiseBoundary, + /// Compiler-owned BigInt value represented as raw native integer SSA. + /// JS-visible BigInt semantics are restored by allocating a BigInt object + /// and NaN-boxing it at the boundary. + #[serde(rename = "small_bigint")] + SmallBigInt, /// Region-local view over buffer bytes. This is not a JS pointer contract: /// it may be consumed only inside the native region that proved its bounds /// and alias facts. @@ -82,6 +93,7 @@ impl NativeRep { match self { Self::JsValueBits => "js_value_bits", Self::JsValue => "js_value", + Self::StringRef => "string_ref", Self::I32 => "i32", Self::I64 => "i64", Self::U32 => "u32", @@ -95,6 +107,7 @@ impl NativeRep { Self::HandleId => "handle_id", Self::NativeHandle => "native_handle", Self::PromiseBoundary => "promise_boundary", + Self::SmallBigInt => "small_bigint", Self::BufferView(_) => "buffer_view", Self::PodRecord { .. } => "pod_record", Self::PodRecordView { .. } => "pod_record_view", @@ -116,6 +129,7 @@ pub(crate) enum ExpectedNativeRep { I1, F64, F32, + StringRef, BufferLen, HandleId, // #854: expected-rep variants matched by is_rep but not yet constructed by @@ -201,6 +215,10 @@ impl LoweredValue { Self::new(SemanticKind::JsValue, NativeRep::JsValueBits, I64, value) } + pub(crate) fn string_ref(value: impl Into) -> Self { + Self::new(SemanticKind::JsValue, NativeRep::StringRef, I64, value) + } + pub(crate) fn native_handle(value: impl Into) -> Self { Self::new(SemanticKind::JsValue, NativeRep::NativeHandle, I64, value) } @@ -214,6 +232,10 @@ impl LoweredValue { ) } + pub(crate) fn small_bigint(value: impl Into) -> Self { + Self::new(SemanticKind::BigInt, NativeRep::SmallBigInt, I128, value) + } + pub(crate) fn buffer_view( data_ptr: impl Into, length: impl Into, @@ -259,6 +281,7 @@ impl LoweredValue { | (ExpectedNativeRep::I1, NativeRep::I1) | (ExpectedNativeRep::F64, NativeRep::F64) | (ExpectedNativeRep::F32, NativeRep::F32) + | (ExpectedNativeRep::StringRef, NativeRep::StringRef) | (ExpectedNativeRep::BufferLen, NativeRep::BufferLen) | (ExpectedNativeRep::HandleId, NativeRep::HandleId) | (ExpectedNativeRep::NativeHandle, NativeRep::NativeHandle) diff --git a/crates/perry-codegen/src/native_value/verify.rs b/crates/perry-codegen/src/native_value/verify.rs index 8ad9dad0e0..042536ed0e 100644 --- a/crates/perry-codegen/src/native_value/verify.rs +++ b/crates/perry-codegen/src/native_value/verify.rs @@ -10,7 +10,7 @@ use super::buffer::{AliasState, BoundsState, BufferAccessMode}; use super::materialize::MaterializationReason; use super::pod::recompute_layout_from_fields; use super::rep::NativeRep; -use crate::types::{DOUBLE, F32, I1, I32, I64, I8, PTR}; +use crate::types::{DOUBLE, F32, I1, I128, I32, I64, I8, PTR}; pub(crate) fn verify_native_rep_records(records: &[NativeRepRecord]) -> Result<()> { let mut errors = Vec::new(); @@ -238,6 +238,7 @@ pub(crate) fn verify_native_rep_records(records: &[NativeRepRecord]) -> Result<( record.function, record.block_label, record.consumer )); } + validate_fact_uses(record, &mut errors); validate_raw_f64_layout_facts(record, &mut errors); validate_packed_f64_loop_record(record, &mut errors); } @@ -252,6 +253,44 @@ pub(crate) fn verify_native_rep_records(records: &[NativeRepRecord]) -> Result<( Ok(()) } +fn validate_fact_uses(record: &NativeRepRecord, errors: &mut Vec) { + for (field, facts) in [ + ("consumed_facts", record.consumed_facts.as_slice()), + ("rejected_facts", record.rejected_facts.as_slice()), + ] { + for fact in facts { + if fact.fact_id.trim().is_empty() { + errors.push(format!( + "{}:{} {} {field} has empty fact_id", + record.function, record.block_label, record.consumer + )); + } + if fact.kind.trim().is_empty() { + errors.push(format!( + "{}:{} {} {field} has empty kind", + record.function, record.block_label, record.consumer + )); + } + if fact.state.trim().is_empty() { + errors.push(format!( + "{}:{} {} {field} has empty state", + record.function, record.block_label, record.consumer + )); + } + if field == "rejected_facts" + && fact.reason.is_none() + && fact.detail.trim().is_empty() + && !matches!(fact.state.as_str(), "rejected" | "invalidated" | "missing") + { + errors.push(format!( + "{}:{} {} rejected fact {} lacks reason/detail", + record.function, record.block_label, record.consumer, fact.fact_id + )); + } + } + } +} + fn raw_f64_checked_native_consumer(record: &NativeRepRecord) -> bool { matches!( record.consumer.as_str(), @@ -259,6 +298,8 @@ fn raw_f64_checked_native_consumer(record: &NativeRepRecord) -> bool { | "js_array_numeric_set_f64_unboxed" | "js_array_numeric_push_f64_unboxed" | "packed_f64_loop_load" + | "packed_i32_loop_load" + | "packed_u32_loop_load" | "packed_f64_loop_store" | "class_field_get.raw_f64_load" | "class_field_set.raw_f64_store" @@ -331,6 +372,8 @@ fn raw_f64_dynamic_fallback_record(record: &NativeRepRecord) -> bool { "js_typed_feedback_array_index_set_fallback_boxed" ) | ("PackedF64LoopGuard", "packed_f64_loop_fallback") + | ("PackedI32LoopGuard", "packed_i32_loop_fallback") + | ("PackedU32LoopGuard", "packed_u32_loop_fallback") | ("ClassFieldGet", "js_object_get_field_by_name_f64") | ("ClassFieldSet", "js_object_set_field_by_name") ) @@ -399,7 +442,13 @@ fn record_has_note(record: &NativeRepRecord, note: &str) -> bool { fn validate_packed_f64_loop_record(record: &NativeRepRecord, errors: &mut Vec) { if !matches!( record.consumer.as_str(), - "packed_f64_loop_guard" | "packed_f64_loop_load" | "packed_f64_loop_store" + "packed_f64_loop_guard" + | "packed_f64_loop_load" + | "packed_f64_loop_store" + | "packed_i32_loop_guard" + | "packed_i32_loop_load" + | "packed_u32_loop_guard" + | "packed_u32_loop_load" ) { return; } @@ -624,7 +673,7 @@ fn validate_native_abi_type_record( "string" | "ptr" | "i64_str" => { matches!( &record.native_rep, - NativeRep::NativeHandle | NativeRep::JsValue + NativeRep::StringRef | NativeRep::NativeHandle | NativeRep::JsValue ) } "bool" => matches!(&record.native_rep, NativeRep::I1 | NativeRep::I32), @@ -888,12 +937,14 @@ fn expected_llvm_type(rep: &NativeRep) -> Option<&'static str> { NativeRep::I1 => I1, NativeRep::F32 => F32, NativeRep::JsValueBits + | NativeRep::StringRef | NativeRep::I64 | NativeRep::U64 | NativeRep::USize | NativeRep::HandleId | NativeRep::NativeHandle | NativeRep::PromiseBoundary => I64, + NativeRep::SmallBigInt => I128, NativeRep::I32 | NativeRep::U32 => I32, NativeRep::BufferLen => I32, NativeRep::U8 => I8, @@ -1072,6 +1123,7 @@ fn valid_native_abi_transition( } NativeAbiTransitionOp::PromiseBox => from == "promise_boundary" && !lossy, NativeAbiTransitionOp::BoolToJsValue => from == "i1" && !lossy, + NativeAbiTransitionOp::BigIntBox => from == "small_bigint" && !lossy, }; } if to != NativeRep::JsValue.name() { @@ -1094,10 +1146,13 @@ fn valid_native_abi_transition( ) && lossy == matches!(from, "u64" | "usize" | "handle_id") } NativeAbiTransitionOp::FloatExtend => from == "f32" && !lossy, - NativeAbiTransitionOp::PointerBox => from == "native_handle" && !lossy, + NativeAbiTransitionOp::PointerBox => { + matches!(from, "native_handle" | "string_ref") && !lossy + } NativeAbiTransitionOp::NativeHandleBox => from == "native_handle" && !lossy, NativeAbiTransitionOp::PromiseBox => from == "promise_boundary" && !lossy, NativeAbiTransitionOp::BoolToJsValue => from == "i1" && !lossy, + NativeAbiTransitionOp::BigIntBox => from == "small_bigint" && !lossy, } } @@ -1161,10 +1216,66 @@ mod tests { kind: "raw_f64_layout".to_string(), local_id: None, state: state.to_string(), + detail: state.to_string(), reason, } } + fn type_fact( + state: &str, + detail: &str, + reason: Option, + ) -> NativeFactUse { + NativeFactUse { + fact_id: format!("test.type_fact.{state}.{detail}"), + kind: "type_fact".to_string(), + local_id: Some(1), + state: state.to_string(), + detail: detail.to_string(), + reason, + } + } + + #[test] + fn verifier_accepts_structured_consumed_and_rejected_facts() { + let mut r = record(); + r.consumed_facts + .push(type_fact("consumed", "packed_i32", None)); + r.rejected_facts.push(type_fact( + "rejected", + "unknown_call_escape", + Some(MaterializationReason::UnknownCallEscape), + )); + + assert!(verify_native_rep_records(&[r]).is_ok()); + } + + #[test] + fn verifier_rejects_malformed_fact_uses() { + let mut r = record(); + r.consumed_facts.push(NativeFactUse { + fact_id: String::new(), + kind: "type_fact".to_string(), + local_id: Some(1), + state: "consumed".to_string(), + detail: "packed_i32".to_string(), + reason: None, + }); + r.rejected_facts.push(NativeFactUse { + fact_id: "test.type_fact.rejected".to_string(), + kind: "type_fact".to_string(), + local_id: Some(1), + state: "guard_failed".to_string(), + detail: String::new(), + reason: None, + }); + + let err = verify_native_rep_records(&[r]).expect_err("malformed facts should fail"); + let text = err.to_string(); + assert!(text.contains("empty fact_id")); + assert!(text.contains("lacks reason/detail")); + } + fn packed_f64_loop_store_record() -> NativeRepRecord { let mut r = record(); r.expr_kind = "PackedF64LoopStore".to_string(); diff --git a/crates/perry-codegen/src/runtime_decls/objects.rs b/crates/perry-codegen/src/runtime_decls/objects.rs index 518e2dfa25..3a24517d70 100644 --- a/crates/perry-codegen/src/runtime_decls/objects.rs +++ b/crates/perry-codegen/src/runtime_decls/objects.rs @@ -263,6 +263,11 @@ pub fn declare_phase_b_objects(module: &mut LlModule) { I32, &[I64, DOUBLE], ); + module.declare_function( + "js_typed_feedback_packed_u32_array_loop_guard", + I32, + &[I64, DOUBLE], + ); module.declare_function( "js_typed_feedback_array_index_get_fallback_boxed", DOUBLE, diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs index 853cd7be8b..00bee1e6e3 100644 --- a/crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs +++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs @@ -1803,8 +1803,12 @@ pub fn declare_stdlib_ffi(module: &mut LlModule) { // immediately. module.declare_function("js_iter_result_set", DOUBLE, &[DOUBLE, I32]); module.declare_function("js_iter_result_set_f64", DOUBLE, &[DOUBLE, I32]); + module.declare_function("js_iter_result_set_i32", DOUBLE, &[I32, I32]); + module.declare_function("js_iter_result_set_i1", DOUBLE, &[I32, I32]); module.declare_function("js_iter_result_get_value", DOUBLE, &[]); module.declare_function("js_iter_result_get_value_f64", DOUBLE, &[]); + module.declare_function("js_iter_result_get_value_i32", I32, &[]); + module.declare_function("js_iter_result_get_value_i1", I32, &[]); module.declare_function("js_iter_result_get_done", DOUBLE, &[]); // Optimized async-step chain: replaces // `Promise.resolve(value).then(then_v_arrow, then_e_arrow)` in diff --git a/crates/perry-codegen/src/runtime_decls/strings.rs b/crates/perry-codegen/src/runtime_decls/strings.rs index fbf510477c..c7e5c4e7b8 100644 --- a/crates/perry-codegen/src/runtime_decls/strings.rs +++ b/crates/perry-codegen/src/runtime_decls/strings.rs @@ -324,11 +324,22 @@ pub fn declare_phase_b_strings(module: &mut LlModule) { module.declare_function("js_throw_error_with_code", VOID, &[PTR, I64, PTR, I64, I32]); module.declare_function("js_map_set", I64, &[I64, DOUBLE, DOUBLE]); module.declare_function("js_map_set_string_number", I64, &[I64, I64, DOUBLE]); + module.declare_function("js_map_set_string_key", I64, &[I64, I64, DOUBLE]); + module.declare_function("js_map_set_string_i32", I64, &[I64, I64, I32]); + module.declare_function("js_map_set_string_u32", I64, &[I64, I64, I32]); + module.declare_function("js_map_set_string_f32", I64, &[I64, I64, F32]); + module.declare_function("js_map_set_string_bool", I64, &[I64, I64, I32]); + module.declare_function("js_map_set_string_string", I64, &[I64, I64, I64]); + module.declare_function("js_map_set_number_key", I64, &[I64, DOUBLE, DOUBLE]); module.declare_function("js_map_get", DOUBLE, &[I64, DOUBLE]); module.declare_function("js_map_get_string_key", DOUBLE, &[I64, I64]); + module.declare_function("js_map_get_number_key", DOUBLE, &[I64, DOUBLE]); module.declare_function("js_map_has", I32, &[I64, DOUBLE]); module.declare_function("js_map_has_string_key", I32, &[I64, I64]); + module.declare_function("js_map_has_number_key", I32, &[I64, DOUBLE]); module.declare_function("js_map_delete", I32, &[I64, DOUBLE]); + module.declare_function("js_map_delete_string_key", I32, &[I64, I64]); + module.declare_function("js_map_delete_number_key", I32, &[I64, DOUBLE]); module.declare_function("js_object_keys", I64, &[I64]); module.declare_function("js_object_keys_value", I64, &[DOUBLE]); module.declare_function("js_for_in_keys_value", I64, &[DOUBLE]); @@ -525,10 +536,25 @@ pub fn declare_phase_b_strings(module: &mut LlModule) { module.declare_function("js_math_to_number", DOUBLE, &[DOUBLE]); module.declare_function("js_set_add", I64, &[I64, DOUBLE]); module.declare_function("js_set_add_string", I64, &[I64, I64]); + module.declare_function("js_set_add_number", I64, &[I64, DOUBLE]); + module.declare_function("js_set_add_i32", I64, &[I64, I32]); + module.declare_function("js_set_add_u32", I64, &[I64, I32]); + module.declare_function("js_set_add_f32", I64, &[I64, F32]); + module.declare_function("js_set_add_bool", I64, &[I64, I32]); module.declare_function("js_set_has", I32, &[I64, DOUBLE]); module.declare_function("js_set_has_string", I32, &[I64, I64]); + module.declare_function("js_set_has_number", I32, &[I64, DOUBLE]); + module.declare_function("js_set_has_i32", I32, &[I64, I32]); + module.declare_function("js_set_has_u32", I32, &[I64, I32]); + module.declare_function("js_set_has_f32", I32, &[I64, F32]); + module.declare_function("js_set_has_bool", I32, &[I64, I32]); module.declare_function("js_set_delete", I32, &[I64, DOUBLE]); module.declare_function("js_set_delete_string", I32, &[I64, I64]); + module.declare_function("js_set_delete_number", I32, &[I64, DOUBLE]); + module.declare_function("js_set_delete_i32", I32, &[I64, I32]); + module.declare_function("js_set_delete_u32", I32, &[I64, I32]); + module.declare_function("js_set_delete_f32", I32, &[I64, F32]); + module.declare_function("js_set_delete_bool", I32, &[I64, I32]); module.declare_function("js_set_size", I32, &[I64]); // #2872: ES2024 Set composition methods. module.declare_function("js_set_union", I64, &[I64, DOUBLE]); @@ -1083,6 +1109,7 @@ pub fn declare_phase_b_strings(module: &mut LlModule) { module.declare_function("js_string_addref", VOID, &[I64]); module.declare_function("js_bigint_from_string", I64, &[PTR, I32]); module.declare_function("js_bigint_from_f64", I64, &[DOUBLE]); + module.declare_function("js_bigint_from_i128_parts", I64, &[I64, I64]); module.declare_function("js_bigint_cmp", I32, &[I64, I64]); // Dynamic bigint arithmetic — lowered from `Expr::Binary` when // either operand is statically bigint-typed. These unbox, call diff --git a/crates/perry-codegen/src/stmt/loops.rs b/crates/perry-codegen/src/stmt/loops.rs index b0ad1cdc93..aa93a64b60 100644 --- a/crates/perry-codegen/src/stmt/loops.rs +++ b/crates/perry-codegen/src/stmt/loops.rs @@ -4,8 +4,8 @@ use super::*; use crate::expr::{ array_kind_fact, effect_fact, emit_typed_feedback_register_site, nanbox_pointer_inline, - raw_f64_layout_fact, BoundedIndexPair, IntRangeFact, PackedF64LoopFact, TypedFeedbackContract, - TypedFeedbackKind, + raw_f64_layout_fact, BoundedIndexPair, IntRangeFact, PackedF64LoopFact, PackedNumericLoopKind, + TypedFeedbackContract, TypedFeedbackKind, }; use crate::loop_purity::body_needs_asm_barrier; use crate::lower_conditional::lower_truthy; @@ -114,6 +114,7 @@ struct DynamicI32Bound { struct PackedF64VersionedLoop { counter_id: u32, array_id: u32, + array_kind: PackedNumericLoopKind, } fn match_numeric_bulk_fill_loop( @@ -295,17 +296,35 @@ fn lower_packed_f64_versioned_for( let arr_expr = perry_hir::Expr::LocalGet(matched.array_id); let arr_box = lower_expr(ctx, &arr_expr)?; + let guard_id = match matched.array_kind { + PackedNumericLoopKind::F64 => "packed_f64_array_loop_guard", + PackedNumericLoopKind::I32 => "packed_i32_array_loop_guard", + PackedNumericLoopKind::U32 => "packed_u32_array_loop_guard", + }; let feedback_site_id = emit_typed_feedback_register_site( ctx, TypedFeedbackKind::ArrayElement, - "array[packed_f64_loop]", - TypedFeedbackContract::packed_f64_array_loop(), + match matched.array_kind { + PackedNumericLoopKind::F64 => "array[packed_f64_loop]", + PackedNumericLoopKind::I32 => "array[packed_i32_loop]", + PackedNumericLoopKind::U32 => "array[packed_u32_loop]", + }, + match matched.array_kind { + PackedNumericLoopKind::F64 => TypedFeedbackContract::packed_f64_array_loop(), + PackedNumericLoopKind::I32 => TypedFeedbackContract::packed_i32_array_loop(), + PackedNumericLoopKind::U32 => TypedFeedbackContract::packed_u32_array_loop(), + }, ); let guard_ok = { let blk = ctx.block(); + let guard_fn = match matched.array_kind { + PackedNumericLoopKind::F64 => "js_typed_feedback_packed_f64_array_loop_guard", + PackedNumericLoopKind::I32 => "js_typed_feedback_packed_i32_array_loop_guard", + PackedNumericLoopKind::U32 => "js_typed_feedback_packed_u32_array_loop_guard", + }; let guard_i32 = blk.call( I32, - "js_typed_feedback_packed_f64_array_loop_guard", + guard_fn, &[(I64, &feedback_site_id), (DOUBLE, &arr_box)], ); blk.icmp_ne(I32, &guard_i32, "0") @@ -315,12 +334,14 @@ fn lower_packed_f64_versioned_for( ctx, matched.array_id, &arr_box, - "packed_f64_array_loop_guard", + guard_id, + matched.array_kind, ); - let fast_pre_idx = ctx.new_block("packed_f64.loop.fast.preheader"); - let slow_pre_idx = ctx.new_block("packed_f64.loop.slow.preheader"); - let merge_idx = ctx.new_block("packed_f64.loop.merge"); + let loop_label = matched.array_kind.loop_label(); + let fast_pre_idx = ctx.new_block(&format!("{loop_label}.loop.fast.preheader")); + let slow_pre_idx = ctx.new_block(&format!("{loop_label}.loop.slow.preheader")); + let merge_idx = ctx.new_block(&format!("{loop_label}.loop.merge")); let fast_pre_label = ctx.block_label(fast_pre_idx); let slow_pre_label = ctx.block_label(slow_pre_idx); let merge_label = ctx.block_label(merge_idx); @@ -334,10 +355,18 @@ fn lower_packed_f64_versioned_for( index_local_id: matched.counter_id, array_local_id: matched.array_id, scope_id: packed_scope_id, - guard_id: "packed_f64_array_loop_guard".to_string(), + guard_id: guard_id.to_string(), store_side_exit_label: slow_pre_label.clone(), + array_kind: matched.array_kind, }); - lower_for_after_init(ctx, init, condition, update, body, "for.packed_f64_fast")?; + lower_for_after_init( + ctx, + init, + condition, + update, + body, + &format!("for.{loop_label}_fast"), + )?; ctx.packed_f64_loop_facts .retain(|fact| fact.scope_id != packed_scope_id); if !ctx.block().is_terminated() { @@ -345,7 +374,14 @@ fn lower_packed_f64_versioned_for( } ctx.current_block = slow_pre_idx; - lower_for_after_init(ctx, init, condition, update, body, "for.packed_f64_slow")?; + lower_for_after_init( + ctx, + init, + condition, + update, + body, + &format!("for.{loop_label}_slow"), + )?; if !ctx.block().is_terminated() { ctx.block().br(&merge_label); } @@ -359,12 +395,13 @@ fn record_packed_f64_loop_guard_artifacts( arr_id: u32, arr_box: &str, guard_id: &str, + array_kind: PackedNumericLoopKind, ) { let guarded_arr = LoweredValue::js_value(arr_box.to_string()); ctx.record_lowered_value_with_access_mode_and_facts( - "PackedF64LoopGuard", + array_kind.guard_expr_kind(), Some(arr_id), - "packed_f64_loop_guard", + array_kind.guard_consumer(), &guarded_arr, Some(BoundsState::Guarded { guard_id: guard_id.to_string(), @@ -375,24 +412,30 @@ fn record_packed_f64_loop_guard_artifacts( None, None, vec![ - array_kind_fact(Some(arr_id), "consumed", "packed_f64", None), + array_kind_fact( + Some(arr_id), + "consumed", + array_kind.array_kind_label(), + None, + ), raw_f64_layout_fact(Some(arr_id), "consumed", guard_id, None), ], Vec::new(), false, false, vec![ - "loop_versioning=packed_f64".to_string(), + format!("loop_versioning={}", array_kind.loop_label()), "index_range=nonnegative_i32".to_string(), "length_range=guarded_i32".to_string(), + "storage_layout=raw_f64_numeric_slots".to_string(), ], ); let fallback_arr = LoweredValue::js_value(arr_box.to_string()); ctx.record_lowered_value_with_access_mode_and_facts( - "PackedF64LoopGuard", + array_kind.guard_expr_kind(), Some(arr_id), - "packed_f64_loop_fallback", + array_kind.fallback_consumer(), &fallback_arr, Some(BoundsState::Unknown), None, @@ -405,7 +448,7 @@ fn record_packed_f64_loop_guard_artifacts( array_kind_fact( Some(arr_id), "rejected", - "packed_f64", + array_kind.array_kind_label(), Some(MaterializationReason::RuntimeApi), ), raw_f64_layout_fact( @@ -423,7 +466,10 @@ fn record_packed_f64_loop_guard_artifacts( ], false, false, - vec!["loop_versioning=fallback".to_string()], + vec![format!( + "loop_versioning={}_fallback", + array_kind.loop_label() + )], ); } @@ -500,20 +546,30 @@ fn match_packed_f64_versioned_loop( { return None; } - let body_is_supported_store = - body_is_supported_packed_f64_loop_store(ctx, body, hoist.arr_id, hoist.counter_id); - let array_proof_ok = if body_is_supported_store { - ctx.native_facts.proves_noalias_array(hoist.arr_id) + let store_array_kind = + supported_packed_numeric_loop_store_kind(ctx, body, hoist.arr_id, hoist.counter_id); + let array_kind = if let Some(store_array_kind) = store_array_kind { + if !ctx.native_facts.proves_noalias_array(hoist.arr_id) { + return None; + } + store_array_kind + } else if ctx.native_facts.proves_packed_i32_array(hoist.arr_id) + && local_is_int32_array(ctx, hoist.arr_id) + { + PackedNumericLoopKind::I32 + } else if ctx.native_facts.proves_packed_u32_array(hoist.arr_id) + && local_is_u32_array(ctx, hoist.arr_id) + { + PackedNumericLoopKind::U32 + } else if ctx.native_facts.proves_packed_f64_array(hoist.arr_id) { + PackedNumericLoopKind::F64 } else { - ctx.native_facts.proves_packed_f64_array(hoist.arr_id) - }; - if !array_proof_ok { return None; - } + }; if !local_is_number_array(ctx, hoist.arr_id) { return None; } - let body_is_supported = body_is_supported_store + let body_is_supported = store_array_kind.is_some() || body .iter() .all(|stmt| stmt_is_packed_f64_loop_safe(ctx, stmt, hoist.arr_id, hoist.counter_id)); @@ -523,6 +579,7 @@ fn match_packed_f64_versioned_loop( Some(PackedF64VersionedLoop { counter_id: hoist.counter_id, array_id: hoist.arr_id, + array_kind, }) } @@ -531,6 +588,29 @@ fn local_is_number_array(ctx: &FnCtx<'_>, local_id: u32) -> bool { ctx.local_types.get(&local_id), Some(perry_types::Type::Array(elem)) if matches!(elem.as_ref(), perry_types::Type::Number | perry_types::Type::Int32) + || matches!(elem.as_ref(), perry_types::Type::Named(name) if name == "PerryU32") + ) +} + +fn local_allows_packed_f64_loop_store(ctx: &FnCtx<'_>, local_id: u32) -> bool { + matches!( + ctx.local_types.get(&local_id), + Some(perry_types::Type::Array(elem)) if matches!(elem.as_ref(), perry_types::Type::Number) + ) +} + +fn local_is_int32_array(ctx: &FnCtx<'_>, local_id: u32) -> bool { + matches!( + ctx.local_types.get(&local_id), + Some(perry_types::Type::Array(elem)) if matches!(elem.as_ref(), perry_types::Type::Int32) + ) +} + +fn local_is_u32_array(ctx: &FnCtx<'_>, local_id: u32) -> bool { + matches!( + ctx.local_types.get(&local_id), + Some(perry_types::Type::Array(elem)) + if matches!(elem.as_ref(), perry_types::Type::Named(name) if name == "PerryU32") ) } @@ -578,22 +658,34 @@ fn stmt_is_packed_f64_loop_safe( } } -fn body_is_supported_packed_f64_loop_store( +fn supported_packed_numeric_loop_store_kind( ctx: &FnCtx<'_>, body: &[Stmt], arr_id: u32, counter_id: u32, -) -> bool { +) -> Option { let [Stmt::Expr(perry_hir::Expr::IndexSet { object, index, value, })] = body else { - return false; + return None; }; - is_packed_f64_loop_index(object, index, arr_id, counter_id) + if !is_packed_f64_loop_index(object, index, arr_id, counter_id) { + return None; + } + if local_is_int32_array(ctx, arr_id) + && expr_is_packed_i32_loop_store_rhs_safe(ctx, value, arr_id, counter_id) + { + return Some(PackedNumericLoopKind::I32); + } + if local_allows_packed_f64_loop_store(ctx, arr_id) && expr_is_packed_f64_loop_store_rhs_safe(ctx, value, arr_id, counter_id) + { + return Some(PackedNumericLoopKind::F64); + } + None } fn expr_is_packed_f64_loop_store_rhs_safe( @@ -604,23 +696,101 @@ fn expr_is_packed_f64_loop_store_rhs_safe( ) -> bool { use perry_hir::Expr; - if !crate::type_analysis::is_numeric_expr(ctx, expr) { - return false; - } match expr { Expr::IndexGet { object, index } => { is_packed_f64_loop_index(object, index, arr_id, counter_id) } - Expr::LocalGet(id) => *id != arr_id, + Expr::LocalGet(id) => *id != arr_id && crate::type_analysis::is_numeric_expr(ctx, expr), Expr::Number(_) | Expr::Integer(_) => true, Expr::Binary { left, right, .. } => { + if !crate::type_analysis::is_numeric_expr(ctx, expr) { + return false; + } expr_is_packed_f64_loop_store_rhs_safe(ctx, left, arr_id, counter_id) && expr_is_packed_f64_loop_store_rhs_safe(ctx, right, arr_id, counter_id) } + Expr::MathAbs(value) => { + expr_is_packed_f64_loop_store_abs_rhs_safe(ctx, value, arr_id, counter_id) + } _ => false, } } +fn expr_is_packed_f64_loop_store_abs_rhs_safe( + ctx: &FnCtx<'_>, + expr: &perry_hir::Expr, + arr_id: u32, + counter_id: u32, +) -> bool { + crate::type_analysis::is_numeric_expr(ctx, expr) + && matches!( + expr, + perry_hir::Expr::IndexGet { object, index } + if is_packed_f64_loop_index(object, index, arr_id, counter_id) + ) +} + +fn expr_is_packed_i32_loop_store_rhs_safe( + ctx: &FnCtx<'_>, + expr: &perry_hir::Expr, + arr_id: u32, + counter_id: u32, +) -> bool { + use perry_hir::{BinaryOp, Expr}; + + match expr { + Expr::IndexGet { object, index } => { + is_packed_f64_loop_index(object, index, arr_id, counter_id) + } + Expr::LocalGet(id) => *id != arr_id && local_is_int32_value(ctx, *id), + Expr::Integer(n) => (i32::MIN as i64..=i32::MAX as i64).contains(n), + Expr::Number(n) + if n.is_finite() + && n.fract() == 0.0 + && *n >= i32::MIN as f64 + && *n <= i32::MAX as f64 => + { + true + } + Expr::MathImul(left, right) => { + expr_is_packed_i32_loop_store_rhs_safe(ctx, left, arr_id, counter_id) + && expr_is_packed_i32_loop_store_rhs_safe(ctx, right, arr_id, counter_id) + } + Expr::Binary { + op: BinaryOp::BitOr, + left, + right, + } if matches!(right.as_ref(), Expr::Integer(0)) => { + expr_is_packed_i32_loop_store_rhs_safe(ctx, left, arr_id, counter_id) + } + Expr::Binary { op, left, right } + if matches!( + op, + BinaryOp::Add + | BinaryOp::Sub + | BinaryOp::Mul + | BinaryOp::BitAnd + | BinaryOp::BitOr + | BinaryOp::BitXor + | BinaryOp::Shl + | BinaryOp::Shr + | BinaryOp::UShr + ) => + { + expr_is_packed_i32_loop_store_rhs_safe(ctx, left, arr_id, counter_id) + && expr_is_packed_i32_loop_store_rhs_safe(ctx, right, arr_id, counter_id) + } + _ => false, + } +} + +fn local_is_int32_value(ctx: &FnCtx<'_>, local_id: u32) -> bool { + matches!( + ctx.local_types.get(&local_id), + Some(perry_types::Type::Int32) + ) || ctx.integer_locals.contains(&local_id) +} + fn expr_is_packed_f64_loop_safe( ctx: &FnCtx<'_>, expr: &perry_hir::Expr, diff --git a/crates/perry-codegen/src/types.rs b/crates/perry-codegen/src/types.rs index 5936a79b2d..e460aa693d 100644 --- a/crates/perry-codegen/src/types.rs +++ b/crates/perry-codegen/src/types.rs @@ -7,6 +7,7 @@ pub type LlvmType = &'static str; pub const DOUBLE: LlvmType = "double"; pub const F32: LlvmType = "float"; +pub const I128: LlvmType = "i128"; pub const I64: LlvmType = "i64"; pub const I32: LlvmType = "i32"; pub const I16: LlvmType = "i16"; diff --git a/crates/perry-codegen/tests/native_proof_regressions.rs b/crates/perry-codegen/tests/native_proof_regressions.rs index 220ab466dc..9855f1b858 100644 --- a/crates/perry-codegen/tests/native_proof_regressions.rs +++ b/crates/perry-codegen/tests/native_proof_regressions.rs @@ -483,6 +483,26 @@ fn number_array_let(id: u32, name: &str, values: Vec) -> Stmt { } } +fn int32_array_let(id: u32, name: &str, values: Vec) -> Stmt { + Stmt::Let { + id, + name: name.to_string(), + ty: Type::Array(Box::new(Type::Int32)), + mutable: true, + init: Some(Expr::Array(values.into_iter().map(int).collect())), + } +} + +fn u32_array_let(id: u32, name: &str, values: Vec) -> Stmt { + Stmt::Let { + id, + name: name.to_string(), + ty: Type::Array(Box::new(Type::Named("PerryU32".to_string()))), + mutable: true, + init: Some(Expr::Array(values.into_iter().map(int).collect())), + } +} + fn bit_or_zero(value: Expr) -> Expr { Expr::Binary { op: BinaryOp::BitOr, @@ -491,6 +511,14 @@ fn bit_or_zero(value: Expr) -> Expr { } } +fn ushr_zero(value: Expr) -> Expr { + Expr::Binary { + op: BinaryOp::UShr, + left: Box::new(value), + right: Box::new(int(0)), + } +} + fn div(left: Expr, right: Expr) -> Expr { Expr::Binary { op: BinaryOp::Div, @@ -682,7 +710,7 @@ fn artifact_schema_v6_records_consumed_native_facts_for_buffer_region() { ]; let artifact = compile_artifact_json("artifact_positive_buffer_region.ts", body); - assert_eq!(artifact["schema_version"], 14); + assert_eq!(artifact["schema_version"], 15); let records = artifact["records"].as_array().unwrap(); assert!( records.iter().any(|record| { @@ -715,7 +743,7 @@ fn artifact_schema_v6_records_rejected_facts_for_buffer_fallback() { ]; let artifact = compile_artifact_json("artifact_rejected_buffer_region.ts", body); - assert_eq!(artifact["schema_version"], 14); + assert_eq!(artifact["schema_version"], 15); let records = artifact["records"].as_array().unwrap(); assert!( records.iter().any(|record| { @@ -761,7 +789,7 @@ fn artifact_schema_v6_records_c_layout_pod_manifest() { ]; let artifact = compile_artifact_json("artifact_c_layout_pod_record.ts", body); - assert_eq!(artifact["schema_version"], 14); + assert_eq!(artifact["schema_version"], 15); assert_eq!(artifact["summary"]["pod_layout_count"], 1); assert_eq!(artifact["summary"]["pod_record_count"], 1); let layouts = artifact["pod_layouts"].as_array().unwrap(); @@ -1258,7 +1286,7 @@ fn artifact_schema_v6_records_pod_dynamic_write_fallback() { ]; let artifact = compile_artifact_json("artifact_c_layout_pod_dynamic_write.ts", body); - assert_eq!(artifact["schema_version"], 14); + assert_eq!(artifact["schema_version"], 15); assert!( artifact["records"] .as_array() @@ -1484,7 +1512,7 @@ fn artifact_schema_v8_rejects_inexact_pod_initializer_values() { ]; let artifact = compile_artifact_json("artifact_c_layout_pod_init_reject.ts", body); - assert_eq!(artifact["schema_version"], 14); + assert_eq!(artifact["schema_version"], 15); assert_eq!(artifact["summary"]["pod_layout_count"], 0); assert_eq!(artifact["summary"]["pod_record_count"], 0); assert!(artifact["pod_layouts"].as_array().unwrap().is_empty()); @@ -1535,7 +1563,7 @@ fn artifact_schema_v6_records_pod_pointerful_field_rejection() { ]; let artifact = compile_artifact_json("artifact_c_layout_pod_reject.ts", body); - assert_eq!(artifact["schema_version"], 14); + assert_eq!(artifact["schema_version"], 15); assert_eq!(artifact["summary"]["pod_layout_count"], 0); assert!(artifact["pod_layouts"].as_array().unwrap().is_empty()); assert!( @@ -1856,6 +1884,63 @@ fn record_has_raw_f64_layout_fact(record: &serde_json::Value, list: &str, state: }) } +fn record_has_array_kind_fact( + record: &serde_json::Value, + list: &str, + state: &str, + detail: &str, +) -> bool { + record[list].as_array().is_some_and(|facts| { + facts.iter().any(|fact| { + fact["kind"] == "array_kind" + && fact["state"] == state + && fact["fact_id"] + .as_str() + .is_some_and(|fact_id| fact_id.ends_with(detail)) + }) + }) +} + +fn record_has_scalar_method_summary_fact( + record: &serde_json::Value, + list: &str, + state: &str, +) -> bool { + record[list].as_array().is_some_and(|facts| { + facts + .iter() + .any(|fact| fact["kind"] == "scalar_method_summary" && fact["state"] == state) + }) +} + +fn record_has_scalar_method_summary_detail( + record: &serde_json::Value, + list: &str, + state: &str, + detail: &str, +) -> bool { + record[list].as_array().is_some_and(|facts| { + facts.iter().any(|fact| { + fact["kind"] == "scalar_method_summary" + && fact["state"] == state + && fact["detail"] == detail + }) + }) +} + +fn record_has_type_fact( + record: &serde_json::Value, + list: &str, + fact_id: &str, + state: &str, +) -> bool { + record[list].as_array().is_some_and(|facts| { + facts.iter().any(|fact| { + fact["kind"] == "type_fact" && fact["fact_id"] == fact_id && fact["state"] == state + }) + }) +} + fn record_has_note(record: &serde_json::Value, expected: &str) -> bool { record["notes"] .as_array() @@ -1919,6 +2004,85 @@ fn artifact_records_native_module_handle_and_promise_boundary_boxing() { ); } +#[test] +fn small_bigint_literal_stays_i128_until_js_boundary() { + let body = vec![Stmt::Return(Some(Expr::BigInt( + "0x7fff_ffff_ffff_ffffn".to_string(), + )))]; + let module = module_with_classes_and_params( + "artifact_small_bigint_literal.ts", + Vec::new(), + Vec::new(), + Type::BigInt, + body, + ); + let ir = compile_ir_for_module_with_opts(module.clone(), empty_opts()).unwrap(); + assert!( + ir.contains("call i64 @js_bigint_from_i128_parts") + && !ir.contains("call i64 @js_bigint_from_string"), + "small BigInt literals should allocate from native i128 parts, not parse strings:\n{ir}" + ); + + let artifact = compile_artifact_json_for_module(module); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "BigInt" + && record["consumer"] == "ordinary_expr_value.small_bigint_literal_i128" + && record["native_rep_name"] == "small_bigint" + && record["llvm_ty"] == "i128" + && record["native_value_state"] == "region_local" + && record_has_note(record, "proof=bigint_literal_fits_i128") + }), + "expected small BigInt literal to be recorded as region-local i128:\n{artifact:#}" + ); + assert!( + records.iter().any(|record| { + record["consumer"] == "materialize_small_bigint" + && record["native_value_state"] == "materialized" + && record["native_abi_transition"]["from_native_rep"] == "small_bigint" + && record["native_abi_transition"]["to_native_rep"] == "js_value" + && record["native_abi_transition"]["op"] == "bigint_box" + && record["native_abi_transition"]["lossy"] == false + }), + "expected small BigInt literal to box only at JS boundary:\n{artifact:#}" + ); +} + +#[test] +fn oversized_bigint_literal_records_small_bigint_rejection_and_falls_back() { + let too_wide = format!("0x1{}n", "0".repeat(32)); + let body = vec![Stmt::Return(Some(Expr::BigInt(too_wide)))]; + let module = module_with_classes_and_params( + "artifact_oversized_bigint_literal.ts", + Vec::new(), + Vec::new(), + Type::BigInt, + body, + ); + let ir = compile_ir_for_module_with_opts(module.clone(), empty_opts()).unwrap(); + assert!( + ir.contains("call i64 @js_bigint_from_string"), + "oversized BigInt literals must keep the arbitrary-precision parser fallback:\n{ir}" + ); + + let artifact = compile_artifact_json_for_module(module); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "BigInt" + && record["consumer"] == "ordinary_expr_value.small_bigint_literal_rejected" + && record["access_mode"] == "dynamic_fallback" + && record_has_note( + record, + "small_bigint_rejected=literal_outside_i128_or_invalid", + ) + && record_has_note(record, "fallback=js_bigint_from_string") + }), + "expected oversized BigInt literal rejection evidence before fallback:\n{artifact:#}" + ); +} + #[test] fn native_library_manifest_lowercase_abi_returns_emit_signatures_and_artifacts() { let opts = native_library_opts(vec![ @@ -2543,151 +2707,171 @@ fn packed_f64_loop_store_update_versions_with_side_exit() { } #[test] -fn map_string_number_set_has_use_string_key_specialization() { +fn packed_i32_loop_read_materializes_integer_native_load_with_fallback() { let module = module_with_classes_and_params( - "map_string_number_specialization.ts", + "packed_i32_loop_read.ts", + Vec::new(), Vec::new(), - vec![ - param(2, "key", Type::String), - param(3, "value", Type::Number), - ], Type::Number, vec![ - Stmt::Let { - id: 1, - name: "m".to_string(), - ty: map_type(Type::String, Type::Number), - mutable: true, - init: Some(Expr::MapNew), - }, - Stmt::Expr(Expr::MapSet { - map: Box::new(local(1)), - key: Box::new(local(2)), - value: Box::new(local(3)), - }), - Stmt::If { - condition: Expr::MapHas { - map: Box::new(local(1)), - key: Box::new(local(2)), - }, - then_branch: vec![Stmt::Return(Some(Expr::MapGet { - map: Box::new(local(1)), - key: Box::new(local(2)), - }))], - else_branch: Some(vec![Stmt::Return(Some(Expr::Number(0.0)))]), - }, + int32_array_let(1, "values", vec![1, 2, 3]), + number_let(3, "sum", true, int(0)), + for_loop( + 4, + length(1), + vec![Stmt::Expr(Expr::LocalSet( + 3, + Box::new(bit_or_zero(add(local(3), index_get(1, local(4))))), + ))], + ), + Stmt::Return(Some(local(3))), ], ); - let ir = compile_ir_for_module_with_opts(module, empty_opts()).unwrap(); - let probe_ir = function_ir_section(&ir, "perry_fn_map_string_number_specialization_ts__probe"); + let ir = compile_ir_for_module_with_opts(module.clone(), empty_opts()).unwrap(); assert!( - probe_ir.contains("call i64 @js_map_set_string_number"), - "Map.set should lower through the string-key/f64 helper:\n{probe_ir}" + ir.contains("call i32 @js_typed_feedback_packed_i32_array_loop_guard"), + "packed-i32 loop should use the i32-specific raw numeric layout guard:\n{ir}" ); assert!( - probe_ir.contains("call i32 @js_map_has_string_key"), - "Map.has should lower through the string-key helper:\n{probe_ir}" + ir.contains("for.packed_i32_fast") && ir.contains("for.packed_i32_slow"), + "packed-i32 loop should emit fast and slow clones:\n{ir}" ); assert!( - probe_ir.contains("call double @js_map_get_string_key"), - "Map.get should lower through the string-key helper while keeping boxed miss semantics:\n{probe_ir}" + !ir.contains("for.packed_f64_fast"), + "Int32[] read loop should be tagged as packed-i32, not packed-f64:\n{ir}" ); + + let artifact = compile_artifact_json_for_module(module); + let records = artifact["records"].as_array().unwrap(); assert!( - !probe_ir.contains("call i64 @js_map_set("), - "specialized map.set path should not call the generic helper:\n{probe_ir}" + records.iter().any(|record| { + record["expr_kind"] == "PackedI32LoopGuard" + && record["consumer"] == "packed_i32_loop_guard" + && record["access_mode"] == "checked_native" + && record_has_array_kind_fact(record, "consumed_facts", "consumed", "packed_i32") + && record_has_raw_f64_layout_fact(record, "consumed_facts", "consumed") + }), + "expected packed-i32 guard proof record:\n{artifact:#}" ); assert!( - !probe_ir.contains("call double @js_map_get("), - "specialized map.get path should not call the generic helper:\n{probe_ir}" + records.iter().any(|record| { + record["expr_kind"] == "PackedI32LoopGuard" + && record["consumer"] == "packed_i32_loop_fallback" + && record["access_mode"] == "dynamic_fallback" + && record["materialization_reason"] == "runtime_api" + && record_has_array_kind_fact(record, "rejected_facts", "rejected", "packed_i32") + && record_has_raw_f64_layout_fact(record, "rejected_facts", "invalidated") + }), + "expected packed-i32 generic fallback evidence:\n{artifact:#}" ); assert!( - !probe_ir.contains("call i32 @js_map_has("), - "specialized map.has path should not call the generic helper:\n{probe_ir}" + records.iter().any(|record| { + record["expr_kind"] == "PackedI32LoopLoad" + && record["consumer"] == "packed_i32_loop_load" + && record["native_rep_name"] == "i32" + && record["llvm_ty"] == "i32" + && record["access_mode"] == "checked_native" + && record_has_array_kind_fact(record, "consumed_facts", "consumed", "packed_i32") + && record_has_raw_f64_layout_fact(record, "consumed_facts", "consumed") + && record_has_note(record, "integer_materialization=fptosi_guarded_packed_i32") + }), + "expected packed-i32 loop load to materialize an i32 native value:\n{artifact:#}" ); } #[test] -fn set_string_add_has_delete_use_string_specialization() { +fn packed_u32_loop_read_materializes_unsigned_native_load_with_fallback() { let module = module_with_classes_and_params( - "set_string_specialization.ts", + "packed_u32_loop_read.ts", Vec::new(), - vec![param(2, "value", Type::String)], - Type::Boolean, + Vec::new(), + Type::Number, vec![ - Stmt::Let { - id: 1, - name: "s".to_string(), - ty: set_type(Type::String), - mutable: true, - init: Some(Expr::SetNew), - }, - Stmt::Expr(Expr::SetAdd { - set_id: 1, - value: Box::new(local(2)), - }), - Stmt::Let { - id: 3, - name: "present".to_string(), - ty: Type::Boolean, - mutable: false, - init: Some(Expr::SetHas { - set: Box::new(local(1)), - value: Box::new(local(2)), - }), - }, - Stmt::Expr(Expr::SetDelete { - set: Box::new(local(1)), - value: Box::new(local(2)), - }), + u32_array_let(1, "values", vec![0, 4_000_000_000]), + number_let(3, "word", true, ushr_zero(int(0))), + for_loop( + 4, + length(1), + vec![Stmt::Expr(Expr::LocalSet( + 3, + Box::new(ushr_zero(index_get(1, local(4)))), + ))], + ), Stmt::Return(Some(local(3))), ], ); - let ir = compile_ir_for_module_with_opts(module, empty_opts()).unwrap(); - let probe_ir = function_ir_section(&ir, "perry_fn_set_string_specialization_ts__probe"); + let ir = compile_ir_for_module_with_opts(module.clone(), empty_opts()).unwrap(); assert!( - probe_ir.contains("call i64 @js_set_add_string"), - "Set.add should lower through the string helper:\n{probe_ir}" + ir.contains("call i32 @js_typed_feedback_packed_u32_array_loop_guard"), + "packed-u32 loop should use the u32-specific raw numeric layout guard:\n{ir}" ); assert!( - probe_ir.contains("call i32 @js_set_has_string"), - "Set.has should lower through the string helper:\n{probe_ir}" + ir.contains("for.packed_u32_fast") && ir.contains("for.packed_u32_slow"), + "packed-u32 loop should emit fast and slow clones:\n{ir}" ); assert!( - probe_ir.contains("call i32 @js_set_delete_string"), - "Set.delete should lower through the string helper:\n{probe_ir}" + !ir.contains("call i32 @js_typed_feedback_packed_i32_array_loop_guard") + && !ir.contains("call i32 @js_typed_feedback_packed_f64_array_loop_guard"), + "PerryU32[] read loop should not reuse signed-i32 or f64 loop guards:\n{ir}" ); + + let artifact = compile_artifact_json_for_module(module); + let records = artifact["records"].as_array().unwrap(); assert!( - !probe_ir.contains("call i64 @js_set_add("), - "specialized set.add path should not call the generic helper:\n{probe_ir}" + records.iter().any(|record| { + record["expr_kind"] == "PackedU32LoopGuard" + && record["consumer"] == "packed_u32_loop_guard" + && record["access_mode"] == "checked_native" + && record_has_array_kind_fact(record, "consumed_facts", "consumed", "packed_u32") + && record_has_raw_f64_layout_fact(record, "consumed_facts", "consumed") + }), + "expected packed-u32 guard proof record:\n{artifact:#}" ); assert!( - !probe_ir.contains("call i32 @js_set_has("), - "specialized set.has path should not call the generic helper:\n{probe_ir}" + records.iter().any(|record| { + record["expr_kind"] == "PackedU32LoopGuard" + && record["consumer"] == "packed_u32_loop_fallback" + && record["access_mode"] == "dynamic_fallback" + && record["materialization_reason"] == "runtime_api" + && record_has_array_kind_fact(record, "rejected_facts", "rejected", "packed_u32") + && record_has_raw_f64_layout_fact(record, "rejected_facts", "invalidated") + }), + "expected packed-u32 generic fallback evidence:\n{artifact:#}" ); assert!( - !probe_ir.contains("call i32 @js_set_delete("), - "specialized set.delete path should not call the generic helper:\n{probe_ir}" + records.iter().any(|record| { + record["expr_kind"] == "PackedU32LoopLoad" + && record["consumer"] == "packed_u32_loop_load" + && record["native_rep_name"] == "u32" + && record["llvm_ty"] == "i32" + && record["access_mode"] == "checked_native" + && record_has_array_kind_fact(record, "consumed_facts", "consumed", "packed_u32") + && record_has_raw_f64_layout_fact(record, "consumed_facts", "consumed") + && record_has_note(record, "integer_materialization=fptoui_guarded_packed_u32") + }), + "expected packed-u32 loop load to materialize a u32 native value:\n{artifact:#}" ); } #[test] -fn packed_f64_loop_rejects_nonnumeric_store_then_later_read() { +fn packed_i32_loop_store_update_versions_with_side_exit() { let module = module_with_classes_and_params( - "packed_f64_nonnumeric_store_then_read.ts", + "packed_i32_store_update_side_exit.ts", Vec::new(), Vec::new(), Type::Number, vec![ - number_array_let(1, "values", vec![1, 2, 3]), + int32_array_let(1, "values", vec![1, 2, 3]), for_loop( 4, length(1), - vec![ - array_set(1, local(4), Expr::String("x".to_string())), - Stmt::Expr(index_get(1, local(4))), - ], + vec![array_set( + 1, + local(4), + bit_or_zero(add(index_get(1, local(4)), int(1))), + )], ), Stmt::Return(Some(int(0))), ], @@ -2695,27 +2879,3692 @@ fn packed_f64_loop_rejects_nonnumeric_store_then_later_read() { let ir = compile_ir_for_module_with_opts(module.clone(), empty_opts()).unwrap(); assert!( - !ir.contains("call i32 @js_typed_feedback_packed_f64_array_loop_guard"), - "nonnumeric store before a later read must not get a packed-f64 clone:\n{ir}" + ir.contains("call i32 @js_typed_feedback_packed_i32_array_loop_guard"), + "safe Int32[] store-update loop should get a packed-i32 loop guard:\n{ir}" ); assert!( - !ir.contains("for.packed_f64_fast"), - "nonnumeric store/read body must not be emitted under the packed-f64 fast clone:\n{ir}" + ir.contains("for.packed_i32_fast") && ir.contains("for.packed_i32_slow"), + "safe Int32[] store-update loop should emit fast and slow clones:\n{ir}" ); assert!( - ir.contains("call void @js_array_note_numeric_write"), - "nonnumeric store into a numeric array must invalidate the raw-f64 layout:\n{ir}" + !ir.contains("call i32 @js_typed_feedback_packed_f64_array_loop_guard"), + "Int32[] store-update loop should not use the packed-f64 loop guard:\n{ir}" + ); + let fast_start = ir + .find("for.packed_i32_fast.body") + .expect("expected packed-i32 fast body"); + let fast_tail = &ir[fast_start..]; + let fast_end = fast_tail + .find("for.packed_i32_fast.update") + .map(|offset| fast_start + offset) + .unwrap_or(ir.len()); + let fast_body = &ir[fast_start..fast_end]; + assert!( + fast_body.contains("fptosi double") && fast_body.contains("add i32"), + "packed-i32 store RHS should stay in the i32 lane before the store guard:\n{fast_body}\n\n{ir}" ); assert!( - ir.contains("call i32 @js_typed_feedback_numeric_array_index_get_guard"), - "later numeric-array read should be guarded independently after the layout-changing store:\n{ir}" + !fast_body.contains("js_array_numeric_value_to_raw_f64"), + "packed-i32 loop body should not canonicalize through the f64 numeric store helper:\n{fast_body}" ); - - let artifact = compile_artifact_json_for_module(module); - let records = artifact["records"].as_array().unwrap(); + let store_fast_start = ir + .find("\npacked_i32_loop_store.fast.") + .map(|pos| pos + 1) + .expect("expected packed-i32 store fast block"); + let store_fast_tail = &ir[store_fast_start..]; + let store_fast_end = store_fast_tail + .find("\npacked_i32_loop_store.fallback.") + .map(|offset| store_fast_start + offset) + .unwrap_or(ir.len()); + let store_fast = &ir[store_fast_start..store_fast_end]; assert!( - !records.iter().any(|record| { - matches!( + store_fast.contains("store double") && !store_fast.contains("js_array_numeric_value_to_raw_f64"), + "packed-i32 fast store should write the exact f64 slot without f64 canonicalization:\n{store_fast}" + ); + + let fallback_start = ir + .find("\npacked_i32_loop_store.fallback.") + .map(|pos| pos + 1) + .expect("expected packed-i32 store fallback block"); + let fallback_tail = &ir[fallback_start..]; + let fallback_end = fallback_tail + .find("\n\n") + .map(|offset| fallback_start + offset) + .unwrap_or(ir.len()); + let fallback_block = &ir[fallback_start..fallback_end]; + assert!( + fallback_block.contains("br label %packed_i32.loop.slow.preheader."), + "packed-i32 store guard failure must side-exit to the slow clone preheader:\n{fallback_block}\n\n{ir}" + ); + assert!( + !fallback_block.contains("js_typed_feedback_array_index_set_fallback_boxed"), + "packed-i32 fast clone must not perform boxed fallback before side-exiting:\n{fallback_block}\n\n{ir}" + ); + + let artifact = compile_artifact_json_for_module(module); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "PackedI32LoopStore" + && record["consumer"] == "packed_i32_loop_store" + && record["native_rep_name"] == "i32" + && record["llvm_ty"] == "i32" + && record["access_mode"] == "checked_native" + && record_has_array_kind_fact(record, "consumed_facts", "consumed", "packed_i32") + && record_has_raw_f64_layout_fact(record, "consumed_facts", "consumed") + && record_has_note(record, "rhs_i32_store=sitofp_i32_to_raw_f64_slot") + && record_has_note(record, "store_guard_failure=side_exit_slow_restart") + }), + "expected checked packed-i32 loop store record:\n{artifact:#}" + ); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "PackedI32LoopStore" + && record["consumer"] == "packed_i32_loop_store_side_exit" + && record["access_mode"] == "dynamic_fallback" + && record["materialization_reason"] == "runtime_api" + && record_has_array_kind_fact(record, "rejected_facts", "rejected", "packed_i32") + && record_has_raw_f64_layout_fact(record, "rejected_facts", "invalidated") + }), + "expected packed-i32 store side-exit fallback evidence:\n{artifact:#}" + ); +} + +#[test] +fn packed_i32_loop_store_rejects_fractional_number_rhs() { + let module = module_with_classes_and_params( + "packed_i32_store_fractional_rhs_rejected.ts", + Vec::new(), + vec![param(2, "delta", Type::Number)], + Type::Number, + vec![ + int32_array_let(1, "values", vec![1, 2, 3]), + for_loop( + 4, + length(1), + vec![array_set( + 1, + local(4), + add(index_get(1, local(4)), local(2)), + )], + ), + Stmt::Return(Some(local(2))), + ], + ); + + let ir = compile_ir_for_module_with_opts(module.clone(), empty_opts()).unwrap(); + assert!( + !ir.contains("call i32 @js_typed_feedback_packed_i32_array_loop_guard"), + "fractional-capable number RHS must not get a packed-i32 store clone:\n{ir}" + ); + assert!( + !ir.contains("call i32 @js_typed_feedback_packed_f64_array_loop_guard"), + "fractional-capable Int32[] store RHS must not fall back to the packed-f64 store clone:\n{ir}" + ); + let artifact = compile_artifact_json_for_module(module); + let records = artifact["records"].as_array().unwrap(); + assert!( + !records.iter().any(|record| { + matches!( + record["expr_kind"].as_str(), + Some( + "PackedI32LoopStore" + | "PackedI32LoopGuard" + | "PackedF64LoopStore" + | "PackedF64LoopGuard" + ) + ) + }), + "fractional-capable Int32[] store should not record packed loop store facts:\n{artifact:#}" + ); +} + +#[test] +fn packed_f64_loop_unary_math_store_versions_with_side_exit() { + let module = module_with_classes_and_params( + "packed_f64_unary_math_store_side_exit.ts", + Vec::new(), + Vec::new(), + Type::Number, + vec![ + number_array_let(1, "values", vec![-1, 2, -3]), + for_loop( + 4, + length(1), + vec![array_set( + 1, + local(4), + Expr::MathAbs(Box::new(index_get(1, local(4)))), + )], + ), + Stmt::Return(Some(int(0))), + ], + ); + + let ir = compile_ir_for_module_with_opts(module.clone(), empty_opts()).unwrap(); + assert!( + ir.contains("call i32 @js_typed_feedback_packed_f64_array_loop_guard"), + "unary numeric math store loop should get a packed-f64 loop guard:\n{ir}" + ); + assert!( + ir.contains("for.packed_f64_fast") && ir.contains("for.packed_f64_slow"), + "unary numeric math store loop should emit fast and slow clones:\n{ir}" + ); + assert!( + ir.contains("call double @llvm.fabs.f64"), + "fast RHS should lower Math.abs over arr[i] as native f64 math:\n{ir}" + ); + let fast_body_start = ir + .find("for.packed_f64_fast.body") + .expect("expected packed-f64 fast body"); + let fast_body_tail = &ir[fast_body_start..]; + let fast_body_end = fast_body_tail + .find("for.packed_f64_fast.update") + .map(|offset| fast_body_start + offset) + .unwrap_or(ir.len()); + let fast_body = &ir[fast_body_start..fast_body_end]; + assert!( + !fast_body.contains("js_math_to_number"), + "packed fast body must not route Math.abs(arr[i]) through JSValue ToNumber:\n{fast_body}\n\n{ir}" + ); + assert!( + ir.contains("call i32 @js_typed_feedback_numeric_array_index_set_guard"), + "fast unary math store should keep a runtime numeric/layout store guard:\n{ir}" + ); + + let fallback_start = ir + .find("\npacked_f64_loop_store.fallback.") + .map(|pos| pos + 1) + .expect("expected packed-f64 store fallback block"); + let fallback_tail = &ir[fallback_start..]; + let fallback_end = fallback_tail + .find("\n\n") + .map(|offset| fallback_start + offset) + .unwrap_or(ir.len()); + let fallback_block = &ir[fallback_start..fallback_end]; + assert!( + fallback_block.contains("br label %packed_f64.loop.slow.preheader."), + "unary math packed store guard failure must side-exit to the slow clone preheader:\n{fallback_block}\n\n{ir}" + ); + assert!( + !fallback_block.contains("js_typed_feedback_array_index_set_fallback_boxed"), + "unary math packed fast clone must not perform a boxed fallback before side-exiting:\n{fallback_block}\n\n{ir}" + ); + let slow_start = ir + .find("for.packed_f64_slow") + .expect("expected packed-f64 slow clone"); + assert!( + ir[slow_start..].contains("call double @js_math_to_number"), + "unary math packed store side exit must restart in the slow clone that preserves ToNumber semantics:\n{ir}" + ); + + let artifact = compile_artifact_json_for_module(module); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "PackedF64LoopLoad" + && record["consumer"] == "packed_f64_loop_load" + && record["access_mode"] == "checked_native" + && record_has_raw_f64_layout_fact(record, "consumed_facts", "consumed") + }), + "Math.abs operand arr[i] should use a packed raw-f64 loop load:\n{artifact:#}" + ); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "PackedF64LoopStore" + && record["consumer"] == "packed_f64_loop_store" + && record["access_mode"] == "checked_native" + && record["notes"].as_array().is_some_and(|notes| { + notes + .iter() + .any(|note| note == "store_guard_failure=side_exit_slow_restart") + && notes + .iter() + .any(|note| note == "rhs_unary_math=llvm.fabs.f64") + }) + && record_has_raw_f64_layout_fact(record, "consumed_facts", "consumed") + }), + "expected checked packed raw-f64 loop store record for unary math RHS:\n{artifact:#}" + ); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "PackedF64LoopStore" + && record["consumer"] == "packed_f64_loop_store_side_exit" + && record["access_mode"] == "dynamic_fallback" + && record["materialization_reason"] == "runtime_api" + && record["fallback_reason"] == "runtime_api" + && record_has_raw_f64_layout_fact(record, "rejected_facts", "rejected") + && record_has_raw_f64_layout_fact(record, "rejected_facts", "invalidated") + }), + "expected unary math packed store side-exit fallback evidence:\n{artifact:#}" + ); +} + +#[test] +fn packed_f64_loop_rejects_coercive_unary_math_store_rhs() { + let module = module_with_classes_and_params( + "packed_f64_unary_math_store_coercion_rejected.ts", + Vec::new(), + Vec::new(), + Type::Number, + vec![ + number_array_let(1, "values", vec![1, 2, 3]), + for_loop( + 4, + length(1), + vec![array_set( + 1, + local(4), + Expr::MathAbs(Box::new(Expr::String("2".to_string()))), + )], + ), + Stmt::Return(Some(int(0))), + ], + ); + + let ir = compile_ir_for_module_with_opts(module.clone(), empty_opts()).unwrap(); + assert!( + !ir.contains("call i32 @js_typed_feedback_packed_f64_array_loop_guard"), + "Math.abs over a coercive JSValue operand must not get a packed-f64 fast clone:\n{ir}" + ); + assert!( + !ir.contains("for.packed_f64_fast"), + "coercive unary math store body must stay out of the packed-f64 fast clone:\n{ir}" + ); + assert!( + ir.contains("call double @js_math_to_number"), + "negative case must exercise the generic ToNumber-preserving math path:\n{ir}" + ); + + let artifact = compile_artifact_json_for_module(module); + let records = artifact["records"].as_array().unwrap(); + assert!( + !records.iter().any(|record| { + matches!( + record["expr_kind"].as_str(), + Some("PackedF64LoopGuard" | "PackedF64LoopStore" | "PackedF64LoopLoad") + ) + }), + "coercive unary math store loop should not record packed-f64 loop facts:\n{artifact:#}" + ); +} + +#[test] +fn map_string_number_set_has_use_string_key_specialization() { + let module = module_with_classes_and_params( + "map_string_number_specialization.ts", + Vec::new(), + vec![ + param(2, "key", Type::String), + param(3, "value", Type::Number), + ], + Type::Number, + vec![ + Stmt::Let { + id: 1, + name: "m".to_string(), + ty: map_type(Type::String, Type::Number), + mutable: true, + init: Some(Expr::MapNew), + }, + Stmt::Expr(Expr::MapSet { + map: Box::new(local(1)), + key: Box::new(local(2)), + value: Box::new(local(3)), + }), + Stmt::If { + condition: Expr::MapHas { + map: Box::new(local(1)), + key: Box::new(local(2)), + }, + then_branch: vec![Stmt::Return(Some(Expr::MapGet { + map: Box::new(local(1)), + key: Box::new(local(2)), + }))], + else_branch: Some(vec![Stmt::Return(Some(Expr::Number(0.0)))]), + }, + ], + ); + + let ir = compile_ir_for_module_with_opts(module, empty_opts()).unwrap(); + let probe_ir = function_ir_section(&ir, "perry_fn_map_string_number_specialization_ts__probe"); + assert!( + probe_ir.contains("call i64 @js_map_set_string_number"), + "Map.set should lower through the string-key/f64 helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_map_set_string_i32"), + "Map.set should not use the narrower int32 value helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_map_set_string_u32"), + "Map.set should not use the narrower uint32 value helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_map_set_string_f32"), + "Map.set should not use the narrower float32 value helper:\n{probe_ir}" + ); + assert!( + probe_ir.contains("call i32 @js_map_has_string_key"), + "Map.has should lower through the string-key helper:\n{probe_ir}" + ); + assert!( + probe_ir.contains("call double @js_map_get_string_key"), + "Map.get should lower through the string-key helper while keeping boxed miss semantics:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_map_set("), + "specialized map.set path should not call the generic helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call double @js_map_get("), + "specialized map.get path should not call the generic helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i32 @js_map_has("), + "specialized map.has path should not call the generic helper:\n{probe_ir}" + ); +} + +#[test] +fn map_number_key_set_get_has_delete_use_guarded_number_key_specialization() { + let module = module_with_classes_and_params( + "map_number_key_specialization.ts", + Vec::new(), + vec![ + param(2, "key", Type::Number), + param(3, "value", Type::Boolean), + ], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "m".to_string(), + ty: map_type(Type::Number, Type::Boolean), + mutable: true, + init: Some(Expr::MapNew), + }, + Stmt::Expr(Expr::MapSet { + map: Box::new(local(1)), + key: Box::new(local(2)), + value: Box::new(local(3)), + }), + Stmt::Let { + id: 4, + name: "present".to_string(), + ty: Type::Boolean, + mutable: false, + init: Some(Expr::MapHas { + map: Box::new(local(1)), + key: Box::new(local(2)), + }), + }, + Stmt::Expr(Expr::MapGet { + map: Box::new(local(1)), + key: Box::new(local(2)), + }), + Stmt::Expr(Expr::MapDelete { + map: Box::new(local(1)), + key: Box::new(local(2)), + }), + Stmt::Return(Some(local(4))), + ], + ); + + let ir = compile_ir_for_module_with_opts(module, empty_opts()).unwrap(); + let probe_ir = function_ir_section(&ir, "perry_fn_map_number_key_specialization_ts__probe"); + assert!( + probe_ir.contains("call i32 @js_typed_f64_arg_guard") + && probe_ir.contains("call double @js_typed_f64_arg_to_raw"), + "Map specialization should guard then unbox the key to raw f64:\n{probe_ir}" + ); + for helper in [ + "call i64 @js_map_set_number_key", + "call i32 @js_map_has_number_key", + "call double @js_map_get_number_key", + "call i32 @js_map_delete_number_key", + ] { + assert!( + probe_ir.contains(helper), + "Map should use guarded numeric-key helper {helper}:\n{probe_ir}" + ); + } + for fallback in [ + "call i64 @js_map_set(", + "call i32 @js_map_has(", + "call double @js_map_get(", + "call i32 @js_map_delete(", + ] { + assert!( + probe_ir.contains(fallback), + "numeric-key guard failure must preserve generic fallback {fallback}:\n{probe_ir}" + ); + } + for string_helper in [ + "@js_map_set_string_key", + "@js_map_set_string_bool", + "@js_map_has_string_key", + "@js_map_delete_string_key", + ] { + assert!( + !probe_ir.contains(string_helper), + "numeric-key map lowering must not use string-key helper {string_helper}:\n{probe_ir}" + ); + } +} + +#[test] +fn map_number_key_string_value_set_uses_string_ref_until_slot() { + let module = module_with_classes_and_params( + "map_number_string_value_specialization.ts", + Vec::new(), + vec![ + param(2, "key", Type::Number), + param(3, "value", Type::String), + ], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "m".to_string(), + ty: map_type(Type::Number, Type::String), + mutable: true, + init: Some(Expr::MapNew), + }, + Stmt::Expr(Expr::MapSet { + map: Box::new(local(1)), + key: Box::new(local(2)), + value: Box::new(local(3)), + }), + Stmt::Return(Some(Expr::MapHas { + map: Box::new(local(1)), + key: Box::new(local(2)), + })), + ], + ); + + let ir = compile_ir_for_module_with_opts(module.clone(), empty_opts()).unwrap(); + let probe_ir = function_ir_section( + &ir, + "perry_fn_map_number_string_value_specialization_ts__probe", + ); + assert!( + probe_ir.contains("call i32 @js_typed_f64_arg_guard") + && probe_ir.contains("call double @js_typed_f64_arg_to_raw"), + "Map.set should keep the existing guarded numeric-key path:\n{probe_ir}" + ); + assert!( + probe_ir.contains("call i64 @js_get_string_pointer_unified"), + "proven string values should be unboxed to a raw string handle before the map slot boundary:\n{probe_ir}" + ); + assert!( + probe_ir.contains("call i64 @js_map_set_number_key"), + "Map.set should still use the numeric-key helper at the slot boundary:\n{probe_ir}" + ); + for string_key_helper in [ + "@js_map_set_string_key", + "@js_map_set_string_string", + "@js_map_has_string_key", + ] { + assert!( + !probe_ir.contains(string_key_helper), + "numeric-key string-value lowering must not use string-key helper {string_key_helper}:\n{probe_ir}" + ); + } + + let artifact = compile_artifact_json_for_module(module); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "MapSet" + && record["consumer"] == "collection_typed_value.map_set_number_string" + && record["native_rep_name"] == "string_ref" + && record["llvm_ty"] == "i64" + && record_has_type_fact( + record, + "consumed_facts", + "map.string_value_helper", + "consumed", + ) + && record_has_note(record, "selected_helper=js_map_set_number_key") + && record_has_note(record, "value_rep=string_ref") + && record_has_note(record, "boxed_value_avoided_until_map_slot=true") + }), + "expected Map.set typed string-value selection record:\n{artifact:#}" + ); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "MapSet" + && record["consumer"] == "collection_number_key.map_set" + && record_has_type_fact( + record, + "consumed_facts", + "map.number_key_helper", + "consumed", + ) + && record_has_note(record, "selected_helper=js_map_set_number_key") + }), + "expected Map.set numeric-key selection record:\n{artifact:#}" + ); +} + +#[test] +fn map_number_key_string_value_rejects_unproven_value() { + let module = module_with_classes_and_params( + "map_number_string_value_rejection.ts", + Vec::new(), + vec![param(2, "key", Type::Number), param(3, "value", Type::Any)], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "m".to_string(), + ty: map_type(Type::Number, Type::String), + mutable: true, + init: Some(Expr::MapNew), + }, + Stmt::Expr(Expr::MapSet { + map: Box::new(local(1)), + key: Box::new(local(2)), + value: Box::new(local(3)), + }), + Stmt::Return(Some(Expr::MapHas { + map: Box::new(local(1)), + key: Box::new(local(2)), + })), + ], + ); + + let ir = compile_ir_for_module_with_opts(module.clone(), empty_opts()).unwrap(); + let probe_ir = function_ir_section(&ir, "perry_fn_map_number_string_value_rejection_ts__probe"); + assert!( + probe_ir.contains("call i64 @js_map_set_number_key"), + "unproven string values should preserve the guarded numeric-key helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_get_string_pointer_unified"), + "unproven values must not be unboxed as string refs:\n{probe_ir}" + ); + + let artifact = compile_artifact_json_for_module(module); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "MapSet" + && record["consumer"] == "collection_typed_value.map_set_number_string_generic" + && record["native_rep_name"] == "js_value" + && record_has_type_fact( + record, + "rejected_facts", + "map.string_value_helper", + "rejected", + ) + && record_has_note(record, "generic_helper=js_map_set_number_key") + && record_has_note( + record, + "typed_collection_rejected=value_expr_not_definitely_string", + ) + && record_has_note(record, "value_rep=js_value") + }), + "expected Map.set unproven string-value rejection record:\n{artifact:#}" + ); +} + +#[test] +fn map_string_key_has_delete_specialize_independent_of_value_type() { + let module = module_with_classes_and_params( + "map_string_boolean_delete_specialization.ts", + Vec::new(), + vec![param(2, "key", Type::String)], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "m".to_string(), + ty: map_type(Type::String, Type::Boolean), + mutable: true, + init: Some(Expr::MapNew), + }, + Stmt::Expr(Expr::MapSet { + map: Box::new(local(1)), + key: Box::new(local(2)), + value: Box::new(Expr::Bool(true)), + }), + Stmt::Let { + id: 4, + name: "present".to_string(), + ty: Type::Boolean, + mutable: false, + init: Some(Expr::MapHas { + map: Box::new(local(1)), + key: Box::new(local(2)), + }), + }, + Stmt::Expr(Expr::MapDelete { + map: Box::new(local(1)), + key: Box::new(local(2)), + }), + Stmt::Return(Some(local(4))), + ], + ); + + let ir = compile_ir_for_module_with_opts(module, empty_opts()).unwrap(); + let probe_ir = function_ir_section( + &ir, + "perry_fn_map_string_boolean_delete_specialization_ts__probe", + ); + assert!( + probe_ir.contains("call i64 @js_map_set_string_bool"), + "Map.set should lower through the typed boolean string-key helper:\n{probe_ir}" + ); + assert!( + probe_ir.contains("call i32 @js_map_has_string_key"), + "Map.has should lower through the string-key helper:\n{probe_ir}" + ); + assert!( + probe_ir.contains("call i32 @js_map_delete_string_key"), + "Map.delete should lower through the string-key helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_map_set("), + "specialized string-key map.set path should not call the generic helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_map_set_string_key"), + "typed boolean string-key map.set path should not call the generic-value string-key helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i32 @js_map_has("), + "specialized string-key map.has path should not call the generic helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i32 @js_map_delete("), + "specialized string-key map.delete path should not call the generic helper:\n{probe_ir}" + ); +} + +#[test] +fn map_string_boolean_param_without_native_i1_proof_uses_generic_value_helper() { + let module = module_with_classes_and_params( + "map_string_boolean_param_fallback.ts", + Vec::new(), + vec![ + param(2, "key", Type::String), + param(3, "value", Type::Boolean), + ], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "m".to_string(), + ty: map_type(Type::String, Type::Boolean), + mutable: true, + init: Some(Expr::MapNew), + }, + Stmt::Expr(Expr::MapSet { + map: Box::new(local(1)), + key: Box::new(local(2)), + value: Box::new(local(3)), + }), + Stmt::Return(Some(Expr::MapHas { + map: Box::new(local(1)), + key: Box::new(local(2)), + })), + ], + ); + + let ir = compile_ir_for_module_with_opts(module, empty_opts()).unwrap(); + let probe_ir = function_ir_section(&ir, "perry_fn_map_string_boolean_param_fallback_ts__probe"); + assert!( + probe_ir.contains("call i64 @js_map_set_string_key"), + "annotation-only boolean map values should keep the generic-value string-key helper until a native-i1 proof exists:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_map_set_string_bool"), + "annotation-only boolean map values must not use the raw bool helper without proof:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_map_set("), + "static string-key map.set should still avoid the fully generic helper:\n{probe_ir}" + ); +} + +#[test] +fn map_string_int32_set_uses_typed_i32_value_helper() { + let module = module_with_classes_and_params( + "map_string_int32_value_specialization.ts", + Vec::new(), + vec![param(2, "key", Type::String)], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "m".to_string(), + ty: map_type(Type::String, Type::Int32), + mutable: true, + init: Some(Expr::MapNew), + }, + Stmt::Expr(Expr::MapSet { + map: Box::new(local(1)), + key: Box::new(local(2)), + value: Box::new(Expr::Integer(42)), + }), + Stmt::Return(Some(Expr::MapHas { + map: Box::new(local(1)), + key: Box::new(local(2)), + })), + ], + ); + + let ir = compile_ir_for_module_with_opts(module, empty_opts()).unwrap(); + let probe_ir = function_ir_section( + &ir, + "perry_fn_map_string_int32_value_specialization_ts__probe", + ); + assert!( + probe_ir.contains("call i64 @js_map_set_string_i32"), + "Map.set should lower through the typed int32-value helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_map_set_string_number"), + "typed int32-value map.set should avoid the f64 number helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_map_set("), + "typed int32-value map.set should not call the generic helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_map_set_string_key"), + "typed int32-value map.set should not call the generic-value string-key helper:\n{probe_ir}" + ); +} + +#[test] +fn map_string_int32_param_without_native_i32_proof_uses_f64_helper() { + let module = module_with_classes_and_params( + "map_string_int32_param_fallback.ts", + Vec::new(), + vec![ + param(2, "key", Type::String), + param(3, "value", Type::Int32), + ], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "m".to_string(), + ty: map_type(Type::String, Type::Int32), + mutable: true, + init: Some(Expr::MapNew), + }, + Stmt::Expr(Expr::MapSet { + map: Box::new(local(1)), + key: Box::new(local(2)), + value: Box::new(local(3)), + }), + Stmt::Return(Some(Expr::MapHas { + map: Box::new(local(1)), + key: Box::new(local(2)), + })), + ], + ); + + let ir = compile_ir_for_module_with_opts(module, empty_opts()).unwrap(); + let probe_ir = function_ir_section(&ir, "perry_fn_map_string_int32_param_fallback_ts__probe"); + assert!( + probe_ir.contains("call i64 @js_map_set_string_number"), + "annotation-only Int32 values should keep the f64 helper until a native-i32 proof or guard exists:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_map_set_string_i32"), + "annotation-only Int32 values must not use the raw i32 helper without proof:\n{probe_ir}" + ); +} + +#[test] +fn map_string_u32_set_uses_typed_u32_value_helper() { + let module = module_with_classes_and_params( + "map_string_u32_value_specialization.ts", + Vec::new(), + vec![param(2, "key", Type::String)], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "m".to_string(), + ty: map_type(Type::String, Type::Named("PerryU32".to_string())), + mutable: true, + init: Some(Expr::MapNew), + }, + Stmt::Expr(Expr::MapSet { + map: Box::new(local(1)), + key: Box::new(local(2)), + value: Box::new(Expr::Integer(4_000_000_000)), + }), + Stmt::Return(Some(Expr::MapHas { + map: Box::new(local(1)), + key: Box::new(local(2)), + })), + ], + ); + + let ir = compile_ir_for_module_with_opts(module, empty_opts()).unwrap(); + let probe_ir = function_ir_section( + &ir, + "perry_fn_map_string_u32_value_specialization_ts__probe", + ); + assert!( + probe_ir.contains("call i64 @js_map_set_string_u32"), + "Map.set should lower through the typed uint32-value helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_map_set_string_number"), + "typed uint32-value map.set should avoid the f64 number helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_map_set("), + "typed uint32-value map.set should not call the generic helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_map_set_string_key"), + "typed uint32-value map.set should not call the generic-value string-key helper:\n{probe_ir}" + ); +} + +#[test] +fn map_string_u32_param_without_native_u32_proof_uses_generic_value_helper() { + let module = module_with_classes_and_params( + "map_string_u32_param_fallback.ts", + Vec::new(), + vec![ + param(2, "key", Type::String), + param(3, "value", Type::Named("PerryU32".to_string())), + ], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "m".to_string(), + ty: map_type(Type::String, Type::Named("PerryU32".to_string())), + mutable: true, + init: Some(Expr::MapNew), + }, + Stmt::Expr(Expr::MapSet { + map: Box::new(local(1)), + key: Box::new(local(2)), + value: Box::new(local(3)), + }), + Stmt::Return(Some(Expr::MapHas { + map: Box::new(local(1)), + key: Box::new(local(2)), + })), + ], + ); + + let ir = compile_ir_for_module_with_opts(module, empty_opts()).unwrap(); + let probe_ir = function_ir_section(&ir, "perry_fn_map_string_u32_param_fallback_ts__probe"); + assert!( + probe_ir.contains("call i64 @js_map_set_string_key"), + "annotation-only PerryU32 values should keep the generic-value string-key helper until a native-u32 proof or guard exists:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_map_set_string_u32"), + "annotation-only PerryU32 values must not use the raw u32 helper without proof:\n{probe_ir}" + ); +} + +#[test] +fn map_string_f32_set_uses_typed_f32_value_helper() { + let module = module_with_classes_and_params( + "map_string_f32_value_specialization.ts", + Vec::new(), + vec![param(2, "key", Type::String)], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "m".to_string(), + ty: map_type(Type::String, Type::Named("PerryF32".to_string())), + mutable: true, + init: Some(Expr::MapNew), + }, + Stmt::Expr(Expr::MapSet { + map: Box::new(local(1)), + key: Box::new(local(2)), + value: Box::new(Expr::Number(1.5)), + }), + Stmt::Return(Some(Expr::MapHas { + map: Box::new(local(1)), + key: Box::new(local(2)), + })), + ], + ); + + let ir = compile_ir_for_module_with_opts(module, empty_opts()).unwrap(); + let probe_ir = function_ir_section( + &ir, + "perry_fn_map_string_f32_value_specialization_ts__probe", + ); + assert!( + probe_ir.contains("call i64 @js_map_set_string_f32"), + "Map.set should lower through the typed float32-value helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_map_set_string_number"), + "typed float32-value map.set should avoid the f64 number helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_map_set("), + "typed float32-value map.set should not call the generic helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_map_set_string_key"), + "typed float32-value map.set should not call the generic-value string-key helper:\n{probe_ir}" + ); +} + +#[test] +fn map_string_f32_param_without_native_f32_proof_uses_generic_value_helper() { + let module = module_with_classes_and_params( + "map_string_f32_param_fallback.ts", + Vec::new(), + vec![ + param(2, "key", Type::String), + param(3, "value", Type::Named("PerryF32".to_string())), + ], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "m".to_string(), + ty: map_type(Type::String, Type::Named("PerryF32".to_string())), + mutable: true, + init: Some(Expr::MapNew), + }, + Stmt::Expr(Expr::MapSet { + map: Box::new(local(1)), + key: Box::new(local(2)), + value: Box::new(local(3)), + }), + Stmt::Return(Some(Expr::MapHas { + map: Box::new(local(1)), + key: Box::new(local(2)), + })), + ], + ); + + let ir = compile_ir_for_module_with_opts(module, empty_opts()).unwrap(); + let probe_ir = function_ir_section(&ir, "perry_fn_map_string_f32_param_fallback_ts__probe"); + assert!( + probe_ir.contains("call i64 @js_map_set_string_key"), + "annotation-only PerryF32 values should keep the generic-value string-key helper until a native-f32 proof or guard exists:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_map_set_string_f32"), + "annotation-only PerryF32 values must not use the raw f32 helper without proof:\n{probe_ir}" + ); +} + +#[test] +fn map_string_string_set_uses_typed_string_value_helper() { + let module = module_with_classes_and_params( + "map_string_string_value_specialization.ts", + Vec::new(), + vec![ + param(2, "key", Type::String), + param(3, "value", Type::String), + ], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "m".to_string(), + ty: map_type(Type::String, Type::String), + mutable: true, + init: Some(Expr::MapNew), + }, + Stmt::Expr(Expr::MapSet { + map: Box::new(local(1)), + key: Box::new(local(2)), + value: Box::new(local(3)), + }), + Stmt::Return(Some(Expr::MapHas { + map: Box::new(local(1)), + key: Box::new(local(2)), + })), + ], + ); + + let ir = compile_ir_for_module_with_opts(module, empty_opts()).unwrap(); + let probe_ir = function_ir_section( + &ir, + "perry_fn_map_string_string_value_specialization_ts__probe", + ); + assert!( + probe_ir.contains("call i64 @js_map_set_string_string"), + "Map.set should lower through the typed string-value helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_map_set("), + "specialized string-value map.set path should not call the generic helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_map_set_string_key"), + "typed string-value map.set path should not call the generic-value string-key helper:\n{probe_ir}" + ); +} + +#[test] +fn map_string_any_set_uses_generic_value_string_key_helper() { + let module = module_with_classes_and_params( + "map_string_any_value_specialization.ts", + Vec::new(), + vec![param(2, "key", Type::String), param(3, "value", Type::Any)], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "m".to_string(), + ty: map_type(Type::String, Type::Any), + mutable: true, + init: Some(Expr::MapNew), + }, + Stmt::Expr(Expr::MapSet { + map: Box::new(local(1)), + key: Box::new(local(2)), + value: Box::new(local(3)), + }), + Stmt::Return(Some(Expr::MapHas { + map: Box::new(local(1)), + key: Box::new(local(2)), + })), + ], + ); + + let ir = compile_ir_for_module_with_opts(module, empty_opts()).unwrap(); + let probe_ir = function_ir_section( + &ir, + "perry_fn_map_string_any_value_specialization_ts__probe", + ); + assert!( + probe_ir.contains("call i64 @js_map_set_string_key"), + "Map.set should keep the generic-value string-key helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_map_set_string_string"), + "unproven string values must not use the typed string-value helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_map_set_string_bool"), + "unproven values must not use the typed boolean helper:\n{probe_ir}" + ); +} + +#[test] +fn map_unproven_number_key_keeps_generic_fallback() { + let module = module_with_classes_and_params( + "map_number_unproven_key_generic.ts", + Vec::new(), + vec![param(2, "key", Type::Any), param(3, "value", Type::Boolean)], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "m".to_string(), + ty: map_type(Type::Number, Type::Boolean), + mutable: true, + init: Some(Expr::MapNew), + }, + Stmt::Expr(Expr::MapSet { + map: Box::new(local(1)), + key: Box::new(local(2)), + value: Box::new(local(3)), + }), + Stmt::Let { + id: 4, + name: "present".to_string(), + ty: Type::Boolean, + mutable: false, + init: Some(Expr::MapHas { + map: Box::new(local(1)), + key: Box::new(local(2)), + }), + }, + Stmt::Expr(Expr::MapDelete { + map: Box::new(local(1)), + key: Box::new(local(2)), + }), + Stmt::Return(Some(local(4))), + ], + ); + + let ir = compile_ir_for_module_with_opts(module, empty_opts()).unwrap(); + let probe_ir = function_ir_section(&ir, "perry_fn_map_number_unproven_key_generic_ts__probe"); + assert!( + probe_ir.contains("call i64 @js_map_set("), + "Map.set with an unproven key should keep the generic helper:\n{probe_ir}" + ); + assert!( + probe_ir.contains("call i32 @js_map_has("), + "Map.has with an unproven key should keep the generic helper:\n{probe_ir}" + ); + assert!( + probe_ir.contains("call i32 @js_map_delete("), + "Map.delete with an unproven key should keep the generic helper:\n{probe_ir}" + ); + for number_helper in [ + "@js_map_set_number_key", + "@js_map_has_number_key", + "@js_map_get_number_key", + "@js_map_delete_number_key", + ] { + assert!( + !probe_ir.contains(number_helper), + "unproven numeric map keys must not use helper {number_helper}:\n{probe_ir}" + ); + } + assert!( + !probe_ir.contains("call i64 @js_map_set_string_bool"), + "non-string map.set must not use the string-key boolean helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_map_set_string_key"), + "non-string map.set must not use the string-key helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i32 @js_map_has_string_key"), + "non-string map.has must not use the string-key helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i32 @js_map_delete_string_key"), + "non-string map.delete must not use the string-key helper:\n{probe_ir}" + ); +} + +#[test] +fn artifact_records_map_string_key_helper_selection_and_rejection() { + let selected_module = module_with_classes_and_params( + "artifact_map_string_key_selection.ts", + Vec::new(), + vec![param(2, "key", Type::String)], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "m".to_string(), + ty: map_type(Type::String, Type::Boolean), + mutable: true, + init: Some(Expr::MapNew), + }, + Stmt::Expr(Expr::MapSet { + map: Box::new(local(1)), + key: Box::new(local(2)), + value: Box::new(Expr::Bool(true)), + }), + Stmt::Let { + id: 4, + name: "present".to_string(), + ty: Type::Boolean, + mutable: false, + init: Some(Expr::MapHas { + map: Box::new(local(1)), + key: Box::new(local(2)), + }), + }, + Stmt::Expr(Expr::MapDelete { + map: Box::new(local(1)), + key: Box::new(local(2)), + }), + Stmt::Return(Some(local(4))), + ], + ); + let artifact = compile_artifact_json_for_module(selected_module); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "MapHas" + && record["consumer"] == "collection_string_key.map_has" + && record["native_rep_name"] == "string_ref" + && record["llvm_ty"] == "i64" + && record["native_value_state"] == "region_local" + && record_has_type_fact( + record, + "consumed_facts", + "map.string_key_helper", + "consumed", + ) + && record_has_note(record, "selected_helper=js_map_has_string_key") + && record_has_note(record, "boxed_key_avoided=true") + }), + "expected map.has string-key helper selection record:\n{artifact:#}" + ); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "MapDelete" + && record["consumer"] == "collection_string_key.map_delete" + && record["native_rep_name"] == "string_ref" + && record["llvm_ty"] == "i64" + && record_has_type_fact( + record, + "consumed_facts", + "map.string_key_helper", + "consumed", + ) + && record_has_note(record, "selected_helper=js_map_delete_string_key") + }), + "expected map.delete string-key helper selection record:\n{artifact:#}" + ); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "MapSet" + && record["consumer"] == "collection_string_key.map_set_string_bool" + && record["native_rep_name"] == "i1" + && record["llvm_ty"] == "i1" + && record_has_type_fact( + record, + "consumed_facts", + "map.string_key_helper", + "consumed", + ) + && record_has_type_fact( + record, + "consumed_facts", + "map.boolean_value_helper", + "consumed", + ) + && record_has_note(record, "selected_helper=js_map_set_string_bool") + && record_has_note(record, "value_rep=i1") + && record_has_note(record, "boxed_key_avoided=true") + && record_has_note(record, "boxed_value_avoided_until_map_slot=true") + }), + "expected map.set typed-boolean string-key helper selection record:\n{artifact:#}" + ); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "MapSet" + && record["consumer"] == "collection_string_key.map_set_string_bool_key" + && record["native_rep_name"] == "string_ref" + && record_has_note(record, "selected_helper=js_map_set_string_bool") + }), + "expected map.set typed-boolean string-key helper key record:\n{artifact:#}" + ); + + let boolean_fallback_module = module_with_classes_and_params( + "artifact_map_string_boolean_value_rejection.ts", + Vec::new(), + vec![ + param(2, "key", Type::String), + param(3, "value", Type::Boolean), + ], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "m".to_string(), + ty: map_type(Type::String, Type::Boolean), + mutable: true, + init: Some(Expr::MapNew), + }, + Stmt::Expr(Expr::MapSet { + map: Box::new(local(1)), + key: Box::new(local(2)), + value: Box::new(local(3)), + }), + Stmt::Return(Some(Expr::MapHas { + map: Box::new(local(1)), + key: Box::new(local(2)), + })), + ], + ); + let boolean_fallback_artifact = compile_artifact_json_for_module(boolean_fallback_module); + let boolean_fallback_records = boolean_fallback_artifact["records"].as_array().unwrap(); + assert!( + boolean_fallback_records.iter().any(|record| { + record["expr_kind"] == "MapSet" + && record["consumer"] == "collection_string_key.map_set_string_key" + && record["native_rep_name"] == "string_ref" + && record_has_note(record, "selected_helper=js_map_set_string_key") + && record_has_note(record, "boxed_key_avoided=true") + }), + "expected annotation-only boolean map.set to use generic-value string-key helper:\n{boolean_fallback_artifact:#}" + ); + assert!( + boolean_fallback_records.iter().any(|record| { + record["expr_kind"] == "MapSet" + && record["consumer"] == "collection_typed_value.map_set_string_bool_generic" + && record["native_rep_name"] == "js_value" + && record_has_type_fact( + record, + "rejected_facts", + "map.boolean_value_helper", + "rejected", + ) + && record_has_note(record, "generic_helper=js_map_set_string_key") + && record_has_note(record, "typed_collection_rejected=value_expr_not_native_i1") + && record_has_note(record, "value_rep=js_value") + }), + "expected annotation-only boolean map.set typed-value rejection record:\n{boolean_fallback_artifact:#}" + ); + + let selected_i32_value_module = module_with_classes_and_params( + "artifact_map_string_i32_value_selection.ts", + Vec::new(), + vec![param(2, "key", Type::String)], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "m".to_string(), + ty: map_type(Type::String, Type::Int32), + mutable: true, + init: Some(Expr::MapNew), + }, + Stmt::Expr(Expr::MapSet { + map: Box::new(local(1)), + key: Box::new(local(2)), + value: Box::new(Expr::Integer(42)), + }), + Stmt::Return(Some(Expr::MapHas { + map: Box::new(local(1)), + key: Box::new(local(2)), + })), + ], + ); + let i32_value_artifact = compile_artifact_json_for_module(selected_i32_value_module); + let i32_value_records = i32_value_artifact["records"].as_array().unwrap(); + assert!( + i32_value_records.iter().any(|record| { + record["expr_kind"] == "MapSet" + && record["consumer"] == "collection_string_key.map_set_string_i32" + && record["native_rep_name"] == "i32" + && record["llvm_ty"] == "i32" + && record_has_type_fact( + record, + "consumed_facts", + "map.string_key_helper", + "consumed", + ) + && record_has_type_fact( + record, + "consumed_facts", + "map.int32_value_helper", + "consumed", + ) + && record_has_note(record, "selected_helper=js_map_set_string_i32") + && record_has_note(record, "value_rep=i32") + && record_has_note(record, "boxed_key_avoided=true") + && record_has_note(record, "boxed_value_avoided_until_map_slot=true") + }), + "expected map.set typed-int32 string-key helper value record:\n{i32_value_artifact:#}" + ); + assert!( + i32_value_records.iter().any(|record| { + record["expr_kind"] == "MapSet" + && record["consumer"] == "collection_string_key.map_set_string_i32_key" + && record["native_rep_name"] == "string_ref" + && record_has_note(record, "selected_helper=js_map_set_string_i32") + }), + "expected map.set typed-int32 string-key helper key record:\n{i32_value_artifact:#}" + ); + + let selected_u32_value_module = module_with_classes_and_params( + "artifact_map_string_u32_value_selection.ts", + Vec::new(), + vec![param(2, "key", Type::String)], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "m".to_string(), + ty: map_type(Type::String, Type::Named("PerryU32".to_string())), + mutable: true, + init: Some(Expr::MapNew), + }, + Stmt::Expr(Expr::MapSet { + map: Box::new(local(1)), + key: Box::new(local(2)), + value: Box::new(Expr::Integer(4_000_000_000)), + }), + Stmt::Return(Some(Expr::MapHas { + map: Box::new(local(1)), + key: Box::new(local(2)), + })), + ], + ); + let u32_value_artifact = compile_artifact_json_for_module(selected_u32_value_module); + let u32_value_records = u32_value_artifact["records"].as_array().unwrap(); + assert!( + u32_value_records.iter().any(|record| { + record["expr_kind"] == "MapSet" + && record["consumer"] == "collection_string_key.map_set_string_u32" + && record["native_rep_name"] == "u32" + && record["llvm_ty"] == "i32" + && record_has_type_fact( + record, + "consumed_facts", + "map.string_key_helper", + "consumed", + ) + && record_has_type_fact( + record, + "consumed_facts", + "map.uint32_value_helper", + "consumed", + ) + && record_has_note(record, "selected_helper=js_map_set_string_u32") + && record_has_note(record, "value_rep=u32") + && record_has_note(record, "boxed_key_avoided=true") + && record_has_note(record, "boxed_value_avoided_until_map_slot=true") + }), + "expected map.set typed-uint32 string-key helper value record:\n{u32_value_artifact:#}" + ); + assert!( + u32_value_records.iter().any(|record| { + record["expr_kind"] == "MapSet" + && record["consumer"] == "collection_string_key.map_set_string_u32_key" + && record["native_rep_name"] == "string_ref" + && record_has_note(record, "selected_helper=js_map_set_string_u32") + }), + "expected map.set typed-uint32 string-key helper key record:\n{u32_value_artifact:#}" + ); + + let selected_f32_value_module = module_with_classes_and_params( + "artifact_map_string_f32_value_selection.ts", + Vec::new(), + vec![param(2, "key", Type::String)], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "m".to_string(), + ty: map_type(Type::String, Type::Named("PerryF32".to_string())), + mutable: true, + init: Some(Expr::MapNew), + }, + Stmt::Expr(Expr::MapSet { + map: Box::new(local(1)), + key: Box::new(local(2)), + value: Box::new(Expr::Number(1.5)), + }), + Stmt::Return(Some(Expr::MapHas { + map: Box::new(local(1)), + key: Box::new(local(2)), + })), + ], + ); + let f32_value_artifact = compile_artifact_json_for_module(selected_f32_value_module); + let f32_value_records = f32_value_artifact["records"].as_array().unwrap(); + assert!( + f32_value_records.iter().any(|record| { + record["expr_kind"] == "MapSet" + && record["consumer"] == "collection_string_key.map_set_string_f32" + && record["native_rep_name"] == "f32" + && record["llvm_ty"] == "float" + && record_has_type_fact( + record, + "consumed_facts", + "map.string_key_helper", + "consumed", + ) + && record_has_type_fact( + record, + "consumed_facts", + "map.float32_value_helper", + "consumed", + ) + && record_has_note(record, "selected_helper=js_map_set_string_f32") + && record_has_note(record, "value_rep=f32") + && record_has_note(record, "boxed_key_avoided=true") + && record_has_note(record, "boxed_value_avoided_until_map_slot=true") + }), + "expected map.set typed-float32 string-key helper value record:\n{f32_value_artifact:#}" + ); + assert!( + f32_value_records.iter().any(|record| { + record["expr_kind"] == "MapSet" + && record["consumer"] == "collection_string_key.map_set_string_f32_key" + && record["native_rep_name"] == "string_ref" + && record_has_note(record, "selected_helper=js_map_set_string_f32") + }), + "expected map.set typed-float32 string-key helper key record:\n{f32_value_artifact:#}" + ); + + let selected_string_value_module = module_with_classes_and_params( + "artifact_map_string_value_selection.ts", + Vec::new(), + vec![ + param(2, "key", Type::String), + param(3, "value", Type::String), + ], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "m".to_string(), + ty: map_type(Type::String, Type::String), + mutable: true, + init: Some(Expr::MapNew), + }, + Stmt::Expr(Expr::MapSet { + map: Box::new(local(1)), + key: Box::new(local(2)), + value: Box::new(local(3)), + }), + Stmt::Return(Some(Expr::MapHas { + map: Box::new(local(1)), + key: Box::new(local(2)), + })), + ], + ); + let string_value_artifact = compile_artifact_json_for_module(selected_string_value_module); + let string_value_records = string_value_artifact["records"].as_array().unwrap(); + assert!( + string_value_records.iter().any(|record| { + record["expr_kind"] == "MapSet" + && record["consumer"] == "collection_string_key.map_set_string_string" + && record["native_rep_name"] == "string_ref" + && record["llvm_ty"] == "i64" + && record_has_type_fact( + record, + "consumed_facts", + "map.string_key_helper", + "consumed", + ) + && record_has_type_fact( + record, + "consumed_facts", + "map.string_value_helper", + "consumed", + ) + && record_has_note(record, "selected_helper=js_map_set_string_string") + && record_has_note(record, "value_rep=string_ref") + && record_has_note(record, "boxed_key_avoided=true") + && record_has_note(record, "boxed_value_avoided_until_map_slot=true") + }), + "expected map.set typed-string string-key helper value record:\n{string_value_artifact:#}" + ); + assert!( + string_value_records.iter().any(|record| { + record["expr_kind"] == "MapSet" + && record["consumer"] == "collection_string_key.map_set_string_string_key" + && record["native_rep_name"] == "string_ref" + && record_has_note(record, "selected_helper=js_map_set_string_string") + }), + "expected map.set typed-string string-key helper key record:\n{string_value_artifact:#}" + ); + + let generic_value_module = module_with_classes_and_params( + "artifact_map_string_any_value_selection.ts", + Vec::new(), + vec![param(2, "key", Type::String), param(3, "value", Type::Any)], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "m".to_string(), + ty: map_type(Type::String, Type::Any), + mutable: true, + init: Some(Expr::MapNew), + }, + Stmt::Expr(Expr::MapSet { + map: Box::new(local(1)), + key: Box::new(local(2)), + value: Box::new(local(3)), + }), + Stmt::Return(Some(Expr::MapHas { + map: Box::new(local(1)), + key: Box::new(local(2)), + })), + ], + ); + let generic_value_artifact = compile_artifact_json_for_module(generic_value_module); + let generic_value_records = generic_value_artifact["records"].as_array().unwrap(); + assert!( + generic_value_records.iter().any(|record| { + record["expr_kind"] == "MapSet" + && record["consumer"] == "collection_string_key.map_set_string_key" + && record["native_rep_name"] == "string_ref" + && record_has_note(record, "selected_helper=js_map_set_string_key") + && record_has_note(record, "boxed_key_avoided=true") + }), + "expected map.set generic-value string-key helper record:\n{generic_value_artifact:#}" + ); + + let selected_get_module = module_with_classes_and_params( + "artifact_map_string_get_selection.ts", + Vec::new(), + vec![ + param(2, "key", Type::String), + param(3, "value", Type::Number), + ], + Type::Number, + vec![ + Stmt::Let { + id: 1, + name: "m".to_string(), + ty: map_type(Type::String, Type::Number), + mutable: true, + init: Some(Expr::MapNew), + }, + Stmt::Expr(Expr::MapSet { + map: Box::new(local(1)), + key: Box::new(local(2)), + value: Box::new(local(3)), + }), + Stmt::If { + condition: Expr::MapHas { + map: Box::new(local(1)), + key: Box::new(local(2)), + }, + then_branch: vec![Stmt::Return(Some(Expr::MapGet { + map: Box::new(local(1)), + key: Box::new(local(2)), + }))], + else_branch: Some(vec![Stmt::Return(Some(Expr::Number(0.0)))]), + }, + ], + ); + let get_artifact = compile_artifact_json_for_module(selected_get_module); + let get_records = get_artifact["records"].as_array().unwrap(); + assert!( + get_records.iter().any(|record| { + record["expr_kind"] == "MapGet" + && record["consumer"] == "collection_string_key.map_get" + && record["native_rep_name"] == "string_ref" + && record["llvm_ty"] == "i64" + && record_has_type_fact( + record, + "consumed_facts", + "map.string_key_helper", + "consumed", + ) + && record_has_note(record, "selected_helper=js_map_get_string_key") + && record_has_note(record, "boxed_key_avoided=true") + }), + "expected map.get string-key helper selection record:\n{get_artifact:#}" + ); + + let fallback_module = module_with_classes_and_params( + "artifact_map_non_string_non_number_key_rejection.ts", + Vec::new(), + vec![ + param(2, "key", Type::Boolean), + param(3, "value", Type::Boolean), + ], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "m".to_string(), + ty: map_type(Type::Boolean, Type::Boolean), + mutable: true, + init: Some(Expr::MapNew), + }, + Stmt::Expr(Expr::MapSet { + map: Box::new(local(1)), + key: Box::new(local(2)), + value: Box::new(local(3)), + }), + Stmt::Let { + id: 4, + name: "present".to_string(), + ty: Type::Boolean, + mutable: false, + init: Some(Expr::MapHas { + map: Box::new(local(1)), + key: Box::new(local(2)), + }), + }, + Stmt::Expr(Expr::MapDelete { + map: Box::new(local(1)), + key: Box::new(local(2)), + }), + Stmt::Return(Some(local(4))), + ], + ); + let artifact = compile_artifact_json_for_module(fallback_module); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "MapSet" + && record["consumer"] == "collection_string_key.map_set_generic" + && record["native_rep_name"] == "js_value" + && record_has_type_fact( + record, + "rejected_facts", + "map.string_key_helper", + "rejected", + ) + && record_has_note(record, "generic_helper=js_map_set") + && record_has_note( + record, + "typed_collection_rejected=receiver_or_key_not_static_string", + ) + }), + "expected map.set non-string-key rejection record:\n{artifact:#}" + ); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "MapHas" + && record["consumer"] == "collection_string_key.map_has_generic" + && record["native_rep_name"] == "js_value" + && record_has_type_fact( + record, + "rejected_facts", + "map.string_key_helper", + "rejected", + ) + && record_has_note(record, "generic_helper=js_map_has") + && record_has_note( + record, + "typed_collection_rejected=receiver_or_key_not_static_string", + ) + }), + "expected map.has non-string-key rejection record:\n{artifact:#}" + ); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "MapDelete" + && record["consumer"] == "collection_string_key.map_delete_generic" + && record["native_rep_name"] == "js_value" + && record_has_type_fact( + record, + "rejected_facts", + "map.string_key_helper", + "rejected", + ) + && record_has_note(record, "generic_helper=js_map_delete") + }), + "expected map.delete non-string-key rejection record:\n{artifact:#}" + ); + + let fallback_get_module = module_with_classes_and_params( + "artifact_map_non_string_non_number_get_rejection.ts", + Vec::new(), + vec![ + param(2, "key", Type::Boolean), + param(3, "value", Type::Number), + ], + Type::Number, + vec![ + Stmt::Let { + id: 1, + name: "m".to_string(), + ty: map_type(Type::Boolean, Type::Number), + mutable: true, + init: Some(Expr::MapNew), + }, + Stmt::Expr(Expr::MapSet { + map: Box::new(local(1)), + key: Box::new(local(2)), + value: Box::new(local(3)), + }), + Stmt::If { + condition: Expr::MapHas { + map: Box::new(local(1)), + key: Box::new(local(2)), + }, + then_branch: vec![Stmt::Return(Some(Expr::MapGet { + map: Box::new(local(1)), + key: Box::new(local(2)), + }))], + else_branch: Some(vec![Stmt::Return(Some(Expr::Number(0.0)))]), + }, + ], + ); + let get_artifact = compile_artifact_json_for_module(fallback_get_module); + let get_records = get_artifact["records"].as_array().unwrap(); + assert!( + get_records.iter().any(|record| { + record["expr_kind"] == "MapGet" + && record["consumer"] == "collection_string_key.map_get_generic" + && record["native_rep_name"] == "js_value" + && record_has_type_fact( + record, + "rejected_facts", + "map.string_key_helper", + "rejected", + ) + && record_has_note(record, "generic_helper=js_map_get") + && record_has_note( + record, + "typed_collection_rejected=receiver_or_key_not_static_string", + ) + }), + "expected map.get non-string-key rejection record:\n{get_artifact:#}" + ); +} + +#[test] +fn artifact_records_map_number_key_helper_selection_and_rejection() { + let selected_module = module_with_classes_and_params( + "artifact_map_number_key_selection.ts", + Vec::new(), + vec![param(2, "key", Type::Number)], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "m".to_string(), + ty: map_type(Type::Number, Type::Boolean), + mutable: true, + init: Some(Expr::MapNew), + }, + Stmt::Expr(Expr::MapSet { + map: Box::new(local(1)), + key: Box::new(local(2)), + value: Box::new(Expr::Bool(true)), + }), + Stmt::Let { + id: 4, + name: "present".to_string(), + ty: Type::Boolean, + mutable: false, + init: Some(Expr::MapHas { + map: Box::new(local(1)), + key: Box::new(local(2)), + }), + }, + Stmt::Expr(Expr::MapDelete { + map: Box::new(local(1)), + key: Box::new(local(2)), + }), + Stmt::Return(Some(local(4))), + ], + ); + let artifact = compile_artifact_json_for_module(selected_module); + let records = artifact["records"].as_array().unwrap(); + for (expr_kind, consumer, helper) in [ + ( + "MapSet", + "collection_number_key.map_set", + "js_map_set_number_key", + ), + ( + "MapHas", + "collection_number_key.map_has", + "js_map_has_number_key", + ), + ( + "MapDelete", + "collection_number_key.map_delete", + "js_map_delete_number_key", + ), + ] { + assert!( + records.iter().any(|record| { + record["expr_kind"] == expr_kind + && record["consumer"] == consumer + && record["native_rep_name"] == "f64" + && record["llvm_ty"] == "double" + && record_has_type_fact( + record, + "consumed_facts", + "map.number_key_helper", + "consumed", + ) + && record_has_note(record, &format!("selected_helper={helper}")) + && record_has_note(record, "key_rep=raw_f64") + && record_has_note(record, "key_guard=js_typed_f64_arg_guard") + }), + "expected map numeric-key helper selection record {consumer}:\n{artifact:#}" + ); + } + for (expr_kind, consumer, helper) in [ + ( + "MapSet", + "collection_number_key.map_set_generic", + "js_map_set", + ), + ( + "MapHas", + "collection_number_key.map_has_generic", + "js_map_has", + ), + ( + "MapDelete", + "collection_number_key.map_delete_generic", + "js_map_delete", + ), + ] { + assert!( + records.iter().any(|record| { + record["expr_kind"] == expr_kind + && record["consumer"] == consumer + && record["native_rep_name"] == "js_value" + && record_has_type_fact( + record, + "rejected_facts", + "map.number_key_helper", + "rejected", + ) + && record_has_note(record, &format!("generic_helper={helper}")) + && record_has_note(record, "typed_collection_rejected=runtime_key_guard_failed") + && record_has_note(record, "key_rep=js_value") + }), + "expected map numeric-key guarded fallback record {consumer}:\n{artifact:#}" + ); + } + + let rejected_module = module_with_classes_and_params( + "artifact_map_number_key_rejection.ts", + Vec::new(), + vec![param(2, "key", Type::Any)], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "m".to_string(), + ty: map_type(Type::Number, Type::Boolean), + mutable: true, + init: Some(Expr::MapNew), + }, + Stmt::Expr(Expr::MapSet { + map: Box::new(local(1)), + key: Box::new(local(2)), + value: Box::new(Expr::Bool(true)), + }), + Stmt::Return(Some(Expr::MapHas { + map: Box::new(local(1)), + key: Box::new(local(2)), + })), + ], + ); + let rejected_artifact = compile_artifact_json_for_module(rejected_module); + let rejected_records = rejected_artifact["records"].as_array().unwrap(); + assert!( + rejected_records.iter().any(|record| { + record["expr_kind"] == "MapSet" + && record["consumer"] == "collection_string_key.map_set_generic" + && record_has_note(record, "generic_helper=js_map_set") + && record_has_note(record, "typed_collection_rejected=receiver_or_key_not_static_string") + }), + "unproven numeric-key map path should still record generic fallback evidence:\n{rejected_artifact:#}" + ); +} + +#[test] +fn set_string_add_has_delete_use_string_specialization() { + let module = module_with_classes_and_params( + "set_string_specialization.ts", + Vec::new(), + vec![param(2, "value", Type::String)], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "s".to_string(), + ty: set_type(Type::String), + mutable: true, + init: Some(Expr::SetNew), + }, + Stmt::Expr(Expr::SetAdd { + set_id: 1, + value: Box::new(local(2)), + }), + Stmt::Let { + id: 3, + name: "present".to_string(), + ty: Type::Boolean, + mutable: false, + init: Some(Expr::SetHas { + set: Box::new(local(1)), + value: Box::new(local(2)), + }), + }, + Stmt::Expr(Expr::SetDelete { + set: Box::new(local(1)), + value: Box::new(local(2)), + }), + Stmt::Return(Some(local(3))), + ], + ); + + let ir = compile_ir_for_module_with_opts(module, empty_opts()).unwrap(); + let probe_ir = function_ir_section(&ir, "perry_fn_set_string_specialization_ts__probe"); + assert!( + probe_ir.contains("call i64 @js_set_add_string"), + "Set.add should lower through the string helper:\n{probe_ir}" + ); + assert!( + probe_ir.contains("call i32 @js_set_has_string"), + "Set.has should lower through the string helper:\n{probe_ir}" + ); + assert!( + probe_ir.contains("call i32 @js_set_delete_string"), + "Set.delete should lower through the string helper:\n{probe_ir}" + ); + assert!( + probe_ir.contains("call i64 @js_get_string_pointer_unified"), + "Set selected path should lower proven values to raw StringRef handles before helper calls:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_set_add("), + "specialized set.add path should not call the generic helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i32 @js_set_has("), + "specialized set.has path should not call the generic helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i32 @js_set_delete("), + "specialized set.delete path should not call the generic helper:\n{probe_ir}" + ); +} + +#[test] +fn set_number_add_has_delete_use_guarded_number_specialization() { + let module = module_with_classes_and_params( + "set_number_specialization.ts", + Vec::new(), + vec![param(2, "value", Type::Number)], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "s".to_string(), + ty: set_type(Type::Number), + mutable: true, + init: Some(Expr::SetNew), + }, + Stmt::Expr(Expr::SetAdd { + set_id: 1, + value: Box::new(local(2)), + }), + Stmt::Let { + id: 3, + name: "present".to_string(), + ty: Type::Boolean, + mutable: false, + init: Some(Expr::SetHas { + set: Box::new(local(1)), + value: Box::new(local(2)), + }), + }, + Stmt::Expr(Expr::SetDelete { + set: Box::new(local(1)), + value: Box::new(local(2)), + }), + Stmt::Return(Some(local(3))), + ], + ); + + let ir = compile_ir_for_module_with_opts(module, empty_opts()).unwrap(); + let probe_ir = function_ir_section(&ir, "perry_fn_set_number_specialization_ts__probe"); + assert!( + probe_ir.contains("call i32 @js_typed_f64_arg_guard") + && probe_ir.contains("call double @js_typed_f64_arg_to_raw"), + "Set specialization should guard then unbox the value to raw f64:\n{probe_ir}" + ); + for helper in [ + "call i64 @js_set_add_number", + "call i32 @js_set_has_number", + "call i32 @js_set_delete_number", + ] { + assert!( + probe_ir.contains(helper), + "Set should use guarded numeric helper {helper}:\n{probe_ir}" + ); + } + for fallback in [ + "call i64 @js_set_add(", + "call i32 @js_set_has(", + "call i32 @js_set_delete(", + ] { + assert!( + probe_ir.contains(fallback), + "numeric Set guard failure must preserve generic fallback {fallback}:\n{probe_ir}" + ); + } +} + +#[test] +fn set_number_specialization_rejects_unproven_value() { + let module = module_with_classes_and_params( + "set_number_unproven.ts", + Vec::new(), + vec![param(2, "value", Type::Any)], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "s".to_string(), + ty: set_type(Type::Number), + mutable: true, + init: Some(Expr::SetNew), + }, + Stmt::Expr(Expr::SetAdd { + set_id: 1, + value: Box::new(local(2)), + }), + Stmt::Return(Some(Expr::SetHas { + set: Box::new(local(1)), + value: Box::new(local(2)), + })), + ], + ); + + let ir = compile_ir_for_module_with_opts(module, empty_opts()).unwrap(); + let probe_ir = function_ir_section(&ir, "perry_fn_set_number_unproven_ts__probe"); + for helper in [ + "@js_set_add_number", + "@js_set_has_number", + "@js_set_delete_number", + ] { + assert!( + !probe_ir.contains(helper), + "unproven Set values must not use helper {helper}:\n{probe_ir}" + ); + } + assert!( + probe_ir.contains("call i64 @js_set_add(") && probe_ir.contains("call i32 @js_set_has("), + "unproven value path should call generic Set helpers:\n{probe_ir}" + ); +} + +#[test] +fn set_int32_add_has_delete_use_i32_specialization() { + let module = module_with_classes_and_params( + "set_int32_specialization.ts", + Vec::new(), + Vec::new(), + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "s".to_string(), + ty: set_type(Type::Int32), + mutable: true, + init: Some(Expr::SetNew), + }, + Stmt::Expr(Expr::SetAdd { + set_id: 1, + value: Box::new(Expr::Integer(42)), + }), + Stmt::Let { + id: 2, + name: "present".to_string(), + ty: Type::Boolean, + mutable: false, + init: Some(Expr::SetHas { + set: Box::new(local(1)), + value: Box::new(Expr::Integer(42)), + }), + }, + Stmt::Expr(Expr::SetDelete { + set: Box::new(local(1)), + value: Box::new(Expr::Integer(42)), + }), + Stmt::Return(Some(local(2))), + ], + ); + + let ir = compile_ir_for_module_with_opts(module, empty_opts()).unwrap(); + let probe_ir = function_ir_section(&ir, "perry_fn_set_int32_specialization_ts__probe"); + assert!( + probe_ir.contains("call i64 @js_set_add_i32"), + "Set.add should lower through the raw int32 helper:\n{probe_ir}" + ); + assert!( + probe_ir.contains("call i32 @js_set_has_i32"), + "Set.has should lower through the raw int32 helper:\n{probe_ir}" + ); + assert!( + probe_ir.contains("call i32 @js_set_delete_i32"), + "Set.delete should lower through the raw int32 helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_set_add("), + "specialized int32 set.add path should not call the generic helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i32 @js_set_has("), + "specialized int32 set.has path should not call the generic helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i32 @js_set_delete("), + "specialized int32 set.delete path should not call the generic helper:\n{probe_ir}" + ); +} + +#[test] +fn set_int32_param_without_native_i32_proof_uses_generic_helpers() { + let module = module_with_classes_and_params( + "set_int32_param_fallback.ts", + Vec::new(), + vec![param(2, "value", Type::Int32)], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "s".to_string(), + ty: set_type(Type::Int32), + mutable: true, + init: Some(Expr::SetNew), + }, + Stmt::Expr(Expr::SetAdd { + set_id: 1, + value: Box::new(local(2)), + }), + Stmt::Let { + id: 3, + name: "present".to_string(), + ty: Type::Boolean, + mutable: false, + init: Some(Expr::SetHas { + set: Box::new(local(1)), + value: Box::new(local(2)), + }), + }, + Stmt::Expr(Expr::SetDelete { + set: Box::new(local(1)), + value: Box::new(local(2)), + }), + Stmt::Return(Some(local(3))), + ], + ); + + let ir = compile_ir_for_module_with_opts(module, empty_opts()).unwrap(); + let probe_ir = function_ir_section(&ir, "perry_fn_set_int32_param_fallback_ts__probe"); + assert!( + probe_ir.contains("call i64 @js_set_add("), + "annotation-only Int32 Set.add should keep the generic helper until a native-i32 proof exists:\n{probe_ir}" + ); + assert!( + probe_ir.contains("call i32 @js_set_has("), + "annotation-only Int32 Set.has should keep the generic helper until a native-i32 proof exists:\n{probe_ir}" + ); + assert!( + probe_ir.contains("call i32 @js_set_delete("), + "annotation-only Int32 Set.delete should keep the generic helper until a native-i32 proof exists:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_set_add_i32"), + "annotation-only Int32 Set.add must not use the raw int32 helper without proof:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i32 @js_set_has_i32"), + "annotation-only Int32 Set.has must not use the raw int32 helper without proof:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i32 @js_set_delete_i32"), + "annotation-only Int32 Set.delete must not use the raw int32 helper without proof:\n{probe_ir}" + ); +} + +#[test] +fn set_u32_add_has_delete_use_u32_specialization() { + let module = module_with_classes_and_params( + "set_u32_specialization.ts", + Vec::new(), + Vec::new(), + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "s".to_string(), + ty: set_type(Type::Named("PerryU32".to_string())), + mutable: true, + init: Some(Expr::SetNew), + }, + Stmt::Expr(Expr::SetAdd { + set_id: 1, + value: Box::new(Expr::Integer(4_000_000_000)), + }), + Stmt::Let { + id: 2, + name: "present".to_string(), + ty: Type::Boolean, + mutable: false, + init: Some(Expr::SetHas { + set: Box::new(local(1)), + value: Box::new(Expr::Integer(4_000_000_000)), + }), + }, + Stmt::Expr(Expr::SetDelete { + set: Box::new(local(1)), + value: Box::new(Expr::Integer(4_000_000_000)), + }), + Stmt::Return(Some(local(2))), + ], + ); + + let ir = compile_ir_for_module_with_opts(module, empty_opts()).unwrap(); + let probe_ir = function_ir_section(&ir, "perry_fn_set_u32_specialization_ts__probe"); + assert!( + probe_ir.contains("call i64 @js_set_add_u32"), + "Set.add should lower through the raw uint32 helper:\n{probe_ir}" + ); + assert!( + probe_ir.contains("call i32 @js_set_has_u32"), + "Set.has should lower through the raw uint32 helper:\n{probe_ir}" + ); + assert!( + probe_ir.contains("call i32 @js_set_delete_u32"), + "Set.delete should lower through the raw uint32 helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_set_add("), + "specialized uint32 set.add path should not call the generic helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i32 @js_set_has("), + "specialized uint32 set.has path should not call the generic helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i32 @js_set_delete("), + "specialized uint32 set.delete path should not call the generic helper:\n{probe_ir}" + ); +} + +#[test] +fn set_u32_param_without_native_u32_proof_uses_generic_helpers() { + let module = module_with_classes_and_params( + "set_u32_param_fallback.ts", + Vec::new(), + vec![param(2, "value", Type::Named("PerryU32".to_string()))], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "s".to_string(), + ty: set_type(Type::Named("PerryU32".to_string())), + mutable: true, + init: Some(Expr::SetNew), + }, + Stmt::Expr(Expr::SetAdd { + set_id: 1, + value: Box::new(local(2)), + }), + Stmt::Let { + id: 3, + name: "present".to_string(), + ty: Type::Boolean, + mutable: false, + init: Some(Expr::SetHas { + set: Box::new(local(1)), + value: Box::new(local(2)), + }), + }, + Stmt::Expr(Expr::SetDelete { + set: Box::new(local(1)), + value: Box::new(local(2)), + }), + Stmt::Return(Some(local(3))), + ], + ); + + let ir = compile_ir_for_module_with_opts(module, empty_opts()).unwrap(); + let probe_ir = function_ir_section(&ir, "perry_fn_set_u32_param_fallback_ts__probe"); + assert!( + probe_ir.contains("call i64 @js_set_add("), + "annotation-only PerryU32 Set.add should keep the generic helper until a native-u32 proof exists:\n{probe_ir}" + ); + assert!( + probe_ir.contains("call i32 @js_set_has("), + "annotation-only PerryU32 Set.has should keep the generic helper until a native-u32 proof exists:\n{probe_ir}" + ); + assert!( + probe_ir.contains("call i32 @js_set_delete("), + "annotation-only PerryU32 Set.delete should keep the generic helper until a native-u32 proof exists:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_set_add_u32"), + "annotation-only PerryU32 Set.add must not use the raw u32 helper without proof:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i32 @js_set_has_u32"), + "annotation-only PerryU32 Set.has must not use the raw u32 helper without proof:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i32 @js_set_delete_u32"), + "annotation-only PerryU32 Set.delete must not use the raw u32 helper without proof:\n{probe_ir}" + ); +} + +#[test] +fn set_f32_add_has_delete_use_f32_specialization() { + let module = module_with_classes_and_params( + "set_f32_specialization.ts", + Vec::new(), + Vec::new(), + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "s".to_string(), + ty: set_type(Type::Named("PerryF32".to_string())), + mutable: true, + init: Some(Expr::SetNew), + }, + Stmt::Expr(Expr::SetAdd { + set_id: 1, + value: Box::new(Expr::Number(1.5)), + }), + Stmt::Let { + id: 2, + name: "present".to_string(), + ty: Type::Boolean, + mutable: false, + init: Some(Expr::SetHas { + set: Box::new(local(1)), + value: Box::new(Expr::Number(1.5)), + }), + }, + Stmt::Expr(Expr::SetDelete { + set: Box::new(local(1)), + value: Box::new(Expr::Number(1.5)), + }), + Stmt::Return(Some(local(2))), + ], + ); + + let ir = compile_ir_for_module_with_opts(module, empty_opts()).unwrap(); + let probe_ir = function_ir_section(&ir, "perry_fn_set_f32_specialization_ts__probe"); + assert!( + probe_ir.contains("call i64 @js_set_add_f32"), + "Set.add should lower through the raw float32 helper:\n{probe_ir}" + ); + assert!( + probe_ir.contains("call i32 @js_set_has_f32"), + "Set.has should lower through the raw float32 helper:\n{probe_ir}" + ); + assert!( + probe_ir.contains("call i32 @js_set_delete_f32"), + "Set.delete should lower through the raw float32 helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_set_add("), + "specialized float32 set.add path should not call the generic helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i32 @js_set_has("), + "specialized float32 set.has path should not call the generic helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i32 @js_set_delete("), + "specialized float32 set.delete path should not call the generic helper:\n{probe_ir}" + ); +} + +#[test] +fn set_f32_param_without_native_f32_proof_uses_generic_helpers() { + let module = module_with_classes_and_params( + "set_f32_param_fallback.ts", + Vec::new(), + vec![param(2, "value", Type::Named("PerryF32".to_string()))], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "s".to_string(), + ty: set_type(Type::Named("PerryF32".to_string())), + mutable: true, + init: Some(Expr::SetNew), + }, + Stmt::Expr(Expr::SetAdd { + set_id: 1, + value: Box::new(local(2)), + }), + Stmt::Let { + id: 3, + name: "present".to_string(), + ty: Type::Boolean, + mutable: false, + init: Some(Expr::SetHas { + set: Box::new(local(1)), + value: Box::new(local(2)), + }), + }, + Stmt::Expr(Expr::SetDelete { + set: Box::new(local(1)), + value: Box::new(local(2)), + }), + Stmt::Return(Some(local(3))), + ], + ); + + let ir = compile_ir_for_module_with_opts(module, empty_opts()).unwrap(); + let probe_ir = function_ir_section(&ir, "perry_fn_set_f32_param_fallback_ts__probe"); + assert!( + probe_ir.contains("call i64 @js_set_add("), + "annotation-only PerryF32 Set.add should keep the generic helper until a native-f32 proof exists:\n{probe_ir}" + ); + assert!( + probe_ir.contains("call i32 @js_set_has("), + "annotation-only PerryF32 Set.has should keep the generic helper until a native-f32 proof exists:\n{probe_ir}" + ); + assert!( + probe_ir.contains("call i32 @js_set_delete("), + "annotation-only PerryF32 Set.delete should keep the generic helper until a native-f32 proof exists:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_set_add_f32"), + "annotation-only PerryF32 Set.add must not use the raw f32 helper without proof:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i32 @js_set_has_f32"), + "annotation-only PerryF32 Set.has must not use the raw f32 helper without proof:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i32 @js_set_delete_f32"), + "annotation-only PerryF32 Set.delete must not use the raw f32 helper without proof:\n{probe_ir}" + ); +} + +#[test] +fn set_boolean_add_has_delete_use_bool_specialization() { + let module = module_with_classes_and_params( + "set_boolean_specialization.ts", + Vec::new(), + Vec::new(), + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "s".to_string(), + ty: set_type(Type::Boolean), + mutable: true, + init: Some(Expr::SetNew), + }, + Stmt::Expr(Expr::SetAdd { + set_id: 1, + value: Box::new(Expr::Bool(true)), + }), + Stmt::Let { + id: 2, + name: "present".to_string(), + ty: Type::Boolean, + mutable: false, + init: Some(Expr::SetHas { + set: Box::new(local(1)), + value: Box::new(Expr::Bool(true)), + }), + }, + Stmt::Expr(Expr::SetDelete { + set: Box::new(local(1)), + value: Box::new(Expr::Bool(true)), + }), + Stmt::Return(Some(local(2))), + ], + ); + + let ir = compile_ir_for_module_with_opts(module, empty_opts()).unwrap(); + let probe_ir = function_ir_section(&ir, "perry_fn_set_boolean_specialization_ts__probe"); + assert!( + probe_ir.contains("call i64 @js_set_add_bool"), + "Set.add should lower through the raw boolean helper:\n{probe_ir}" + ); + assert!( + probe_ir.contains("call i32 @js_set_has_bool"), + "Set.has should lower through the raw boolean helper:\n{probe_ir}" + ); + assert!( + probe_ir.contains("call i32 @js_set_delete_bool"), + "Set.delete should lower through the raw boolean helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_set_add("), + "specialized boolean set.add path should not call the generic helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i32 @js_set_has("), + "specialized boolean set.has path should not call the generic helper:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i32 @js_set_delete("), + "specialized boolean set.delete path should not call the generic helper:\n{probe_ir}" + ); +} + +#[test] +fn set_boolean_param_without_native_i1_proof_uses_generic_helpers() { + let module = module_with_classes_and_params( + "set_boolean_param_fallback.ts", + Vec::new(), + vec![param(2, "value", Type::Boolean)], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "s".to_string(), + ty: set_type(Type::Boolean), + mutable: true, + init: Some(Expr::SetNew), + }, + Stmt::Expr(Expr::SetAdd { + set_id: 1, + value: Box::new(local(2)), + }), + Stmt::Let { + id: 3, + name: "present".to_string(), + ty: Type::Boolean, + mutable: false, + init: Some(Expr::SetHas { + set: Box::new(local(1)), + value: Box::new(local(2)), + }), + }, + Stmt::Expr(Expr::SetDelete { + set: Box::new(local(1)), + value: Box::new(local(2)), + }), + Stmt::Return(Some(local(3))), + ], + ); + + let ir = compile_ir_for_module_with_opts(module, empty_opts()).unwrap(); + let probe_ir = function_ir_section(&ir, "perry_fn_set_boolean_param_fallback_ts__probe"); + assert!( + probe_ir.contains("call i64 @js_set_add("), + "annotation-only boolean Set.add should keep the generic helper until a native-i1 proof exists:\n{probe_ir}" + ); + assert!( + probe_ir.contains("call i32 @js_set_has("), + "annotation-only boolean Set.has should keep the generic helper until a native-i1 proof exists:\n{probe_ir}" + ); + assert!( + probe_ir.contains("call i32 @js_set_delete("), + "annotation-only boolean Set.delete should keep the generic helper until a native-i1 proof exists:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i64 @js_set_add_bool"), + "annotation-only boolean Set.add must not use the raw bool helper without proof:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i32 @js_set_has_bool"), + "annotation-only boolean Set.has must not use the raw bool helper without proof:\n{probe_ir}" + ); + assert!( + !probe_ir.contains("call i32 @js_set_delete_bool"), + "annotation-only boolean Set.delete must not use the raw bool helper without proof:\n{probe_ir}" + ); +} + +#[test] +fn artifact_records_set_string_key_helper_selection_and_rejection() { + let selected_module = module_with_classes_and_params( + "artifact_set_string_key_selection.ts", + Vec::new(), + vec![param(2, "value", Type::String)], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "s".to_string(), + ty: set_type(Type::String), + mutable: true, + init: Some(Expr::SetNew), + }, + Stmt::Expr(Expr::SetAdd { + set_id: 1, + value: Box::new(local(2)), + }), + Stmt::Let { + id: 3, + name: "present".to_string(), + ty: Type::Boolean, + mutable: false, + init: Some(Expr::SetHas { + set: Box::new(local(1)), + value: Box::new(local(2)), + }), + }, + Stmt::Expr(Expr::SetDelete { + set: Box::new(local(1)), + value: Box::new(local(2)), + }), + Stmt::Return(Some(local(3))), + ], + ); + let artifact = compile_artifact_json_for_module(selected_module); + let records = artifact["records"].as_array().unwrap(); + for (expr_kind, consumer, helper) in [ + ( + "SetAdd", + "collection_string_key.set_add", + "js_set_add_string", + ), + ( + "SetHas", + "collection_string_key.set_has", + "js_set_has_string", + ), + ( + "SetDelete", + "collection_string_key.set_delete", + "js_set_delete_string", + ), + ] { + assert!( + records.iter().any(|record| { + record["expr_kind"] == expr_kind + && record["consumer"] == consumer + && record["native_rep_name"] == "string_ref" + && record["llvm_ty"] == "i64" + && record_has_type_fact( + record, + "consumed_facts", + "set.string_key_helper", + "consumed", + ) + && record_has_note(record, &format!("selected_helper={helper}")) + && record_has_note(record, "boxed_key_avoided=true") + }), + "expected {consumer} string-key helper selection record:\n{artifact:#}" + ); + } + for (expr_kind, consumer, helper) in [ + ( + "SetAdd", + "collection_typed_value.set_add_string", + "js_set_add_string", + ), + ( + "SetHas", + "collection_typed_value.set_has_string", + "js_set_has_string", + ), + ( + "SetDelete", + "collection_typed_value.set_delete_string", + "js_set_delete_string", + ), + ] { + assert!( + records.iter().any(|record| { + record["expr_kind"] == expr_kind + && record["consumer"] == consumer + && record["native_rep_name"] == "string_ref" + && record["llvm_ty"] == "i64" + && record_has_type_fact( + record, + "consumed_facts", + "set.string_value_helper", + "consumed", + ) + && record_has_note(record, &format!("selected_helper={helper}")) + && record_has_note(record, "value_rep=string_ref") + && record_has_note(record, "boxed_value_avoided_until_set_slot=true") + }), + "expected {consumer} string-value helper selection record:\n{artifact:#}" + ); + } + + let fallback_module = module_with_classes_and_params( + "artifact_set_non_string_key_rejection.ts", + Vec::new(), + vec![param(2, "value", Type::Number)], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "s".to_string(), + ty: set_type(Type::Any), + mutable: true, + init: Some(Expr::SetNew), + }, + Stmt::Expr(Expr::SetAdd { + set_id: 1, + value: Box::new(local(2)), + }), + Stmt::Let { + id: 3, + name: "present".to_string(), + ty: Type::Boolean, + mutable: false, + init: Some(Expr::SetHas { + set: Box::new(local(1)), + value: Box::new(local(2)), + }), + }, + Stmt::Expr(Expr::SetDelete { + set: Box::new(local(1)), + value: Box::new(local(2)), + }), + Stmt::Return(Some(local(3))), + ], + ); + let artifact = compile_artifact_json_for_module(fallback_module); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "SetHas" + && record["consumer"] == "collection_string_key.set_has_generic" + && record["native_rep_name"] == "js_value" + && record_has_type_fact( + record, + "rejected_facts", + "set.string_key_helper", + "rejected", + ) + && record_has_note(record, "generic_helper=js_set_has") + && record_has_note( + record, + "typed_collection_rejected=receiver_or_value_not_static_string", + ) + }), + "expected set.has non-string rejection record:\n{artifact:#}" + ); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "SetDelete" + && record["consumer"] == "collection_string_key.set_delete_generic" + && record["native_rep_name"] == "js_value" + && record_has_type_fact( + record, + "rejected_facts", + "set.string_key_helper", + "rejected", + ) + && record_has_note(record, "generic_helper=js_set_delete") + }), + "expected set.delete non-string rejection record:\n{artifact:#}" + ); +} + +#[test] +fn artifact_records_set_number_value_helper_selection_and_rejection() { + let selected_module = module_with_classes_and_params( + "artifact_set_number_value_selection.ts", + Vec::new(), + vec![param(2, "value", Type::Number)], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "s".to_string(), + ty: set_type(Type::Number), + mutable: true, + init: Some(Expr::SetNew), + }, + Stmt::Expr(Expr::SetAdd { + set_id: 1, + value: Box::new(local(2)), + }), + Stmt::Let { + id: 3, + name: "present".to_string(), + ty: Type::Boolean, + mutable: false, + init: Some(Expr::SetHas { + set: Box::new(local(1)), + value: Box::new(local(2)), + }), + }, + Stmt::Expr(Expr::SetDelete { + set: Box::new(local(1)), + value: Box::new(local(2)), + }), + Stmt::Return(Some(local(3))), + ], + ); + let artifact = compile_artifact_json_for_module(selected_module); + let records = artifact["records"].as_array().unwrap(); + for (expr_kind, consumer, helper) in [ + ( + "SetAdd", + "collection_number_value.set_add", + "js_set_add_number", + ), + ( + "SetHas", + "collection_number_value.set_has", + "js_set_has_number", + ), + ( + "SetDelete", + "collection_number_value.set_delete", + "js_set_delete_number", + ), + ] { + assert!( + records.iter().any(|record| { + record["expr_kind"] == expr_kind + && record["consumer"] == consumer + && record["native_rep_name"] == "f64" + && record["llvm_ty"] == "double" + && record_has_type_fact( + record, + "consumed_facts", + "set.number_value_helper", + "consumed", + ) + && record_has_note(record, &format!("selected_helper={helper}")) + && record_has_note(record, "value_rep=raw_f64") + && record_has_note(record, "value_guard=js_typed_f64_arg_guard") + }), + "expected Set helper selection record {consumer}:\n{artifact:#}" + ); + } + for (expr_kind, consumer, helper) in [ + ( + "SetAdd", + "collection_number_value.set_add_generic", + "js_set_add", + ), + ( + "SetHas", + "collection_number_value.set_has_generic", + "js_set_has", + ), + ( + "SetDelete", + "collection_number_value.set_delete_generic", + "js_set_delete", + ), + ] { + assert!( + records.iter().any(|record| { + record["expr_kind"] == expr_kind + && record["consumer"] == consumer + && record["native_rep_name"] == "js_value" + && record_has_type_fact( + record, + "rejected_facts", + "set.number_value_helper", + "rejected", + ) + && record_has_note(record, &format!("generic_helper={helper}")) + && record_has_note( + record, + "typed_collection_rejected=runtime_value_guard_failed", + ) + && record_has_note(record, "value_rep=js_value") + }), + "expected Set guarded fallback record {consumer}:\n{artifact:#}" + ); + } + + let rejected_module = module_with_classes_and_params( + "artifact_set_number_value_rejection.ts", + Vec::new(), + vec![param(2, "value", Type::Any)], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "s".to_string(), + ty: set_type(Type::Number), + mutable: true, + init: Some(Expr::SetNew), + }, + Stmt::Expr(Expr::SetAdd { + set_id: 1, + value: Box::new(local(2)), + }), + Stmt::Return(Some(Expr::SetHas { + set: Box::new(local(1)), + value: Box::new(local(2)), + })), + ], + ); + let rejected_artifact = compile_artifact_json_for_module(rejected_module); + let rejected_records = rejected_artifact["records"].as_array().unwrap(); + assert!( + rejected_records.iter().any(|record| { + record["expr_kind"] == "SetAdd" + && record["consumer"] == "collection_number_value.set_add_generic" + && record["native_rep_name"] == "js_value" + && record_has_type_fact( + record, + "rejected_facts", + "set.number_value_helper", + "rejected", + ) + && record_has_note(record, "generic_helper=js_set_add") + && record_has_note(record, "typed_collection_rejected=value_expr_not_numeric") + }), + "expected unproven Set value rejection record:\n{rejected_artifact:#}" + ); +} + +#[test] +fn artifact_records_set_int32_value_helper_selection_and_rejection() { + let selected_module = module_with_classes_and_params( + "artifact_set_int32_value_selection.ts", + Vec::new(), + Vec::new(), + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "s".to_string(), + ty: set_type(Type::Int32), + mutable: true, + init: Some(Expr::SetNew), + }, + Stmt::Expr(Expr::SetAdd { + set_id: 1, + value: Box::new(Expr::Integer(7)), + }), + Stmt::Let { + id: 2, + name: "present".to_string(), + ty: Type::Boolean, + mutable: false, + init: Some(Expr::SetHas { + set: Box::new(local(1)), + value: Box::new(Expr::Integer(7)), + }), + }, + Stmt::Expr(Expr::SetDelete { + set: Box::new(local(1)), + value: Box::new(Expr::Integer(7)), + }), + Stmt::Return(Some(local(2))), + ], + ); + let artifact = compile_artifact_json_for_module(selected_module); + let records = artifact["records"].as_array().unwrap(); + for (expr_kind, consumer, helper) in [ + ( + "SetAdd", + "collection_typed_value.set_add_i32", + "js_set_add_i32", + ), + ( + "SetHas", + "collection_typed_value.set_has_i32", + "js_set_has_i32", + ), + ( + "SetDelete", + "collection_typed_value.set_delete_i32", + "js_set_delete_i32", + ), + ] { + assert!( + records.iter().any(|record| { + record["expr_kind"] == expr_kind + && record["consumer"] == consumer + && record["native_rep_name"] == "i32" + && record["llvm_ty"] == "i32" + && record_has_type_fact( + record, + "consumed_facts", + "set.int32_value_helper", + "consumed", + ) + && record_has_note(record, &format!("selected_helper={helper}")) + && record_has_note(record, "value_rep=i32") + && record_has_note(record, "boxed_value_avoided_until_set_slot=true") + }), + "expected {consumer} int32-value helper selection record:\n{artifact:#}" + ); + } + + let fallback_module = module_with_classes_and_params( + "artifact_set_int32_value_rejection.ts", + Vec::new(), + vec![param(2, "value", Type::Int32)], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "s".to_string(), + ty: set_type(Type::Int32), + mutable: true, + init: Some(Expr::SetNew), + }, + Stmt::Expr(Expr::SetAdd { + set_id: 1, + value: Box::new(local(2)), + }), + Stmt::Let { + id: 3, + name: "present".to_string(), + ty: Type::Boolean, + mutable: false, + init: Some(Expr::SetHas { + set: Box::new(local(1)), + value: Box::new(local(2)), + }), + }, + Stmt::Expr(Expr::SetDelete { + set: Box::new(local(1)), + value: Box::new(local(2)), + }), + Stmt::Return(Some(local(3))), + ], + ); + let fallback_artifact = compile_artifact_json_for_module(fallback_module); + let fallback_records = fallback_artifact["records"].as_array().unwrap(); + for (expr_kind, consumer, helper) in [ + ( + "SetAdd", + "collection_typed_value.set_add_generic", + "js_set_add", + ), + ( + "SetHas", + "collection_typed_value.set_has_generic", + "js_set_has", + ), + ( + "SetDelete", + "collection_typed_value.set_delete_generic", + "js_set_delete", + ), + ] { + assert!( + fallback_records.iter().any(|record| { + record["expr_kind"] == expr_kind + && record["consumer"] == consumer + && record["native_rep_name"] == "js_value" + && record_has_type_fact( + record, + "rejected_facts", + "set.int32_value_helper", + "rejected", + ) + && record_has_note(record, &format!("generic_helper={helper}")) + && record_has_note( + record, + "typed_collection_rejected=value_expr_not_native_i32", + ) + && record_has_note(record, "value_rep=js_value") + }), + "expected {consumer} int32-value helper rejection record:\n{fallback_artifact:#}" + ); + } +} + +#[test] +fn artifact_records_set_u32_value_helper_selection_and_rejection() { + let selected_module = module_with_classes_and_params( + "artifact_set_u32_value_selection.ts", + Vec::new(), + Vec::new(), + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "s".to_string(), + ty: set_type(Type::Named("PerryU32".to_string())), + mutable: true, + init: Some(Expr::SetNew), + }, + Stmt::Expr(Expr::SetAdd { + set_id: 1, + value: Box::new(Expr::Integer(4_000_000_000)), + }), + Stmt::Let { + id: 2, + name: "present".to_string(), + ty: Type::Boolean, + mutable: false, + init: Some(Expr::SetHas { + set: Box::new(local(1)), + value: Box::new(Expr::Integer(4_000_000_000)), + }), + }, + Stmt::Expr(Expr::SetDelete { + set: Box::new(local(1)), + value: Box::new(Expr::Integer(4_000_000_000)), + }), + Stmt::Return(Some(local(2))), + ], + ); + let artifact = compile_artifact_json_for_module(selected_module); + let records = artifact["records"].as_array().unwrap(); + for (expr_kind, consumer, helper) in [ + ( + "SetAdd", + "collection_typed_value.set_add_u32", + "js_set_add_u32", + ), + ( + "SetHas", + "collection_typed_value.set_has_u32", + "js_set_has_u32", + ), + ( + "SetDelete", + "collection_typed_value.set_delete_u32", + "js_set_delete_u32", + ), + ] { + assert!( + records.iter().any(|record| { + record["expr_kind"] == expr_kind + && record["consumer"] == consumer + && record["native_rep_name"] == "u32" + && record["llvm_ty"] == "i32" + && record_has_type_fact( + record, + "consumed_facts", + "set.uint32_value_helper", + "consumed", + ) + && record_has_note(record, &format!("selected_helper={helper}")) + && record_has_note(record, "value_rep=u32") + && record_has_note(record, "boxed_value_avoided_until_set_slot=true") + }), + "expected {consumer} uint32-value helper selection record:\n{artifact:#}" + ); + } + + let fallback_module = module_with_classes_and_params( + "artifact_set_u32_value_rejection.ts", + Vec::new(), + vec![param(2, "value", Type::Named("PerryU32".to_string()))], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "s".to_string(), + ty: set_type(Type::Named("PerryU32".to_string())), + mutable: true, + init: Some(Expr::SetNew), + }, + Stmt::Expr(Expr::SetAdd { + set_id: 1, + value: Box::new(local(2)), + }), + Stmt::Let { + id: 3, + name: "present".to_string(), + ty: Type::Boolean, + mutable: false, + init: Some(Expr::SetHas { + set: Box::new(local(1)), + value: Box::new(local(2)), + }), + }, + Stmt::Expr(Expr::SetDelete { + set: Box::new(local(1)), + value: Box::new(local(2)), + }), + Stmt::Return(Some(local(3))), + ], + ); + let fallback_artifact = compile_artifact_json_for_module(fallback_module); + let fallback_records = fallback_artifact["records"].as_array().unwrap(); + for (expr_kind, consumer, helper) in [ + ( + "SetAdd", + "collection_typed_value.set_add_generic", + "js_set_add", + ), + ( + "SetHas", + "collection_typed_value.set_has_generic", + "js_set_has", + ), + ( + "SetDelete", + "collection_typed_value.set_delete_generic", + "js_set_delete", + ), + ] { + assert!( + fallback_records.iter().any(|record| { + record["expr_kind"] == expr_kind + && record["consumer"] == consumer + && record["native_rep_name"] == "js_value" + && record_has_type_fact( + record, + "rejected_facts", + "set.uint32_value_helper", + "rejected", + ) + && record_has_note(record, &format!("generic_helper={helper}")) + && record_has_note( + record, + "typed_collection_rejected=value_expr_not_native_u32", + ) + && record_has_note(record, "value_rep=js_value") + }), + "expected {consumer} uint32-value helper rejection record:\n{fallback_artifact:#}" + ); + } +} + +#[test] +fn artifact_records_set_f32_value_helper_selection_and_rejection() { + let selected_module = module_with_classes_and_params( + "artifact_set_f32_value_selection.ts", + Vec::new(), + Vec::new(), + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "s".to_string(), + ty: set_type(Type::Named("PerryF32".to_string())), + mutable: true, + init: Some(Expr::SetNew), + }, + Stmt::Expr(Expr::SetAdd { + set_id: 1, + value: Box::new(Expr::Number(1.5)), + }), + Stmt::Let { + id: 2, + name: "present".to_string(), + ty: Type::Boolean, + mutable: false, + init: Some(Expr::SetHas { + set: Box::new(local(1)), + value: Box::new(Expr::Number(1.5)), + }), + }, + Stmt::Expr(Expr::SetDelete { + set: Box::new(local(1)), + value: Box::new(Expr::Number(1.5)), + }), + Stmt::Return(Some(local(2))), + ], + ); + let artifact = compile_artifact_json_for_module(selected_module); + let records = artifact["records"].as_array().unwrap(); + for (expr_kind, consumer, helper) in [ + ( + "SetAdd", + "collection_typed_value.set_add_f32", + "js_set_add_f32", + ), + ( + "SetHas", + "collection_typed_value.set_has_f32", + "js_set_has_f32", + ), + ( + "SetDelete", + "collection_typed_value.set_delete_f32", + "js_set_delete_f32", + ), + ] { + assert!( + records.iter().any(|record| { + record["expr_kind"] == expr_kind + && record["consumer"] == consumer + && record["native_rep_name"] == "f32" + && record["llvm_ty"] == "float" + && record_has_type_fact( + record, + "consumed_facts", + "set.float32_value_helper", + "consumed", + ) + && record_has_note(record, &format!("selected_helper={helper}")) + && record_has_note(record, "value_rep=f32") + && record_has_note(record, "boxed_value_avoided_until_set_slot=true") + }), + "expected {consumer} float32-value helper selection record:\n{artifact:#}" + ); + } + + let fallback_module = module_with_classes_and_params( + "artifact_set_f32_value_rejection.ts", + Vec::new(), + vec![param(2, "value", Type::Named("PerryF32".to_string()))], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "s".to_string(), + ty: set_type(Type::Named("PerryF32".to_string())), + mutable: true, + init: Some(Expr::SetNew), + }, + Stmt::Expr(Expr::SetAdd { + set_id: 1, + value: Box::new(local(2)), + }), + Stmt::Let { + id: 3, + name: "present".to_string(), + ty: Type::Boolean, + mutable: false, + init: Some(Expr::SetHas { + set: Box::new(local(1)), + value: Box::new(local(2)), + }), + }, + Stmt::Expr(Expr::SetDelete { + set: Box::new(local(1)), + value: Box::new(local(2)), + }), + Stmt::Return(Some(local(3))), + ], + ); + let fallback_artifact = compile_artifact_json_for_module(fallback_module); + let fallback_records = fallback_artifact["records"].as_array().unwrap(); + for (expr_kind, consumer, helper) in [ + ( + "SetAdd", + "collection_typed_value.set_add_generic", + "js_set_add", + ), + ( + "SetHas", + "collection_typed_value.set_has_generic", + "js_set_has", + ), + ( + "SetDelete", + "collection_typed_value.set_delete_generic", + "js_set_delete", + ), + ] { + assert!( + fallback_records.iter().any(|record| { + record["expr_kind"] == expr_kind + && record["consumer"] == consumer + && record["native_rep_name"] == "js_value" + && record_has_type_fact( + record, + "rejected_facts", + "set.float32_value_helper", + "rejected", + ) + && record_has_note(record, &format!("generic_helper={helper}")) + && record_has_note( + record, + "typed_collection_rejected=value_expr_not_native_f32", + ) + && record_has_note(record, "value_rep=js_value") + }), + "expected {consumer} float32-value helper rejection record:\n{fallback_artifact:#}" + ); + } +} + +#[test] +fn artifact_records_set_boolean_value_helper_selection_and_rejection() { + let selected_module = module_with_classes_and_params( + "artifact_set_boolean_value_selection.ts", + Vec::new(), + Vec::new(), + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "s".to_string(), + ty: set_type(Type::Boolean), + mutable: true, + init: Some(Expr::SetNew), + }, + Stmt::Expr(Expr::SetAdd { + set_id: 1, + value: Box::new(Expr::Bool(true)), + }), + Stmt::Let { + id: 2, + name: "present".to_string(), + ty: Type::Boolean, + mutable: false, + init: Some(Expr::SetHas { + set: Box::new(local(1)), + value: Box::new(Expr::Bool(true)), + }), + }, + Stmt::Expr(Expr::SetDelete { + set: Box::new(local(1)), + value: Box::new(Expr::Bool(true)), + }), + Stmt::Return(Some(local(2))), + ], + ); + let artifact = compile_artifact_json_for_module(selected_module); + let records = artifact["records"].as_array().unwrap(); + for (expr_kind, consumer, helper) in [ + ( + "SetAdd", + "collection_typed_value.set_add_bool", + "js_set_add_bool", + ), + ( + "SetHas", + "collection_typed_value.set_has_bool", + "js_set_has_bool", + ), + ( + "SetDelete", + "collection_typed_value.set_delete_bool", + "js_set_delete_bool", + ), + ] { + assert!( + records.iter().any(|record| { + record["expr_kind"] == expr_kind + && record["consumer"] == consumer + && record["native_rep_name"] == "i1" + && record["llvm_ty"] == "i1" + && record_has_type_fact( + record, + "consumed_facts", + "set.boolean_value_helper", + "consumed", + ) + && record_has_note(record, &format!("selected_helper={helper}")) + && record_has_note(record, "value_rep=i1") + && record_has_note(record, "boxed_value_avoided_until_set_slot=true") + }), + "expected {consumer} boolean-value helper selection record:\n{artifact:#}" + ); + } + + let fallback_module = module_with_classes_and_params( + "artifact_set_boolean_value_rejection.ts", + Vec::new(), + vec![param(2, "value", Type::Boolean)], + Type::Boolean, + vec![ + Stmt::Let { + id: 1, + name: "s".to_string(), + ty: set_type(Type::Boolean), + mutable: true, + init: Some(Expr::SetNew), + }, + Stmt::Expr(Expr::SetAdd { + set_id: 1, + value: Box::new(local(2)), + }), + Stmt::Let { + id: 3, + name: "present".to_string(), + ty: Type::Boolean, + mutable: false, + init: Some(Expr::SetHas { + set: Box::new(local(1)), + value: Box::new(local(2)), + }), + }, + Stmt::Expr(Expr::SetDelete { + set: Box::new(local(1)), + value: Box::new(local(2)), + }), + Stmt::Return(Some(local(3))), + ], + ); + let fallback_artifact = compile_artifact_json_for_module(fallback_module); + let fallback_records = fallback_artifact["records"].as_array().unwrap(); + for (expr_kind, consumer, helper) in [ + ( + "SetAdd", + "collection_typed_value.set_add_generic", + "js_set_add", + ), + ( + "SetHas", + "collection_typed_value.set_has_generic", + "js_set_has", + ), + ( + "SetDelete", + "collection_typed_value.set_delete_generic", + "js_set_delete", + ), + ] { + assert!( + fallback_records.iter().any(|record| { + record["expr_kind"] == expr_kind + && record["consumer"] == consumer + && record["native_rep_name"] == "js_value" + && record_has_type_fact( + record, + "rejected_facts", + "set.boolean_value_helper", + "rejected", + ) + && record_has_note(record, &format!("generic_helper={helper}")) + && record_has_note(record, "typed_collection_rejected=value_expr_not_native_i1") + && record_has_note(record, "value_rep=js_value") + }), + "expected {consumer} boolean-value helper rejection record:\n{fallback_artifact:#}" + ); + } +} + +#[test] +fn packed_f64_loop_rejects_nonnumeric_store_then_later_read() { + let module = module_with_classes_and_params( + "packed_f64_nonnumeric_store_then_read.ts", + Vec::new(), + Vec::new(), + Type::Number, + vec![ + number_array_let(1, "values", vec![1, 2, 3]), + for_loop( + 4, + length(1), + vec![ + array_set(1, local(4), Expr::String("x".to_string())), + Stmt::Expr(index_get(1, local(4))), + ], + ), + Stmt::Return(Some(int(0))), + ], + ); + + let ir = compile_ir_for_module_with_opts(module.clone(), empty_opts()).unwrap(); + assert!( + !ir.contains("call i32 @js_typed_feedback_packed_f64_array_loop_guard"), + "nonnumeric store before a later read must not get a packed-f64 clone:\n{ir}" + ); + assert!( + !ir.contains("for.packed_f64_fast"), + "nonnumeric store/read body must not be emitted under the packed-f64 fast clone:\n{ir}" + ); + assert!( + ir.contains("call void @js_array_note_numeric_write"), + "nonnumeric store into a numeric array must invalidate the raw-f64 layout:\n{ir}" + ); + assert!( + ir.contains("call i32 @js_typed_feedback_numeric_array_index_get_guard"), + "later numeric-array read should be guarded independently after the layout-changing store:\n{ir}" + ); + + let artifact = compile_artifact_json_for_module(module); + let records = artifact["records"].as_array().unwrap(); + assert!( + !records.iter().any(|record| { + matches!( record["expr_kind"].as_str(), Some("PackedF64LoopGuard" | "PackedF64LoopStore" | "PackedF64LoopLoad") ) @@ -3536,6 +7385,38 @@ fn compiler_private_async_iter_result_f64_body() -> Vec { ] } +fn compiler_private_async_iter_result_i1_body() -> Vec { + vec![ + Stmt::Expr(Expr::IterResultSet(Box::new(Expr::Bool(true)), false)), + Stmt::Let { + id: 21, + name: "__step_bool".to_string(), + ty: Type::Boolean, + mutable: false, + init: Some(Expr::BooleanCoerce(Box::new(Expr::IterResultGetValue))), + }, + Stmt::Return(Some(Expr::LocalGet(21))), + ] +} + +fn compiler_private_async_iter_result_i32_body() -> Vec { + vec![ + Stmt::Expr(Expr::IterResultSet( + Box::new(Expr::Binary { + op: BinaryOp::BitOr, + left: Box::new(Expr::Integer(17)), + right: Box::new(Expr::Integer(0)), + }), + false, + )), + Stmt::Return(Some(Expr::Binary { + op: BinaryOp::BitOr, + left: Box::new(Expr::IterResultGetValue), + right: Box::new(Expr::Integer(0)), + })), + ] +} + fn compiler_private_async_iter_result_generic_body() -> Vec { vec![ Stmt::Expr(Expr::IterResultSet( @@ -3553,6 +7434,20 @@ fn compiler_private_async_iter_result_annotated_numeric_param_body() -> Vec Vec { + vec![ + Stmt::Expr(Expr::IterResultSet(Box::new(Expr::LocalGet(31)), false)), + Stmt::Return(Some(Expr::IterResultGetValue)), + ] +} + +fn compiler_private_async_iter_result_annotated_i32_param_body() -> Vec { + vec![ + Stmt::Expr(Expr::IterResultSet(Box::new(Expr::LocalGet(32)), false)), + Stmt::Return(Some(Expr::IterResultGetValue)), + ] +} + #[test] fn compiler_private_async_control_cells_use_primitive_heap_boxes() { let ir = compile_ir( @@ -3605,36 +7500,134 @@ fn compiler_private_async_iter_result_f64_slot_uses_typed_handoff() { "numeric async iter-result consumer should use the raw f64 getter:\n{ir}" ); assert!( - !ir.contains("call double @js_iter_result_set("), - "numeric async iter-result payload should avoid the generic JSValue setter:\n{ir}" + !ir.contains("call double @js_iter_result_set("), + "numeric async iter-result payload should avoid the generic JSValue setter:\n{ir}" + ); +} + +#[test] +fn compiler_private_async_iter_result_i1_slot_uses_typed_handoff() { + let ir = compile_ir( + "compiler_private_async_iter_result_i1.ts", + compiler_private_async_iter_result_i1_body(), + ); + + assert!( + ir.contains("call double @js_iter_result_set_i1"), + "proven boolean async iter-result payload should use the raw i1 setter:\n{ir}" + ); + assert!( + ir.contains("call i32 @js_iter_result_get_value_i1"), + "proven boolean async iter-result consumer should use the raw i1 getter:\n{ir}" + ); + assert!( + !ir.contains("call double @js_iter_result_set("), + "proven boolean async iter-result payload should avoid the generic JSValue setter:\n{ir}" + ); + assert!( + !ir.contains("call i32 @js_is_truthy"), + "raw i1 async iter-result consumers should not re-enter generic truthiness in generated IR:\n{ir}" + ); +} + +#[test] +fn compiler_private_async_iter_result_i32_slot_uses_typed_handoff() { + let ir = compile_ir( + "compiler_private_async_iter_result_i32.ts", + compiler_private_async_iter_result_i32_body(), + ); + + assert!( + ir.contains("call double @js_iter_result_set_i32"), + "proven Int32 async iter-result payload should use the raw i32 setter:\n{ir}" + ); + assert!( + ir.contains("call i32 @js_iter_result_get_value_i32"), + "Int32 async iter-result consumer should use the raw i32 getter:\n{ir}" + ); + assert!( + !ir.contains("call double @js_iter_result_set("), + "proven Int32 async iter-result payload should avoid the generic JSValue setter:\n{ir}" + ); + assert!( + !ir.contains("call double @js_iter_result_set_f64"), + "proven Int32 async iter-result payload should not widen through the raw f64 setter:\n{ir}" + ); +} + +#[test] +fn compiler_private_async_iter_result_annotated_numeric_payload_is_coerced_before_raw_slot() { + let ir = compile_ir_for_module_with_opts( + module_with_classes_and_params( + "compiler_private_async_iter_result_annotated_numeric_param.ts", + Vec::new(), + vec![param(30, "value", Type::Number)], + Type::Number, + compiler_private_async_iter_result_annotated_numeric_param_body(), + ), + empty_opts(), + ) + .unwrap(); + + assert!( + ir.contains("call double @js_number_coerce"), + "annotation-only numeric async payloads must be coerced before raw f64 storage:\n{ir}" + ); + assert!( + ir.contains("call double @js_iter_result_set_f64"), + "coerced numeric async payload should still use the raw f64 scratch slot:\n{ir}" + ); + assert!( + !ir.contains("call double @js_iter_result_set("), + "coerced numeric async payload should avoid the generic JSValue setter:\n{ir}" + ); +} + +#[test] +fn compiler_private_async_iter_result_annotated_boolean_payload_stays_generic() { + let ir = compile_ir_for_module_with_opts( + module_with_classes_and_params( + "compiler_private_async_iter_result_annotated_boolean_param.ts", + Vec::new(), + vec![param(31, "value", Type::Boolean)], + Type::Boolean, + compiler_private_async_iter_result_annotated_boolean_param_body(), + ), + empty_opts(), + ) + .unwrap(); + + assert!( + ir.contains("call double @js_iter_result_set("), + "annotation-only boolean async payloads must preserve the runtime JSValue:\n{ir}" + ); + assert!( + !ir.contains("call double @js_iter_result_set_i1"), + "annotation-only boolean async payloads must not be narrowed to raw i1:\n{ir}" ); } #[test] -fn compiler_private_async_iter_result_annotated_numeric_payload_is_coerced_before_raw_slot() { +fn compiler_private_async_iter_result_annotated_i32_payload_stays_off_raw_i32_slot() { let ir = compile_ir_for_module_with_opts( module_with_classes_and_params( - "compiler_private_async_iter_result_annotated_numeric_param.ts", + "compiler_private_async_iter_result_annotated_i32_param.ts", Vec::new(), - vec![param(30, "value", Type::Number)], - Type::Number, - compiler_private_async_iter_result_annotated_numeric_param_body(), + vec![param(32, "value", Type::Int32)], + Type::Int32, + compiler_private_async_iter_result_annotated_i32_param_body(), ), empty_opts(), ) .unwrap(); assert!( - ir.contains("call double @js_number_coerce"), - "annotation-only numeric async payloads must be coerced before raw f64 storage:\n{ir}" + !ir.contains("call double @js_iter_result_set_i32"), + "annotation-only Int32 async payloads must not use the raw i32 slot without proof:\n{ir}" ); assert!( ir.contains("call double @js_iter_result_set_f64"), - "coerced numeric async payload should still use the raw f64 scratch slot:\n{ir}" - ); - assert!( - !ir.contains("call double @js_iter_result_set("), - "coerced numeric async payload should avoid the generic JSValue setter:\n{ir}" + "annotation-only Int32 async payloads should keep the existing numeric-compatible raw f64 slot:\n{ir}" ); } @@ -3677,6 +7670,50 @@ fn artifact_records_compiler_private_async_iter_result_f64_handoff() { } } +#[test] +fn artifact_records_compiler_private_async_iter_result_i32_handoff() { + let artifact = compile_artifact_json( + "artifact_compiler_private_async_iter_result_i32.ts", + compiler_private_async_iter_result_i32_body(), + ); + let records = artifact["records"].as_array().unwrap(); + for consumer in [ + "compiler_private_async_iter_result_set_i32", + "compiler_private_async_iter_result_get_i32", + ] { + assert!( + records.iter().any(|record| { + record["consumer"] == consumer + && record["native_rep_name"] == "i32" + && record["llvm_ty"] == "i32" + }), + "expected async iter-result i32 artifact record {consumer}:\n{artifact:#}" + ); + } +} + +#[test] +fn artifact_records_compiler_private_async_iter_result_i1_handoff() { + let artifact = compile_artifact_json( + "artifact_compiler_private_async_iter_result_i1.ts", + compiler_private_async_iter_result_i1_body(), + ); + let records = artifact["records"].as_array().unwrap(); + for consumer in [ + "compiler_private_async_iter_result_set_i1", + "compiler_private_async_iter_result_get_i1", + ] { + assert!( + records.iter().any(|record| { + record["consumer"] == consumer + && record["native_rep_name"] == "i1" + && record["llvm_ty"] == "i1" + }), + "expected async iter-result i1 artifact record {consumer}:\n{artifact:#}" + ); + } +} + #[test] fn artifact_records_compiler_private_async_control_cells() { let artifact = compile_artifact_json( @@ -3837,6 +7874,65 @@ fn typed_f64_rejected_signature_module(case: &str) -> Module { module } +fn typed_f64_mixed_clone_test_module() -> Module { + let mut module = typed_f64_clone_test_module(false); + module.name = "typed_f64_mixed_function_abi.ts".to_string(); + module.functions[0].params = vec![ + param(1, "a", Type::Number), + param(2, "b", Type::Int32), + param(6, "flag", Type::Boolean), + ]; + module.functions[0].body = vec![Stmt::Return(Some(Expr::Binary { + op: BinaryOp::Add, + left: Box::new(local(1)), + right: Box::new(local(2)), + }))]; + module.functions[1].params = vec![ + param(3, "x", Type::Number), + param(4, "y", Type::Int32), + param(7, "flag", Type::Boolean), + ]; + module.functions[1].body = vec![Stmt::Return(Some(Expr::Call { + callee: Box::new(Expr::FuncRef(1)), + args: vec![local(3), local(4), local(7)], + type_args: Vec::new(), + byte_offset: 0, + }))]; + module +} + +fn typed_f64_i32_local_clone_test_module() -> Module { + let mut module = typed_f64_clone_test_module(false); + module.name = "typed_f64_i32_local_function_abi.ts".to_string(); + module.functions[0].params = vec![param(1, "a", Type::Number), param(2, "b", Type::Int32)]; + module.functions[0].body = vec![ + Stmt::Let { + id: 5, + name: "mask".to_string(), + ty: Type::Int32, + mutable: false, + init: Some(Expr::Binary { + op: BinaryOp::BitOr, + left: Box::new(local(2)), + right: Box::new(int(1)), + }), + }, + Stmt::Return(Some(Expr::Binary { + op: BinaryOp::Add, + left: Box::new(local(1)), + right: Box::new(local(5)), + })), + ]; + module.functions[1].params = vec![param(3, "x", Type::Number), param(4, "y", Type::Int32)]; + module.functions[1].body = vec![Stmt::Return(Some(Expr::Call { + callee: Box::new(Expr::FuncRef(1)), + args: vec![local(3), local(4)], + type_args: Vec::new(), + byte_offset: 0, + }))]; + module +} + fn typed_i1_clone_test_module() -> Module { typed_i1_clone_test_module_named("typed_i1_function_abi.ts") } @@ -4376,6 +8472,63 @@ fn typed_f64_method_clone_module() -> Module { ) } +fn typed_f64_i32_local_method_clone_module() -> Module { + let mut calc = class(203, "Calc", Vec::new()); + calc.methods.push(Function { + id: 204, + name: "mix".to_string(), + type_params: Vec::new(), + params: vec![param(21, "a", Type::Number), param(22, "b", Type::Int32)], + return_type: Type::Number, + body: vec![ + Stmt::Let { + id: 25, + name: "mask".to_string(), + ty: Type::Int32, + mutable: false, + init: Some(Expr::Binary { + op: BinaryOp::BitOr, + left: Box::new(local(22)), + right: Box::new(int(1)), + }), + }, + Stmt::Return(Some(Expr::Binary { + op: BinaryOp::Add, + left: Box::new(local(21)), + right: Box::new(local(25)), + })), + ], + is_async: false, + is_generator: false, + is_strict: false, + is_exported: false, + captures: Vec::new(), + decorators: Vec::new(), + was_plain_async: false, + was_unrolled: false, + }); + + module_with_classes_and_params( + "typed_f64_i32_local_method_abi.ts", + vec![calc], + vec![ + param(1, "receiver", Type::Named("Calc".to_string())), + param(2, "x", Type::Number), + param(3, "y", Type::Int32), + ], + Type::Number, + vec![Stmt::Return(Some(Expr::Call { + callee: Box::new(Expr::PropertyGet { + object: Box::new(local(1)), + property: "mix".to_string(), + }), + args: vec![local(2), local(3)], + type_args: Vec::new(), + byte_offset: 0, + }))], + ) +} + fn typed_f64_method_negative_module(case: &str) -> Module { let mut calc = class(202, "Calc", vec![class_field("x", Type::Number)]); let mut params = vec![param(21, "a", Type::Number), param(22, "b", Type::Number)]; @@ -4657,64 +8810,204 @@ fn typed_f64_closure_clone_module(case: &str) -> Module { mutable: false, init: Some(number(1.5)), }); - captures.push(30); + captures.push(30); + body_expr = Expr::Binary { + op: BinaryOp::Add, + left: Box::new(body_expr), + right: Box::new(local(30)), + }; + } + "mutable_capture" => { + prefix.push(Stmt::Let { + id: 30, + name: "scale".to_string(), + ty: Type::Number, + mutable: true, + init: Some(number(1.5)), + }); + captures.push(30); + mutable_captures.push(30); + body_expr = Expr::Binary { + op: BinaryOp::Add, + left: Box::new(body_expr), + right: Box::new(local(30)), + }; + } + other => panic!("unknown typed-f64 closure fixture: {other}"), + } + + let mut body = prefix; + body.extend([ + Stmt::Let { + id: 10, + name: "adder".to_string(), + ty: Type::Function(perry_types::FunctionType { + params: vec![ + ("a".to_string(), Type::Number, false), + ("b".to_string(), Type::Number, false), + ], + return_type: Box::new(Type::Number), + is_async: false, + is_generator: false, + }), + mutable: false, + init: Some(Expr::Closure { + func_id: 300, + params, + return_type: Type::Number, + body: vec![ + Stmt::Let { + id: 33, + name: "sum".to_string(), + ty: Type::Number, + mutable: false, + init: Some(body_expr), + }, + Stmt::Return(Some(Expr::Binary { + op: BinaryOp::Mul, + left: Box::new(local(33)), + right: Box::new(number(2.0)), + })), + ], + captures, + mutable_captures, + captures_this: false, + captures_new_target: false, + enclosing_class: None, + is_arrow: true, + is_async: false, + is_generator: false, + is_strict: false, + }), + }, + Stmt::Return(Some(Expr::Call { + callee: Box::new(local(10)), + args: vec![number(2.0), number(3.0)], + type_args: Vec::new(), + byte_offset: 0, + })), + ]); + + module("typed_f64_closure_abi.ts", body) +} + +fn typed_i32_closure_clone_module(case: &str) -> Module { + let mut params = vec![param(31, "a", Type::Int32), param(32, "b", Type::Int32)]; + let mut prefix = Vec::new(); + let mut captures = Vec::new(); + let mut mutable_captures = Vec::new(); + let mut local_ty = Type::Function(perry_types::FunctionType { + params: vec![ + ("a".to_string(), Type::Int32, false), + ("b".to_string(), Type::Int32, false), + ], + return_type: Box::new(Type::Int32), + is_async: false, + is_generator: false, + }); + let mut return_type = Type::Int32; + let mut first_let_ty = Type::Int32; + let mut body_expr = Expr::Binary { + op: BinaryOp::BitXor, + left: Box::new(local(31)), + right: Box::new(local(32)), + }; + let mut return_expr = Expr::Binary { + op: BinaryOp::BitOr, + left: Box::new(local(33)), + right: Box::new(int(7)), + }; + match case { + "eligible" => {} + "capture" => { + prefix.push(Stmt::Let { + id: 30, + name: "mask".to_string(), + ty: Type::Int32, + mutable: false, + init: Some(int(3)), + }); + captures.push(30); + return_expr = Expr::Binary { + op: BinaryOp::BitAnd, + left: Box::new(return_expr), + right: Box::new(local(30)), + }; + } + "number_param" => { + params[0].ty = Type::Number; + local_ty = Type::Function(perry_types::FunctionType { + params: vec![ + ("a".to_string(), Type::Number, false), + ("b".to_string(), Type::Int32, false), + ], + return_type: Box::new(Type::Int32), + is_async: false, + is_generator: false, + }); + } + "number_return" => { + return_type = Type::Number; + local_ty = Type::Function(perry_types::FunctionType { + params: vec![ + ("a".to_string(), Type::Int32, false), + ("b".to_string(), Type::Int32, false), + ], + return_type: Box::new(Type::Number), + is_async: false, + is_generator: false, + }); + } + "unsafe_add" => { + first_let_ty = Type::Int32; body_expr = Expr::Binary { op: BinaryOp::Add, - left: Box::new(body_expr), - right: Box::new(local(30)), + left: Box::new(local(31)), + right: Box::new(local(32)), }; } "mutable_capture" => { prefix.push(Stmt::Let { id: 30, - name: "scale".to_string(), - ty: Type::Number, + name: "mask".to_string(), + ty: Type::Int32, mutable: true, - init: Some(number(1.5)), + init: Some(int(3)), }); captures.push(30); mutable_captures.push(30); - body_expr = Expr::Binary { - op: BinaryOp::Add, - left: Box::new(body_expr), + return_expr = Expr::Binary { + op: BinaryOp::BitAnd, + left: Box::new(return_expr), right: Box::new(local(30)), }; } - other => panic!("unknown typed-f64 closure fixture: {other}"), + "dynamic" => { + local_ty = Type::Any; + } + other => panic!("unknown typed-i32 closure fixture: {other}"), } let mut body = prefix; body.extend([ Stmt::Let { id: 10, - name: "adder".to_string(), - ty: Type::Function(perry_types::FunctionType { - params: vec![ - ("a".to_string(), Type::Number, false), - ("b".to_string(), Type::Number, false), - ], - return_type: Box::new(Type::Number), - is_async: false, - is_generator: false, - }), + name: "mix_i32".to_string(), + ty: local_ty, mutable: false, init: Some(Expr::Closure { - func_id: 300, + func_id: 303, params, - return_type: Type::Number, + return_type: return_type.clone(), body: vec![ Stmt::Let { id: 33, - name: "sum".to_string(), - ty: Type::Number, + name: "mixed".to_string(), + ty: first_let_ty, mutable: false, init: Some(body_expr), }, - Stmt::Return(Some(Expr::Binary { - op: BinaryOp::Mul, - left: Box::new(local(33)), - right: Box::new(number(2.0)), - })), + Stmt::Return(Some(return_expr)), ], captures, mutable_captures, @@ -4729,13 +9022,19 @@ fn typed_f64_closure_clone_module(case: &str) -> Module { }, Stmt::Return(Some(Expr::Call { callee: Box::new(local(10)), - args: vec![number(2.0), number(3.0)], + args: vec![int(11), int(5)], type_args: Vec::new(), byte_offset: 0, })), ]); - module("typed_f64_closure_abi.ts", body) + module_with_classes_and_params( + &format!("typed_i32_closure_{case}.ts"), + Vec::new(), + Vec::new(), + return_type, + body, + ) } fn typed_i1_method_clone_module(case: &str) -> Module { @@ -4904,6 +9203,86 @@ fn typed_i32_method_clone_module(case: &str) -> Module { ) } +fn typed_string_method_clone_module(case: &str) -> Module { + let mut labeler = class(206, "Labeler", Vec::new()); + let mut params = vec![param(21, "s", Type::String)]; + let mut return_type = Type::String; + let mut body = vec![ + Stmt::Let { + id: 25, + name: "copy".to_string(), + ty: Type::String, + mutable: false, + init: Some(local(21)), + }, + Stmt::Return(Some(local(25))), + ]; + let mut receiver_ty = Type::Named("Labeler".to_string()); + match case { + "eligible" => {} + "any_param" => { + params[0].ty = Type::Any; + } + "number_param" => { + params[0].ty = Type::Number; + return_type = Type::Number; + body = vec![Stmt::Return(Some(number(1.0)))]; + } + "default_param" => { + params[0].default = Some(Expr::String("fallback".to_string())); + } + "rest_param" => { + params[0].is_rest = true; + } + "concat_body" => { + body = vec![Stmt::Return(Some(Expr::Binary { + op: BinaryOp::Add, + left: Box::new(local(21)), + right: Box::new(local(21)), + }))]; + } + "dynamic_receiver" => { + receiver_ty = Type::Any; + } + other => panic!("unknown typed-string method fixture: {other}"), + } + labeler.methods.push(Function { + id: 240, + name: "pick".to_string(), + type_params: Vec::new(), + params, + return_type, + body, + is_async: false, + is_generator: false, + is_strict: false, + is_exported: false, + captures: Vec::new(), + decorators: Vec::new(), + was_plain_async: false, + was_unrolled: false, + }); + + module_with_classes_and_params( + &format!("typed_string_method_{case}.ts"), + vec![labeler], + vec![ + param(1, "receiver", receiver_ty), + param(2, "x", Type::String), + ], + Type::String, + vec![Stmt::Return(Some(Expr::Call { + callee: Box::new(Expr::PropertyGet { + object: Box::new(local(1)), + property: "pick".to_string(), + }), + args: vec![local(2)], + type_args: Vec::new(), + byte_offset: 0, + }))], + ) +} + fn typed_i1_numeric_predicate_method_module() -> Module { let mut meter = class(204, "Meter", Vec::new()); meter.methods.push(Function { @@ -5298,6 +9677,82 @@ fn scalar_method_shadowed_by_field_module() -> Module { module } +fn scalar_method_numeric_local_temp_module(case: &str, mutable_temp: bool) -> Module { + let mut module = scalar_method_summary_module(); + module.name = format!("scalar_method_numeric_local_temp_{case}.ts"); + module.classes[0].methods.clear(); + module.classes[0].methods.push(Function { + id: 103, + name: "weighted".to_string(), + type_params: Vec::new(), + params: vec![param(12, "scale", Type::Number)], + return_type: Type::Number, + body: vec![ + Stmt::Let { + id: 130, + name: "shifted".to_string(), + ty: Type::Number, + mutable: mutable_temp, + init: Some(Expr::Binary { + op: BinaryOp::Add, + left: Box::new(Expr::PropertyGet { + object: Box::new(Expr::This), + property: "x".to_string(), + }), + right: Box::new(local(12)), + }), + }, + Stmt::Let { + id: 131, + name: "scaled".to_string(), + ty: Type::Number, + mutable: false, + init: Some(Expr::Binary { + op: BinaryOp::Mul, + left: Box::new(local(130)), + right: Box::new(Expr::PropertyGet { + object: Box::new(Expr::This), + property: "y".to_string(), + }), + }), + }, + Stmt::Return(Some(local(131))), + ], + is_async: false, + is_generator: false, + is_strict: false, + is_exported: false, + captures: Vec::new(), + decorators: Vec::new(), + was_plain_async: false, + was_unrolled: false, + }); + module.functions[0].body = vec![ + Stmt::Let { + id: 20, + name: "p".to_string(), + ty: Type::Named("Point".to_string()), + mutable: false, + init: Some(Expr::New { + class_name: "Point".to_string(), + args: vec![number(1.25), number(2.75)], + type_args: Vec::new(), + byte_offset: 0, + }), + }, + Stmt::Return(Some(Expr::Call { + callee: Box::new(Expr::PropertyGet { + object: Box::new(local(20)), + property: "weighted".to_string(), + }), + args: vec![number(3.0)], + type_args: Vec::new(), + byte_offset: 0, + })), + ]; + module +} + fn scalar_predicate_method_body(field: &str) -> Expr { Expr::Compare { op: CompareOp::Gt, @@ -5341,9 +9796,156 @@ fn scalar_method_boolean_predicate_module() -> Module { id: 102, name: "isAbove".to_string(), type_params: Vec::new(), - params: vec![param(12, "limit", Type::Number)], - return_type: Type::Boolean, - body: vec![Stmt::Return(Some(scalar_predicate_method_body("x")))], + params: vec![param(12, "limit", Type::Number)], + return_type: Type::Boolean, + body: vec![Stmt::Return(Some(scalar_predicate_method_body("x")))], + is_async: false, + is_generator: false, + is_strict: false, + is_exported: false, + captures: Vec::new(), + decorators: Vec::new(), + was_plain_async: false, + was_unrolled: false, + }); + module +} + +fn scalar_method_boolean_public_numeric_arg_module(case: &str, arg_ty: Type) -> Module { + let mut module = scalar_method_boolean_predicate_module(); + module.name = format!("scalar_method_boolean_guarded_{case}_arg.ts"); + module.functions[0].params = vec![param(70, "limit", arg_ty)]; + module.functions[0].body = vec![ + Stmt::Let { + id: 20, + name: "p".to_string(), + ty: Type::Named("Point".to_string()), + mutable: false, + init: Some(Expr::New { + class_name: "Point".to_string(), + args: vec![number(4.0), number(2.0)], + type_args: Vec::new(), + byte_offset: 0, + }), + }, + Stmt::Return(Some(Expr::Call { + callee: Box::new(Expr::PropertyGet { + object: Box::new(local(20)), + property: "isAbove".to_string(), + }), + args: vec![local(70)], + type_args: Vec::new(), + byte_offset: 0, + })), + ]; + module +} + +fn scalar_method_boolean_public_numeric_expr_arg_module() -> Module { + let mut module = scalar_method_boolean_predicate_module(); + module.name = "scalar_method_boolean_guarded_expr_arg.ts".to_string(); + module.functions[0].params = vec![ + param(70, "limit", Type::Number), + param(71, "delta", Type::Int32), + ]; + module.functions[0].body = vec![ + Stmt::Let { + id: 20, + name: "p".to_string(), + ty: Type::Named("Point".to_string()), + mutable: false, + init: Some(Expr::New { + class_name: "Point".to_string(), + args: vec![number(4.0), number(2.0)], + type_args: Vec::new(), + byte_offset: 0, + }), + }, + Stmt::Return(Some(Expr::Call { + callee: Box::new(Expr::PropertyGet { + object: Box::new(local(20)), + property: "isAbove".to_string(), + }), + args: vec![Expr::Binary { + op: BinaryOp::Add, + left: Box::new(local(70)), + right: Box::new(Expr::Binary { + op: BinaryOp::Mul, + left: Box::new(local(71)), + right: Box::new(int(2)), + }), + }], + type_args: Vec::new(), + byte_offset: 0, + })), + ]; + module +} + +fn scalar_method_int32_bitwise_module(case: &str, field_ty: Type, arg_ty: Type) -> Module { + let mut flags = class( + 111, + "Flags", + vec![ + class_field("mask", field_ty.clone()), + class_field("salt", field_ty), + ], + ); + flags.constructor = Some(Function { + id: 110, + name: "Flags_constructor".to_string(), + type_params: Vec::new(), + params: vec![ + param(10, "mask", Type::Int32), + param(11, "salt", Type::Int32), + ], + return_type: Type::Any, + body: vec![ + Stmt::Expr(Expr::PropertySet { + object: Box::new(Expr::This), + property: "mask".to_string(), + value: Box::new(local(10)), + }), + Stmt::Expr(Expr::PropertySet { + object: Box::new(Expr::This), + property: "salt".to_string(), + value: Box::new(local(11)), + }), + ], + is_async: false, + is_generator: false, + is_strict: false, + is_exported: false, + captures: Vec::new(), + decorators: Vec::new(), + was_plain_async: false, + was_unrolled: false, + }); + flags.methods.push(Function { + id: 111, + name: "mix".to_string(), + type_params: Vec::new(), + params: vec![param(12, "extra", arg_ty.clone())], + return_type: Type::Int32, + body: vec![Stmt::Return(Some(Expr::Binary { + op: BinaryOp::BitAnd, + left: Box::new(Expr::Binary { + op: BinaryOp::BitOr, + left: Box::new(Expr::Binary { + op: BinaryOp::BitXor, + left: Box::new(Expr::PropertyGet { + object: Box::new(Expr::This), + property: "mask".to_string(), + }), + right: Box::new(local(12)), + }), + right: Box::new(Expr::PropertyGet { + object: Box::new(Expr::This), + property: "salt".to_string(), + }), + }), + right: Box::new(int(255)), + }))], is_async: false, is_generator: false, is_strict: false, @@ -5353,34 +9955,104 @@ fn scalar_method_boolean_predicate_module() -> Module { was_plain_async: false, was_unrolled: false, }); + + let arg_is_any = matches!(&arg_ty, Type::Any); + let call_arg = if arg_is_any { local(70) } else { int(12) }; + let params = if arg_is_any { + vec![param(70, "extra", Type::Any)] + } else { + Vec::new() + }; + module_with_classes_and_params( + &format!("scalar_method_int32_bitwise_{case}.ts"), + vec![flags], + params, + Type::Int32, + vec![ + Stmt::Let { + id: 20, + name: "flags".to_string(), + ty: Type::Named("Flags".to_string()), + mutable: false, + init: Some(Expr::New { + class_name: "Flags".to_string(), + args: vec![int(42), int(7)], + type_args: Vec::new(), + byte_offset: 0, + }), + }, + Stmt::Return(Some(Expr::Call { + callee: Box::new(Expr::PropertyGet { + object: Box::new(local(20)), + property: "mix".to_string(), + }), + args: vec![call_arg], + type_args: Vec::new(), + byte_offset: 0, + })), + ], + ) +} + +fn scalar_method_int32_bitwise_public_arg_module() -> Module { + let mut module = scalar_method_int32_bitwise_module("guarded_arg", Type::Int32, Type::Int32); + module.functions[0].params = vec![param(70, "extra", Type::Int32)]; + if let Stmt::Return(Some(Expr::Call { args, .. })) = &mut module.functions[0].body[1] { + args[0] = local(70); + } else { + panic!("unexpected int32 bitwise scalar method fixture body"); + } module } -fn scalar_method_boolean_public_numeric_arg_module(case: &str, arg_ty: Type) -> Module { - let mut module = scalar_method_boolean_predicate_module(); - module.name = format!("scalar_method_boolean_guarded_{case}_arg.ts"); - module.functions[0].params = vec![param(70, "limit", arg_ty)]; - module.functions[0].body = vec![ +fn scalar_method_int32_unsigned_shift_module() -> Module { + let mut module = scalar_method_int32_bitwise_module("unsigned_shift", Type::Int32, Type::Int32); + module.classes[0].methods[0].body = vec![Stmt::Return(Some(Expr::Binary { + op: BinaryOp::UShr, + left: Box::new(Expr::PropertyGet { + object: Box::new(Expr::This), + property: "mask".to_string(), + }), + right: Box::new(int(0)), + }))]; + module +} + +fn scalar_method_int32_bitwise_local_temp_module() -> Module { + let mut module = scalar_method_int32_bitwise_module("local_temp", Type::Int32, Type::Int32); + module.classes[0].methods[0].body = vec![ Stmt::Let { - id: 20, - name: "p".to_string(), - ty: Type::Named("Point".to_string()), + id: 130, + name: "mixed".to_string(), + ty: Type::Int32, mutable: false, - init: Some(Expr::New { - class_name: "Point".to_string(), - args: vec![number(4.0), number(2.0)], - type_args: Vec::new(), - byte_offset: 0, + init: Some(Expr::Binary { + op: BinaryOp::BitXor, + left: Box::new(Expr::PropertyGet { + object: Box::new(Expr::This), + property: "mask".to_string(), + }), + right: Box::new(local(12)), }), }, - Stmt::Return(Some(Expr::Call { - callee: Box::new(Expr::PropertyGet { - object: Box::new(local(20)), - property: "isAbove".to_string(), + Stmt::Let { + id: 131, + name: "shifted".to_string(), + ty: Type::Int32, + mutable: false, + init: Some(Expr::Binary { + op: BinaryOp::Shl, + left: Box::new(local(130)), + right: Box::new(int(1)), + }), + }, + Stmt::Return(Some(Expr::Binary { + op: BinaryOp::BitOr, + left: Box::new(local(131)), + right: Box::new(Expr::PropertyGet { + object: Box::new(Expr::This), + property: "salt".to_string(), }), - args: vec![local(70)], - type_args: Vec::new(), - byte_offset: 0, })), ]; module @@ -5531,6 +10203,36 @@ fn scalar_method_boolean_negative_module(case: &str) -> Module { })), ]; } + "any_arg_expr" => { + module.functions[0].params = vec![param(70, "limit", Type::Any)]; + module.functions[0].body = vec![ + Stmt::Let { + id: 20, + name: "p".to_string(), + ty: Type::Named("Point".to_string()), + mutable: false, + init: Some(Expr::New { + class_name: "Point".to_string(), + args: vec![number(4.0), number(2.0)], + type_args: Vec::new(), + byte_offset: 0, + }), + }, + Stmt::Return(Some(Expr::Call { + callee: Box::new(Expr::PropertyGet { + object: Box::new(local(20)), + property: "isAbove".to_string(), + }), + args: vec![Expr::Binary { + op: BinaryOp::Add, + left: Box::new(local(70)), + right: Box::new(int(1)), + }], + type_args: Vec::new(), + byte_offset: 0, + })), + ]; + } other => panic!("unknown scalar method predicate negative fixture: {other}"), } module @@ -5676,106 +10378,246 @@ fn typed_string_function_clone_emits_internal_clone_and_guarded_wrapper() { ); assert!( ir.contains(&format!( - "define internal double @{generic_body}(double %arg1)" + "define internal double @{generic_body}(double %arg1)" + )), + "generic JSValue ABI body must remain emitted separately:\n{ir}" + ); + assert!( + wrapper_ir.contains("call i32 @js_typed_string_arg_guard"), + "public wrapper should guard string JSValue args:\n{wrapper_ir}" + ); + assert!( + wrapper_ir.contains("call i64 @js_typed_string_arg_to_raw"), + "public wrapper should unbox string args to raw handles:\n{wrapper_ir}" + ); + assert!( + wrapper_ir.contains(&format!("call i64 @{typed}(i64 ")), + "public wrapper should call the raw string clone:\n{wrapper_ir}" + ); + assert!( + wrapper_ir.contains("call double @js_nanbox_string(i64 "), + "typed string result should box at the public ABI boundary:\n{wrapper_ir}" + ); + assert!( + wrapper_ir.contains(&format!("call double @{generic_body}(")), + "string-guard failure should keep a generic body fallback:\n{wrapper_ir}" + ); + assert!( + caller_ir.contains("typed_string_call.fast") + && caller_ir.contains("typed_string_call.fallback") + && caller_ir.contains("call i32 @js_typed_string_arg_guard") + && caller_ir.contains("call i64 @js_typed_string_arg_to_raw") + && caller_ir.contains(&format!("call i64 @{typed}(i64 ")) + && caller_ir.contains("call double @js_nanbox_string(i64 "), + "same-module direct string call should guard/unbox, call the raw clone, and box at the call boundary:\n{caller_ir}" + ); + assert!( + caller_ir.contains(&format!("call double @{generic_body}(double ")), + "direct string-call guard failure should target the internal generic body:\n{caller_ir}" + ); + assert!( + !caller_ir.contains(&format!("call double @{public}(double ")), + "direct string-call guard failure must not recurse through the public wrapper:\n{caller_ir}" + ); +} + +#[test] +fn typed_string_function_clone_rejects_unsupported_string_shapes() { + for case in [ + "any_param", + "number_param", + "default_param", + "rest_param", + "concat_body", + ] { + let ir = String::from_utf8( + compile_module(&typed_string_clone_test_module(case), empty_opts()).unwrap(), + ) + .unwrap(); + assert!( + !ir.contains("__typed_string") && !ir.contains("__generic"), + "{case} must stay on the ordinary JSValue ABI:\n{ir}" + ); + } +} + +#[test] +fn artifact_records_typed_string_direct_call_selection() { + let artifact = compile_artifact_json_for_module(typed_string_clone_test_module("positive")); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "Call" + && record["consumer"] == "typed_string_func_ref_call" + && record["native_rep_name"] == "js_value" + && record["native_value_state"] == "region_local" + && record["notes"].as_array().is_some_and(|notes| { + notes.iter().any(|note| { + note.as_str().is_some_and(|text| { + text.contains( + "typed_clone=perry_fn_typed_string_function_abi_ts__id__typed_string", + ) + }) + }) && notes.iter().any(|note| { + note.as_str().is_some_and(|text| { + text.contains( + "generic_body=perry_fn_typed_string_function_abi_ts__id__generic", + ) + }) + }) && notes + .iter() + .any(|note| note == "typed_signature=string(i64, ...)->string") + && notes + .iter() + .any(|note| note == "boxed_result_at=direct_call_boundary") + }) + }), + "expected typed-string direct-call artifact:\n{artifact:#}" + ); +} + +#[test] +fn typed_f64_function_clone_accepts_mixed_raw_signature_and_direct_call() { + let ir = String::from_utf8( + compile_module(&typed_f64_mixed_clone_test_module(), empty_opts()).unwrap(), + ) + .unwrap(); + let public = "perry_fn_typed_f64_mixed_function_abi_ts__add"; + let typed = "perry_fn_typed_f64_mixed_function_abi_ts__add__typed_f64"; + let generic_body = "perry_fn_typed_f64_mixed_function_abi_ts__add__generic"; + let caller = "perry_fn_typed_f64_mixed_function_abi_ts__caller"; + let wrapper_ir = function_ir_section(&ir, public); + let typed_ir = defined_function_ir_section(&ir, typed); + let caller_ir = defined_function_ir_section(&ir, caller); + + assert!( + ir.contains(&format!( + "define internal double @{typed}(double %arg1, i32 %arg2, i1 %arg6)" + )), + "typed f64 clone should carry mixed raw params internally:\n{ir}" + ); + assert!( + ir.contains(&format!( + "define double @{public}(double %arg1, double %arg2, double %arg6)" + )), + "public wrapper must preserve the JSValue ABI:\n{ir}" + ); + assert!( + typed_ir.contains("sitofp i32 %arg2 to double") + && typed_ir.contains("fadd double") + && !typed_ir.contains("js_typed_f64_arg_to_raw") + && !typed_ir.contains("js_nanbox"), + "typed clone body should avoid JSValue traffic on the hot path:\n{typed_ir}" + ); + assert!( + wrapper_ir.contains("call i32 @js_typed_f64_arg_guard") + && wrapper_ir.contains("call i32 @js_typed_i32_arg_guard") + && wrapper_ir.contains("call i32 @js_typed_i1_arg_guard") + && wrapper_ir.contains(&format!("call double @{typed}(double %")) + && wrapper_ir.contains(&format!("call double @{generic_body}(")), + "public wrapper should guard mixed JSValue args and keep generic fallback:\n{wrapper_ir}" + ); + assert!( + caller_ir.contains("typed_f64_call.fast") + && caller_ir.contains("call i32 @js_typed_i32_arg_to_raw") + && caller_ir.contains("call i32 @js_typed_i1_arg_to_raw") + && caller_ir.contains(&format!("call double @{typed}(double ")) + && caller_ir.contains(&format!("call double @{generic_body}(")) + && !caller_ir.contains(&format!("call double @{public}(")), + "same-module direct call should use the mixed raw clone plus generic body fallback, not the public wrapper:\n{caller_ir}" + ); + + let artifact = compile_artifact_json_for_module(typed_f64_mixed_clone_test_module()); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "Call" + && record["consumer"] == "typed_f64_func_ref_call" + && record["native_value_state"] == "region_local" + && record["notes"].as_array().is_some_and(|notes| { + notes.iter().any(|note| { + note.as_str().is_some_and(|text| { + text.contains(&format!("typed_clone={typed}")) + && text.contains(&format!("generic_body={generic_body}")) + }) + }) && notes + .iter() + .any(|note| note == "typed_signature=f64(f64, ...)->f64") + }) + }), + "expected mixed typed-f64 direct call artifact:\n{artifact:#}" + ); +} + +#[test] +fn typed_f64_function_clone_keeps_i32_locals_raw_until_f64_use() { + let ir = String::from_utf8( + compile_module(&typed_f64_i32_local_clone_test_module(), empty_opts()).unwrap(), + ) + .unwrap(); + let public = "perry_fn_typed_f64_i32_local_function_abi_ts__add"; + let typed = "perry_fn_typed_f64_i32_local_function_abi_ts__add__typed_f64"; + let generic_body = "perry_fn_typed_f64_i32_local_function_abi_ts__add__generic"; + let caller = "perry_fn_typed_f64_i32_local_function_abi_ts__caller"; + let wrapper_ir = function_ir_section(&ir, public); + let typed_ir = defined_function_ir_section(&ir, typed); + let caller_ir = defined_function_ir_section(&ir, caller); + + assert!( + ir.contains(&format!( + "define internal double @{typed}(double %arg1, i32 %arg2)" )), - "generic JSValue ABI body must remain emitted separately:\n{ir}" - ); - assert!( - wrapper_ir.contains("call i32 @js_typed_string_arg_guard"), - "public wrapper should guard string JSValue args:\n{wrapper_ir}" - ); - assert!( - wrapper_ir.contains("call i64 @js_typed_string_arg_to_raw"), - "public wrapper should unbox string args to raw handles:\n{wrapper_ir}" - ); - assert!( - wrapper_ir.contains(&format!("call i64 @{typed}(i64 ")), - "public wrapper should call the raw string clone:\n{wrapper_ir}" - ); - assert!( - wrapper_ir.contains("call double @js_nanbox_string(i64 "), - "typed string result should box at the public ABI boundary:\n{wrapper_ir}" - ); - assert!( - wrapper_ir.contains(&format!("call double @{generic_body}(")), - "string-guard failure should keep a generic body fallback:\n{wrapper_ir}" + "typed f64 clone should accept the raw i32 parameter:\n{ir}" ); assert!( - caller_ir.contains("typed_string_call.fast") - && caller_ir.contains("typed_string_call.fallback") - && caller_ir.contains("call i32 @js_typed_string_arg_guard") - && caller_ir.contains("call i64 @js_typed_string_arg_to_raw") - && caller_ir.contains(&format!("call i64 @{typed}(i64 ")) - && caller_ir.contains("call double @js_nanbox_string(i64 "), - "same-module direct string call should guard/unbox, call the raw clone, and box at the call boundary:\n{caller_ir}" + typed_ir.contains(" or i32 %arg2, 1") + && typed_ir.contains("sitofp i32 ") + && typed_ir.contains(" fadd double") + && !typed_ir.contains("js_typed_i32_arg_to_raw") + && !typed_ir.contains("js_nanbox"), + "typed f64 clone should keep the Int32 local raw until it flows into f64 arithmetic:\n{typed_ir}" ); assert!( - caller_ir.contains(&format!("call double @{generic_body}(double ")), - "direct string-call guard failure should target the internal generic body:\n{caller_ir}" + wrapper_ir.contains("call i32 @js_typed_i32_arg_guard") + && wrapper_ir.contains("call i32 @js_typed_i32_arg_to_raw") + && wrapper_ir.contains(&format!("call double @{typed}(double ")) + && wrapper_ir.contains(&format!("call double @{generic_body}(")), + "public wrapper should guard/unbox the Int32 ABI arg and keep the generic fallback:\n{wrapper_ir}" ); assert!( - !caller_ir.contains(&format!("call double @{public}(double ")), - "direct string-call guard failure must not recurse through the public wrapper:\n{caller_ir}" + caller_ir.contains("typed_f64_call.fast") + && caller_ir.contains("call i32 @js_typed_i32_arg_guard") + && caller_ir.contains("call i32 @js_typed_i32_arg_to_raw") + && caller_ir.contains(&format!("call double @{typed}(double ")) + && caller_ir.contains(&format!("call double @{generic_body}(")) + && !caller_ir.contains(&format!("call double @{public}(")), + "same-module direct call should target the mixed raw clone with generic-body fallback:\n{caller_ir}" ); -} - -#[test] -fn typed_string_function_clone_rejects_unsupported_string_shapes() { - for case in [ - "any_param", - "number_param", - "default_param", - "rest_param", - "concat_body", - ] { - let ir = String::from_utf8( - compile_module(&typed_string_clone_test_module(case), empty_opts()).unwrap(), - ) - .unwrap(); - assert!( - !ir.contains("__typed_string") && !ir.contains("__generic"), - "{case} must stay on the ordinary JSValue ABI:\n{ir}" - ); - } -} -#[test] -fn artifact_records_typed_string_direct_call_selection() { - let artifact = compile_artifact_json_for_module(typed_string_clone_test_module("positive")); + let artifact = compile_artifact_json_for_module(typed_f64_i32_local_clone_test_module()); let records = artifact["records"].as_array().unwrap(); assert!( records.iter().any(|record| { record["expr_kind"] == "Call" - && record["consumer"] == "typed_string_func_ref_call" - && record["native_rep_name"] == "js_value" + && record["consumer"] == "typed_f64_func_ref_call" && record["native_value_state"] == "region_local" && record["notes"].as_array().is_some_and(|notes| { notes.iter().any(|note| { note.as_str().is_some_and(|text| { - text.contains( - "typed_clone=perry_fn_typed_string_function_abi_ts__id__typed_string", - ) - }) - }) && notes.iter().any(|note| { - note.as_str().is_some_and(|text| { - text.contains( - "generic_body=perry_fn_typed_string_function_abi_ts__id__generic", - ) + text.contains(&format!("typed_clone={typed}")) + && text.contains(&format!("generic_body={generic_body}")) }) }) && notes .iter() - .any(|note| note == "typed_signature=string(i64, ...)->string") - && notes - .iter() - .any(|note| note == "boxed_result_at=direct_call_boundary") + .any(|note| note == "typed_signature=f64(f64, ...)->f64") }) }), - "expected typed-string direct-call artifact:\n{artifact:#}" + "expected typed-f64 direct-call artifact for raw i32 local clone:\n{artifact:#}" ); } #[test] -fn typed_f64_function_clone_rejects_any_and_mixed_parameter_signatures() { +fn typed_f64_function_clone_rejects_any_and_unsafe_mixed_parameter_signatures() { for case in ["any", "mixed"] { let ir = String::from_utf8( compile_module(&typed_f64_rejected_signature_module(case), empty_opts()).unwrap(), @@ -5783,7 +10625,7 @@ fn typed_f64_function_clone_rejects_any_and_mixed_parameter_signatures() { .unwrap(); assert!( !ir.contains("__typed_f64") && !ir.contains("__generic"), - "{case} non-numeric ABI surface must stay generic:\n{ir}" + "{case} unsafe ABI surface must stay generic:\n{ir}" ); } } @@ -6387,55 +11229,246 @@ fn typed_i32_method_clone_emits_internal_clone_and_guarded_direct_call() { let typed = "perry_method_typed_i32_method_eligible_ts__Bits__mix_i32__typed_i32"; let generic_body = "perry_method_typed_i32_method_eligible_ts__Bits__mix_i32__generic"; let wrapper_ir = function_ir_section(&ir, public); - let typed_ir = defined_function_ir_section(&ir, typed); - let caller_ir = - defined_function_ir_section(&ir, "perry_fn_typed_i32_method_eligible_ts__probe"); + let typed_ir = defined_function_ir_section(&ir, typed); + let caller_ir = + defined_function_ir_section(&ir, "perry_fn_typed_i32_method_eligible_ts__probe"); + + assert!( + ir.contains(&format!( + "define internal i32 @{typed}(i32 %arg21, i32 %arg22)" + )), + "typed-i32 method clone should use raw i32 params and i32 return:\n{ir}" + ); + assert!( + typed_ir.contains(" xor i32 %arg21, %arg22") + && typed_ir.contains(" or i32 ") + && !typed_ir.contains(" fadd ") + && !typed_ir.contains(" sitofp "), + "typed-i32 method body should stay in native i32 SSA:\n{typed_ir}" + ); + assert!( + ir.contains(&format!( + "define double @{public}(double %this_arg, double %arg21, double %arg22)" + )) && ir.contains(&format!( + "define internal double @{generic_body}(double %this_arg, double %arg21, double %arg22)" + )), + "typed-i32 method should expose a public JSValue wrapper and keep an internal generic body:\n{ir}" + ); + assert!( + wrapper_ir.contains("call i32 @js_typed_i32_arg_guard") + && wrapper_ir.contains("call i32 @js_typed_i32_arg_to_raw") + && wrapper_ir.contains(&format!("call i32 @{typed}(i32 ")) + && wrapper_ir.contains(INT32_TAG_I64), + "public method wrapper should guard/unbox Int32 args and box raw i32 at the ABI edge:\n{wrapper_ir}" + ); + assert!( + caller_ir.contains("call i32 @js_method_direct_shape_guard") + && caller_ir.contains("typed_i32_method.fast") + && caller_ir.contains("typed_i32_method.generic") + && caller_ir.contains("call i32 @js_typed_i32_arg_guard") + && caller_ir.contains("call i32 @js_typed_i32_arg_to_raw") + && caller_ir.contains(&format!("call i32 @{typed}(i32 ")) + && caller_ir.contains(INT32_TAG_I64), + "exact direct method call should guard receiver/method identity, then guard/unbox Int32 args and call the clone:\n{caller_ir}" + ); + assert!( + caller_ir.contains(&format!("call double @{generic_body}(")), + "direct typed-i32 guard failure should target the internal generic method body:\n{caller_ir}" + ); + assert!( + !caller_ir.contains(&format!("call double @{public}(")), + "direct typed-i32 guard failure must not recurse through the public wrapper:\n{caller_ir}" + ); + assert!( + !ir.contains(&format!("ptrtoint (ptr @{typed}")) + && !ir.contains(&format!("ptrtoint ptr @{typed}")) + && !ir.contains(&format!("ptrtoint (ptr @{generic_body}")) + && !ir.contains(&format!("ptrtoint ptr @{generic_body}")), + "runtime vtable must register the public wrapper, not internal typed/generic bodies:\n{ir}" + ); +} + +#[test] +fn typed_i32_method_public_trampoline_dispatches_before_generic_body() { + let ir = String::from_utf8( + compile_module(&typed_i32_method_clone_module("eligible"), empty_opts()).unwrap(), + ) + .unwrap(); + let public = "perry_method_typed_i32_method_eligible_ts__Bits__mix_i32"; + let typed = "perry_method_typed_i32_method_eligible_ts__Bits__mix_i32__typed_i32"; + let generic_body = "perry_method_typed_i32_method_eligible_ts__Bits__mix_i32__generic"; + let wrapper_ir = function_ir_section(&ir, public); + + let typed_call = wrapper_ir + .find(&format!("call i32 @{typed}(")) + .unwrap_or_else(|| { + panic!("public method wrapper should call typed-i32 clone:\n{wrapper_ir}") + }); + let fallback_call = wrapper_ir + .find(&format!("call double @{generic_body}(")) + .unwrap_or_else(|| { + panic!("public method wrapper should call generic body fallback:\n{wrapper_ir}") + }); + assert!( + typed_call < fallback_call, + "public method wrapper should dispatch to typed clone before generic fallback:\n{wrapper_ir}" + ); + assert!( + wrapper_ir.contains("call i32 @js_typed_i32_arg_guard") + && wrapper_ir.contains("call i32 @js_typed_i32_arg_to_raw"), + "public method wrapper should guard and unbox Int32 JSValue args:\n{wrapper_ir}" + ); + assert!( + !wrapper_ir.contains(&format!("call double @{public}(")), + "public method wrapper must not recursively call itself:\n{wrapper_ir}" + ); +} + +#[test] +fn artifact_records_typed_i32_method_clone_selection() { + let artifact = compile_artifact_json_for_module(typed_i32_method_clone_module("eligible")); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "MethodCall" + && record["consumer"] == "typed_i32_method_direct_call" + && record["native_rep_name"] == "js_value" + && record["llvm_ty"] == "double" + && record["native_value_state"] == "region_local" + && record["notes"].as_array().is_some_and(|notes| { + notes.iter().any(|note| { + note.as_str().is_some_and(|text| { + text.contains( + "typed_clone=perry_method_typed_i32_method_eligible_ts__Bits__mix_i32__typed_i32", + ) + }) + }) && notes.iter().any(|note| { + note + == "generic_method=perry_method_typed_i32_method_eligible_ts__Bits__mix_i32__generic" + }) && notes.iter().any(|note| note == "receiver_class=Bits") + && notes.iter().any(|note| note == "method=mix_i32") + && notes + .iter() + .any(|note| note == "typed_signature=i32(i32, ...)->i32") + && notes + .iter() + .any(|note| note == "boxed_result_at=direct_call_boundary") + }) + }), + "expected typed-i32 method direct-call artifact:\n{artifact:#}" + ); +} + +#[test] +fn typed_i32_method_clone_rejects_number_param_number_return_and_unsafe_add() { + for case in ["number_param", "number_return", "unsafe_add"] { + let ir = String::from_utf8( + compile_module(&typed_i32_method_clone_module(case), empty_opts()).unwrap(), + ) + .unwrap(); + assert!( + !ir.contains("__typed_i32"), + "{case} method must stay off the typed-i32 method ABI:\n{ir}" + ); + } +} + +#[test] +fn typed_f64_method_clone_keeps_i32_locals_raw_until_f64_use() { + let ir = String::from_utf8( + compile_module(&typed_f64_i32_local_method_clone_module(), empty_opts()).unwrap(), + ) + .unwrap(); + let public = "perry_method_typed_f64_i32_local_method_abi_ts__Calc__mix"; + let typed = "perry_method_typed_f64_i32_local_method_abi_ts__Calc__mix__typed_f64"; + let generic_body = "perry_method_typed_f64_i32_local_method_abi_ts__Calc__mix__generic"; + let wrapper_ir = function_ir_section(&ir, public); + let typed_ir = defined_function_ir_section(&ir, typed); + let caller_ir = + defined_function_ir_section(&ir, "perry_fn_typed_f64_i32_local_method_abi_ts__probe"); + + assert!( + ir.contains(&format!( + "define internal double @{typed}(double %arg21, i32 %arg22)" + )), + "typed f64 method clone should accept the raw i32 parameter:\n{ir}" + ); + assert!( + typed_ir.contains(" or i32 %arg22, 1") + && typed_ir.contains("sitofp i32 ") + && typed_ir.contains(" fadd double") + && !typed_ir.contains("js_typed_i32_arg_to_raw") + && !typed_ir.contains("js_nanbox"), + "typed f64 method clone should keep the Int32 local raw until f64 arithmetic:\n{typed_ir}" + ); + assert!( + wrapper_ir.contains("call i32 @js_typed_i32_arg_guard") + && wrapper_ir.contains("call i32 @js_typed_i32_arg_to_raw") + && wrapper_ir.contains(&format!("call double @{typed}(double ")) + && wrapper_ir.contains(&format!("call double @{generic_body}(")), + "public method wrapper should guard/unbox the Int32 ABI arg and keep fallback:\n{wrapper_ir}" + ); + assert!( + caller_ir.contains("typed_f64_method.fast") + && caller_ir.contains("typed_f64_method.generic") + && caller_ir.contains("call i32 @js_typed_i32_arg_guard") + && caller_ir.contains("call i32 @js_typed_i32_arg_to_raw") + && caller_ir.contains(&format!("call double @{typed}(double ")) + && caller_ir.contains(&format!("call double @{generic_body}(")), + "exact direct method call should use the raw clone with generic-body fallback:\n{caller_ir}" + ); +} + +#[test] +fn typed_string_method_clone_emits_internal_clone_and_guarded_direct_call() { + let ir = String::from_utf8( + compile_module(&typed_string_method_clone_module("eligible"), empty_opts()).unwrap(), + ) + .unwrap(); + let public = "perry_method_typed_string_method_eligible_ts__Labeler__pick"; + let typed = "perry_method_typed_string_method_eligible_ts__Labeler__pick__typed_string"; + let generic_body = "perry_method_typed_string_method_eligible_ts__Labeler__pick__generic"; + let caller = "perry_fn_typed_string_method_eligible_ts__probe"; + let wrapper_ir = function_ir_section(&ir, public); + let caller_ir = defined_function_ir_section(&ir, caller); assert!( - ir.contains(&format!( - "define internal i32 @{typed}(i32 %arg21, i32 %arg22)" - )), - "typed-i32 method clone should use raw i32 params and i32 return:\n{ir}" - ); - assert!( - typed_ir.contains(" xor i32 %arg21, %arg22") - && typed_ir.contains(" or i32 ") - && !typed_ir.contains(" fadd ") - && !typed_ir.contains(" sitofp "), - "typed-i32 method body should stay in native i32 SSA:\n{typed_ir}" + ir.contains(&format!("define internal i64 @{typed}(i64 %arg21)")), + "typed-string method clone should use raw i64 StringHeader handles:\n{ir}" ); assert!( ir.contains(&format!( - "define double @{public}(double %this_arg, double %arg21, double %arg22)" + "define double @{public}(double %this_arg, double %arg21)" )) && ir.contains(&format!( - "define internal double @{generic_body}(double %this_arg, double %arg21, double %arg22)" + "define internal double @{generic_body}(double %this_arg, double %arg21)" )), - "typed-i32 method should expose a public JSValue wrapper and keep an internal generic body:\n{ir}" + "typed-string method should expose a public JSValue wrapper and keep an internal generic body:\n{ir}" ); assert!( - wrapper_ir.contains("call i32 @js_typed_i32_arg_guard") - && wrapper_ir.contains("call i32 @js_typed_i32_arg_to_raw") - && wrapper_ir.contains(&format!("call i32 @{typed}(i32 ")) - && wrapper_ir.contains(INT32_TAG_I64), - "public method wrapper should guard/unbox Int32 args and box raw i32 at the ABI edge:\n{wrapper_ir}" + wrapper_ir.contains("call i32 @js_typed_string_arg_guard") + && wrapper_ir.contains("call i64 @js_typed_string_arg_to_raw") + && wrapper_ir.contains(&format!("call i64 @{typed}(i64 ")) + && wrapper_ir.contains("call double @js_nanbox_string(i64 ") + && wrapper_ir.contains(&format!("call double @{generic_body}(")), + "public method wrapper should guard/unbox string args, call the raw clone, box the result, and keep generic fallback:\n{wrapper_ir}" ); assert!( caller_ir.contains("call i32 @js_method_direct_shape_guard") - && caller_ir.contains("typed_i32_method.fast") - && caller_ir.contains("typed_i32_method.generic") - && caller_ir.contains("call i32 @js_typed_i32_arg_guard") - && caller_ir.contains("call i32 @js_typed_i32_arg_to_raw") - && caller_ir.contains(&format!("call i32 @{typed}(i32 ")) - && caller_ir.contains(INT32_TAG_I64), - "exact direct method call should guard receiver/method identity, then guard/unbox Int32 args and call the clone:\n{caller_ir}" + && caller_ir.contains("typed_string_method.fast") + && caller_ir.contains("typed_string_method.generic") + && caller_ir.contains("call i32 @js_typed_string_arg_guard") + && caller_ir.contains("call i64 @js_typed_string_arg_to_raw") + && caller_ir.contains(&format!("call i64 @{typed}(i64 ")) + && caller_ir.contains("call double @js_nanbox_string(i64 "), + "exact direct method call should guard receiver/method identity, then guard/unbox string args and call the raw clone:\n{caller_ir}" ); assert!( caller_ir.contains(&format!("call double @{generic_body}(")), - "direct typed-i32 guard failure should target the internal generic method body:\n{caller_ir}" + "direct typed-string guard failure should target the internal generic method body:\n{caller_ir}" ); assert!( !caller_ir.contains(&format!("call double @{public}(")), - "direct typed-i32 guard failure must not recurse through the public wrapper:\n{caller_ir}" + "direct typed-string guard failure must not recurse through the public wrapper:\n{caller_ir}" ); assert!( !ir.contains(&format!("ptrtoint (ptr @{typed}")) @@ -6447,49 +11480,13 @@ fn typed_i32_method_clone_emits_internal_clone_and_guarded_direct_call() { } #[test] -fn typed_i32_method_public_trampoline_dispatches_before_generic_body() { - let ir = String::from_utf8( - compile_module(&typed_i32_method_clone_module("eligible"), empty_opts()).unwrap(), - ) - .unwrap(); - let public = "perry_method_typed_i32_method_eligible_ts__Bits__mix_i32"; - let typed = "perry_method_typed_i32_method_eligible_ts__Bits__mix_i32__typed_i32"; - let generic_body = "perry_method_typed_i32_method_eligible_ts__Bits__mix_i32__generic"; - let wrapper_ir = function_ir_section(&ir, public); - - let typed_call = wrapper_ir - .find(&format!("call i32 @{typed}(")) - .unwrap_or_else(|| { - panic!("public method wrapper should call typed-i32 clone:\n{wrapper_ir}") - }); - let fallback_call = wrapper_ir - .find(&format!("call double @{generic_body}(")) - .unwrap_or_else(|| { - panic!("public method wrapper should call generic body fallback:\n{wrapper_ir}") - }); - assert!( - typed_call < fallback_call, - "public method wrapper should dispatch to typed clone before generic fallback:\n{wrapper_ir}" - ); - assert!( - wrapper_ir.contains("call i32 @js_typed_i32_arg_guard") - && wrapper_ir.contains("call i32 @js_typed_i32_arg_to_raw"), - "public method wrapper should guard and unbox Int32 JSValue args:\n{wrapper_ir}" - ); - assert!( - !wrapper_ir.contains(&format!("call double @{public}(")), - "public method wrapper must not recursively call itself:\n{wrapper_ir}" - ); -} - -#[test] -fn artifact_records_typed_i32_method_clone_selection() { - let artifact = compile_artifact_json_for_module(typed_i32_method_clone_module("eligible")); +fn artifact_records_typed_string_method_clone_selection() { + let artifact = compile_artifact_json_for_module(typed_string_method_clone_module("eligible")); let records = artifact["records"].as_array().unwrap(); assert!( records.iter().any(|record| { record["expr_kind"] == "MethodCall" - && record["consumer"] == "typed_i32_method_direct_call" + && record["consumer"] == "typed_string_method_direct_call" && record["native_rep_name"] == "js_value" && record["llvm_ty"] == "double" && record["native_value_state"] == "region_local" @@ -6497,40 +11494,99 @@ fn artifact_records_typed_i32_method_clone_selection() { notes.iter().any(|note| { note.as_str().is_some_and(|text| { text.contains( - "typed_clone=perry_method_typed_i32_method_eligible_ts__Bits__mix_i32__typed_i32", + "typed_clone=perry_method_typed_string_method_eligible_ts__Labeler__pick__typed_string", ) }) }) && notes.iter().any(|note| { note - == "generic_method=perry_method_typed_i32_method_eligible_ts__Bits__mix_i32__generic" - }) && notes.iter().any(|note| note == "receiver_class=Bits") - && notes.iter().any(|note| note == "method=mix_i32") + == "generic_method=perry_method_typed_string_method_eligible_ts__Labeler__pick__generic" + }) && notes.iter().any(|note| note == "receiver_class=Labeler") + && notes.iter().any(|note| note == "method=pick") && notes .iter() - .any(|note| note == "typed_signature=i32(i32, ...)->i32") + .any(|note| note == "typed_signature=string(string)->string") && notes .iter() .any(|note| note == "boxed_result_at=direct_call_boundary") }) }), - "expected typed-i32 method direct-call artifact:\n{artifact:#}" + "expected typed-string method direct-call artifact:\n{artifact:#}" ); } #[test] -fn typed_i32_method_clone_rejects_number_param_number_return_and_unsafe_add() { - for case in ["number_param", "number_return", "unsafe_add"] { +fn typed_string_method_clone_rejects_unsupported_string_shapes() { + for case in [ + "any_param", + "number_param", + "default_param", + "rest_param", + "concat_body", + ] { let ir = String::from_utf8( - compile_module(&typed_i32_method_clone_module(case), empty_opts()).unwrap(), + compile_module(&typed_string_method_clone_module(case), empty_opts()).unwrap(), ) .unwrap(); assert!( - !ir.contains("__typed_i32"), - "{case} method must stay off the typed-i32 method ABI:\n{ir}" + !ir.contains("__typed_string") && !ir.contains("typed_string_method.fast"), + "{case} method must stay off the typed-string method ABI:\n{ir}" ); } } +#[test] +fn artifact_records_typed_string_method_clone_rejection_reason() { + let artifact = compile_artifact_json_for_module(typed_string_method_clone_module("any_param")); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["consumer"] == "typed_string_method_clone_decision" + && record["expr_kind"] == "TypedCloneDecision" + && record["native_rep_name"] == "js_value" + && record["notes"].as_array().is_some_and(|notes| { + notes + .iter() + .any(|note| note == "typed_clone_rejected=param_not_string") + && notes + .iter() + .any(|note| note == "typed_clone_kind=typed_string_method") + && notes.iter().any(|note| note == "class=Labeler") + && notes.iter().any(|note| note == "method=pick") + }) + }), + "expected typed-string method rejection artifact for unsupported param:\n{artifact:#}" + ); +} + +#[test] +fn typed_string_method_clone_rejects_dynamic_receiver_direct_call_site() { + let ir = String::from_utf8( + compile_module( + &typed_string_method_clone_module("dynamic_receiver"), + empty_opts(), + ) + .unwrap(), + ) + .unwrap(); + let public = "perry_method_typed_string_method_dynamic_receiver_ts__Labeler__pick"; + let typed = "perry_method_typed_string_method_dynamic_receiver_ts__Labeler__pick__typed_string"; + let caller_ir = defined_function_ir_section( + &ir, + "perry_fn_typed_string_method_dynamic_receiver_ts__probe", + ); + + assert!( + ir.contains(&format!("define internal i64 @{typed}(i64 %arg21)")) + && ir.contains(&format!("define double @{public}(")), + "eligible method should still expose its public wrapper even when this call site is dynamic:\n{ir}" + ); + assert!( + !caller_ir.contains("typed_string_method.fast") + && !caller_ir.contains(&format!("call i64 @{typed}(")), + "dynamic receiver call site must not route directly to the typed-string method clone:\n{caller_ir}" + ); +} + #[test] fn typed_i1_method_clone_emits_internal_clone_and_guarded_direct_call() { let ir = String::from_utf8( @@ -7173,6 +12229,188 @@ fn typed_f64_closure_clone_rejects_any_parameter_and_mutable_capture() { } } +#[test] +fn typed_i32_closure_clone_emits_internal_clone_and_guarded_direct_call() { + let ir = String::from_utf8( + compile_module(&typed_i32_closure_clone_module("eligible"), empty_opts()).unwrap(), + ) + .unwrap(); + let public = "perry_closure_typed_i32_closure_eligible_ts__303"; + let generic_body = "perry_closure_typed_i32_closure_eligible_ts__303__generic"; + let typed = "perry_closure_typed_i32_closure_eligible_ts__303__typed_i32"; + let wrapper_ir = function_ir_section(&ir, public); + assert!( + ir.contains(&format!( + "define internal i32 @{typed}(i64 %this_closure, i32 %arg31, i32 %arg32)" + )), + "typed-i32 closure clone should carry the closure handle plus i32 params and i32 return:\n{ir}" + ); + assert!( + ir.contains(&format!("define double @{public}(i64 %this_closure")) + && ir.contains(&format!( + "define internal double @{generic_body}(i64 %this_closure" + )), + "typed-i32 closure should expose a public wrapper and keep an internal generic body:\n{ir}" + ); + assert!( + ir.contains(&format!( + "call i64 @js_closure_alloc_singleton(ptr @{public}" + )), + "closure allocation must keep storing the public wrapper pointer:\n{ir}" + ); + assert!( + wrapper_ir.contains("call i32 @js_typed_i32_arg_guard") + && wrapper_ir.contains("call i32 @js_typed_i32_arg_to_raw") + && wrapper_ir.contains(&format!("call i32 @{typed}(i64 %this_closure")), + "public closure wrapper should guard/unbox Int32 JSValue args and call the typed clone:\n{wrapper_ir}" + ); + assert!( + ir.contains("call i32 @js_typed_feedback_closure_direct_call_guard"), + "{ir}" + ); + assert!( + ir.contains("closure_direct.typed_i32") + && ir.contains("call i32 @js_typed_i32_arg_guard") + && ir.contains("call i32 @js_typed_i32_arg_to_raw") + && ir.contains(&format!("call i32 @{typed}(i64 ")), + "direct local closure call should guard/unbox Int32 args and call the raw clone:\n{ir}" + ); + assert!( + ir.contains(&format!("call double @{generic_body}(i64 ")), + "Int32-guard failure should target the internal generic closure body:\n{ir}" + ); + assert!( + !ir.contains(&format!("call double @{public}(i64 ")), + "typed guard failure must not recursively call the public closure wrapper:\n{ir}" + ); + assert!( + ir.contains("call double @js_closure_call2"), + "closure identity/arity guard failure should keep runtime dispatch fallback:\n{ir}" + ); +} + +#[test] +fn artifact_records_typed_i32_closure_clone_selection() { + let artifact = compile_artifact_json_for_module(typed_i32_closure_clone_module("eligible")); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "ClosureCall" + && record["consumer"] == "typed_i32_closure_direct_call" + && record["native_rep_name"] == "js_value" + && record["llvm_ty"] == "double" + && record["native_value_state"] == "region_local" + && record["notes"].as_array().is_some_and(|notes| { + notes.iter().any(|note| { + note.as_str().is_some_and(|text| { + text.contains( + "typed_clone=perry_closure_typed_i32_closure_eligible_ts__303__typed_i32", + ) + }) + }) && notes.iter().any(|note| { + note == "generic_closure=perry_closure_typed_i32_closure_eligible_ts__303__generic" + }) && notes.iter().any(|note| note == "closure_func_id=303") + && notes + .iter() + .any(|note| note == "typed_signature=i32(i64 closure, i32, ...)->i32") + && notes + .iter() + .any(|note| note == "boxed_result_at=direct_call_boundary") + }) + }), + "expected typed-i32 closure clone selection artifact:\n{artifact:#}" + ); +} + +#[test] +fn typed_i32_closure_clone_accepts_immutable_i32_capture() { + let ir = String::from_utf8( + compile_module(&typed_i32_closure_clone_module("capture"), empty_opts()).unwrap(), + ) + .unwrap(); + let typed = "perry_closure_typed_i32_closure_capture_ts__303__typed_i32"; + let typed_ir = defined_function_ir_section(&ir, typed); + assert!( + typed_ir.contains("call i64 @js_closure_get_capture_bits(i64 %this_closure, i32 0)") + && typed_ir.contains("bitcast i64") + && typed_ir.contains("call i32 @js_typed_i32_arg_to_raw"), + "typed-i32 captured closure should load immutable Int32 capture through the closure handle:\n{typed_ir}" + ); + assert!( + ir.contains(&format!("call i32 @{typed}(i64 ")), + "typed direct call should pass the closure handle to the captured clone:\n{ir}" + ); +} + +#[test] +fn typed_i32_closure_clone_rejects_annotation_unsafe_and_mutable_capture() { + for case in [ + "number_param", + "number_return", + "unsafe_add", + "mutable_capture", + ] { + let ir = String::from_utf8( + compile_module(&typed_i32_closure_clone_module(case), empty_opts()).unwrap(), + ) + .unwrap(); + assert!( + !ir.contains("__typed_i32"), + "{case} closure must stay on the generic closure ABI:\n{ir}" + ); + } + + let artifact = compile_artifact_json_for_module(typed_i32_closure_clone_module("unsafe_add")); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["consumer"] == "typed_i32_closure_clone_decision" + && record["notes"].as_array().is_some_and(|notes| { + notes.iter().any(|note| { + note == "typed_clone_rejected=return_expr_not_typed_i32_safe" + || note == "typed_clone_rejected=body_not_straight_line_typed" + }) && notes + .iter() + .any(|note| note == "typed_clone_kind=typed_i32_closure") + }) + }), + "expected typed-i32 closure rejection artifact:\n{artifact:#}" + ); +} + +#[test] +fn typed_i32_closure_clone_rejects_dynamic_callee_call_site() { + let ir = String::from_utf8( + compile_module(&typed_i32_closure_clone_module("dynamic"), empty_opts()).unwrap(), + ) + .unwrap(); + let caller = "perry_fn_typed_i32_closure_dynamic_ts__probe"; + let public = "perry_closure_typed_i32_closure_dynamic_ts__303"; + let generic_body = "perry_closure_typed_i32_closure_dynamic_ts__303__generic"; + let typed = "perry_closure_typed_i32_closure_dynamic_ts__303__typed_i32"; + let caller_ir = function_ir_section(&ir, caller); + let wrapper_ir = function_ir_section(&ir, public); + assert!( + ir.contains(&format!("define internal i32 @{typed}(i64 %this_closure")), + "eligible closure should still have an internal typed-i32 clone:\n{ir}" + ); + assert!( + !caller_ir.contains(&format!("call i32 @{typed}(")) + && !caller_ir.contains("call i32 @js_typed_i32_arg_guard"), + "dynamic closure callee must not direct-call the typed-i32 clone:\n{caller_ir}" + ); + assert!( + wrapper_ir.contains("call i32 @js_typed_i32_arg_guard") + && wrapper_ir.contains(&format!("call i32 @{typed}(")) + && wrapper_ir.contains(&format!("call double @{generic_body}(")), + "dynamic runtime dispatch should enter the public closure wrapper, which owns typed-i32 guards:\n{wrapper_ir}" + ); + assert!( + caller_ir.contains("call double @js_closure_call2"), + "dynamic closure callee should dispatch through the generic closure fallback:\n{ir}" + ); +} + #[test] fn typed_i1_closure_clone_emits_internal_clone_and_guarded_direct_call() { let ir = String::from_utf8( @@ -7610,6 +12848,13 @@ fn artifact_records_scalar_replaced_method_summary_inline() { && record["consumer"] == "scalar_method_summary_inline" && record["local_id"] == 20 && record["native_value_state"] == "region_local" + && record_has_scalar_method_summary_fact(record, "consumed_facts", "consumed") + && record_has_scalar_method_summary_detail( + record, + "consumed_facts", + "consumed", + "exact_receiver_summary", + ) && record["notes"].as_array().is_some_and(|notes| { notes.iter().any(|note| note == "class=Point") && notes.iter().any(|note| note == "method=sum") @@ -7633,6 +12878,62 @@ fn scalar_method_summary_rejects_own_property_shadow() { ); } +#[test] +fn scalar_replaced_numeric_method_with_local_temps_inlines_without_dispatch_or_allocation() { + let module = scalar_method_numeric_local_temp_module("inline", false); + let ir = String::from_utf8(compile_module(&module, empty_opts()).unwrap()).unwrap(); + assert!( + !ir.contains("call double @js_native_call_method"), + "scalar-replaced numeric method with local temps should not dispatch dynamically:\n{ir}" + ); + assert!( + !ir.contains("call i64 @js_object_alloc"), + "scalar-replaced numeric method with local temps should not materialize the receiver:\n{ir}" + ); + assert!( + ir.contains("fadd double") && ir.contains("fmul double"), + "numeric local temp summary should rebuild native arithmetic in the inlined body:\n{ir}" + ); + + let artifact = compile_artifact_json_for_module(module); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "ScalarMethodCall" + && record["consumer"] == "scalar_method_summary_inline" + && record_has_scalar_method_summary_fact(record, "consumed_facts", "consumed") + && record_has_scalar_method_summary_detail( + record, + "consumed_facts", + "consumed", + "exact_receiver_summary", + ) + && record_has_note(record, "method=weighted") + && record_has_note(record, "summary_return=number") + }), + "expected scalar numeric local-temp summary inline artifact:\n{artifact:#}" + ); +} + +#[test] +fn scalar_method_local_temp_rejects_mutable_binding() { + let module = scalar_method_numeric_local_temp_module("mutable", true); + let ir = String::from_utf8(compile_module(&module, empty_opts()).unwrap()).unwrap(); + assert!( + ir.contains("call double @js_native_call_method"), + "mutable local temp must keep dynamic method dispatch fallback:\n{ir}" + ); + assert!( + ir.contains("call i64 @js_object_alloc"), + "mutable local temp must materialize the scalar receiver for fallback:\n{ir}" + ); + let artifact = compile_artifact_json_for_module(module); + assert!( + !artifact_has_scalar_method_inline(&artifact, "weighted"), + "mutable local temp must not record a scalar method summary inline:\n{artifact:#}" + ); +} + #[test] fn scalar_replaced_boolean_method_predicate_inlines_without_dispatch_or_allocation() { let ir = String::from_utf8( @@ -7664,6 +12965,22 @@ fn artifact_records_scalar_replaced_boolean_method_predicate_inline() { artifact_has_scalar_method_inline(&artifact, "isAbove"), "expected scalar boolean method predicate summary inline artifact:\n{artifact:#}" ); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "ScalarMethodCall" + && record["consumer"] == "scalar_method_summary_inline" + && record_has_scalar_method_summary_fact(record, "consumed_facts", "consumed") + && record_has_scalar_method_summary_detail( + record, + "consumed_facts", + "consumed", + "exact_receiver_summary", + ) + && record_has_note(record, "method=isAbove") + }), + "expected scalar boolean method predicate inline record to consume the scalar method summary fact:\n{artifact:#}" + ); } #[test] @@ -7704,8 +13021,28 @@ fn scalar_method_boolean_predicate_rejects_unproven_numeric_arguments() { "any arg must keep generic method dispatch:\n{ir}" ); assert!( - ir.contains("call i64 @js_object_alloc"), - "any arg fallback must materialize the scalar receiver before dispatch:\n{ir}" + ir.contains("call i64 @js_object_alloc_class_inline_keys"), + "any arg fallback must materialize the scalar receiver with stable class keys before dispatch:\n{ir}" + ); + assert!( + ir.contains("call void @js_gc_init_typed_shape_layout"), + "any arg fallback materialization must install typed shape pointer/raw-f64 bitmap evidence:\n{ir}" + ); + let fallback_block = { + let start = ir + .find("call i64 @js_object_alloc_class_inline_keys") + .unwrap_or_else(|| panic!("missing scalar receiver materialization call:\n{ir}")); + let end = ir[start..] + .find("call double @js_native_call_method_by_id") + .map(|offset| start + offset) + .unwrap_or_else(|| { + panic!("missing scalar method by-id dispatch after fallback:\n{ir}") + }); + &ir[start..end] + }; + assert!( + !fallback_block.contains("call void @js_object_set_field_by_name"), + "stable scalar receiver materialization should restore known fields with direct slots, not named dynamic stores:\n{fallback_block}" ); assert!( !ir.contains("scalar_method_arg_guard.fast"), @@ -7717,6 +13054,92 @@ fn scalar_method_boolean_predicate_rejects_unproven_numeric_arguments() { !artifact_has_scalar_method_inline(&artifact, "isAbove"), "any arg must not record a scalar method summary inline:\n{artifact:#}" ); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().filter(|record| { + record["expr_kind"] == "ScalarReceiverMaterializeField" + && record["consumer"] == "scalar_receiver_materialize.direct_field_store" + && record["local_id"] == 20 + && record["access_mode"] == "checked_native" + && record["materialization_reason"] == "runtime_api" + && record_has_note(record, "receiver_materialization=direct_slot") + && record_has_note(record, "field_layout=fixed_slot_array") + && record_has_note(record, "raw_f64_field=1") + && record_has_note(record, "pointer_bitmap=non_pointer") + }).count() == 2, + "fallback materialization should restore both scalar numeric fields through direct fixed slots:\n{artifact:#}" + ); + assert!( + records.iter().filter(|record| { + record["expr_kind"] == "WriteBarrierElided" + && record["consumer"] == "write_barrier.elided_scalar_receiver_materialize_raw_f64" + && record["local_id"] == 20 + && record_has_note(record, "reason=scalar_receiver_raw_f64_field_pointer_free") + && record_has_note(record, "pointer_bitmap=non_pointer") + }).count() == 2, + "fallback materialization should record raw-f64 pointer-free barrier elision for both fields:\n{artifact:#}" + ); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "ScalarMethodCall" + && record["consumer"] == "scalar_method_summary_materialized_fallback" + && record["access_mode"] == "dynamic_fallback" + && record["materialization_reason"] == "runtime_api" + && record_has_scalar_method_summary_fact(record, "rejected_facts", "generic_arg") + && record_has_scalar_method_summary_detail( + record, + "rejected_facts", + "generic_arg", + "generic_argument", + ) + && record_has_note(record, "scalar_method_fallback=generic_arg") + && record_has_note(record, "method=isAbove") + }), + "any arg fallback should record rejected scalar method summary evidence:\n{artifact:#}" + ); +} + +#[test] +fn scalar_method_boolean_predicate_rejects_unproven_numeric_argument_expressions() { + let module = scalar_method_boolean_negative_module("any_arg_expr"); + let ir = String::from_utf8(compile_module(&module, empty_opts()).unwrap()).unwrap(); + assert!( + ir.contains("call double @js_native_call_method_by_id"), + "any arg expression must keep generic method dispatch:\n{ir}" + ); + assert!( + ir.contains("call i64 @js_object_alloc"), + "any arg expression fallback must materialize the scalar receiver before dispatch:\n{ir}" + ); + assert!( + !ir.contains("scalar_method_arg_guard.fast"), + "any arg expression must not use the guarded scalar inline path:\n{ir}" + ); + + let artifact = compile_artifact_json_for_module(module); + assert!( + !artifact_has_scalar_method_inline(&artifact, "isAbove"), + "any arg expression must not record a scalar method summary inline:\n{artifact:#}" + ); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "ScalarMethodCall" + && record["consumer"] == "scalar_method_summary_materialized_fallback" + && record["access_mode"] == "dynamic_fallback" + && record["materialization_reason"] == "runtime_api" + && record_has_scalar_method_summary_fact(record, "rejected_facts", "generic_arg") + && record_has_scalar_method_summary_detail( + record, + "rejected_facts", + "generic_arg", + "generic_argument", + ) + && record_has_note(record, "scalar_method_fallback=generic_arg") + && record_has_note(record, "method=isAbove") + }), + "any arg expression fallback should record rejected scalar method summary evidence:\n{artifact:#}" + ); } #[test] @@ -7751,6 +13174,311 @@ fn scalar_method_boolean_predicate_guards_public_numeric_arguments() { artifact_has_scalar_method_inline(&artifact, "isAbove"), "{case} public numeric arg should still record scalar inline fast path:\n{artifact:#}" ); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "ScalarMethodCall" + && record["consumer"] == "scalar_method_summary_inline" + && record_has_scalar_method_summary_fact(record, "consumed_facts", "consumed") + && record_has_scalar_method_summary_detail( + record, + "consumed_facts", + "consumed", + "guarded_numeric_args_fast_path", + ) + && record_has_note(record, "arg_guard=js_typed_f64_arg_guard") + && record_has_note(record, "method=isAbove") + }), + "{case} public numeric arg should record guarded scalar inline summary evidence:\n{artifact:#}" + ); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "ScalarMethodCall" + && record["consumer"] == "scalar_method_summary_materialized_fallback" + && record["access_mode"] == "dynamic_fallback" + && record["materialization_reason"] == "runtime_api" + && record_has_scalar_method_summary_fact( + record, + "rejected_facts", + "arg_guard_failed", + ) + && record_has_scalar_method_summary_detail( + record, + "rejected_facts", + "arg_guard_failed", + "guarded_numeric_args_fallback", + ) + && record_has_note(record, "scalar_method_fallback=arg_guard_failed") + && record_has_note(record, "arg_guard=js_typed_f64_arg_guard") + && record_has_note(record, "method=isAbove") + }), + "{case} public numeric arg should record guarded scalar fallback summary evidence:\n{artifact:#}" + ); + } +} + +#[test] +fn scalar_method_boolean_predicate_guards_public_numeric_argument_expressions() { + let module = scalar_method_boolean_public_numeric_expr_arg_module(); + let ir = String::from_utf8(compile_module(&module, empty_opts()).unwrap()).unwrap(); + assert!( + ir.contains("scalar_method_arg_guard.fast") + && ir.contains("scalar_method_arg_guard.fallback") + && ir.matches("call i32 @js_typed_f64_arg_guard").count() >= 2 + && ir.matches("call double @js_typed_f64_arg_to_raw").count() >= 2 + && ir.contains("fmul double") + && ir.contains("fadd double"), + "public numeric arg expression should guard/unbox locals and rebuild arithmetic as raw f64 before scalar inline:\n{ir}" + ); + assert!( + ir.contains("call double @js_native_call_method_by_id"), + "public numeric arg expression should keep a generic fallback:\n{ir}" + ); + let fast = ir + .find("scalar_method_arg_guard.fast") + .unwrap_or_else(|| panic!("missing guarded fast block:\n{ir}")); + let fallback = ir + .find("scalar_method_arg_guard.fallback") + .unwrap_or_else(|| panic!("missing guarded fallback block:\n{ir}")); + let materialize = ir + .find("call i64 @js_object_alloc") + .unwrap_or_else(|| panic!("fallback should materialize receiver:\n{ir}")); + let dispatch = ir + .find("call double @js_native_call_method_by_id") + .unwrap_or_else(|| panic!("fallback should dispatch generically:\n{ir}")); + assert!( + fast < fallback && fallback < materialize && materialize < dispatch, + "guarded expression fast path must precede materialized generic fallback:\n{ir}" + ); + + let artifact = compile_artifact_json_for_module(module); + assert!( + artifact_has_scalar_method_inline(&artifact, "isAbove"), + "public numeric arg expression should record scalar inline fast path:\n{artifact:#}" + ); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "ScalarMethodCall" + && record["consumer"] == "scalar_method_summary_inline" + && record_has_scalar_method_summary_fact(record, "consumed_facts", "consumed") + && record_has_scalar_method_summary_detail( + record, + "consumed_facts", + "consumed", + "guarded_numeric_args_fast_path", + ) + && record_has_note(record, "method=isAbove") + && record_has_note(record, "receiver=scalar_replaced") + && record_has_note(record, "arg_guard=public_numeric_expr") + && record_has_note(record, "guarded_arg_count=1") + }), + "public numeric arg expression should record guarded scalar inline summary evidence:\n{artifact:#}" + ); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "ScalarMethodCall" + && record["consumer"] == "scalar_method_summary_materialized_fallback" + && record["access_mode"] == "dynamic_fallback" + && record["materialization_reason"] == "runtime_api" + && record_has_scalar_method_summary_fact(record, "rejected_facts", "arg_guard_failed") + && record_has_scalar_method_summary_detail( + record, + "rejected_facts", + "arg_guard_failed", + "guarded_numeric_args_fallback", + ) + && record_has_note(record, "scalar_method_fallback=arg_guard_failed") + && record_has_note(record, "arg_guard=public_numeric_expr") + && record_has_note(record, "method=isAbove") + }), + "public numeric arg expression should record guarded scalar fallback summary evidence:\n{artifact:#}" + ); +} + +#[test] +fn scalar_replaced_int32_bitwise_method_inlines_without_dispatch_or_allocation() { + let module = scalar_method_int32_bitwise_module("inline", Type::Int32, Type::Int32); + let ir = String::from_utf8(compile_module(&module, empty_opts()).unwrap()).unwrap(); + assert!( + !ir.contains("call double @js_native_call_method"), + "scalar-replaced Int32 bitwise method should not dispatch dynamically:\n{ir}" + ); + assert!( + !ir.contains("call double @perry_method_scalar_method_int32_bitwise_inline_ts__Flags_mix"), + "scalar-replaced Int32 bitwise method should inline the method body:\n{ir}" + ); + assert!( + !ir.contains("call i64 @js_object_alloc"), + "scalar-replaced Int32 bitwise receiver should not heap-allocate:\n{ir}" + ); + assert!( + ir.contains("xor i32") && ir.contains("or i32") && ir.contains("and i32"), + "Int32 bitwise summary should lower to native i32 operators in the inlined body:\n{ir}" + ); + + let artifact = compile_artifact_json_for_module(module); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "ScalarMethodCall" + && record["consumer"] == "scalar_method_summary_inline" + && record["local_id"] == 20 + && record_has_scalar_method_summary_fact(record, "consumed_facts", "consumed") + && record_has_scalar_method_summary_detail( + record, + "consumed_facts", + "consumed", + "exact_receiver_summary", + ) + && record_has_note(record, "class=Flags") + && record_has_note(record, "method=mix") + && record_has_note(record, "receiver=scalar_replaced") + && record_has_note(record, "summary_return=int32") + }), + "expected Int32 scalar method summary inline artifact:\n{artifact:#}" + ); +} + +#[test] +fn scalar_method_int32_bitwise_guards_public_int32_argument_and_preserves_fallback() { + let module = scalar_method_int32_bitwise_public_arg_module(); + let ir = String::from_utf8(compile_module(&module, empty_opts()).unwrap()).unwrap(); + assert!( + ir.contains("scalar_method_arg_guard.fast") + && ir.contains("scalar_method_arg_guard.fallback") + && ir.contains("call i32 @js_typed_i32_arg_guard") + && ir.contains("call i32 @js_typed_i32_arg_to_raw"), + "public Int32 arg should guard/unbox before scalar Int32 summary inline:\n{ir}" + ); + assert!( + ir.contains("call double @js_native_call_method_by_id"), + "public Int32 arg should keep a generic by-ID fallback:\n{ir}" + ); + let materialize = ir + .find("call i64 @js_object_alloc") + .unwrap_or_else(|| panic!("fallback should materialize receiver:\n{ir}")); + let dispatch = ir + .find("call double @js_native_call_method_by_id") + .unwrap_or_else(|| panic!("fallback should dispatch generically:\n{ir}")); + assert!( + materialize < dispatch, + "fallback must materialize before generic dispatch:\n{ir}" + ); + + let artifact = compile_artifact_json_for_module(module); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "ScalarMethodCall" + && record["consumer"] == "scalar_method_summary_inline" + && record_has_scalar_method_summary_fact(record, "consumed_facts", "consumed") + && record_has_scalar_method_summary_detail( + record, + "consumed_facts", + "consumed", + "guarded_numeric_args_fast_path", + ) + && record_has_note(record, "method=mix") + && record_has_note(record, "summary_return=int32") + && record_has_note(record, "arg_guard=js_typed_i32_arg_guard") + && record_has_note(record, "guarded_arg_count=1") + }), + "guarded public Int32 arg should record scalar inline fast path:\n{artifact:#}" + ); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "ScalarMethodCall" + && record["consumer"] == "scalar_method_summary_materialized_fallback" + && record["access_mode"] == "dynamic_fallback" + && record["materialization_reason"] == "runtime_api" + && record_has_scalar_method_summary_fact( + record, + "rejected_facts", + "arg_guard_failed", + ) + && record_has_scalar_method_summary_detail( + record, + "rejected_facts", + "arg_guard_failed", + "guarded_numeric_args_fallback", + ) + && record_has_note(record, "scalar_method_fallback=arg_guard_failed") + && record_has_note(record, "arg_guard=js_typed_i32_arg_guard") + && record_has_note(record, "method=mix") + }), + "guarded public Int32 arg should record scalar fallback evidence:\n{artifact:#}" + ); +} + +#[test] +fn scalar_replaced_int32_bitwise_method_with_local_temps_inlines_without_dispatch() { + let module = scalar_method_int32_bitwise_local_temp_module(); + let ir = String::from_utf8(compile_module(&module, empty_opts()).unwrap()).unwrap(); + assert!( + !ir.contains("call double @js_native_call_method"), + "scalar-replaced Int32 local-temp method should not dispatch dynamically:\n{ir}" + ); + assert!( + !ir.contains("call i64 @js_object_alloc"), + "scalar-replaced Int32 local-temp method should not materialize the receiver:\n{ir}" + ); + assert!( + ir.contains("xor i32") && ir.contains("shl i32") && ir.contains("or i32"), + "Int32 local temp summary should keep bitwise temps in native i32:\n{ir}" + ); + + let artifact = compile_artifact_json_for_module(module); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "ScalarMethodCall" + && record["consumer"] == "scalar_method_summary_inline" + && record_has_scalar_method_summary_fact(record, "consumed_facts", "consumed") + && record_has_scalar_method_summary_detail( + record, + "consumed_facts", + "consumed", + "exact_receiver_summary", + ) + && record_has_note(record, "method=mix") + && record_has_note(record, "summary_return=int32") + }), + "expected Int32 local-temp scalar method summary inline artifact:\n{artifact:#}" + ); +} + +#[test] +fn scalar_method_int32_bitwise_rejects_unproven_or_unsigned_shapes() { + for (case, module) in [ + ( + "number_field", + scalar_method_int32_bitwise_module("number_field", Type::Number, Type::Int32), + ), + ( + "unsigned_shift", + scalar_method_int32_unsigned_shift_module(), + ), + ( + "any_arg", + scalar_method_int32_bitwise_module("any_arg", Type::Int32, Type::Any), + ), + ] { + let ir = String::from_utf8(compile_module(&module, empty_opts()).unwrap()).unwrap(); + assert!( + ir.contains("call double @js_native_call_method"), + "{case} must keep dynamic method dispatch fallback:\n{ir}" + ); + assert!( + ir.contains("call i64 @js_object_alloc"), + "{case} must keep heap allocation fallback for the receiver:\n{ir}" + ); + + let artifact = compile_artifact_json_for_module(module); + assert!( + !artifact_has_scalar_method_inline(&artifact, "mix"), + "{case} must not record a scalar Int32 method summary inline:\n{artifact:#}" + ); } } diff --git a/crates/perry-codegen/tests/native_proof_regressions/invalidation.rs b/crates/perry-codegen/tests/native_proof_regressions/invalidation.rs index 12736302a6..8b3e49043b 100644 --- a/crates/perry-codegen/tests/native_proof_regressions/invalidation.rs +++ b/crates/perry-codegen/tests/native_proof_regressions/invalidation.rs @@ -294,6 +294,66 @@ fn assert_no_packed_f64_loop_artifacts(artifact: &serde_json::Value) { ); } +fn assert_no_packed_i32_loop(ir: &str) { + assert!( + !ir.contains("for.packed_i32_fast"), + "invalidated array proof must not emit a packed-i32 fast clone:\n{ir}" + ); +} + +fn assert_no_packed_u32_loop(ir: &str) { + assert!( + !ir.contains("call i32 @js_typed_feedback_packed_u32_array_loop_guard"), + "invalidated array proof must not emit a packed-u32 loop guard:\n{ir}" + ); + assert!( + !ir.contains("for.packed_u32_fast"), + "invalidated array proof must not emit a packed-u32 fast clone:\n{ir}" + ); +} + +fn assert_no_packed_i32_loop_artifacts(artifact: &serde_json::Value) { + let records = artifact["records"].as_array().unwrap(); + assert!( + !records.iter().any(|record| { + matches!( + record["consumer"].as_str(), + Some( + "packed_i32_loop_guard" + | "packed_i32_loop_fallback" + | "packed_i32_loop_load" + | "packed_i32_loop_load_f64" + ) + ) || record["expr_kind"] + .as_str() + .is_some_and(|kind| kind.starts_with("PackedI32Loop")) + || record_has_array_kind_fact(record, "consumed_facts", "consumed", "packed_i32") + }), + "invalidated alias mutation must not emit packed-i32 loop artifact records:\n{artifact:#}" + ); +} + +fn assert_no_packed_u32_loop_artifacts(artifact: &serde_json::Value) { + let records = artifact["records"].as_array().unwrap(); + assert!( + !records.iter().any(|record| { + matches!( + record["consumer"].as_str(), + Some( + "packed_u32_loop_guard" + | "packed_u32_loop_fallback" + | "packed_u32_loop_load" + | "packed_u32_loop_load_f64" + ) + ) || record["expr_kind"] + .as_str() + .is_some_and(|kind| kind.starts_with("PackedU32Loop")) + || record_has_array_kind_fact(record, "consumed_facts", "consumed", "packed_u32") + }), + "invalidated alias mutation must not emit packed-u32 loop artifact records:\n{artifact:#}" + ); +} + fn record_has_effect_fact( record: &serde_json::Value, list: &str, @@ -346,6 +406,47 @@ fn packed_f64_read_loop_uses_stable_noalias_array_proof() { ); } +#[test] +fn packed_i32_read_loop_uses_i32_specific_loop_guard_and_no_slow_helper_in_fast_clone() { + let body = vec![ + int32_array_let(1, "arr", vec![1, 2, 3]), + number_let(3, "sum", true, int(0)), + for_loop( + 4, + length(1), + vec![Stmt::Expr(Expr::LocalSet( + 3, + Box::new(add(local(3), index_get(1, local(4)))), + ))], + ), + Stmt::Return(Some(local(3))), + ]; + + let ir = compile_ir("packed_i32_read_loop_stable_array.ts", body); + assert!( + ir.contains("call i32 @js_typed_feedback_packed_i32_array_loop_guard"), + "stable noalias Int32[] should get a packed-i32 loop guard:\n{ir}" + ); + assert!( + !ir.contains("call i32 @js_typed_feedback_packed_f64_array_loop_guard"), + "packed-i32 proof must not reuse the f64 loop guard:\n{ir}" + ); + assert!( + ir.contains("for.packed_i32_fast"), + "stable noalias Int32[] should emit the packed-i32 fast clone:\n{ir}" + ); + let fast_clone = block_between( + &ir, + "\nfor.packed_i32_fast.cond.", + "\nfor.packed_i32_fast.exit.", + ); + assert!( + !fast_clone.contains("js_typed_feedback_array_index_get_fallback_boxed") + && !fast_clone.contains("js_array_get_f64"), + "packed-i32 fast clone should use raw-slot loads without slow helpers:\n{fast_clone}" + ); +} + #[test] fn packed_f64_read_loop_rejects_prior_array_alias() { let ir = compile_ir( @@ -763,6 +864,69 @@ fn loop_local_array_alias_push_blocks_packed_f64_loop_and_artifacts() { assert_no_packed_f64_loop_artifacts(&artifact); } +#[test] +fn loop_local_array_alias_push_blocks_packed_i32_loop_and_artifacts() { + let body = vec![ + int32_array_let(1, "arr", vec![1, 2, 3]), + number_let(3, "sum", true, int(0)), + for_loop( + 4, + length(1), + vec![ + array_alias_let(2, "alias", 1), + Stmt::Expr(Expr::ArrayPush { + array_id: 2, + value: Box::new(int(4)), + }), + Stmt::Expr(Expr::LocalSet( + 3, + Box::new(bit_or_zero(add(local(3), index_get(1, local(4))))), + )), + ], + ), + Stmt::Return(Some(local(3))), + ]; + + let ir = compile_ir("packed_i32_loop_local_alias_push.ts", body.clone()); + assert_no_packed_f64_loop(&ir); + assert_no_packed_i32_loop(&ir); + + let artifact = compile_artifact_json("artifact_packed_i32_loop_local_alias_push.ts", body); + assert_no_packed_i32_loop_artifacts(&artifact); +} + +#[test] +fn loop_local_array_alias_push_blocks_packed_u32_loop_and_artifacts() { + let body = vec![ + u32_array_let(1, "arr", vec![0, 4_000_000_000]), + number_let(3, "word", true, ushr_zero(int(0))), + for_loop( + 4, + length(1), + vec![ + array_alias_let(2, "alias", 1), + Stmt::Expr(Expr::ArrayPush { + array_id: 2, + value: Box::new(int(5)), + }), + Stmt::Expr(Expr::LocalSet( + 3, + Box::new(ushr_zero(index_get(1, local(4)))), + )), + ], + ), + Stmt::Return(Some(local(3))), + ]; + + let ir = compile_ir("packed_u32_loop_local_alias_push.ts", body.clone()); + assert_no_packed_f64_loop(&ir); + assert_no_packed_i32_loop(&ir); + assert_no_packed_u32_loop(&ir); + + let artifact = compile_artifact_json("artifact_packed_u32_loop_local_alias_push.ts", body); + assert_no_packed_u32_loop_artifacts(&artifact); +} + #[test] fn inclusive_local_length_bound_does_not_use_local_length_bound_fact() { let body = vec![ diff --git a/crates/perry-runtime/src/bigint.rs b/crates/perry-runtime/src/bigint.rs index c2ab7907d5..231904dfe2 100644 --- a/crates/perry-runtime/src/bigint.rs +++ b/crates/perry-runtime/src/bigint.rs @@ -204,6 +204,22 @@ pub extern "C" fn js_bigint_from_i64(value: i64) -> *mut BigIntHeader { bigint_alloc_with_limbs(limbs) } +/// Create a BigInt from a compiler-owned signed 128-bit temporary, passed as +/// raw low/high 64-bit words so generated LLVM can keep small BigInt literal +/// arithmetic native until the JS-visible BigInt object boundary. +#[no_mangle] +pub extern "C" fn js_bigint_from_i128_parts(lo: u64, hi: i64) -> *mut BigIntHeader { + let bits = ((hi as u64 as u128) << 64) | (lo as u128); + let value = bits as i128; + let mut limbs = ZERO_LIMBS; + write_i128(value, &mut limbs); + bigint_alloc_with_limbs(limbs) +} + +#[used] +static KEEP_JS_BIGINT_FROM_I128_PARTS: extern "C" fn(u64, i64) -> *mut BigIntHeader = + js_bigint_from_i128_parts; + /// Create a BigInt from a JS value (the `BigInt(value)` coercion). /// /// Matches Node/ECMAScript `ToBigInt` semantics (#2754, #2907): @@ -1491,6 +1507,30 @@ mod tests { } } + #[test] + fn test_bigint_from_i128_parts_preserves_wide_small_result() { + let value = (i64::MAX as i128) + 1; + let lo = value as u128 as u64; + let hi = ((value as u128) >> 64) as u64 as i64; + let bi = js_bigint_from_i128_parts(lo, hi); + unsafe { + assert_eq!((*bi).limbs[0], 0x8000_0000_0000_0000); + assert_eq!((*bi).limbs[1], 0); + assert!(fits_in_i64(&(*bi).limbs).is_none()); + } + + let negative = -((i64::MAX as i128) + 2); + let lo = negative as u128 as u64; + let hi = ((negative as u128) >> 64) as u64 as i64; + let bi = js_bigint_from_i128_parts(lo, hi); + unsafe { + assert_eq!((*bi).limbs[0], 0x7fff_ffff_ffff_ffff); + assert_eq!((*bi).limbs[1], u64::MAX); + assert_eq!((*bi).limbs[BIGINT_LIMBS - 1], u64::MAX); + assert!(fits_in_i64(&(*bi).limbs).is_none()); + } + } + #[test] fn test_bigint_from_string() { let s = "123456789"; diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs index 595ec8d588..edb9f48272 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs @@ -724,6 +724,51 @@ fn test_promise_iter_result_raw_f64_slot_is_not_scanned_as_root() { crate::promise::js_iter_result_set(0.0, 0); } +#[test] +fn test_promise_iter_result_raw_i32_slot_is_not_scanned_as_root() { + let nursery_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); + let stale_pointer_like = f64::from_bits(POINTER_TAG | (nursery_user as u64 & POINTER_MASK)); + crate::promise::js_iter_result_set(stale_pointer_like, 0); + crate::promise::js_iter_result_set_i32(-17, 0); + + let mut marked = Vec::new(); + crate::promise::scan_iter_result_root(&mut |value| marked.push(value.to_bits())); + + assert!( + marked.is_empty(), + "raw i32 iter-result slots must not scan stale pointer-shaped JSValue bits" + ); + assert_eq!( + crate::promise::js_iter_result_get_value().to_bits(), + crate::value::JSValue::int32(-17).bits() + ); + assert_eq!(crate::promise::js_iter_result_get_value_i32(), -17); + assert_eq!(crate::promise::js_iter_result_get_value_f64(), -17.0); + crate::promise::js_iter_result_set(0.0, 0); +} + +#[test] +fn test_promise_iter_result_raw_i1_slot_is_not_scanned_as_root() { + let nursery_user = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT); + let stale_pointer_like = f64::from_bits(POINTER_TAG | (nursery_user as u64 & POINTER_MASK)); + crate::promise::js_iter_result_set(stale_pointer_like, 0); + crate::promise::js_iter_result_set_i1(1, 0); + + let mut marked = Vec::new(); + crate::promise::scan_iter_result_root(&mut |value| marked.push(value.to_bits())); + + assert!( + marked.is_empty(), + "raw i1 iter-result slots must not scan stale pointer-shaped JSValue bits" + ); + assert_eq!( + crate::promise::js_iter_result_get_value().to_bits(), + crate::value::TAG_TRUE + ); + assert_eq!(crate::promise::js_iter_result_get_value_i1(), 1); + crate::promise::js_iter_result_set(0.0, 0); +} + #[test] fn test_evacuation_verify_detects_stale_forwarded_root_slot() { let _guard = ShadowAndGlobalRootResetGuard; diff --git a/crates/perry-runtime/src/map.rs b/crates/perry-runtime/src/map.rs index c6bfcf9ad9..2c13333007 100644 --- a/crates/perry-runtime/src/map.rs +++ b/crates/perry-runtime/src/map.rs @@ -451,6 +451,18 @@ fn normalize_zero(key: f64) -> f64 { } } +#[inline(always)] +fn normalize_number_key_from_boxed(key: f64) -> Option { + let js_value = crate::value::JSValue::from_bits(key.to_bits()); + if js_value.is_int32() { + Some(normalize_zero(js_value.as_int32() as f64)) + } else if js_value.is_number() { + Some(normalize_zero(key)) + } else { + None + } +} + /// Extract a string pointer from a value that might be NaN-boxed with various tags. /// Returns the raw pointer if the value looks like it contains a string pointer, or null otherwise. /// Does NOT handle SHORT_STRING_TAG (SSO) — those don't carry a heap pointer; @@ -823,6 +835,66 @@ unsafe fn ensure_capacity(map: *mut MapHeader) -> bool { true } +unsafe fn map_set_string_key_value( + map: *mut MapHeader, + key: *const StringHeader, + value: f64, +) -> *mut MapHeader { + let idx = find_string_key_index(map, key); + + if idx >= 0 { + let entries = entries_ptr_mut(map); + let value_slot = entries.add((idx as usize) * 2 + 1); + // GC_STORE_AUDIT(EXTERNAL_BARRIERED): map value slot uses the shared external-slot helper. + crate::gc::runtime_store_external_jsvalue_slot( + map as usize, + value_slot as usize, + value.to_bits(), + ); + return map; + } + + let grew = ensure_capacity(map); + let size = (*map).size; + let entries = entries_ptr_mut(map); + if grew && size > 0 { + crate::gc::runtime_dirty_external_slot_span( + map as usize, + entries as usize, + size as usize * 2, + ); + } + + let key_value = boxed_heap_string_key(key); + let key_slot = entries.add((size as usize) * 2); + let value_slot = entries.add((size as usize) * 2 + 1); + // GC_STORE_AUDIT(EXTERNAL_BARRIERED): map append key/value slots use the shared external-slot helper. + crate::gc::runtime_store_external_jsvalue_slot( + map as usize, + key_slot as usize, + key_value.to_bits(), + ); + crate::gc::runtime_store_external_jsvalue_slot( + map as usize, + value_slot as usize, + value.to_bits(), + ); + + (*map).size = size + 1; + + if let Some(h) = string_content_hash(key_value.to_bits()) { + MAP_STRING_INDEX.with(|idx| { + let mut idx = idx.borrow_mut(); + let slot = idx + .entry(map as usize) + .or_insert_with(std::collections::HashMap::new); + slot.entry(h).or_insert_with(Vec::new).push(size); + }); + } + + map +} + /// Set a key-value pair in the map /// The map pointer is stable (never reallocated) #[no_mangle] @@ -908,6 +980,18 @@ pub extern "C" fn js_map_set(map: *mut MapHeader, key: f64, value: f64) -> *mut } } +#[no_mangle] +pub extern "C" fn js_map_set_number_key( + map: *mut MapHeader, + key: f64, + value: f64, +) -> *mut MapHeader { + let Some(key) = normalize_number_key_from_boxed(key) else { + return js_map_set(map, key, value); + }; + js_map_set(map, key, value) +} + #[no_mangle] pub extern "C" fn js_map_set_string_number( map: *mut MapHeader, @@ -918,61 +1002,91 @@ pub extern "C" fn js_map_set_string_number( if map.is_null() { return map; } - unsafe { - let idx = find_string_key_index(map, key); + unsafe { map_set_string_key_value(map, key, value) } +} - if idx >= 0 { - let entries = entries_ptr_mut(map); - let value_slot = entries.add((idx as usize) * 2 + 1); - // GC_STORE_AUDIT(EXTERNAL_BARRIERED): map value slot uses the shared external-slot helper. - crate::gc::runtime_store_external_jsvalue_slot( - map as usize, - value_slot as usize, - value.to_bits(), - ); - return map; - } +#[no_mangle] +pub extern "C" fn js_map_set_string_key( + map: *mut MapHeader, + key: *const StringHeader, + value: f64, +) -> *mut MapHeader { + let map = clean_map_ptr_mut(map); + if map.is_null() { + return map; + } + unsafe { map_set_string_key_value(map, key, value) } +} - let grew = ensure_capacity(map); - let size = (*map).size; - let entries = entries_ptr_mut(map); - if grew && size > 0 { - crate::gc::runtime_dirty_external_slot_span( - map as usize, - entries as usize, - size as usize * 2, - ); - } +#[no_mangle] +pub extern "C" fn js_map_set_string_i32( + map: *mut MapHeader, + key: *const StringHeader, + value: i32, +) -> *mut MapHeader { + let map = clean_map_ptr_mut(map); + if map.is_null() { + return map; + } + let value_bits = crate::value::INT32_TAG | ((value as u32) as u64); + unsafe { map_set_string_key_value(map, key, f64::from_bits(value_bits)) } +} - let key_value = boxed_heap_string_key(key); - let key_slot = entries.add((size as usize) * 2); - let value_slot = entries.add((size as usize) * 2 + 1); - // GC_STORE_AUDIT(EXTERNAL_BARRIERED): map append key/value slots use the shared external-slot helper. - crate::gc::runtime_store_external_jsvalue_slot( - map as usize, - key_slot as usize, - key_value.to_bits(), - ); - crate::gc::runtime_store_external_jsvalue_slot( - map as usize, - value_slot as usize, - value.to_bits(), - ); +#[no_mangle] +pub extern "C" fn js_map_set_string_u32( + map: *mut MapHeader, + key: *const StringHeader, + value: u32, +) -> *mut MapHeader { + let map = clean_map_ptr_mut(map); + if map.is_null() { + return map; + } + unsafe { map_set_string_key_value(map, key, f64::from(value)) } +} - (*map).size = size + 1; +#[no_mangle] +pub extern "C" fn js_map_set_string_f32( + map: *mut MapHeader, + key: *const StringHeader, + value: f32, +) -> *mut MapHeader { + let map = clean_map_ptr_mut(map); + if map.is_null() { + return map; + } + unsafe { map_set_string_key_value(map, key, f64::from(value)) } +} - if let Some(h) = string_content_hash(key_value.to_bits()) { - MAP_STRING_INDEX.with(|idx| { - let mut idx = idx.borrow_mut(); - let slot = idx - .entry(map as usize) - .or_insert_with(std::collections::HashMap::new); - slot.entry(h).or_insert_with(Vec::new).push(size); - }); - } +#[no_mangle] +pub extern "C" fn js_map_set_string_bool( + map: *mut MapHeader, + key: *const StringHeader, + value: i32, +) -> *mut MapHeader { + let map = clean_map_ptr_mut(map); + if map.is_null() { + return map; + } + let value_bits = if value != 0 { + crate::value::TAG_TRUE + } else { + crate::value::TAG_FALSE + }; + unsafe { map_set_string_key_value(map, key, f64::from_bits(value_bits)) } +} - map +#[no_mangle] +pub extern "C" fn js_map_set_string_string( + map: *mut MapHeader, + key: *const StringHeader, + value: *const StringHeader, +) -> *mut MapHeader { + let map = clean_map_ptr_mut(map); + if map.is_null() { + return map; } + unsafe { map_set_string_key_value(map, key, boxed_heap_string_key(value)) } } /// Get a value from the map by key @@ -996,6 +1110,14 @@ pub extern "C" fn js_map_get(map: *const MapHeader, key: f64) -> f64 { } } +#[no_mangle] +pub extern "C" fn js_map_get_number_key(map: *const MapHeader, key: f64) -> f64 { + let Some(key) = normalize_number_key_from_boxed(key) else { + return js_map_get(map, key); + }; + js_map_get(map, key) +} + #[no_mangle] pub extern "C" fn js_map_get_string_key(map: *const MapHeader, key: *const StringHeader) -> f64 { let map = clean_map_ptr(map); @@ -1032,6 +1154,14 @@ pub extern "C" fn js_map_has(map: *const MapHeader, key: f64) -> i32 { } } +#[no_mangle] +pub extern "C" fn js_map_has_number_key(map: *const MapHeader, key: f64) -> i32 { + let Some(key) = normalize_number_key_from_boxed(key) else { + return js_map_has(map, key); + }; + js_map_has(map, key) +} + #[no_mangle] pub extern "C" fn js_map_has_string_key(map: *const MapHeader, key: *const StringHeader) -> i32 { let map = clean_map_ptr(map); @@ -1047,6 +1177,26 @@ pub extern "C" fn js_map_has_string_key(map: *const MapHeader, key: *const Strin } } +#[no_mangle] +pub extern "C" fn js_map_delete_string_key(map: *mut MapHeader, key: *const StringHeader) -> i32 { + let map = clean_map_ptr_mut(map); + if map.is_null() { + return 0; + } + unsafe { + let idx = find_string_key_index(map, key); + delete_entry_at_index(map, idx) + } +} + +#[no_mangle] +pub extern "C" fn js_map_delete_number_key(map: *mut MapHeader, key: f64) -> i32 { + let Some(key) = normalize_number_key_from_boxed(key) else { + return js_map_delete(map, key); + }; + js_map_delete(map, key) +} + // Codegen emits these string-key typed lowering helpers directly from // generated LLVM IR. Keep roots prevent whole-program LTO/dead-strip from // removing the exported symbols when the Rust crate graph has no caller. @@ -1057,11 +1207,62 @@ static KEEP_JS_MAP_SET_STRING_NUMBER: extern "C" fn( f64, ) -> *mut MapHeader = js_map_set_string_number; #[used] +static KEEP_JS_MAP_SET_NUMBER_KEY: extern "C" fn(*mut MapHeader, f64, f64) -> *mut MapHeader = + js_map_set_number_key; +#[used] +static KEEP_JS_MAP_SET_STRING_KEY: extern "C" fn( + *mut MapHeader, + *const StringHeader, + f64, +) -> *mut MapHeader = js_map_set_string_key; +#[used] +static KEEP_JS_MAP_SET_STRING_I32: extern "C" fn( + *mut MapHeader, + *const StringHeader, + i32, +) -> *mut MapHeader = js_map_set_string_i32; +#[used] +static KEEP_JS_MAP_SET_STRING_U32: extern "C" fn( + *mut MapHeader, + *const StringHeader, + u32, +) -> *mut MapHeader = js_map_set_string_u32; +#[used] +static KEEP_JS_MAP_SET_STRING_F32: extern "C" fn( + *mut MapHeader, + *const StringHeader, + f32, +) -> *mut MapHeader = js_map_set_string_f32; +#[used] +static KEEP_JS_MAP_SET_STRING_BOOL: extern "C" fn( + *mut MapHeader, + *const StringHeader, + i32, +) -> *mut MapHeader = js_map_set_string_bool; +#[used] +static KEEP_JS_MAP_SET_STRING_STRING: extern "C" fn( + *mut MapHeader, + *const StringHeader, + *const StringHeader, +) -> *mut MapHeader = js_map_set_string_string; +#[used] static KEEP_JS_MAP_GET_STRING_KEY: extern "C" fn(*const MapHeader, *const StringHeader) -> f64 = js_map_get_string_key; #[used] +static KEEP_JS_MAP_GET_NUMBER_KEY: extern "C" fn(*const MapHeader, f64) -> f64 = + js_map_get_number_key; +#[used] static KEEP_JS_MAP_HAS_STRING_KEY: extern "C" fn(*const MapHeader, *const StringHeader) -> i32 = js_map_has_string_key; +#[used] +static KEEP_JS_MAP_HAS_NUMBER_KEY: extern "C" fn(*const MapHeader, f64) -> i32 = + js_map_has_number_key; +#[used] +static KEEP_JS_MAP_DELETE_STRING_KEY: extern "C" fn(*mut MapHeader, *const StringHeader) -> i32 = + js_map_delete_string_key; +#[used] +static KEEP_JS_MAP_DELETE_NUMBER_KEY: extern "C" fn(*mut MapHeader, f64) -> i32 = + js_map_delete_number_key; /// Delete a key from the map /// Returns 1 if deleted, 0 if key not found @@ -1074,46 +1275,49 @@ pub extern "C" fn js_map_delete(map: *mut MapHeader, key: f64) -> i32 { let key = normalize_zero(key); unsafe { let idx = find_key_index(map, key); + delete_entry_at_index(map, idx) + } +} - if idx < 0 { - return 0; - } - - let size = (*map).size; - let entries = entries_ptr_mut(map); - - // #2831: preserve insertion order. JS Map iteration must keep the - // relative order of surviving entries after a delete (and a - // delete-then-re-add appends at the end). The previous swap-and-pop - // moved the last entry into the hole, reordering iteration. Shift - // every entry after `idx` down by one slot instead. - for i in (idx as usize)..(size as usize - 1) { - let next_key = ptr::read(entries.add((i + 1) * 2)); - let next_value = ptr::read(entries.add((i + 1) * 2 + 1)); - // GC_STORE_AUDIT(EXTERNAL_BARRIERED): map compaction slots use the shared external-slot helper. - crate::gc::runtime_store_external_jsvalue_slot( - map as usize, - entries.add(i * 2) as usize, - next_key.to_bits(), - ); - crate::gc::runtime_store_external_jsvalue_slot( - map as usize, - entries.add(i * 2 + 1) as usize, - next_value.to_bits(), - ); - } +unsafe fn delete_entry_at_index(map: *mut MapHeader, idx: i32) -> i32 { + if idx < 0 { + return 0; + } + let size = (*map).size; + let idx = idx as usize; + if idx >= size as usize { + return 0; + } + let entries = entries_ptr_mut(map); + + // #2831: preserve insertion order. JS Map iteration must keep the + // relative order of surviving entries after a delete (and a + // delete-then-re-add appends at the end). The previous swap-and-pop + // moved the last entry into the hole, reordering iteration. Shift + // every entry after `idx` down by one slot instead. + for i in idx..(size as usize - 1) { + let next_key = ptr::read(entries.add((i + 1) * 2)); + let next_value = ptr::read(entries.add((i + 1) * 2 + 1)); + // GC_STORE_AUDIT(EXTERNAL_BARRIERED): map compaction slots use the shared external-slot helper. + crate::gc::runtime_store_external_jsvalue_slot( + map as usize, + entries.add(i * 2) as usize, + next_key.to_bits(), + ); + crate::gc::runtime_store_external_jsvalue_slot( + map as usize, + entries.add(i * 2 + 1) as usize, + next_value.to_bits(), + ); + } - (*map).size = size - 1; + (*map).size = size - 1; - // The shift changes the entry index of every surviving key at or - // after `idx`, so the O(1) lookup side-tables can't be patched in - // place cheaply — rebuild them from the compacted buffer. Small - // maps don't use the side-table fast path (linear scan under - // SIDE_TABLE_THRESHOLD), so this only matters for large maps where - // a full rebuild is still O(size) like the shift itself. - rebuild_map_index(map); - 1 - } + // The shift changes the entry index of every surviving key at or + // after `idx`, so the O(1) lookup side-tables can't be patched in + // place cheaply. Rebuild them from the compacted buffer. + rebuild_map_index(map); + 1 } /// Rebuild the numeric + string lookup side-tables for `map` from its @@ -1655,7 +1859,156 @@ mod tests { assert_eq!(js_map_get(map, boxed_heap_string_key(key_a)), 9.25); assert_eq!(js_map_get_string_key(map, key_a), 9.25); + assert_eq!(js_map_delete_string_key(map, key_b), 1); + assert_eq!(js_map_size(map), 0); + assert_eq!(js_map_has_string_key(map, key_a), 0); + assert_eq!(js_map_get_string_key(map, key_a).to_bits(), TAG_UNDEFINED); + assert_eq!(js_map_delete_string_key(map, key_a), 0); + let missing = js_string_from_bytes(b"missing".as_ptr(), 7); assert_eq!(js_map_get_string_key(map, missing).to_bits(), TAG_UNDEFINED); + + js_map_set_string_key(map, key_a, f64::from_bits(crate::value::TAG_TRUE)); + assert_eq!(js_map_size(map), 1); + assert_eq!( + js_map_get_string_key(map, key_b).to_bits(), + crate::value::TAG_TRUE + ); + + js_map_set_string_key(map, key_b, f64::from_bits(crate::value::TAG_FALSE)); + assert_eq!( + js_map_size(map), + 1, + "same-content string keys should update generic JSValue entries" + ); + assert_eq!( + js_map_get_string_key(map, key_a).to_bits(), + crate::value::TAG_FALSE + ); + + js_map_set_string_bool(map, key_a, 1); + assert_eq!(js_map_size(map), 1); + assert_eq!( + js_map_get_string_key(map, key_b).to_bits(), + crate::value::TAG_TRUE + ); + + js_map_set_string_bool(map, key_b, 0); + assert_eq!( + js_map_size(map), + 1, + "same-content string keys should update typed boolean entries" + ); + assert_eq!( + js_map_get_string_key(map, key_a).to_bits(), + crate::value::TAG_FALSE + ); + + js_map_set_string_i32(map, key_a, 42); + assert_eq!(js_map_size(map), 1); + assert_eq!( + js_map_get_string_key(map, key_b).to_bits(), + crate::value::JSValue::int32(42).bits() + ); + + js_map_set_string_i32(map, key_b, -7); + assert_eq!( + js_map_size(map), + 1, + "same-content string keys should update typed int32 entries" + ); + assert_eq!( + js_map_get_string_key(map, key_a).to_bits(), + crate::value::JSValue::int32(-7).bits() + ); + + js_map_set_string_u32(map, key_a, u32::MAX); + assert_eq!(js_map_size(map), 1); + assert_eq!( + js_map_get_string_key(map, key_b).to_bits(), + (u32::MAX as f64).to_bits() + ); + + js_map_set_string_u32(map, key_b, 4_000_000_000); + assert_eq!( + js_map_size(map), + 1, + "same-content string keys should update typed uint32 entries" + ); + assert_eq!( + js_map_get_string_key(map, key_a).to_bits(), + 4_000_000_000_f64.to_bits() + ); + + js_map_set_string_f32(map, key_a, 1.5); + assert_eq!(js_map_size(map), 1); + assert_eq!(js_map_get_string_key(map, key_b), 1.5); + + js_map_set_string_f32(map, key_b, -2.25); + assert_eq!( + js_map_size(map), + 1, + "same-content string keys should update typed float32 entries" + ); + assert_eq!(js_map_get_string_key(map, key_a), -2.25); + + let value_a = js_string_from_bytes(b"ready".as_ptr(), 5); + let value_b = js_string_from_bytes(b"done".as_ptr(), 4); + js_map_set_string_string(map, key_a, value_a); + assert_eq!(js_map_size(map), 1); + assert_eq!( + js_map_get_string_key(map, key_b).to_bits(), + boxed_heap_string_key(value_a).to_bits() + ); + + js_map_set_string_string(map, key_b, value_b); + assert_eq!( + js_map_size(map), + 1, + "same-content string keys should update typed string value entries" + ); + assert_eq!( + js_map_get_string_key(map, key_a).to_bits(), + boxed_heap_string_key(value_b).to_bits() + ); + } + + #[test] + fn number_key_specialized_helpers_preserve_numeric_keys_and_fallback() { + let map = js_map_alloc(4); + + js_map_set_number_key(map, -0.0, 7.5); + assert_eq!(js_map_size(map), 1); + assert_eq!(js_map_has_number_key(map, 0.0), 1); + assert_eq!(js_map_get_number_key(map, 0.0), 7.5); + assert!( + test_map_numeric_index_contains(map, 0.0), + "numeric helper should populate the numeric side-table" + ); + + js_map_set_number_key(map, 0.0, 9.25); + assert_eq!( + js_map_size(map), + 1, + "-0 and +0 should update the same numeric-key entry" + ); + assert_eq!(js_map_get(map, -0.0), 9.25); + assert_eq!(js_map_delete_number_key(map, -0.0), 1); + assert_eq!(js_map_has_number_key(map, 0.0), 0); + + let string_key = js_string_from_bytes(b"fallback".as_ptr(), 8); + let boxed_string_key = boxed_heap_string_key(string_key); + js_map_set_number_key(map, boxed_string_key, 13.0); + assert_eq!( + js_map_get_number_key(map, boxed_string_key), + 13.0, + "nonnumeric calls to the numeric helper should preserve generic fallback semantics" + ); + assert!( + test_map_string_index_contains(map, boxed_string_key), + "fallback insertion should still update the string content side-table" + ); + assert_eq!(js_map_delete_number_key(map, boxed_string_key), 1); + assert_eq!(js_map_has(map, boxed_string_key), 0); } } diff --git a/crates/perry-runtime/src/promise/mod.rs b/crates/perry-runtime/src/promise/mod.rs index ae094dc81a..fdcba30888 100644 --- a/crates/perry-runtime/src/promise/mod.rs +++ b/crates/perry-runtime/src/promise/mod.rs @@ -270,8 +270,12 @@ pub(crate) fn mt_profile_register() { // objects so `for...of` and external consumers see the spec shape. thread_local! { static ITER_RESULT_VALUE: std::cell::Cell = const { std::cell::Cell::new(0.0) }; + static ITER_RESULT_VALUE_I32: std::cell::Cell = const { std::cell::Cell::new(0) }; + static ITER_RESULT_VALUE_I1: std::cell::Cell = const { std::cell::Cell::new(false) }; static ITER_RESULT_DONE: std::cell::Cell = const { std::cell::Cell::new(false) }; static ITER_RESULT_VALUE_IS_RAW_F64: std::cell::Cell = const { std::cell::Cell::new(false) }; + static ITER_RESULT_VALUE_IS_RAW_I32: std::cell::Cell = const { std::cell::Cell::new(false) }; + static ITER_RESULT_VALUE_IS_RAW_I1: std::cell::Cell = const { std::cell::Cell::new(false) }; } pub static MT_ITER_RESULT_SET_COUNT: AtomicU64 = AtomicU64::new(0); @@ -285,6 +289,8 @@ pub extern "C" fn js_iter_result_set(value: f64, done: i32) -> f64 { ITER_RESULT_VALUE.with(|c| c.set(value)); ITER_RESULT_DONE.with(|c| c.set(done != 0)); ITER_RESULT_VALUE_IS_RAW_F64.with(|c| c.set(false)); + ITER_RESULT_VALUE_IS_RAW_I32.with(|c| c.set(false)); + ITER_RESULT_VALUE_IS_RAW_I1.with(|c| c.set(false)); f64::from_bits(crate::value::TAG_UNDEFINED) } @@ -296,12 +302,48 @@ pub extern "C" fn js_iter_result_set_f64(value: f64, done: i32) -> f64 { ITER_RESULT_VALUE.with(|c| c.set(value)); ITER_RESULT_DONE.with(|c| c.set(done != 0)); ITER_RESULT_VALUE_IS_RAW_F64.with(|c| c.set(true)); + ITER_RESULT_VALUE_IS_RAW_I32.with(|c| c.set(false)); + ITER_RESULT_VALUE_IS_RAW_I1.with(|c| c.set(false)); + f64::from_bits(crate::value::TAG_UNDEFINED) +} + +/// Write a raw signed-Int32 iter-result payload. The value half is not a +/// JSValue root and must not be scanned by GC while the side flag is set. +#[no_mangle] +pub extern "C" fn js_iter_result_set_i32(value: i32, done: i32) -> f64 { + bump(&MT_ITER_RESULT_SET_COUNT); + ITER_RESULT_VALUE_I32.with(|c| c.set(value)); + ITER_RESULT_DONE.with(|c| c.set(done != 0)); + ITER_RESULT_VALUE_IS_RAW_F64.with(|c| c.set(false)); + ITER_RESULT_VALUE_IS_RAW_I32.with(|c| c.set(true)); + ITER_RESULT_VALUE_IS_RAW_I1.with(|c| c.set(false)); + f64::from_bits(crate::value::TAG_UNDEFINED) +} + +/// Write a raw boolean iter-result payload. The value half is not a JSValue +/// root and must not be scanned by GC while the side flag is set. +#[no_mangle] +pub extern "C" fn js_iter_result_set_i1(value: i32, done: i32) -> f64 { + bump(&MT_ITER_RESULT_SET_COUNT); + ITER_RESULT_VALUE_I1.with(|c| c.set(value != 0)); + ITER_RESULT_DONE.with(|c| c.set(done != 0)); + ITER_RESULT_VALUE_IS_RAW_F64.with(|c| c.set(false)); + ITER_RESULT_VALUE_IS_RAW_I32.with(|c| c.set(false)); + ITER_RESULT_VALUE_IS_RAW_I1.with(|c| c.set(true)); f64::from_bits(crate::value::TAG_UNDEFINED) } /// Read the value half of the iter-result scratch slot. #[no_mangle] pub extern "C" fn js_iter_result_get_value() -> f64 { + if ITER_RESULT_VALUE_IS_RAW_I32.with(|c| c.get()) { + let value = ITER_RESULT_VALUE_I32.with(|c| c.get()); + return f64::from_bits(crate::value::JSValue::int32(value).bits()); + } + if ITER_RESULT_VALUE_IS_RAW_I1.with(|c| c.get()) { + let value = ITER_RESULT_VALUE_I1.with(|c| c.get()); + return f64::from_bits(crate::value::JSValue::bool(value).bits()); + } ITER_RESULT_VALUE.with(|c| c.get()) } @@ -309,14 +351,55 @@ pub extern "C" fn js_iter_result_get_value() -> f64 { /// generic JSValue writes are coerced using ordinary JS number coercion. #[no_mangle] pub extern "C" fn js_iter_result_get_value_f64() -> f64 { - let value = ITER_RESULT_VALUE.with(|c| c.get()); if ITER_RESULT_VALUE_IS_RAW_F64.with(|c| c.get()) { - value + ITER_RESULT_VALUE.with(|c| c.get()) + } else if ITER_RESULT_VALUE_IS_RAW_I32.with(|c| c.get()) { + ITER_RESULT_VALUE_I32.with(|c| c.get()) as f64 } else { - crate::builtins::js_number_coerce(value) + crate::builtins::js_number_coerce(js_iter_result_get_value()) } } +/// Read the value half for signed-Int32 consumers. Raw-i32 writes return +/// directly; generic JSValue and other raw primitive writes use JS ToInt32. +#[no_mangle] +pub extern "C" fn js_iter_result_get_value_i32() -> i32 { + if ITER_RESULT_VALUE_IS_RAW_I32.with(|c| c.get()) { + return ITER_RESULT_VALUE_I32.with(|c| c.get()); + } + if ITER_RESULT_VALUE_IS_RAW_I1.with(|c| c.get()) { + return if ITER_RESULT_VALUE_I1.with(|c| c.get()) { + 1 + } else { + 0 + }; + } + let number = if ITER_RESULT_VALUE_IS_RAW_F64.with(|c| c.get()) { + ITER_RESULT_VALUE.with(|c| c.get()) + } else { + crate::builtins::js_number_coerce(js_iter_result_get_value()) + }; + if !number.is_finite() { + 0 + } else { + (number as i64) as i32 + } +} + +/// Read the value half for boolean consumers. Raw-i1 writes return directly; +/// generic JSValue and other raw primitive writes use ordinary JS truthiness. +#[no_mangle] +pub extern "C" fn js_iter_result_get_value_i1() -> i32 { + if ITER_RESULT_VALUE_IS_RAW_I1.with(|c| c.get()) { + return if ITER_RESULT_VALUE_I1.with(|c| c.get()) { + 1 + } else { + 0 + }; + } + crate::value::js_is_truthy(js_iter_result_get_value()) +} + /// Read the done half as a NaN-boxed bool (TAG_TRUE / TAG_FALSE) so it /// can flow into any control-flow / property context without a /// separate conversion. @@ -338,7 +421,10 @@ pub fn scan_iter_result_root(mark: &mut dyn FnMut(f64)) { } pub fn scan_iter_result_root_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { - if !ITER_RESULT_VALUE_IS_RAW_F64.with(|c| c.get()) { + let is_raw_primitive = ITER_RESULT_VALUE_IS_RAW_F64.with(|c| c.get()) + || ITER_RESULT_VALUE_IS_RAW_I32.with(|c| c.get()) + || ITER_RESULT_VALUE_IS_RAW_I1.with(|c| c.get()); + if !is_raw_primitive { ITER_RESULT_VALUE.with(|c| { visitor.visit_cell_f64_slot(c); }); @@ -350,10 +436,18 @@ static KEEP_JS_ITER_RESULT_SET: extern "C" fn(f64, i32) -> f64 = js_iter_result_ #[used] static KEEP_JS_ITER_RESULT_SET_F64: extern "C" fn(f64, i32) -> f64 = js_iter_result_set_f64; #[used] +static KEEP_JS_ITER_RESULT_SET_I32: extern "C" fn(i32, i32) -> f64 = js_iter_result_set_i32; +#[used] +static KEEP_JS_ITER_RESULT_SET_I1: extern "C" fn(i32, i32) -> f64 = js_iter_result_set_i1; +#[used] static KEEP_JS_ITER_RESULT_GET_VALUE: extern "C" fn() -> f64 = js_iter_result_get_value; #[used] static KEEP_JS_ITER_RESULT_GET_VALUE_F64: extern "C" fn() -> f64 = js_iter_result_get_value_f64; #[used] +static KEEP_JS_ITER_RESULT_GET_VALUE_I32: extern "C" fn() -> i32 = js_iter_result_get_value_i32; +#[used] +static KEEP_JS_ITER_RESULT_GET_VALUE_I1: extern "C" fn() -> i32 = js_iter_result_get_value_i1; +#[used] static KEEP_JS_ITER_RESULT_GET_DONE: extern "C" fn() -> f64 = js_iter_result_get_done; /// Promise state diff --git a/crates/perry-runtime/src/set.rs b/crates/perry-runtime/src/set.rs index 0ab8c2aa72..73169927e0 100644 --- a/crates/perry-runtime/src/set.rs +++ b/crates/perry-runtime/src/set.rs @@ -360,6 +360,18 @@ fn normalize_zero(value: f64) -> f64 { } } +#[inline(always)] +fn normalize_number_value_from_boxed(value: f64) -> Option { + let js_value = crate::value::JSValue::from_bits(value.to_bits()); + if js_value.is_int32() { + Some(normalize_zero(js_value.as_int32() as f64)) + } else if js_value.is_number() { + Some(normalize_zero(value)) + } else { + None + } +} + /// Compare two strings by content #[cfg(test)] unsafe fn strings_equal(a: *const StringHeader, b: *const StringHeader) -> bool { @@ -665,6 +677,14 @@ pub extern "C" fn js_set_add(set: *mut SetHeader, value: f64) -> *mut SetHeader } } +#[no_mangle] +pub extern "C" fn js_set_add_number(set: *mut SetHeader, value: f64) -> *mut SetHeader { + let Some(value) = normalize_number_value_from_boxed(value) else { + return js_set_add(set, value); + }; + js_set_add(set, value) +} + #[no_mangle] pub extern "C" fn js_set_add_string( set: *mut SetHeader, @@ -712,6 +732,52 @@ pub extern "C" fn js_set_add_string( } } +#[inline(always)] +fn boxed_i32_value(value: i32) -> f64 { + f64::from_bits(crate::value::JSValue::int32(value).bits()) +} + +#[no_mangle] +pub extern "C" fn js_set_add_i32(set: *mut SetHeader, value: i32) -> *mut SetHeader { + let set = clean_set_ptr(set as *const SetHeader) as *mut SetHeader; + if set.is_null() { + return set; + } + js_set_add(set, boxed_i32_value(value)) +} + +#[no_mangle] +pub extern "C" fn js_set_add_u32(set: *mut SetHeader, value: u32) -> *mut SetHeader { + let set = clean_set_ptr(set as *const SetHeader) as *mut SetHeader; + if set.is_null() { + return set; + } + js_set_add(set, f64::from(value)) +} + +#[no_mangle] +pub extern "C" fn js_set_add_f32(set: *mut SetHeader, value: f32) -> *mut SetHeader { + let set = clean_set_ptr(set as *const SetHeader) as *mut SetHeader; + if set.is_null() { + return set; + } + js_set_add(set, f64::from(value)) +} + +#[no_mangle] +pub extern "C" fn js_set_add_bool(set: *mut SetHeader, value: i32) -> *mut SetHeader { + let set = clean_set_ptr(set as *const SetHeader) as *mut SetHeader; + if set.is_null() { + return set; + } + let boxed = if value != 0 { + f64::from_bits(crate::value::TAG_TRUE) + } else { + f64::from_bits(crate::value::TAG_FALSE) + }; + js_set_add(set, boxed) +} + /// Check if the set has a value /// Returns 1 if found, 0 if not found #[no_mangle] @@ -726,6 +792,14 @@ pub extern "C" fn js_set_has(set: *const SetHeader, value: f64) -> i32 { } } +#[no_mangle] +pub extern "C" fn js_set_has_number(set: *const SetHeader, value: f64) -> i32 { + let Some(value) = normalize_number_value_from_boxed(value) else { + return js_set_has(set, value); + }; + js_set_has(set, value) +} + #[no_mangle] pub extern "C" fn js_set_has_string(set: *const SetHeader, value: *const StringHeader) -> i32 { let set = clean_set_ptr(set); @@ -742,6 +816,47 @@ pub extern "C" fn js_set_has_string(set: *const SetHeader, value: *const StringH } } +#[no_mangle] +pub extern "C" fn js_set_has_i32(set: *const SetHeader, value: i32) -> i32 { + let set = clean_set_ptr(set); + if set.is_null() { + return 0; + } + js_set_has(set, boxed_i32_value(value)) +} + +#[no_mangle] +pub extern "C" fn js_set_has_u32(set: *const SetHeader, value: u32) -> i32 { + let set = clean_set_ptr(set); + if set.is_null() { + return 0; + } + js_set_has(set, f64::from(value)) +} + +#[no_mangle] +pub extern "C" fn js_set_has_f32(set: *const SetHeader, value: f32) -> i32 { + let set = clean_set_ptr(set); + if set.is_null() { + return 0; + } + js_set_has(set, f64::from(value)) +} + +#[no_mangle] +pub extern "C" fn js_set_has_bool(set: *const SetHeader, value: i32) -> i32 { + let set = clean_set_ptr(set); + if set.is_null() { + return 0; + } + let boxed = if value != 0 { + f64::from_bits(crate::value::TAG_TRUE) + } else { + f64::from_bits(crate::value::TAG_FALSE) + }; + js_set_has(set, boxed) +} + /// Delete a value from the set /// Returns 1 if deleted, 0 if value not found #[no_mangle] @@ -781,6 +896,14 @@ pub extern "C" fn js_set_delete(set: *mut SetHeader, value: f64) -> i32 { } } +#[no_mangle] +pub extern "C" fn js_set_delete_number(set: *mut SetHeader, value: f64) -> i32 { + let Some(value) = normalize_number_value_from_boxed(value) else { + return js_set_delete(set, value); + }; + js_set_delete(set, value) +} + #[no_mangle] pub extern "C" fn js_set_delete_string(set: *mut SetHeader, value: *const StringHeader) -> i32 { let set = clean_set_ptr(set as *const SetHeader) as *mut SetHeader; @@ -791,6 +914,47 @@ pub extern "C" fn js_set_delete_string(set: *mut SetHeader, value: *const String js_set_delete(set, value) } +#[no_mangle] +pub extern "C" fn js_set_delete_i32(set: *mut SetHeader, value: i32) -> i32 { + let set = clean_set_ptr(set as *const SetHeader) as *mut SetHeader; + if set.is_null() { + return 0; + } + js_set_delete(set, boxed_i32_value(value)) +} + +#[no_mangle] +pub extern "C" fn js_set_delete_u32(set: *mut SetHeader, value: u32) -> i32 { + let set = clean_set_ptr(set as *const SetHeader) as *mut SetHeader; + if set.is_null() { + return 0; + } + js_set_delete(set, f64::from(value)) +} + +#[no_mangle] +pub extern "C" fn js_set_delete_f32(set: *mut SetHeader, value: f32) -> i32 { + let set = clean_set_ptr(set as *const SetHeader) as *mut SetHeader; + if set.is_null() { + return 0; + } + js_set_delete(set, f64::from(value)) +} + +#[no_mangle] +pub extern "C" fn js_set_delete_bool(set: *mut SetHeader, value: i32) -> i32 { + let set = clean_set_ptr(set as *const SetHeader) as *mut SetHeader; + if set.is_null() { + return 0; + } + let boxed = if value != 0 { + f64::from_bits(crate::value::TAG_TRUE) + } else { + f64::from_bits(crate::value::TAG_FALSE) + }; + js_set_delete(set, boxed) +} + // Codegen emits these string-key typed lowering helpers directly from // generated LLVM IR. Keep roots prevent whole-program LTO/dead-strip from // removing the exported symbols when the Rust crate graph has no caller. @@ -800,11 +964,42 @@ static KEEP_JS_SET_ADD_STRING: extern "C" fn( *const StringHeader, ) -> *mut SetHeader = js_set_add_string; #[used] +static KEEP_JS_SET_ADD_NUMBER: extern "C" fn(*mut SetHeader, f64) -> *mut SetHeader = + js_set_add_number; +#[used] static KEEP_JS_SET_HAS_STRING: extern "C" fn(*const SetHeader, *const StringHeader) -> i32 = js_set_has_string; #[used] +static KEEP_JS_SET_HAS_NUMBER: extern "C" fn(*const SetHeader, f64) -> i32 = js_set_has_number; +#[used] static KEEP_JS_SET_DELETE_STRING: extern "C" fn(*mut SetHeader, *const StringHeader) -> i32 = js_set_delete_string; +#[used] +static KEEP_JS_SET_DELETE_NUMBER: extern "C" fn(*mut SetHeader, f64) -> i32 = js_set_delete_number; +#[used] +static KEEP_JS_SET_ADD_I32: extern "C" fn(*mut SetHeader, i32) -> *mut SetHeader = js_set_add_i32; +#[used] +static KEEP_JS_SET_HAS_I32: extern "C" fn(*const SetHeader, i32) -> i32 = js_set_has_i32; +#[used] +static KEEP_JS_SET_DELETE_I32: extern "C" fn(*mut SetHeader, i32) -> i32 = js_set_delete_i32; +#[used] +static KEEP_JS_SET_ADD_U32: extern "C" fn(*mut SetHeader, u32) -> *mut SetHeader = js_set_add_u32; +#[used] +static KEEP_JS_SET_HAS_U32: extern "C" fn(*const SetHeader, u32) -> i32 = js_set_has_u32; +#[used] +static KEEP_JS_SET_DELETE_U32: extern "C" fn(*mut SetHeader, u32) -> i32 = js_set_delete_u32; +#[used] +static KEEP_JS_SET_ADD_F32: extern "C" fn(*mut SetHeader, f32) -> *mut SetHeader = js_set_add_f32; +#[used] +static KEEP_JS_SET_HAS_F32: extern "C" fn(*const SetHeader, f32) -> i32 = js_set_has_f32; +#[used] +static KEEP_JS_SET_DELETE_F32: extern "C" fn(*mut SetHeader, f32) -> i32 = js_set_delete_f32; +#[used] +static KEEP_JS_SET_ADD_BOOL: extern "C" fn(*mut SetHeader, i32) -> *mut SetHeader = js_set_add_bool; +#[used] +static KEEP_JS_SET_HAS_BOOL: extern "C" fn(*const SetHeader, i32) -> i32 = js_set_has_bool; +#[used] +static KEEP_JS_SET_DELETE_BOOL: extern "C" fn(*mut SetHeader, i32) -> i32 = js_set_delete_bool; /// Clear all elements from the set #[no_mangle] @@ -1589,6 +1784,104 @@ mod tests { assert_eq!(js_set_has_string(set, s1), 0); } + #[test] + fn test_set_number_specialized_helpers_preserve_numeric_values_and_fallback() { + let set = js_set_alloc(4); + + js_set_add_number(set, -0.0); + assert_eq!(js_set_size(set), 1); + assert_eq!(js_set_has_number(set, 0.0), 1); + assert!( + test_set_index_contains(set, 0.0), + "numeric helper should populate the Set side-table" + ); + + js_set_add_number(set, 0.0); + assert_eq!( + js_set_size(set), + 1, + "-0 and +0 should deduplicate through the numeric helper" + ); + assert_eq!(js_set_delete_number(set, -0.0), 1); + assert_eq!(js_set_has_number(set, 0.0), 0); + + let string = js_string_from_bytes(b"fallback".as_ptr(), 8); + let boxed_string = + f64::from_bits(crate::value::STRING_TAG | (string as u64 & crate::value::POINTER_MASK)); + js_set_add_number(set, boxed_string); + assert_eq!( + js_set_has_number(set, boxed_string), + 1, + "nonnumeric calls to the numeric helper should preserve generic fallback semantics" + ); + assert_eq!(js_set_delete_number(set, boxed_string), 1); + assert_eq!(js_set_has(set, boxed_string), 0); + } + + #[test] + fn test_set_i32_specialized_helpers_use_int32_keys() { + let set = js_set_alloc(4); + js_set_add_i32(set, 42); + js_set_add_i32(set, 42); + assert_eq!(js_set_size(set), 1); + assert_eq!(js_set_has_i32(set, 42), 1); + assert_eq!(js_set_has_i32(set, -7), 0); + + let boxed = f64::from_bits(crate::value::JSValue::int32(42).bits()); + assert_eq!(js_set_has(set, boxed), 1); + assert_eq!(js_set_delete_i32(set, 42), 1); + assert_eq!(js_set_has(set, boxed), 0); + assert_eq!(js_set_delete_i32(set, 42), 0); + } + + #[test] + fn test_set_u32_specialized_helpers_use_number_keys() { + let set = js_set_alloc(4); + js_set_add_u32(set, u32::MAX); + js_set_add_u32(set, u32::MAX); + assert_eq!(js_set_size(set), 1); + assert_eq!(js_set_has_u32(set, u32::MAX), 1); + assert_eq!(js_set_has_u32(set, 7), 0); + + let boxed = u32::MAX as f64; + assert_eq!(js_set_has(set, boxed), 1); + assert_eq!(js_set_delete_u32(set, u32::MAX), 1); + assert_eq!(js_set_has(set, boxed), 0); + assert_eq!(js_set_delete_u32(set, u32::MAX), 0); + } + + #[test] + fn test_set_f32_specialized_helpers_use_number_keys() { + let set = js_set_alloc(4); + js_set_add_f32(set, 1.5); + js_set_add_f32(set, 1.5); + assert_eq!(js_set_size(set), 1); + assert_eq!(js_set_has_f32(set, 1.5), 1); + assert_eq!(js_set_has_f32(set, -2.25), 0); + + let boxed = 1.5_f64; + assert_eq!(js_set_has(set, boxed), 1); + assert_eq!(js_set_delete_f32(set, 1.5), 1); + assert_eq!(js_set_has(set, boxed), 0); + assert_eq!(js_set_delete_f32(set, 1.5), 0); + } + + #[test] + fn test_set_bool_specialized_helpers_use_boolean_keys() { + let set = js_set_alloc(4); + js_set_add_bool(set, 1); + js_set_add_bool(set, 1); + assert_eq!(js_set_size(set), 1); + assert_eq!(js_set_has_bool(set, 1), 1); + assert_eq!(js_set_has_bool(set, 0), 0); + + let boxed = f64::from_bits(crate::value::TAG_TRUE); + assert_eq!(js_set_has(set, boxed), 1); + assert_eq!(js_set_delete_bool(set, 1), 1); + assert_eq!(js_set_has(set, boxed), 0); + assert_eq!(js_set_delete_bool(set, 1), 0); + } + #[test] fn test_set_mixed_number_values() { let set = js_set_alloc(4); diff --git a/crates/perry-runtime/src/typed_feedback.rs b/crates/perry-runtime/src/typed_feedback.rs index 3d874efcca..a2c015d3e5 100644 --- a/crates/perry-runtime/src/typed_feedback.rs +++ b/crates/perry-runtime/src/typed_feedback.rs @@ -1175,6 +1175,57 @@ fn packed_f64_array_loop_guard(arr: *const ArrayHeader) -> bool { crate::array::js_array_is_numeric_f64_layout(raw_addr as *const ArrayHeader) != 0 } +fn packed_i32_array_loop_guard(arr: *const ArrayHeader) -> bool { + if !packed_f64_array_loop_guard(arr) { + return false; + } + let raw_addr = normalize_raw_object_addr(arr as u64); + unsafe { + let arr = raw_addr as *const ArrayHeader; + let len = (*arr).length as usize; + if len > 16_000_000 { + return false; + } + let elements = + (raw_addr as *const u8).add(std::mem::size_of::()) as *const f64; + for i in 0..len { + let value = *elements.add(i); + if !value.is_finite() + || value.fract() != 0.0 + || value < i32::MIN as f64 + || value > i32::MAX as f64 + { + return false; + } + } + } + true +} + +fn packed_u32_array_loop_guard(arr: *const ArrayHeader) -> bool { + if !packed_f64_array_loop_guard(arr) { + return false; + } + let raw_addr = normalize_raw_object_addr(arr as u64); + unsafe { + let arr = raw_addr as *const ArrayHeader; + let len = (*arr).length as usize; + if len > 16_000_000 { + return false; + } + let elements = + (raw_addr as *const u8).add(std::mem::size_of::()) as *const f64; + for i in 0..len { + let value = *elements.add(i); + if !value.is_finite() || value.fract() != 0.0 || value < 0.0 || value > u32::MAX as f64 + { + return false; + } + } + } + true +} + fn numeric_array_push_guard(arr: *const ArrayHeader, value: f64) -> bool { let raw_addr = normalize_raw_object_addr(arr as u64); let Some(header) = gc_header_for_user_addr(raw_addr) else { @@ -1439,6 +1490,80 @@ pub extern "C" fn js_typed_feedback_packed_f64_array_loop_guard( } } +#[no_mangle] +pub extern "C" fn js_typed_feedback_packed_i32_array_loop_guard( + site_id: u64, + receiver: f64, +) -> i32 { + let raw_addr = normalize_raw_object_addr(receiver.to_bits()); + if !typed_feedback_enabled() { + return packed_i32_array_loop_guard(raw_addr as *const ArrayHeader) as i32; + } + let (class_id, heap_type, aux, element_kind) = classify_array(raw_addr, None); + let observation = Observation { + source: ObservationSource::Array, + object_addr: 0, + shape_addr: 0, + key_hash: 0, + class_id, + heap_type, + aux, + value_tag: element_kind, + }; + let pass = guard_observe( + site_id, + TypedFeedbackSiteKind::ArrayElement, + observation, + packed_i32_array_loop_guard(raw_addr as *const ArrayHeader), + ); + if pass { + 1 + } else { + 0 + } +} + +#[used] +static KEEP_JS_TYPED_FEEDBACK_PACKED_I32_ARRAY_LOOP_GUARD: extern "C" fn(u64, f64) -> i32 = + js_typed_feedback_packed_i32_array_loop_guard; + +#[no_mangle] +pub extern "C" fn js_typed_feedback_packed_u32_array_loop_guard( + site_id: u64, + receiver: f64, +) -> i32 { + let raw_addr = normalize_raw_object_addr(receiver.to_bits()); + if !typed_feedback_enabled() { + return packed_u32_array_loop_guard(raw_addr as *const ArrayHeader) as i32; + } + let (class_id, heap_type, aux, element_kind) = classify_array(raw_addr, None); + let observation = Observation { + source: ObservationSource::Array, + object_addr: 0, + shape_addr: 0, + key_hash: 0, + class_id, + heap_type, + aux, + value_tag: element_kind, + }; + let pass = guard_observe( + site_id, + TypedFeedbackSiteKind::ArrayElement, + observation, + packed_u32_array_loop_guard(raw_addr as *const ArrayHeader), + ); + if pass { + 1 + } else { + 0 + } +} + +#[used] +static KEEP_JS_TYPED_FEEDBACK_PACKED_U32_ARRAY_LOOP_GUARD: extern "C" fn(u64, f64) -> i32 = + js_typed_feedback_packed_u32_array_loop_guard; + #[no_mangle] pub extern "C" fn js_typed_feedback_array_index_get_fallback_boxed( site_id: u64, diff --git a/crates/perry-runtime/src/typed_feedback/tests.rs b/crates/perry-runtime/src/typed_feedback/tests.rs index 26ea4690f0..b940844c93 100644 --- a/crates/perry-runtime/src/typed_feedback/tests.rs +++ b/crates/perry-runtime/src/typed_feedback/tests.rs @@ -777,6 +777,67 @@ fn typed_feedback_numeric_array_get_guard_requires_numeric_layout() { assert_eq!(site.fallback_calls, 0); } +#[test] +fn typed_feedback_packed_i32_loop_guard_rejects_fractional_numeric_layout() { + let _guard = TYPED_FEEDBACK_TEST_LOCK.lock().unwrap(); + reset_typed_feedback_for_tests(); + register(70, TypedFeedbackSiteKind::ArrayElement, "packed_i32_loop"); + + let ints = [1.0, 2.0, 3.0]; + let int_arr = crate::array::js_array_from_f64(ints.as_ptr(), ints.len() as u32); + let int_box = crate::value::js_nanbox_pointer(int_arr as i64); + assert_eq!( + js_typed_feedback_packed_i32_array_loop_guard(70, int_box), + 1 + ); + + let fractional = [1.0, 2.5, 3.0]; + let fractional_arr = + crate::array::js_array_from_f64(fractional.as_ptr(), fractional.len() as u32); + let fractional_box = crate::value::js_nanbox_pointer(fractional_arr as i64); + assert_eq!( + crate::array::js_array_is_numeric_f64_layout(fractional_arr), + 1 + ); + assert_eq!( + js_typed_feedback_packed_i32_array_loop_guard(70, fractional_box), + 0 + ); + + let site = &typed_feedback_snapshot().sites[0]; + assert_eq!(site.guard_passes, 1); + assert_eq!(site.guard_failures, 1); +} + +#[test] +fn typed_feedback_packed_u32_loop_guard_rejects_signed_fractional_and_overflow_layouts() { + let _guard = TYPED_FEEDBACK_TEST_LOCK.lock().unwrap(); + reset_typed_feedback_for_tests(); + register(71, TypedFeedbackSiteKind::ArrayElement, "packed_u32_loop"); + + let uints = [0.0, 4_294_967_295.0]; + let uint_arr = crate::array::js_array_from_f64(uints.as_ptr(), uints.len() as u32); + let uint_box = crate::value::js_nanbox_pointer(uint_arr as i64); + assert_eq!( + js_typed_feedback_packed_u32_array_loop_guard(71, uint_box), + 1 + ); + + for values in [[-1.0, 2.0], [1.5, 2.0], [4_294_967_296.0, 2.0]] { + let arr = crate::array::js_array_from_f64(values.as_ptr(), values.len() as u32); + let arr_box = crate::value::js_nanbox_pointer(arr as i64); + assert_eq!(crate::array::js_array_is_numeric_f64_layout(arr), 1); + assert_eq!( + js_typed_feedback_packed_u32_array_loop_guard(71, arr_box), + 0 + ); + } + + let site = &typed_feedback_snapshot().sites[0]; + assert_eq!(site.guard_passes, 1); + assert_eq!(site.guard_failures, 3); +} + #[test] fn typed_feedback_numeric_array_set_guard_requires_numeric_value_and_layout() { let _guard = TYPED_FEEDBACK_TEST_LOCK.lock().unwrap(); @@ -1014,6 +1075,27 @@ fn numeric_array_helpers_have_lto_keepalive_anchors() { } } +#[test] +fn typed_feedback_array_loop_helpers_have_lto_keepalive_anchors() { + let typed_feedback = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/src/typed_feedback.rs" + )); + + assert_lto_keepalive_anchor( + typed_feedback, + "KEEP_JS_TYPED_FEEDBACK_PACKED_I32_ARRAY_LOOP_GUARD", + "static KEEP_JS_TYPED_FEEDBACK_PACKED_I32_ARRAY_LOOP_GUARD: extern \"C\" fn(u64, f64) -> i32", + "js_typed_feedback_packed_i32_array_loop_guard", + ); + assert_lto_keepalive_anchor( + typed_feedback, + "KEEP_JS_TYPED_FEEDBACK_PACKED_U32_ARRAY_LOOP_GUARD", + "static KEEP_JS_TYPED_FEEDBACK_PACKED_U32_ARRAY_LOOP_GUARD: extern \"C\" fn(u64, f64) -> i32", + "js_typed_feedback_packed_u32_array_loop_guard", + ); +} + #[test] fn representation_lowering_helpers_have_lto_keepalive_anchors() { let native_abi = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/native_abi.rs")); @@ -1033,6 +1115,7 @@ fn representation_lowering_helpers_have_lto_keepalive_anchors() { let set = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/set.rs")); let boxes = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/box.rs")); let closure_alloc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/closure/alloc.rs")); + let promise = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/promise/mod.rs")); for (src, static_name, signature, target) in [ ( @@ -1179,36 +1262,192 @@ fn representation_lowering_helpers_have_lto_keepalive_anchors() { "static KEEP_JS_MAP_SET_STRING_NUMBER: extern \"C\" fn(", "js_map_set_string_number", ), + ( + map, + "KEEP_JS_MAP_SET_STRING_KEY", + "static KEEP_JS_MAP_SET_STRING_KEY: extern \"C\" fn(", + "js_map_set_string_key", + ), + ( + map, + "KEEP_JS_MAP_SET_STRING_I32", + "static KEEP_JS_MAP_SET_STRING_I32: extern \"C\" fn(", + "js_map_set_string_i32", + ), + ( + map, + "KEEP_JS_MAP_SET_STRING_U32", + "static KEEP_JS_MAP_SET_STRING_U32: extern \"C\" fn(", + "js_map_set_string_u32", + ), + ( + map, + "KEEP_JS_MAP_SET_STRING_F32", + "static KEEP_JS_MAP_SET_STRING_F32: extern \"C\" fn(", + "js_map_set_string_f32", + ), + ( + map, + "KEEP_JS_MAP_SET_STRING_BOOL", + "static KEEP_JS_MAP_SET_STRING_BOOL: extern \"C\" fn(", + "js_map_set_string_bool", + ), + ( + map, + "KEEP_JS_MAP_SET_STRING_STRING", + "static KEEP_JS_MAP_SET_STRING_STRING: extern \"C\" fn(", + "js_map_set_string_string", + ), + ( + map, + "KEEP_JS_MAP_SET_NUMBER_KEY", + "static KEEP_JS_MAP_SET_NUMBER_KEY: extern \"C\" fn(*mut MapHeader, f64, f64) -> *mut MapHeader", + "js_map_set_number_key", + ), ( map, "KEEP_JS_MAP_HAS_STRING_KEY", "static KEEP_JS_MAP_HAS_STRING_KEY: extern \"C\" fn(*const MapHeader, *const StringHeader) -> i32", "js_map_has_string_key", ), + ( + map, + "KEEP_JS_MAP_HAS_NUMBER_KEY", + "static KEEP_JS_MAP_HAS_NUMBER_KEY: extern \"C\" fn(*const MapHeader, f64) -> i32", + "js_map_has_number_key", + ), ( map, "KEEP_JS_MAP_GET_STRING_KEY", "static KEEP_JS_MAP_GET_STRING_KEY: extern \"C\" fn(*const MapHeader, *const StringHeader) -> f64", "js_map_get_string_key", ), + ( + map, + "KEEP_JS_MAP_GET_NUMBER_KEY", + "static KEEP_JS_MAP_GET_NUMBER_KEY: extern \"C\" fn(*const MapHeader, f64) -> f64", + "js_map_get_number_key", + ), + ( + map, + "KEEP_JS_MAP_DELETE_STRING_KEY", + "static KEEP_JS_MAP_DELETE_STRING_KEY: extern \"C\" fn(*mut MapHeader, *const StringHeader) -> i32", + "js_map_delete_string_key", + ), + ( + map, + "KEEP_JS_MAP_DELETE_NUMBER_KEY", + "static KEEP_JS_MAP_DELETE_NUMBER_KEY: extern \"C\" fn(*mut MapHeader, f64) -> i32", + "js_map_delete_number_key", + ), ( set, "KEEP_JS_SET_ADD_STRING", "static KEEP_JS_SET_ADD_STRING: extern \"C\" fn(", "js_set_add_string", ), + ( + set, + "KEEP_JS_SET_ADD_NUMBER", + "static KEEP_JS_SET_ADD_NUMBER: extern \"C\" fn(*mut SetHeader, f64) -> *mut SetHeader", + "js_set_add_number", + ), ( set, "KEEP_JS_SET_HAS_STRING", "static KEEP_JS_SET_HAS_STRING: extern \"C\" fn(*const SetHeader, *const StringHeader) -> i32", "js_set_has_string", ), + ( + set, + "KEEP_JS_SET_HAS_NUMBER", + "static KEEP_JS_SET_HAS_NUMBER: extern \"C\" fn(*const SetHeader, f64) -> i32", + "js_set_has_number", + ), ( set, "KEEP_JS_SET_DELETE_STRING", "static KEEP_JS_SET_DELETE_STRING: extern \"C\" fn(*mut SetHeader, *const StringHeader) -> i32", "js_set_delete_string", ), + ( + set, + "KEEP_JS_SET_DELETE_NUMBER", + "static KEEP_JS_SET_DELETE_NUMBER: extern \"C\" fn(*mut SetHeader, f64) -> i32", + "js_set_delete_number", + ), + ( + set, + "KEEP_JS_SET_ADD_I32", + "static KEEP_JS_SET_ADD_I32: extern \"C\" fn(*mut SetHeader, i32) -> *mut SetHeader", + "js_set_add_i32", + ), + ( + set, + "KEEP_JS_SET_HAS_I32", + "static KEEP_JS_SET_HAS_I32: extern \"C\" fn(*const SetHeader, i32) -> i32", + "js_set_has_i32", + ), + ( + set, + "KEEP_JS_SET_DELETE_I32", + "static KEEP_JS_SET_DELETE_I32: extern \"C\" fn(*mut SetHeader, i32) -> i32", + "js_set_delete_i32", + ), + ( + set, + "KEEP_JS_SET_ADD_U32", + "static KEEP_JS_SET_ADD_U32: extern \"C\" fn(*mut SetHeader, u32) -> *mut SetHeader", + "js_set_add_u32", + ), + ( + set, + "KEEP_JS_SET_HAS_U32", + "static KEEP_JS_SET_HAS_U32: extern \"C\" fn(*const SetHeader, u32) -> i32", + "js_set_has_u32", + ), + ( + set, + "KEEP_JS_SET_DELETE_U32", + "static KEEP_JS_SET_DELETE_U32: extern \"C\" fn(*mut SetHeader, u32) -> i32", + "js_set_delete_u32", + ), + ( + set, + "KEEP_JS_SET_ADD_F32", + "static KEEP_JS_SET_ADD_F32: extern \"C\" fn(*mut SetHeader, f32) -> *mut SetHeader", + "js_set_add_f32", + ), + ( + set, + "KEEP_JS_SET_HAS_F32", + "static KEEP_JS_SET_HAS_F32: extern \"C\" fn(*const SetHeader, f32) -> i32", + "js_set_has_f32", + ), + ( + set, + "KEEP_JS_SET_DELETE_F32", + "static KEEP_JS_SET_DELETE_F32: extern \"C\" fn(*mut SetHeader, f32) -> i32", + "js_set_delete_f32", + ), + ( + set, + "KEEP_JS_SET_ADD_BOOL", + "static KEEP_JS_SET_ADD_BOOL: extern \"C\" fn(*mut SetHeader, i32) -> *mut SetHeader", + "js_set_add_bool", + ), + ( + set, + "KEEP_JS_SET_HAS_BOOL", + "static KEEP_JS_SET_HAS_BOOL: extern \"C\" fn(*const SetHeader, i32) -> i32", + "js_set_has_bool", + ), + ( + set, + "KEEP_JS_SET_DELETE_BOOL", + "static KEEP_JS_SET_DELETE_BOOL: extern \"C\" fn(*mut SetHeader, i32) -> i32", + "js_set_delete_bool", + ), ( boxes, "KEEP_JS_I32_BOX_ALLOC", @@ -1245,6 +1484,30 @@ fn representation_lowering_helpers_have_lto_keepalive_anchors() { "static KEEP_JS_BOOL_BOX_SET: extern \"C\" fn(*mut BoolBox, i32)", "js_bool_box_set", ), + ( + promise, + "KEEP_JS_ITER_RESULT_SET_I32", + "static KEEP_JS_ITER_RESULT_SET_I32: extern \"C\" fn(i32, i32) -> f64", + "js_iter_result_set_i32", + ), + ( + promise, + "KEEP_JS_ITER_RESULT_SET_I1", + "static KEEP_JS_ITER_RESULT_SET_I1: extern \"C\" fn(i32, i32) -> f64", + "js_iter_result_set_i1", + ), + ( + promise, + "KEEP_JS_ITER_RESULT_GET_VALUE_I32", + "static KEEP_JS_ITER_RESULT_GET_VALUE_I32: extern \"C\" fn() -> i32", + "js_iter_result_get_value_i32", + ), + ( + promise, + "KEEP_JS_ITER_RESULT_GET_VALUE_I1", + "static KEEP_JS_ITER_RESULT_GET_VALUE_I1: extern \"C\" fn() -> i32", + "js_iter_result_get_value_i1", + ), ( trace, "static K29", diff --git a/crates/perry-runtime/src/typed_feedback/trace.rs b/crates/perry-runtime/src/typed_feedback/trace.rs index 6a1437af34..91d4740453 100644 --- a/crates/perry-runtime/src/typed_feedback/trace.rs +++ b/crates/perry-runtime/src/typed_feedback/trace.rs @@ -383,20 +383,21 @@ mod keep_typed_feedback { #[used] static K12: extern "C" fn(u64, f64, f64, i32, i32) -> i32 = js_typed_feedback_plain_array_index_get_guard; #[used] static K13: extern "C" fn(u64, f64, f64, i32, i32) -> i32 = js_typed_feedback_numeric_array_index_get_guard; #[used] static K14: extern "C" fn(u64, f64) -> i32 = js_typed_feedback_packed_f64_array_loop_guard; - #[used] static K15: extern "C" fn(u64, f64, f64) -> f64 = js_typed_feedback_array_index_get_fallback_boxed; - #[used] static K16: extern "C" fn(u64, *mut ArrayHeader, u32, f64) = js_typed_feedback_array_set_f64; - #[used] static K17: extern "C" fn(u64, *mut ArrayHeader, u32, f64) -> *mut ArrayHeader = js_typed_feedback_array_set_f64_extend; - #[used] static K18: extern "C" fn(u64, f64, i32, f64, i32) -> i32 = js_typed_feedback_plain_array_index_set_guard; - #[used] static K19: extern "C" fn(u64, f64, i32, f64, i32) -> i32 = js_typed_feedback_numeric_array_index_set_guard; - #[used] static K20: extern "C" fn(u64, f64, f64) -> i32 = js_typed_feedback_numeric_array_push_guard; - #[used] static K21: extern "C" fn(u64, f64, f64, f64) -> f64 = js_typed_feedback_array_index_set_fallback_boxed; - #[used] static K22: extern "C" fn(u64, *const ArrayHeader, u32) = js_typed_feedback_observe_array_element; - #[used] static K23: extern "C" fn(u64, *mut ArrayHeader, *const crate::StringHeader, f64) -> *mut ArrayHeader = js_typed_feedback_array_set_string_key; - #[used] static K24: extern "C" fn(u64, *mut ArrayHeader, f64, f64) -> *mut ArrayHeader = js_typed_feedback_array_set_index_or_string; - #[used] static K25: extern "C" fn(u64, i64, f64, f64) = js_typed_feedback_object_set_index_polymorphic; - #[used] static K26: extern "C" fn(u64, *mut ObjectHeader, u32, *const crate::StringHeader, f64) = js_typed_feedback_object_set_unboxed_f64_field; - #[used] static K27: extern "C" fn(u64, f64) -> f64 = js_typed_feedback_observe_helper_return; - #[used] static K28: extern "C" fn() = js_typed_feedback_maybe_dump_trace; - #[used] static K29: unsafe extern "C" fn(u64, f64, i64, *const f64, usize) -> f64 = js_typed_feedback_native_call_method_by_id; - #[used] static K30: unsafe extern "C" fn(u64, f64, i64, i64) -> f64 = js_typed_feedback_native_call_method_apply_by_id; + #[used] static K15: extern "C" fn(u64, f64) -> i32 = js_typed_feedback_packed_u32_array_loop_guard; + #[used] static K16: extern "C" fn(u64, f64, f64) -> f64 = js_typed_feedback_array_index_get_fallback_boxed; + #[used] static K17: extern "C" fn(u64, *mut ArrayHeader, u32, f64) = js_typed_feedback_array_set_f64; + #[used] static K18: extern "C" fn(u64, *mut ArrayHeader, u32, f64) -> *mut ArrayHeader = js_typed_feedback_array_set_f64_extend; + #[used] static K19: extern "C" fn(u64, f64, i32, f64, i32) -> i32 = js_typed_feedback_plain_array_index_set_guard; + #[used] static K20: extern "C" fn(u64, f64, i32, f64, i32) -> i32 = js_typed_feedback_numeric_array_index_set_guard; + #[used] static K21: extern "C" fn(u64, f64, f64) -> i32 = js_typed_feedback_numeric_array_push_guard; + #[used] static K22: extern "C" fn(u64, f64, f64, f64) -> f64 = js_typed_feedback_array_index_set_fallback_boxed; + #[used] static K23: extern "C" fn(u64, *const ArrayHeader, u32) = js_typed_feedback_observe_array_element; + #[used] static K24: extern "C" fn(u64, *mut ArrayHeader, *const crate::StringHeader, f64) -> *mut ArrayHeader = js_typed_feedback_array_set_string_key; + #[used] static K25: extern "C" fn(u64, *mut ArrayHeader, f64, f64) -> *mut ArrayHeader = js_typed_feedback_array_set_index_or_string; + #[used] static K26: extern "C" fn(u64, i64, f64, f64) = js_typed_feedback_object_set_index_polymorphic; + #[used] static K27: extern "C" fn(u64, *mut ObjectHeader, u32, *const crate::StringHeader, f64) = js_typed_feedback_object_set_unboxed_f64_field; + #[used] static K28: extern "C" fn(u64, f64) -> f64 = js_typed_feedback_observe_helper_return; + #[used] static K29: extern "C" fn() = js_typed_feedback_maybe_dump_trace; + #[used] static K30: unsafe extern "C" fn(u64, f64, i64, *const f64, usize) -> f64 = js_typed_feedback_native_call_method_by_id; + #[used] static K31: unsafe extern "C" fn(u64, f64, i64, i64) -> f64 = js_typed_feedback_native_call_method_apply_by_id; } diff --git a/crates/perry/src/commands/compile/lowering_report.rs b/crates/perry/src/commands/compile/lowering_report.rs index daf0820f50..008ce19256 100644 --- a/crates/perry/src/commands/compile/lowering_report.rs +++ b/crates/perry/src/commands/compile/lowering_report.rs @@ -113,6 +113,11 @@ pub(super) struct LoweringSummary { pub barrier_eliminations: u64, pub barrier_emissions: u64, pub scalar_replacements: u64, + pub scalar_replacement_fallbacks: u64, + pub scalar_replacement_rejections: u64, + pub typed_path_selections: u64, + pub typed_path_fallbacks: u64, + pub typed_path_rejections: u64, pub typed_clone_selections: u64, pub typed_clone_fallback_decisions: u64, pub generic_fallback_emissions: u64, @@ -123,6 +128,10 @@ pub(super) struct LoweringSummary { pub pod_records: u64, pub pod_record_views: u64, pub pod_materializations: u64, + pub collection_helper_selections: u64, + pub collection_helper_fallback_decisions: u64, + pub collection_typed_value_selections: u64, + pub collection_typed_value_fallback_decisions: u64, pub native_rep_counts: BTreeMap, pub native_value_state_counts: BTreeMap, pub access_mode_counts: BTreeMap, @@ -132,12 +141,28 @@ pub(super) struct LoweringSummary { pub typed_clone_decision_counts: BTreeMap, pub typed_clone_selection_reason_counts: BTreeMap, pub typed_clone_rejection_reason_counts: BTreeMap, + pub typed_path_decision_counts: BTreeMap, + pub typed_path_selection_reason_counts: BTreeMap, + pub typed_path_fallback_reason_counts: BTreeMap, + pub typed_path_rejection_reason_counts: BTreeMap, + pub collection_helper_decision_counts: BTreeMap, + pub collection_helper_family_counts: BTreeMap, + pub collection_helper_selection_reason_counts: BTreeMap, + pub collection_helper_rejection_reason_counts: BTreeMap, + pub collection_typed_value_decision_counts: BTreeMap, + pub collection_typed_value_selection_reason_counts: BTreeMap, + pub collection_typed_value_rejection_reason_counts: BTreeMap, pub generic_fallback_reason_counts: BTreeMap, pub dynamic_boundary_reason_counts: BTreeMap, pub box_reason_counts: BTreeMap, pub unbox_or_coercion_reason_counts: BTreeMap, pub runtime_property_get_reason_counts: BTreeMap, pub direct_field_load_reason_counts: BTreeMap, + pub scalar_replacement_decision_counts: BTreeMap, + pub scalar_replacement_selection_reason_counts: BTreeMap, + pub scalar_replacement_rejection_reason_counts: BTreeMap, + pub scalar_replacement_fallback_reason_counts: BTreeMap, + pub scalar_replacement_reason_counts: BTreeMap, pub bounds_eliminated_reason_counts: BTreeMap, pub bounds_kept_reason_counts: BTreeMap, pub barrier_elimination_reason_counts: BTreeMap, @@ -155,6 +180,9 @@ pub(super) struct LoweringEvidence { pub direct_field_loads: Vec, pub runtime_property_gets: Vec, pub scalar_replacements: Vec, + pub collection_helper_decisions: Vec, + pub collection_typed_value_decisions: Vec, + pub typed_path_decisions: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -172,6 +200,8 @@ pub(super) struct EvidenceRow { pub reason_category: Option, pub typed_clone: Option, pub generic_fallback: Option, + pub consumed_facts: Vec, + pub rejected_facts: Vec, pub notes: Vec, } @@ -340,6 +370,14 @@ fn aggregate_record( if is_dynamic_fallback { let reason = dynamic_boundary_reason(record); increment(&mut summary.dynamic_boundary_reason_counts, &reason); + push_typed_path_evidence( + summary, + evidence, + module, + record, + "fallback", + format!("dynamic_boundary:{reason}"), + ); push_evidence( &mut evidence.dynamic_fallbacks, module, @@ -416,14 +454,128 @@ fn aggregate_record( ); } - if expr_kind.starts_with("Scalar") || consumer.starts_with("scalar_object_") { - summary.scalar_replacements += 1; + if let Some((decision, reason)) = scalar_replacement_decision(record, &expr_kind, &consumer) { + increment(&mut summary.scalar_replacement_decision_counts, &decision); + match decision.as_str() { + "selected" => { + summary.scalar_replacements += 1; + increment( + &mut summary.scalar_replacement_selection_reason_counts, + &reason, + ); + } + "fallback" => { + summary.scalar_replacement_fallbacks += 1; + increment( + &mut summary.scalar_replacement_fallback_reason_counts, + &reason, + ); + } + "rejected" => { + summary.scalar_replacement_rejections += 1; + increment( + &mut summary.scalar_replacement_rejection_reason_counts, + &reason, + ); + } + _ => {} + } + increment(&mut summary.scalar_replacement_reason_counts, &reason); push_evidence( &mut evidence.scalar_replacements, module, record, - Some("scalar_replacement".to_string()), - Some(NOT_RECORDED.to_string()), + Some(format!("scalar_replacement_{decision}")), + Some(reason), + ); + } + + if let Some(family) = collection_helper_family(&consumer) { + increment(&mut summary.collection_helper_family_counts, &family); + if let Some(reason) = collection_helper_selection_reason(&consumer, ¬es) { + summary.collection_helper_selections += 1; + increment(&mut summary.collection_helper_decision_counts, "selected"); + increment( + &mut summary.collection_helper_selection_reason_counts, + &reason, + ); + push_evidence( + &mut evidence.collection_helper_decisions, + module, + record, + Some("collection_helper_selected".to_string()), + Some(reason), + ); + } else if let Some(reason) = collection_helper_rejection_reason(record, ¬es) { + summary.collection_helper_fallback_decisions += 1; + summary.generic_fallback_emissions += 1; + increment(&mut summary.collection_helper_decision_counts, "rejected"); + increment( + &mut summary.collection_helper_rejection_reason_counts, + &reason, + ); + if let Some(generic_reason) = collection_generic_fallback_reason(&consumer, ¬es) { + increment(&mut summary.generic_fallback_reason_counts, &generic_reason); + } + push_evidence( + &mut evidence.collection_helper_decisions, + module, + record, + Some("collection_helper_rejected".to_string()), + Some(reason), + ); + } + } + + if let Some(reason) = collection_typed_value_selection_reason(record, ¬es) { + summary.collection_typed_value_selections += 1; + increment( + &mut summary.collection_typed_value_decision_counts, + "selected", + ); + increment( + &mut summary.collection_typed_value_selection_reason_counts, + &reason, + ); + push_evidence( + &mut evidence.collection_typed_value_decisions, + module, + record, + Some("collection_typed_value_selected".to_string()), + Some(reason.clone()), + ); + push_typed_path_evidence( + summary, + evidence, + module, + record, + "selected", + "collection_typed_value_selected".to_string(), + ); + } else if let Some(reason) = collection_typed_value_rejection_reason(record, ¬es) { + summary.collection_typed_value_fallback_decisions += 1; + increment( + &mut summary.collection_typed_value_decision_counts, + "rejected", + ); + increment( + &mut summary.collection_typed_value_rejection_reason_counts, + &reason, + ); + push_evidence( + &mut evidence.collection_typed_value_decisions, + module, + record, + Some("collection_typed_value_rejected".to_string()), + Some(reason.clone()), + ); + push_typed_path_evidence( + summary, + evidence, + module, + record, + "rejected", + format!("collection_typed_value:{reason}"), ); } @@ -432,9 +584,25 @@ fn aggregate_record( increment(&mut summary.typed_clone_decision_counts, "selected"); let reason = typed_clone_selection_reason(&consumer); increment(&mut summary.typed_clone_selection_reason_counts, &reason); + push_typed_path_evidence( + summary, + evidence, + module, + record, + "selected", + format!("typed_clone:{reason}"), + ); if let Some(reason) = generic_fallback_reason(record, ¬es) { summary.generic_fallback_emissions += 1; increment(&mut summary.generic_fallback_reason_counts, &reason); + push_typed_path_evidence( + summary, + evidence, + module, + record, + "fallback", + format!("generic_fallback:{reason}"), + ); } if generic_fallback_name(¬es).is_some() || notes_text.contains("fallback") { summary.typed_clone_fallback_decisions += 1; @@ -449,6 +617,14 @@ fn aggregate_record( } else if let Some(reason) = typed_clone_rejection_reason(record, ¬es) { increment(&mut summary.typed_clone_decision_counts, "rejected"); increment(&mut summary.typed_clone_rejection_reason_counts, &reason); + push_typed_path_evidence( + summary, + evidence, + module, + record, + "rejected", + format!("typed_clone:{reason}"), + ); push_evidence( &mut evidence.typed_clone_decisions, module, @@ -482,6 +658,10 @@ fn print_text_report(report: &ExplainLoweringReport) { summary.typed_clone_selections, summary.typed_clone_fallback_decisions ); + println!( + " typed paths: {} selected {} fallback {} rejected", + summary.typed_path_selections, summary.typed_path_fallbacks, summary.typed_path_rejections + ); println!( " runtime property gets: {} direct field loads: {} bounds eliminations: {}", summary.runtime_property_gets, summary.direct_field_loads, summary.bounds_eliminations @@ -490,6 +670,21 @@ fn print_text_report(report: &ExplainLoweringReport) { " barrier eliminations: {} barrier emissions: {} scalar replacements: {}", summary.barrier_eliminations, summary.barrier_emissions, summary.scalar_replacements ); + if summary.scalar_replacement_fallbacks > 0 || summary.scalar_replacement_rejections > 0 { + println!( + " scalar replacement fallbacks: {} rejections: {}", + summary.scalar_replacement_fallbacks, summary.scalar_replacement_rejections + ); + } + println!( + " collection helpers: {} selected {} rejected/generic", + summary.collection_helper_selections, summary.collection_helper_fallback_decisions + ); + println!( + " collection typed values: {} selected {} rejected/generic", + summary.collection_typed_value_selections, + summary.collection_typed_value_fallback_decisions + ); if !summary.native_rep_counts.is_empty() { println!( @@ -527,6 +722,78 @@ fn print_text_report(report: &ExplainLoweringReport) { format_counts(&summary.typed_clone_rejection_reason_counts) ); } + if !summary.typed_path_decision_counts.is_empty() { + println!( + " typed path decisions: {}", + format_counts(&summary.typed_path_decision_counts) + ); + } + if !summary.typed_path_selection_reason_counts.is_empty() { + println!( + " typed path selection reasons: {}", + format_counts(&summary.typed_path_selection_reason_counts) + ); + } + if !summary.typed_path_fallback_reason_counts.is_empty() { + println!( + " typed path fallback reasons: {}", + format_counts(&summary.typed_path_fallback_reason_counts) + ); + } + if !summary.typed_path_rejection_reason_counts.is_empty() { + println!( + " typed path rejection reasons: {}", + format_counts(&summary.typed_path_rejection_reason_counts) + ); + } + if !summary.collection_helper_decision_counts.is_empty() { + println!( + " collection helper decisions: {}", + format_counts(&summary.collection_helper_decision_counts) + ); + } + if !summary.collection_helper_family_counts.is_empty() { + println!( + " collection helper families: {}", + format_counts(&summary.collection_helper_family_counts) + ); + } + if !summary.collection_helper_selection_reason_counts.is_empty() { + println!( + " collection helper selection reasons: {}", + format_counts(&summary.collection_helper_selection_reason_counts) + ); + } + if !summary.collection_helper_rejection_reason_counts.is_empty() { + println!( + " collection helper rejection reasons: {}", + format_counts(&summary.collection_helper_rejection_reason_counts) + ); + } + if !summary.collection_typed_value_decision_counts.is_empty() { + println!( + " collection typed value decisions: {}", + format_counts(&summary.collection_typed_value_decision_counts) + ); + } + if !summary + .collection_typed_value_selection_reason_counts + .is_empty() + { + println!( + " collection typed value selection reasons: {}", + format_counts(&summary.collection_typed_value_selection_reason_counts) + ); + } + if !summary + .collection_typed_value_rejection_reason_counts + .is_empty() + { + println!( + " collection typed value rejection reasons: {}", + format_counts(&summary.collection_typed_value_rejection_reason_counts) + ); + } if !summary.generic_fallback_reason_counts.is_empty() { println!( " generic fallback reasons: {}", @@ -551,6 +818,42 @@ fn print_text_report(report: &ExplainLoweringReport) { format_counts(&summary.unbox_or_coercion_reason_counts) ); } + if !summary.scalar_replacement_reason_counts.is_empty() { + println!( + " scalar replacement reasons: {}", + format_counts(&summary.scalar_replacement_reason_counts) + ); + } + if !summary.scalar_replacement_decision_counts.is_empty() { + println!( + " scalar replacement decisions: {}", + format_counts(&summary.scalar_replacement_decision_counts) + ); + } + if !summary + .scalar_replacement_selection_reason_counts + .is_empty() + { + println!( + " scalar replacement selection reasons: {}", + format_counts(&summary.scalar_replacement_selection_reason_counts) + ); + } + if !summary.scalar_replacement_fallback_reason_counts.is_empty() { + println!( + " scalar replacement fallback reasons: {}", + format_counts(&summary.scalar_replacement_fallback_reason_counts) + ); + } + if !summary + .scalar_replacement_rejection_reason_counts + .is_empty() + { + println!( + " scalar replacement rejection reasons: {}", + format_counts(&summary.scalar_replacement_rejection_reason_counts) + ); + } if !summary.bounds_eliminated_reason_counts.is_empty() { println!( " bounds eliminated reasons: {}", @@ -611,10 +914,45 @@ fn push_evidence( reason_category, typed_clone: typed_clone_name(¬es), generic_fallback: generic_fallback_name(¬es), + consumed_facts: fact_labels(record, "consumed_facts"), + rejected_facts: fact_labels(record, "rejected_facts"), notes, }); } +fn push_typed_path_evidence( + summary: &mut LoweringSummary, + evidence: &mut LoweringEvidence, + module: &str, + record: &Value, + decision: &str, + reason: String, +) { + match decision { + "selected" => { + summary.typed_path_selections += 1; + increment(&mut summary.typed_path_selection_reason_counts, &reason); + } + "fallback" => { + summary.typed_path_fallbacks += 1; + increment(&mut summary.typed_path_fallback_reason_counts, &reason); + } + "rejected" => { + summary.typed_path_rejections += 1; + increment(&mut summary.typed_path_rejection_reason_counts, &reason); + } + _ => {} + } + increment(&mut summary.typed_path_decision_counts, decision); + push_evidence( + &mut evidence.typed_path_decisions, + module, + record, + Some(format!("typed_path_{decision}")), + Some(reason), + ); +} + fn increment(counts: &mut BTreeMap, key: &str) { *counts.entry(key.to_string()).or_insert(0) += 1; } @@ -657,6 +995,27 @@ fn notes(record: &Value) -> Vec { .unwrap_or_default() } +fn fact_labels(record: &Value, field: &str) -> Vec { + record + .get(field) + .and_then(Value::as_array) + .map(|facts| { + facts + .iter() + .filter_map(|fact| { + let fact_id = string_field(fact, "fact_id")?; + let state = + string_field(fact, "state").unwrap_or_else(|| NOT_RECORDED.to_string()); + let reason = string_field(fact, "reason") + .or_else(|| string_field(fact, "detail")) + .unwrap_or_else(|| NOT_RECORDED.to_string()); + Some(format!("{fact_id}:{state}:{reason}")) + }) + .collect() + }) + .unwrap_or_default() +} + fn note_value(notes: &[String], key: &str) -> Option { for note in notes { for part in note.split(';') { @@ -701,6 +1060,77 @@ fn generic_fallback_reason(record: &Value, notes: &[String]) -> Option { None } +fn collection_helper_family(consumer: &str) -> Option { + consumer + .strip_prefix("collection_string_key.") + .map(|_| "collection_string_key".to_string()) + .or_else(|| { + consumer + .strip_prefix("collection_typed_value.") + .map(|_| "collection_typed_value".to_string()) + }) +} + +fn collection_helper_selection_reason(consumer: &str, notes: &[String]) -> Option { + let helper = note_value(notes, "selected_helper")?; + Some(format!("{consumer}:{helper}")) +} + +fn collection_helper_rejection_reason(record: &Value, notes: &[String]) -> Option { + note_value(notes, "typed_collection_rejected") + .or_else(|| native_fact_reason(record, "rejected_facts", "type_fact")) + .map(|reason| { + let helper = + note_value(notes, "generic_helper").unwrap_or_else(|| "generic".to_string()); + format!("{reason}:{helper}") + }) +} + +fn collection_generic_fallback_reason(consumer: &str, notes: &[String]) -> Option { + note_value(notes, "generic_helper").map(|helper| format!("{consumer}:{helper}")) +} + +fn collection_typed_value_selection_reason(record: &Value, notes: &[String]) -> Option { + let (fact_id, _) = collection_typed_value_fact(record, "consumed_facts", "consumed")?; + let helper = note_value(notes, "selected_helper").unwrap_or_else(|| "selected".to_string()); + Some(format!("{fact_id}:{helper}")) +} + +fn collection_typed_value_rejection_reason(record: &Value, notes: &[String]) -> Option { + let (fact_id, fact_reason) = collection_typed_value_fact(record, "rejected_facts", "rejected")?; + let reason = note_value(notes, "typed_collection_rejected") + .or(fact_reason) + .unwrap_or_else(|| NOT_RECORDED.to_string()); + let helper = note_value(notes, "generic_helper").unwrap_or_else(|| "generic".to_string()); + Some(format!("{fact_id}:{reason}:{helper}")) +} + +fn collection_typed_value_fact( + record: &Value, + field: &str, + state: &str, +) -> Option<(String, Option)> { + record + .get(field) + .and_then(Value::as_array)? + .iter() + .find_map(|fact| { + if string_field(fact, "kind").as_deref() != Some("type_fact") { + return None; + } + if string_field(fact, "state").as_deref() != Some(state) { + return None; + } + let fact_id = string_field(fact, "fact_id")?; + if !matches!(fact_id.split_once('.'), Some(("map" | "set", _))) + || !fact_id.ends_with("_value_helper") + { + return None; + } + Some((fact_id, string_field(fact, "reason"))) + }) +} + fn typed_clone_selection_reason(consumer: &str) -> String { match consumer { "typed_f64_func_ref_call" => "typed_f64_function_direct_call", @@ -735,6 +1165,7 @@ fn native_fact_reason(record: &Value, field: &str, kind_prefix: &str) -> Option< return None; } string_field(fact, "reason") + .or_else(|| string_field(fact, "detail")) .or_else(|| string_field(fact, "state")) .or_else(|| Some(NOT_RECORDED.to_string())) }) @@ -846,6 +1277,55 @@ fn direct_field_load_reason(record: &Value) -> String { .unwrap_or_else(|| NOT_RECORDED.to_string()) } +fn scalar_replacement_reason(record: &Value) -> String { + let notes = notes(record); + native_fact_reason(record, "consumed_facts", "scalar_method_summary") + .map(|reason| format!("scalar_method_summary:{reason}")) + .or_else(|| { + native_fact_reason(record, "rejected_facts", "scalar_method_summary") + .map(|reason| format!("scalar_method_materialized_fallback:{reason}")) + }) + .or_else(|| { + note_value(¬es, "scalar_method_fallback") + .map(|reason| format!("scalar_method_materialized_fallback:{reason}")) + }) + .or_else(|| { + let direct_reason = direct_field_load_reason(record); + (direct_reason != NOT_RECORDED).then_some(direct_reason) + }) + .or_else(|| { + let consumer = string_field(record, "consumer").unwrap_or_default(); + match consumer.as_str() { + "scalar_method_summary_inline" => Some("scalar_method_summary_inline".to_string()), + "scalar_method_summary_materialized_fallback" => { + Some("scalar_method_materialized_fallback".to_string()) + } + _ => None, + } + }) + .unwrap_or_else(|| NOT_RECORDED.to_string()) +} + +fn scalar_replacement_decision( + record: &Value, + expr_kind: &str, + consumer: &str, +) -> Option<(String, String)> { + let reason = scalar_replacement_reason(record); + let decision = match consumer { + "scalar_method_summary_inline" => "selected", + "scalar_method_summary_materialized_fallback" | "scalar_method_summary_fallback" => { + "fallback" + } + "scalar_method_summary_rejected" => "rejected", + _ if expr_kind.starts_with("Scalar") || consumer.starts_with("scalar_object_") => { + "selected" + } + _ => return None, + }; + Some((decision.to_string(), reason)) +} + fn barrier_elimination_reason( _expr_kind: &str, _consumer: &str, @@ -1090,6 +1570,37 @@ mod tests { report.summary.typed_clone_decision_counts.get("selected"), Some(&1) ); + assert_eq!(report.summary.typed_path_selections, 1); + assert_eq!(report.summary.typed_path_fallbacks, 2); + assert_eq!( + report.summary.typed_path_decision_counts.get("selected"), + Some(&1) + ); + assert_eq!( + report.summary.typed_path_decision_counts.get("fallback"), + Some(&2) + ); + assert_eq!( + report + .summary + .typed_path_selection_reason_counts + .get("typed_clone:typed_f64_function_direct_call"), + Some(&1) + ); + assert_eq!( + report + .summary + .typed_path_fallback_reason_counts + .get("generic_fallback:generic_wrapper"), + Some(&1) + ); + assert_eq!( + report + .summary + .typed_path_fallback_reason_counts + .get("dynamic_boundary:runtime_api"), + Some(&1) + ); assert_eq!( report.summary.typed_clone_decision_counts.get(NOT_RECORDED), Some(&1) @@ -1170,10 +1681,12 @@ mod tests { let json = serde_json::to_value(&report).unwrap(); assert!(json["summary"]["typed_clone_selection_reason_counts"].is_object()); + assert!(json["summary"]["typed_path_decision_counts"].is_object()); assert!(json["summary"]["dynamic_boundary_reason_counts"].is_object()); assert!(json["summary"]["box_reason_counts"].is_object()); assert!(json["summary"]["unbox_or_coercion_reason_counts"].is_object()); assert!(json["evidence"]["typed_clone_decisions"][0]["typed_clone"].is_string()); + assert!(json["evidence"]["typed_path_decisions"].is_array()); } #[test] @@ -1218,6 +1731,12 @@ mod tests { report.summary.typed_clone_decision_counts.get("rejected"), Some(&1) ); + assert_eq!(report.summary.typed_path_selections, 1); + assert_eq!(report.summary.typed_path_rejections, 1); + assert_eq!( + report.summary.typed_path_decision_counts.get("rejected"), + Some(&1) + ); assert_eq!( report .summary @@ -1298,6 +1817,220 @@ mod tests { ); } + #[test] + fn report_counts_collection_helper_selection_and_rejection_reasons() { + let artifact = json!({ + "schema_version": 14, + "module": "collections.ts", + "records": [ + { + "function": "probe", + "source_function": "probe", + "expr_kind": "MapSet", + "consumer": "collection_string_key.map_set_string_bool", + "native_rep_name": "i1", + "native_value_state": "region_local", + "consumed_facts": [ + { + "fact_id": "map.string_key_helper", + "kind": "type_fact", + "local_id": null, + "state": "consumed" + }, + { + "fact_id": "map.boolean_value_helper", + "kind": "type_fact", + "local_id": null, + "state": "consumed" + } + ], + "notes": [ + "selected_helper=js_map_set_string_bool", + "key_rep=string_ref", + "value_rep=i1", + "boxed_key_avoided=true", + "boxed_value_avoided_until_map_slot=true" + ] + }, + { + "function": "probe", + "source_function": "probe", + "expr_kind": "SetAdd", + "consumer": "collection_typed_value.set_add_bool", + "native_rep_name": "i1", + "native_value_state": "region_local", + "consumed_facts": [ + { + "fact_id": "set.boolean_value_helper", + "kind": "type_fact", + "local_id": null, + "state": "consumed" + } + ], + "notes": [ + "selected_helper=js_set_add_bool", + "value_rep=i1", + "boxed_value_avoided_until_set_slot=true" + ] + }, + { + "function": "probe", + "source_function": "probe", + "expr_kind": "SetHas", + "consumer": "collection_typed_value.set_has_generic", + "native_rep_name": "js_value", + "native_value_state": "region_local", + "rejected_facts": [ + { + "fact_id": "set.boolean_value_helper", + "kind": "type_fact", + "local_id": null, + "state": "rejected" + } + ], + "notes": [ + "generic_helper=js_set_has", + "typed_collection_rejected=value_expr_not_native_i1", + "value_rep=js_value" + ] + } + ] + }); + let report = build_report_from_artifacts( + Path::new("/tmp/lowering"), + vec![(PathBuf::from("native-reps.json"), artifact)], + ); + + assert_eq!(report.summary.collection_helper_selections, 2); + assert_eq!(report.summary.collection_helper_fallback_decisions, 1); + assert_eq!(report.summary.collection_typed_value_selections, 2); + assert_eq!(report.summary.collection_typed_value_fallback_decisions, 1); + assert_eq!(report.summary.generic_fallback_emissions, 1); + assert_eq!( + report + .summary + .collection_helper_decision_counts + .get("selected"), + Some(&2) + ); + assert_eq!( + report + .summary + .collection_helper_decision_counts + .get("rejected"), + Some(&1) + ); + assert_eq!( + report + .summary + .collection_helper_family_counts + .get("collection_string_key"), + Some(&1) + ); + assert_eq!( + report + .summary + .collection_helper_family_counts + .get("collection_typed_value"), + Some(&2) + ); + assert_eq!( + report + .summary + .collection_helper_selection_reason_counts + .get("collection_string_key.map_set_string_bool:js_map_set_string_bool"), + Some(&1) + ); + assert_eq!( + report + .summary + .collection_helper_selection_reason_counts + .get("collection_typed_value.set_add_bool:js_set_add_bool"), + Some(&1) + ); + assert_eq!( + report + .summary + .collection_helper_rejection_reason_counts + .get("value_expr_not_native_i1:js_set_has"), + Some(&1) + ); + assert_eq!( + report + .summary + .generic_fallback_reason_counts + .get("collection_typed_value.set_has_generic:js_set_has"), + Some(&1) + ); + assert_eq!( + report + .summary + .collection_typed_value_decision_counts + .get("selected"), + Some(&2) + ); + assert_eq!( + report + .summary + .collection_typed_value_decision_counts + .get("rejected"), + Some(&1) + ); + assert_eq!( + report + .summary + .collection_typed_value_selection_reason_counts + .get("map.boolean_value_helper:js_map_set_string_bool"), + Some(&1) + ); + assert_eq!( + report + .summary + .collection_typed_value_selection_reason_counts + .get("set.boolean_value_helper:js_set_add_bool"), + Some(&1) + ); + assert_eq!( + report + .summary + .collection_typed_value_rejection_reason_counts + .get("set.boolean_value_helper:value_expr_not_native_i1:js_set_has"), + Some(&1) + ); + assert_eq!(report.evidence.collection_helper_decisions.len(), 3); + assert_eq!(report.evidence.collection_typed_value_decisions.len(), 3); + assert_eq!( + report.evidence.collection_helper_decisions[0] + .decision + .as_deref(), + Some("collection_helper_selected") + ); + assert_eq!( + report.evidence.collection_helper_decisions[2] + .decision + .as_deref(), + Some("collection_helper_rejected") + ); + assert_eq!( + report.evidence.collection_typed_value_decisions[0] + .decision + .as_deref(), + Some("collection_typed_value_selected") + ); + assert_eq!( + report.evidence.collection_typed_value_decisions[2] + .decision + .as_deref(), + Some("collection_typed_value_rejected") + ); + + let json = serde_json::to_value(&report).unwrap(); + assert!(json["summary"]["collection_helper_decision_counts"].is_object()); + assert!(json["summary"]["collection_typed_value_decision_counts"].is_object()); + assert!(json["evidence"]["collection_helper_decisions"].is_array()); + assert!(json["evidence"]["collection_typed_value_decisions"].is_array()); + } + #[test] fn report_counts_field_bounds_and_scalar_evidence() { let artifact = json!({ @@ -1385,6 +2118,19 @@ mod tests { .get("scalar_replacement_raw_f64_field"), Some(&1) ); + assert_eq!( + report + .summary + .scalar_replacement_reason_counts + .get("scalar_replacement_raw_f64_field"), + Some(&1) + ); + assert_eq!( + report.evidence.scalar_replacements[0] + .reason_category + .as_deref(), + Some("scalar_replacement_raw_f64_field") + ); assert_eq!( report .summary @@ -1467,6 +2213,10 @@ mod tests { .summary .direct_field_load_reason_counts .contains_key(NOT_RECORDED)); + assert!(!report + .summary + .scalar_replacement_reason_counts + .contains_key(NOT_RECORDED)); assert!(!report .summary .barrier_emission_reason_counts @@ -1476,4 +2226,182 @@ mod tests { .bounds_kept_reason_counts .contains_key(NOT_RECORDED)); } + + #[test] + fn report_classifies_scalar_method_inline_and_materialized_fallback_facts() { + let artifact = json!({ + "schema_version": 14, + "module": "scalar_methods.ts", + "records": [ + { + "function": "probe", + "source_function": "probe", + "expr_kind": "ScalarMethodCall", + "consumer": "scalar_method_summary_inline", + "native_rep_name": "f64", + "native_value_state": "region_local", + "consumed_facts": [ + { + "fact_id": "native_region.scalar_method_summary.1.Point.len", + "kind": "scalar_method_summary", + "local_id": 1, + "state": "consumed", + "detail": "exact_receiver_summary" + } + ], + "notes": [ + "class=Point", + "method=len", + "receiver=scalar_replaced", + "arg_proof=proven_numeric" + ] + }, + { + "function": "probe", + "source_function": "probe", + "expr_kind": "ScalarMethodCall", + "consumer": "scalar_method_summary_materialized_fallback", + "native_rep_name": "js_value", + "native_value_state": "materialized", + "access_mode": "dynamic_fallback", + "materialization_reason": "runtime_api", + "rejected_facts": [ + { + "fact_id": "native_region.scalar_method_summary.1.Point.len", + "kind": "scalar_method_summary", + "local_id": 1, + "state": "arg_guard_failed", + "detail": "guarded_numeric_args_fallback" + } + ], + "notes": [ + "class=Point", + "method=len", + "receiver=scalar_replaced", + "scalar_method_fallback=arg_guard_failed", + "arg_guard=js_typed_f64_arg_guard" + ] + }, + { + "function": "probe", + "source_function": "probe", + "expr_kind": "ScalarMethodCall", + "consumer": "scalar_method_summary_materialized_fallback", + "native_rep_name": "js_value", + "native_value_state": "materialized", + "access_mode": "dynamic_fallback", + "materialization_reason": "runtime_api", + "rejected_facts": [ + { + "fact_id": "native_region.scalar_method_summary.1.Point.len", + "kind": "scalar_method_summary", + "local_id": 1, + "state": "generic_arg", + "detail": "generic_argument" + } + ], + "notes": [ + "class=Point", + "method=len", + "receiver=scalar_replaced", + "scalar_method_fallback=generic_arg" + ] + } + ] + }); + let report = build_report_from_artifacts( + Path::new("/tmp/lowering"), + vec![(PathBuf::from("native-reps.json"), artifact)], + ); + + assert_eq!(report.summary.scalar_replacements, 1); + assert_eq!(report.summary.scalar_replacement_fallbacks, 2); + assert_eq!(report.summary.scalar_replacement_rejections, 0); + assert_eq!( + report + .summary + .scalar_replacement_decision_counts + .get("selected"), + Some(&1) + ); + assert_eq!( + report + .summary + .scalar_replacement_decision_counts + .get("fallback"), + Some(&2) + ); + assert_eq!( + report + .summary + .scalar_replacement_reason_counts + .get("scalar_method_summary:exact_receiver_summary"), + Some(&1) + ); + assert_eq!( + report + .summary + .scalar_replacement_reason_counts + .get("scalar_method_materialized_fallback:guarded_numeric_args_fallback"), + Some(&1) + ); + assert_eq!( + report + .summary + .scalar_replacement_reason_counts + .get("scalar_method_materialized_fallback:generic_argument"), + Some(&1) + ); + assert_eq!( + report + .summary + .scalar_replacement_selection_reason_counts + .get("scalar_method_summary:exact_receiver_summary"), + Some(&1) + ); + assert_eq!( + report + .summary + .scalar_replacement_fallback_reason_counts + .get("scalar_method_materialized_fallback:guarded_numeric_args_fallback"), + Some(&1) + ); + assert_eq!( + report + .summary + .scalar_replacement_fallback_reason_counts + .get("scalar_method_materialized_fallback:generic_argument"), + Some(&1) + ); + assert_eq!( + report.evidence.scalar_replacements[0].decision.as_deref(), + Some("scalar_replacement_selected") + ); + assert_eq!( + report.evidence.scalar_replacements[0] + .reason_category + .as_deref(), + Some("scalar_method_summary:exact_receiver_summary") + ); + assert_eq!( + report.evidence.scalar_replacements[1].decision.as_deref(), + Some("scalar_replacement_fallback") + ); + assert_eq!( + report.evidence.scalar_replacements[1] + .reason_category + .as_deref(), + Some("scalar_method_materialized_fallback:guarded_numeric_args_fallback") + ); + assert_eq!( + report.evidence.scalar_replacements[2].decision.as_deref(), + Some("scalar_replacement_fallback") + ); + assert_eq!( + report.evidence.scalar_replacements[2] + .reason_category + .as_deref(), + Some("scalar_method_materialized_fallback:generic_argument") + ); + } } diff --git a/crates/perry/src/commands/compile/optimized_libs.rs b/crates/perry/src/commands/compile/optimized_libs.rs index 3ef74280f8..da9ff6c15e 100644 --- a/crates/perry/src/commands/compile/optimized_libs.rs +++ b/crates/perry/src/commands/compile/optimized_libs.rs @@ -815,7 +815,10 @@ pub(super) fn build_optimized_libs( // pulled only by them) — enabled only when the program uses a heap-snapshot // API or `process.report`. The env-driven GC/typed-feedback dev trace JSON // ride this feature and stay off in size-optimized binaries. - if ctx.uses_diagnostics { + let gc_trace_requested = std::env::var("PERRY_GC_TRACE") + .ok() + .is_some_and(|value| value == "1" || value.eq_ignore_ascii_case("true")); + if ctx.uses_diagnostics || gc_trace_requested { cross_features.push("perry-runtime/diagnostics".to_string()); } // Per-Node-module gating: `node:dgram`'s implementation + dispatch arm are diff --git a/scripts/check_runtime_symbols.sh b/scripts/check_runtime_symbols.sh index 402d868d28..438f571c09 100755 --- a/scripts/check_runtime_symbols.sh +++ b/scripts/check_runtime_symbols.sh @@ -65,6 +65,8 @@ SENTINELS=( js_typed_feedback_plain_array_index_get_guard js_typed_feedback_numeric_array_index_get_guard js_typed_feedback_packed_f64_array_loop_guard + js_typed_feedback_packed_i32_array_loop_guard + js_typed_feedback_packed_u32_array_loop_guard js_typed_feedback_array_index_get_fallback_boxed js_typed_feedback_array_set_f64 js_typed_feedback_array_set_f64_extend @@ -78,11 +80,37 @@ SENTINELS=( js_typed_feedback_object_set_index_polymorphic js_typed_feedback_object_set_unboxed_f64_field js_map_set_string_number + js_map_set_string_key + js_map_set_string_i32 + js_map_set_string_u32 + js_map_set_string_f32 + js_map_set_string_bool + js_map_set_string_string + js_map_set_number_key js_map_get_string_key + js_map_get_number_key js_map_has_string_key + js_map_has_number_key + js_map_delete_string_key + js_map_delete_number_key js_set_add_string + js_set_add_number js_set_has_string + js_set_has_number js_set_delete_string + js_set_delete_number + js_set_add_i32 + js_set_has_i32 + js_set_delete_i32 + js_set_add_u32 + js_set_has_u32 + js_set_delete_u32 + js_set_add_f32 + js_set_has_f32 + js_set_delete_f32 + js_set_add_bool + js_set_has_bool + js_set_delete_bool js_i32_box_alloc js_i32_box_get js_i32_box_set @@ -91,8 +119,12 @@ SENTINELS=( js_bool_box_set js_iter_result_set js_iter_result_set_f64 + js_iter_result_set_i32 + js_iter_result_set_i1 js_iter_result_get_value js_iter_result_get_value_f64 + js_iter_result_get_value_i32 + js_iter_result_get_value_i1 js_iter_result_get_done js_typed_feedback_native_call_method_by_id js_typed_feedback_native_call_method_apply_by_id diff --git a/scripts/compiler_output_harness/analyzers.py b/scripts/compiler_output_harness/analyzers.py index 6d003b5816..eee5151b68 100644 --- a/scripts/compiler_output_harness/analyzers.py +++ b/scripts/compiler_output_harness/analyzers.py @@ -10,6 +10,7 @@ from typing import Any from .common import ( + ARRAY_SLOW_PATH_HELPERS, BUFFER_SLOW_PATH_HELPERS, DYNAMIC_PROPERTY_HELPERS, RUNTIME_CALL_PREFIXES, @@ -221,12 +222,19 @@ def structural_counters(ir_before: str, ir_after: str, assembly: str) -> dict[st "runtime_calls": runtime_calls, "boxed_number_allocations": after_calls.get("js_boxed_number_new", 0), "write_barriers": after_calls.get("js_write_barrier", 0) - + after_calls.get("js_write_barrier_slot", 0), + + after_calls.get("js_write_barrier_slot", 0) + + after_calls.get("js_write_barrier_root_nanbox", 0) + + after_calls.get("js_write_barrier_root_heap_word", 0), "buffer_slow_path_calls": sum( count for name, count in after_calls.items() if any(helper in name for helper in BUFFER_SLOW_PATH_HELPERS) ), + "array_slow_path_calls": sum( + count + for name, count in after_calls.items() + if any(helper in name for helper in ARRAY_SLOW_PATH_HELPERS) + ), "dynamic_property_calls": sum( count for name, count in after_calls.items() @@ -409,6 +417,7 @@ def runtime_counter_summary( gc_collections = 0 traced_allocations = 0 traced_write_barriers = 0 + gc_trace_unavailable = False gc_trace_enabled: bool | None = None if benchmark is not None: if isinstance(benchmark.get("gc_trace_enabled"), bool): @@ -420,13 +429,17 @@ def runtime_counter_summary( row_trace_enabled if gc_trace_enabled is None else gc_trace_enabled and row_trace_enabled - ) + ) trace = row.get("gc_trace_summary", {}) + gc_trace_unavailable = gc_trace_unavailable or bool( + trace.get("diagnostics_disabled") + ) gc_collections += int(trace.get("gc_events", 0) or 0) traced_allocations += int(trace.get("malloc_kind_allocations", 0) or 0) traced_write_barriers += int(trace.get("write_barrier_calls", 0) or 0) return { "gc_trace_enabled": gc_trace_enabled, + "gc_trace_unavailable": gc_trace_unavailable, "runtime_calls_static": sum(int(v) for v in runtime_calls.values()), "runtime_call_names_static": runtime_calls, "allocations_traced": traced_allocations, @@ -439,13 +452,19 @@ def runtime_counter_summary( "buffer_slow_path_accesses_static": int( after.get("buffer_slow_path_calls", 0) or 0 ), + "array_slow_path_accesses_static": int( + after.get("array_slow_path_calls", 0) or 0 + ), } def summarize_gc_trace(stderr_text: str) -> dict[str, Any]: events = [] + diagnostics_disabled = False for line in stderr_text.splitlines(): line = line.strip() + if "diagnostics feature disabled" in line: + diagnostics_disabled = True if not line.startswith("{"): continue try: @@ -467,6 +486,7 @@ def summarize_gc_trace(stderr_text: str) -> dict[str, Any]: "gc_events": len(events), "write_barrier_calls": write_barrier_calls, "malloc_kind_allocations": allocations, + "diagnostics_disabled": diagnostics_disabled, } diff --git a/scripts/compiler_output_harness/capture.py b/scripts/compiler_output_harness/capture.py index c57bda9063..5dd3fb9067 100644 --- a/scripts/compiler_output_harness/capture.py +++ b/scripts/compiler_output_harness/capture.py @@ -33,7 +33,7 @@ write_text, ) from .spec import WORKLOADS -from .verification import verify_artifacts +from .verification import TRACE_RUNTIME_BUDGET_FIELDS, verify_artifacts SUITES: dict[str, list[str]] = { @@ -114,9 +114,11 @@ def resolve_benchmark_runs(args: argparse.Namespace) -> int: return runs -def _compile_env(clang: str) -> dict[str, str]: +def _compile_env(clang: str, *, enable_gc_trace: bool = False) -> dict[str, str]: env = {**os.environ, "PERRY_LLVM_KEEP_IR": "1", "PERRY_NO_CACHE": "1"} env["PERRY_LLVM_CLANG"] = clang + if enable_gc_trace: + env["PERRY_GC_TRACE"] = "1" return env @@ -198,6 +200,10 @@ def capture(args: argparse.Namespace) -> int: clang = resolve_clang(args.clang) analysis_extra_clang_args = list(args.clang_arg or []) runs = resolve_benchmark_runs(args) + trace_budget_fields = set( + workload_info.get("runtime_budgets", {}) + ).intersection(TRACE_RUNTIME_BUDGET_FIELDS) + compile_gc_trace = bool(trace_budget_fields and not args.no_gc_trace) binary = (out_dir / args.workload).resolve() commands: dict[str, Any] = {} @@ -217,7 +223,7 @@ def capture(args: argparse.Namespace) -> int: commands["hir"] = run_command( hir_cmd, cwd=out_dir, - env=_compile_env(clang), + env=_compile_env(clang, enable_gc_trace=compile_gc_trace), timeout=args.compile_timeout, stdout_path=hir_stdout, stderr_path=hir_stderr, @@ -236,7 +242,7 @@ def capture(args: argparse.Namespace) -> int: commands["compile"] = run_command( compile_cmd, cwd=out_dir, - env=_compile_env(clang), + env=_compile_env(clang, enable_gc_trace=compile_gc_trace), timeout=args.compile_timeout, stdout_path=compile_stdout, stderr_path=compile_stderr, @@ -479,7 +485,7 @@ def capture_suite(args: argparse.Namespace) -> int: per_workload = copy.copy(args) per_workload.workload = workload per_workload.out_dir = str(workload_out) - per_workload.gate = False + per_workload.gate = bool(args.gate) per_workload.print_summary = False per_workload.verify_native_regions = True try: diff --git a/scripts/compiler_output_harness/common.py b/scripts/compiler_output_harness/common.py index 1b413c0aa1..78e8cd91f9 100644 --- a/scripts/compiler_output_harness/common.py +++ b/scripts/compiler_output_harness/common.py @@ -37,6 +37,13 @@ BUFFER_SLOW_PATH_HELPERS = ( "js_buffer_get", "js_buffer_set", + # Buffer byte indexing currently lowers through the Uint8Array helper + # surface, so count those helpers for the Buffer-byte material gate too. + "js_uint8array_get", + "js_uint8array_set", +) + +ARRAY_SLOW_PATH_HELPERS = ( "js_typed_array_get", "js_typed_array_set", "js_uint8array_get", diff --git a/scripts/compiler_output_harness/verification.py b/scripts/compiler_output_harness/verification.py index 5da2c54d34..5642020a48 100644 --- a/scripts/compiler_output_harness/verification.py +++ b/scripts/compiler_output_harness/verification.py @@ -118,6 +118,19 @@ def runtime_budget_results( ), } ) + if trace_budget_fields and runtime_summary.get("gc_trace_unavailable") is True: + results.append( + { + "field": "gc_trace_unavailable", + "actual": 1, + "maximum": 0, + "passed": False, + "detail": ( + "PERRY_GC_TRACE was requested, but the linked runtime " + "reported diagnostics feature disabled" + ), + } + ) for field, maximum in sorted(budgets.items()): actual = int(runtime_summary.get(field, 0) or 0) results.append( diff --git a/scripts/native_abi_evidence_packet.sh b/scripts/native_abi_evidence_packet.sh index 37341b0b60..accc4b2286 100755 --- a/scripts/native_abi_evidence_packet.sh +++ b/scripts/native_abi_evidence_packet.sh @@ -38,12 +38,40 @@ if ! [[ "$RUNS" =~ ^[0-9]+$ ]] || [[ "$RUNS" -lt 1 ]]; then exit 2 fi +if [[ "$GATE" -eq 1 && "$RUNS" -lt 5 ]]; then + echo "--gate requires --runs >= 5 so p95 packet timing evidence is meaningful" >&2 + exit 2 +fi + if [[ -z "$OUT" ]]; then OUT="tmp/native-abi-evidence-$(date -u +%Y%m%dT%H%M%SZ)" fi cd "$ROOT" +ORIGINAL_RUSTC_WRAPPER="${RUSTC_WRAPPER:-}" +ORIGINAL_RUSTFLAGS="${RUSTFLAGS:-}" +CARGO_CONFIG_RUSTC_WRAPPER="" +for cargo_config in "${CARGO_HOME:-$HOME/.cargo}/config.toml" "$ROOT/.cargo/config.toml"; do + if [[ -f "$cargo_config" ]]; then + cargo_config_text="$(tr -d '[:space:]' < "$cargo_config")" + if [[ "$cargo_config_text" == *"rustc-wrapper="* ]]; then + CARGO_CONFIG_RUSTC_WRAPPER="configured" + break + fi + fi +done +SCRUBBED_RUSTC_WRAPPER=0 +if [[ "${PERRY_EVIDENCE_KEEP_RUSTC_WRAPPER:-0}" != "1" && ( -n "$ORIGINAL_RUSTC_WRAPPER" || -n "$CARGO_CONFIG_RUSTC_WRAPPER" ) ]]; then + export RUSTC_WRAPPER="" + SCRUBBED_RUSTC_WRAPPER=1 +fi +if [[ -n "${PERRY_EVIDENCE_RUSTFLAGS:-}" ]]; then + export RUSTFLAGS="$PERRY_EVIDENCE_RUSTFLAGS" +elif [[ -z "$ORIGINAL_RUSTFLAGS" ]]; then + export RUSTFLAGS="-Awarnings" +fi + PYTHON_BIN="${PYTHON:-}" if [[ -z "$PYTHON_BIN" ]]; then if command -v python3.11 >/dev/null 2>&1; then @@ -90,8 +118,9 @@ mkdir -p "$OUT_ABS/logs" METADATA="$OUT_ABS/metadata.json" write_metadata() { - "$PYTHON_BIN" - "$METADATA" "$RUNS" "$GATE" "$PYTHON_BIN" <<'PY' + "$PYTHON_BIN" - "$METADATA" "$RUNS" "$GATE" "$PYTHON_BIN" "$ORIGINAL_RUSTC_WRAPPER" "$CARGO_CONFIG_RUSTC_WRAPPER" "$SCRUBBED_RUSTC_WRAPPER" "${RUSTC_WRAPPER:-}" "$ORIGINAL_RUSTFLAGS" "${RUSTFLAGS:-}" <<'PY' import json +import os import sys from datetime import datetime, timezone from pathlib import Path @@ -107,6 +136,17 @@ existing.update({ "gate": sys.argv[3] == "1", "python": sys.argv[4], "commands": existing.get("commands", {}), + "environment": { + **existing.get("environment", {}), + "rustc_wrapper_original": sys.argv[5], + "rustc_wrapper_config": sys.argv[6], + "rustc_wrapper_scrubbed": sys.argv[7] == "1", + "rustc_wrapper_effective": sys.argv[8], + "rustflags_original": sys.argv[9], + "rustflags_effective": sys.argv[10], + "keep_rustc_wrapper": os.environ.get("PERRY_EVIDENCE_KEEP_RUSTC_WRAPPER") == "1", + "rustflags_override": os.environ.get("PERRY_EVIDENCE_RUSTFLAGS", ""), + }, "tool_versions": existing.get("tool_versions", {}), }) path.write_text(json.dumps(existing, indent=2, sort_keys=True) + "\n", encoding="utf-8") @@ -223,6 +263,39 @@ resolve_perry() { printf '%s\n' "$ROOT/target/debug/perry" } +resolve_runtime_archive() { + local perry_bin="$1" + local dir + dir="$(dirname "$perry_bin")" + for candidate in "$dir/libperry_runtime.a" "$dir/perry_runtime.lib"; do + if [[ -f "$candidate" ]]; then + printf '%s\n' "$candidate" + return + fi + done + printf '%s\n' "$dir/libperry_runtime.a" +} + +snapshot_tool_artifacts() { + local perry_bin="$1" + local runtime_archive="$2" + local tools_dir="$OUT_ABS/tools" + mkdir -p "$tools_dir" + + if [[ -x "$perry_bin" ]]; then + cp "$perry_bin" "$tools_dir/perry" + chmod +x "$tools_dir/perry" + PERRY_BIN_RESOLVED="$tools_dir/perry" + fi + + if [[ -f "$runtime_archive" ]]; then + local runtime_name + runtime_name="$(basename "$runtime_archive")" + cp "$runtime_archive" "$tools_dir/$runtime_name" + RUNTIME_ARCHIVE_RESOLVED="$tools_dir/$runtime_name" + fi +} + write_metadata capture_tool_versions @@ -232,10 +305,17 @@ echo "runs: $RUNS" echo "python: $PYTHON_BIN" PERRY_BIN_RESOLVED="$(resolve_perry)" -if [[ ! -x "$PERRY_BIN_RESOLVED" ]]; then +if [[ -z "$PERRY_ARG" ]]; then + packet_build=(cargo build -p perry -p perry-runtime) + case "$PERRY_BIN_RESOLVED" in + "$ROOT/target/release/"*) packet_build=(cargo build --release -p perry -p perry-runtime) ;; + esac + run_logged "packet" "build" "$OUT_ABS/logs/build.log" "${packet_build[@]}" + PERRY_BIN_RESOLVED="$(resolve_perry)" +elif [[ ! -x "$PERRY_BIN_RESOLVED" ]]; then run_logged "packet" "build" "$OUT_ABS/logs/build.log" cargo build -p perry else - record_command "packet" "build" "skipped" 0 "" "using existing Perry binary" + record_command "packet" "build" "skipped" 0 "" "using explicit Perry binary" fi if [[ ! -x "$PERRY_BIN_RESOLVED" ]]; then @@ -244,17 +324,63 @@ else record_command "packet" "resolve_perry" "pass" 0 "" "$PERRY_BIN_RESOLVED" fi -"$PYTHON_BIN" - "$METADATA" "$PERRY_BIN_RESOLVED" <<'PY' +RUNTIME_ARCHIVE_RESOLVED="$(resolve_runtime_archive "$PERRY_BIN_RESOLVED")" +if [[ -z "$PERRY_ARG" && -f "$RUNTIME_ARCHIVE_RESOLVED" ]]; then + record_command "packet" "build_runtime_archive" "skipped" 0 "" "built by packet build" +elif [[ ! -f "$RUNTIME_ARCHIVE_RESOLVED" ]]; then + runtime_build=(cargo build -p perry-runtime) + case "$PERRY_BIN_RESOLVED" in + "$ROOT/target/release/"*) runtime_build=(cargo build --release -p perry-runtime) ;; + esac + run_logged "packet" "build_runtime_archive" "$OUT_ABS/logs/build-runtime-archive.log" \ + "${runtime_build[@]}" + RUNTIME_ARCHIVE_RESOLVED="$(resolve_runtime_archive "$PERRY_BIN_RESOLVED")" +else + record_command "packet" "build_runtime_archive" "skipped" 0 "" "using existing runtime archive" +fi + +snapshot_tool_artifacts "$PERRY_BIN_RESOLVED" "$RUNTIME_ARCHIVE_RESOLVED" + +"$PYTHON_BIN" - "$METADATA" "$PERRY_BIN_RESOLVED" "$RUNTIME_ARCHIVE_RESOLVED" <<'PY' +import hashlib import json import sys from pathlib import Path path = Path(sys.argv[1]) +repo = Path.cwd() data = json.loads(path.read_text(encoding="utf-8")) data["perry"] = sys.argv[2] +data["runtime_archive"] = sys.argv[3] +archive = Path(sys.argv[3]) +if archive.exists(): + data["runtime_archive_sha256"] = hashlib.sha256(archive.read_bytes()).hexdigest() + +runtime_inputs = [] +for base in (repo / "crates" / "perry-runtime" / "src",): + runtime_inputs.extend(sorted(p for p in base.rglob("*") if p.is_file())) +for extra in ( + repo / "crates" / "perry-runtime" / "Cargo.toml", + repo / "crates" / "perry-runtime" / "build.rs", + repo / "scripts" / "check_runtime_symbols.sh", +): + if extra.exists(): + runtime_inputs.append(extra) +digest = hashlib.sha256() +for source in sorted(set(runtime_inputs)): + rel = source.relative_to(repo).as_posix() + digest.update(rel.encode("utf-8")) + digest.update(b"\0") + digest.update(hashlib.sha256(source.read_bytes()).digest()) + digest.update(b"\0") +data["runtime_source_digest"] = digest.hexdigest() +data["runtime_source_digest_inputs"] = len(set(runtime_inputs)) path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8") PY +run_logged "release" "runtime_symbols" "$OUT_ABS/logs/runtime-symbols.log" \ + bash scripts/check_runtime_symbols.sh "$RUNTIME_ARCHIVE_RESOLVED" + run_logged "correctness" "native_abi_contract" "$OUT_ABS/correctness/native-abi-contract/command.log" \ env "PERRY=$PERRY_BIN_RESOLVED" "PERRY_NATIVE_ABI_EVIDENCE_DIR=$OUT_ABS/correctness/native-abi-contract" \ bash tests/test_native_abi_contract.sh diff --git a/scripts/native_abi_evidence_report.py b/scripts/native_abi_evidence_report.py index 170ef7b9a1..6db4ffc7df 100755 --- a/scripts/native_abi_evidence_report.py +++ b/scripts/native_abi_evidence_report.py @@ -5,6 +5,7 @@ import argparse import json +import re from datetime import datetime, timezone from pathlib import Path from typing import Any, Optional @@ -23,6 +24,39 @@ ), } +GATE_MATRIX_SPEC = ( + { + "area": "native_abi_correctness", + "label": "Native ABI correctness", + "evidence": "native_abi_contract and C-layout POD fixtures", + "gate": "runtime PASS output plus required native-rep ABI tokens", + }, + { + "area": "native_region_artifacts", + "label": "Native-region artifact chain", + "evidence": "native-abi-proof compiler-output retained HIR/LLVM/object/native-rep artifacts", + "gate": "required artifacts, structural safety checks, checksum checks, and packet contracts", + }, + { + "area": "explain_lowering_accounting", + "label": "Explain-lowering accounting", + "evidence": "native-rep records summarized into boxes, conversions, fallbacks, barriers, and typed records", + "gate": "typed/control material accounting rows must pass quantitative thresholds", + }, + { + "area": "runtime_safety", + "label": "Runtime safety", + "evidence": "native async runtime tests and GC/rooting checks", + "gate": "required runtime test names must pass and be present in logs", + }, + { + "area": "release_symbols", + "label": "Release/LTO symbol guard", + "evidence": "runtime archive symbol sentinel scan", + "gate": "archive must define all sentinel symbols", + }, +) + REQUIRED_CORRECTNESS = { "native_abi_contract": { "label": "Selected native ABI contract", @@ -62,6 +96,16 @@ "test_native_async_completion_token_roots_survive_copied_minor_gc", ) +REQUIRED_RELEASE_SYMBOL_TOKENS = ( + "defines all", + "sentinel symbols", +) +REQUIRED_RELEASE_SENTINEL_COUNT = 101 +REQUIRED_RELEASE_FINGERPRINT_FIELDS = ( + "runtime_archive_sha256", + "runtime_source_digest", +) + REQUIRED_COMPILER_ARTIFACTS = ( "hir", "llvm_before_opt", @@ -77,20 +121,170 @@ "native_reps_no_unexpected_materialization_reasons", ) +REQUIRED_PACKET_STDOUT_CHECKS = { + "native_abi_packet_typed": "native_abi_packet_typed_checksum", + "native_abi_packet_control": "native_abi_packet_control_checksum", +} + DELTA_FIELDS = ( "boxed_number_allocations_static", "buffer_slow_path_accesses_static", + "array_slow_path_accesses_static", "allocations_traced", "write_barriers_static", + "write_barriers_traced", "runtime_calls_static", ) REQUIRED_IMPROVEMENT_FIELDS = ( "boxed_number_allocations_static", "buffer_slow_path_accesses_static", + "array_slow_path_accesses_static", "allocations_traced", ) +MATERIAL_REDUCTION_THRESHOLDS = { + "allocations_traced": 95.0, + "write_barriers_static": 75.0, + "write_barriers_traced": 95.0, + "runtime_calls_static": 25.0, +} + +MATERIAL_ELIMINATION_FIELDS = ( + "boxed_number_allocations_static", + "buffer_slow_path_accesses_static", + "array_slow_path_accesses_static", +) + +MATERIAL_SPEEDUP_THRESHOLDS = { + "median_wall_ms": 2.0, + "p95_wall_ms": 1.5, +} + +MATERIAL_REQUIRED_STAT_QUALITY = "timing" + +MATERIAL_ACCOUNTING_CONTRACT = ( + { + "field": "boxed_number_allocations_static", + "category": "boxes", + "source": "optimized IR helper counter", + "typed_max": 0, + "control_min": 1, + "reduction_min": 100.0, + "proves": "typed packet avoids boxed Number allocation helpers", + }, + { + "field": "buffer_slow_path_accesses_static", + "category": "helpers", + "source": "optimized IR helper counter", + "typed_max": 0, + "control_min": 1, + "reduction_min": 100.0, + "proves": "typed packet avoids Buffer slow-path helpers", + }, + { + "field": "array_slow_path_accesses_static", + "category": "helpers", + "source": "optimized IR helper counter", + "typed_max": 0, + "control_min": 1, + "reduction_min": 100.0, + "proves": "typed packet avoids typed-array/Uint8Array slow-path helpers", + }, + { + "field": "runtime_calls_static", + "category": "helpers", + "source": "optimized IR runtime-call counter", + "control_min": 1, + "reduction_min": MATERIAL_REDUCTION_THRESHOLDS["runtime_calls_static"], + "proves": "typed packet removes representative runtime helper call sites", + }, + { + "field": "allocations_traced", + "category": "allocations", + "source": "GC trace allocation counter", + "control_min": 1, + "reduction_min": MATERIAL_REDUCTION_THRESHOLDS["allocations_traced"], + "proves": "typed packet removes representative traced runtime allocations", + }, + { + "field": "write_barriers_static", + "category": "barriers", + "source": "optimized IR write-barrier counter", + "control_min": 1, + "reduction_min": MATERIAL_REDUCTION_THRESHOLDS["write_barriers_static"], + "proves": "typed packet removes representative static write-barrier helper sites", + }, + { + "field": "write_barriers_traced", + "category": "barriers", + "source": "GC trace write-barrier counter", + "control_min": 1, + "reduction_min": MATERIAL_REDUCTION_THRESHOLDS["write_barriers_traced"], + "proves": "typed packet removes representative runtime write-barrier traffic", + }, + { + "field": "median_wall_ms", + "category": "benchmark", + "source": "packet timing", + "speedup_min": MATERIAL_SPEEDUP_THRESHOLDS["median_wall_ms"], + "proves": "typed packet has material median wall-time speedup", + }, + { + "field": "p95_wall_ms", + "category": "benchmark", + "source": "packet timing", + "speedup_min": MATERIAL_SPEEDUP_THRESHOLDS["p95_wall_ms"], + "proves": "typed packet keeps tail latency materially faster", + }, +) + +MATERIAL_CONTRACTS = { + "reductions": MATERIAL_REDUCTION_THRESHOLDS, + "eliminations": {field: 0 for field in MATERIAL_ELIMINATION_FIELDS}, + "speedups": MATERIAL_SPEEDUP_THRESHOLDS, + "stat_quality": MATERIAL_REQUIRED_STAT_QUALITY, +} + +PACKET_WORKLOAD_CONTRACTS: dict[str, dict[str, Any]] = { + "native_abi_packet_typed": { + "source": "benchmarks/compiler_output/fixtures/native_abi_packet_typed.ts", + "kind": "native_abi_packet_typed", + "zero_static_fields": ( + "boxed_number_allocations_static", + "buffer_slow_path_accesses_static", + "array_slow_path_accesses_static", + ), + "required_native_records": ( + { + "name": "typed_unchecked_buffer_view", + "native_rep_name": "buffer_view", + "consumer_contains": "BufferView", + "access_mode": "unchecked_native", + "bounds_state": "proven_or_guarded", + }, + { + "name": "typed_unchecked_u8_access", + "native_rep_name": "u8", + "consumer_contains": "u8_", + "access_mode": "unchecked_native", + "bounds_state": "proven_or_guarded", + }, + ), + }, + "native_abi_packet_control": { + "source": "benchmarks/compiler_output/fixtures/native_abi_packet_control.ts", + "kind": "native_abi_packet_control", + "positive_static_fields": ( + "boxed_number_allocations_static", + "buffer_slow_path_accesses_static", + "array_slow_path_accesses_static", + "write_barriers_static", + "runtime_calls_static", + ), + }, +} + def utc_now() -> str: return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") @@ -183,16 +377,38 @@ def rel(path: Path, root: Path) -> str: def ratio_delta(control: Optional[float], typed: Optional[float]) -> dict[str, Any]: if control is None or typed is None: - return {"control": control, "typed": typed, "delta": None, "delta_pct": None} - pct = None if control == 0 else ((typed - control) / control) * 100.0 + return { + "control": control, + "typed": typed, + "delta": None, + "delta_pct": None, + "reduction_pct": None, + "speedup": None, + } + delta = typed - control + pct = None if control == 0 else (delta / control) * 100.0 + reduction_pct = None if control == 0 else ((control - typed) / control) * 100.0 + speedup = None if typed <= 0 else control / typed return { "control": control, "typed": typed, - "delta": typed - control, + "delta": delta, "delta_pct": None if pct is None else round(pct, 1), + "reduction_pct": None if reduction_pct is None else round(reduction_pct, 1), + "speedup": None if speedup is None else round(speedup, 3), } +def release_sentinel_counts(log: str) -> list[int]: + counts: list[int] = [] + for match in re.finditer(r"defines all\s+(\d+)\s+sentinel symbols", log): + try: + counts.append(int(match.group(1))) + except ValueError: + continue + return counts + + def native_reps_text(evidence_dir: Path) -> str: text = read_text(evidence_dir / "native-reps.txt") if text: @@ -280,6 +496,286 @@ def native_reps_ok(manifest: dict[str, Any], artifact_root: Path) -> bool: ) +def state_name(value: Any) -> str: + if value is None: + return "" + if isinstance(value, str): + return value + if isinstance(value, dict) and value: + return str(next(iter(value.keys()))) + return str(value) + + +def bounds_allows_inbounds(value: Any) -> bool: + return state_name(value) in {"proven", "guarded"} + + +def native_rep_records( + manifest: dict[str, Any], + artifact_root: Path, +) -> tuple[list[dict[str, Any]], int]: + retained = nested(manifest, "artifacts", "native_reps", default=[]) + if not isinstance(retained, list): + return ([], 0) + records: list[dict[str, Any]] = [] + artifact_count = 0 + for row in retained: + if not isinstance(row, dict): + continue + path = resolve_path(row.get("native_reps_artifact"), artifact_root) + if not path or not path.exists(): + continue + artifact_count += 1 + artifact = load_json(path, {}) + artifact_records = artifact.get("records", []) if isinstance(artifact, dict) else [] + for record in artifact_records: + if isinstance(record, dict): + records.append(record) + return (records, artifact_count) + + +def packet_record_matches(record: dict[str, Any], required: dict[str, Any]) -> bool: + if "native_rep_name" in required and state_name(record.get("native_rep_name")) != str( + required["native_rep_name"] + ): + return False + if "native_value_state" in required and state_name(record.get("native_value_state")) != str( + required["native_value_state"] + ): + return False + if "consumer_contains" in required and str(required["consumer_contains"]) not in str( + record.get("consumer") or "" + ): + return False + if "access_mode" in required and state_name(record.get("access_mode")) != str( + required["access_mode"] + ): + return False + if "materialization_reason" in required and state_name( + record.get("materialization_reason") + ) != str(required["materialization_reason"]): + return False + if "fallback_reason" in required and state_name(record.get("fallback_reason")) != str( + required["fallback_reason"] + ): + return False + if required.get("bounds_state") == "proven_or_guarded" and not bounds_allows_inbounds( + record.get("bounds_state") + ): + return False + return True + + +def count_key(counts: dict[str, int], value: Any) -> None: + key = state_name(value) + if key: + counts[key] = counts.get(key, 0) + 1 + + +def record_notes(record: dict[str, Any]) -> list[str]: + notes = record.get("notes", []) + if not isinstance(notes, list): + return [] + return [str(note) for note in notes if isinstance(note, str)] + + +def transition_op(record: dict[str, Any], field: str) -> str: + value = record.get(field) + if not isinstance(value, dict): + return "" + return state_name(value.get("op")) + + +def transition_to_rep(record: dict[str, Any]) -> str: + value = record.get("native_abi_transition") + if not isinstance(value, dict): + return "" + return state_name(value.get("to_native_rep")) + + +def is_unbox_or_coercion_op(op: str) -> bool: + return op in { + "js_value_to_bits", + "bits_to_js_value", + "signed_int_to_float", + "unsigned_int_to_float", + "float_extend", + } + + +def explain_lowering_accounting( + records: list[dict[str, Any]], + runtime_summary: Any, +) -> dict[str, Any]: + native_rep_counts: dict[str, int] = {} + native_value_state_counts: dict[str, int] = {} + access_mode_counts: dict[str, int] = {} + materialization_reason_counts: dict[str, int] = {} + fallback_reason_counts: dict[str, int] = {} + boxes = 0 + unboxes_or_coercions = 0 + dynamic_fallbacks = 0 + barrier_eliminations = 0 + barrier_emissions = 0 + typed_native_records = 0 + js_value_bits_records = 0 + + for record in records: + native_rep = state_name(record.get("native_rep_name")) + native_value_state = state_name(record.get("native_value_state")) + access_mode = state_name(record.get("access_mode")) + materialization_reason = state_name(record.get("materialization_reason")) + fallback_reason = state_name(record.get("fallback_reason")) + notes = record_notes(record) + notes_text = ";".join(notes) + consumer = str(record.get("consumer") or "") + expr_kind = str(record.get("expr_kind") or "") + + count_key(native_rep_counts, native_rep) + count_key(native_value_state_counts, native_value_state) + count_key(access_mode_counts, access_mode) + count_key(materialization_reason_counts, materialization_reason) + count_key(fallback_reason_counts, fallback_reason) + + if native_rep and native_rep != "js_value": + typed_native_records += 1 + if native_rep == "js_value_bits": + js_value_bits_records += 1 + + if materialization_reason or transition_to_rep(record) == "js_value": + boxes += 1 + for op in ( + transition_op(record, "native_abi_transition"), + transition_op(record, "scalar_conversion"), + ): + if is_unbox_or_coercion_op(op): + unboxes_or_coercions += 1 + + if access_mode == "dynamic_fallback" or native_value_state == "dynamic_fallback" or fallback_reason: + dynamic_fallbacks += 1 + + if ( + "barrier=elided" in notes_text + or "barrier_eliminated" in notes_text + or "write_barrier=0" in notes_text + or "without_barrier" in notes_text + ): + barrier_eliminations += 1 + elif ( + "barrier=emitted" in notes_text + or "write_barrier=1" in notes_text + or consumer == "write_barrier.child_bits" + or "write_barrier_slot" in consumer + or "write_barrier_root" in consumer + or expr_kind == "WriteBarrier" + ): + barrier_emissions += 1 + + summary = runtime_summary if isinstance(runtime_summary, dict) else {} + return { + "record_count": len(records), + "typed_native_records": typed_native_records, + "js_value_bits_records": js_value_bits_records, + "boxes_inserted": boxes, + "unboxes_or_coercions": unboxes_or_coercions, + "dynamic_fallbacks": dynamic_fallbacks, + "barrier_eliminations": barrier_eliminations, + "barrier_emissions": barrier_emissions, + "native_rep_counts": dict(sorted(native_rep_counts.items())), + "native_value_state_counts": dict(sorted(native_value_state_counts.items())), + "access_mode_counts": dict(sorted(access_mode_counts.items())), + "materialization_reason_counts": dict(sorted(materialization_reason_counts.items())), + "fallback_reason_counts": dict(sorted(fallback_reason_counts.items())), + "runtime_counter_summary": { + field: int_value(summary.get(field)) + for field in DELTA_FIELDS + if field in summary + }, + } + + +def packet_workload_contract( + workload: str, + manifest: dict[str, Any], + artifact_root: Path, +) -> dict[str, Any]: + contract = PACKET_WORKLOAD_CONTRACTS.get(workload) + if not contract: + return {"status": "skipped"} + + errors: list[str] = [] + if manifest.get("workload") != workload: + errors.append( + f"manifest workload must be {workload!r} (got {manifest.get('workload')!r})" + ) + expected_kind = contract["kind"] + if manifest.get("workload_kind") != expected_kind: + errors.append( + f"manifest workload_kind must be {expected_kind!r} " + f"(got {manifest.get('workload_kind')!r})" + ) + expected_source = contract["source"] + if manifest.get("source") != expected_source: + errors.append( + f"manifest source must be {expected_source!r} (got {manifest.get('source')!r})" + ) + + summary = manifest.get("runtime_counter_summary", {}) + if not isinstance(summary, dict): + summary = {} + static_counter_checks: list[dict[str, Any]] = [] + for field in contract.get("zero_static_fields", ()) or (): + value = number_value(summary.get(field)) + passed = value == 0 + static_counter_checks.append( + {"field": field, "expected": "zero", "actual": value, "passed": passed} + ) + if not passed: + errors.append(f"{field} must be zero for {workload} (got {value})") + for field in contract.get("positive_static_fields", ()) or (): + value = number_value(summary.get(field)) + passed = value is not None and value > 0 + static_counter_checks.append( + {"field": field, "expected": "positive", "actual": value, "passed": passed} + ) + if not passed: + errors.append(f"{field} must be positive for {workload} (got {value})") + + records, artifact_count = native_rep_records(manifest, artifact_root) + required_record_checks: list[dict[str, Any]] = [] + for required in contract.get("required_native_records", ()) or (): + matches = [record for record in records if packet_record_matches(record, required)] + min_count = int(required.get("min", 1) or 1) + passed = len(matches) >= min_count + required_record_checks.append( + { + "name": required.get("name", "native_record"), + "required": required, + "matches": len(matches), + "min": min_count, + "passed": passed, + } + ) + if not passed: + errors.append( + f"required native-rep record {required.get('name', 'native_record')!r} " + f"matched {len(matches)} records, expected at least {min_count}" + ) + + return { + "status": "fail" if errors else "pass", + "expected_source": expected_source, + "expected_kind": expected_kind, + "manifest_source": manifest.get("source"), + "manifest_workload_kind": manifest.get("workload_kind"), + "native_rep_artifacts": artifact_count, + "native_rep_records": len(records), + "static_counter_checks": static_counter_checks, + "required_native_records": required_record_checks, + "errors": errors, + } + + def compiler_output_summary( root: Path, metadata: dict[str, Any], @@ -332,11 +828,36 @@ def compiler_output_summary( failing_safety = [ check for check in safety_checks if check.get("status") != "pass" ] + required_stdout_name = REQUIRED_PACKET_STDOUT_CHECKS.get(name) + stdout_checks = [] + missing_stdout_checks = [] + failing_stdout_checks = [] + if required_stdout_name: + stdout_checks = [ + check + for check in structural.get("checks", []) or [] + if isinstance(check, dict) and check.get("name") == required_stdout_name + ] + failing_stdout_checks = [ + check for check in stdout_checks if check.get("status") != "pass" + ] + if not stdout_checks: + missing_stdout_checks.append(required_stdout_name) + packet_contract = packet_workload_contract(name, manifest, artifact_dir) workload_status = "pass" if row.get("status") != "pass" or structural.get("status") != "pass": workload_status = "fail" if missing_artifacts or failing_safety: workload_status = "fail" + if missing_stdout_checks or failing_stdout_checks: + workload_status = "fail" + if packet_contract.get("status") == "fail": + workload_status = "fail" + workload_errors = list(row.get("errors") or []) + list(structural.get("errors") or []) + if packet_contract.get("status") == "fail": + workload_errors.extend( + f"packet_contract: {error}" for error in packet_contract.get("errors", []) + ) workloads[name] = { "status": workload_status, "suite_status": row.get("status"), @@ -347,12 +868,23 @@ def compiler_output_summary( "missing_artifacts": missing_artifacts, "safety_checks": safety_checks, "failing_safety_checks": failing_safety, + "stdout_checks": stdout_checks, + "missing_stdout_checks": missing_stdout_checks, + "failing_stdout_checks": failing_stdout_checks, + "packet_contract": packet_contract, + "explain_lowering_accounting": explain_lowering_accounting( + native_rep_records(manifest, artifact_dir)[0], + manifest.get("runtime_counter_summary", {}), + ), "runtime_counter_summary": manifest.get("runtime_counter_summary", {}), "benchmark": manifest.get("benchmark", {}), - "errors": list(row.get("errors") or []) + list(structural.get("errors") or []), + "errors": workload_errors, } if gate and workload_status != "pass": - errors.append(f"compiler-output:{name}: {workload_status}; {workloads[name]['errors'] or missing_artifacts}") + errors.append( + f"compiler-output:{name}: {workload_status}; " + f"{workloads[name]['errors'] or missing_artifacts or missing_stdout_checks or failing_stdout_checks}" + ) required = {"native_abi_packet_typed", "native_abi_packet_control"} missing_required = sorted(required - set(workloads)) @@ -368,6 +900,83 @@ def compiler_output_summary( } +def material_accounting_rows(fields: dict[str, dict[str, Any]]) -> list[dict[str, Any]]: + rows: list[dict[str, Any]] = [] + for spec in MATERIAL_ACCOUNTING_CONTRACT: + field = str(spec["field"]) + delta = fields.get(field, {}) + control = delta.get("control") + typed = delta.get("typed") + failures: list[str] = [] + thresholds: dict[str, Any] = {} + + if control is None or typed is None: + failures.append("control and typed values are required") + else: + if "control_min" in spec: + control_min = float(spec["control_min"]) + thresholds["control_min"] = control_min + if control < control_min: + failures.append( + f"control baseline must be >= {control_min:g} (observed={control})" + ) + if "typed_max" in spec: + typed_max = float(spec["typed_max"]) + thresholds["typed_max"] = typed_max + if typed > typed_max: + failures.append( + f"typed value must be <= {typed_max:g} (observed={typed})" + ) + if "reduction_min" in spec: + reduction_min = float(spec["reduction_min"]) + thresholds["reduction_min_pct"] = reduction_min + if control <= 0: + failures.append( + f"positive control baseline required for reduction (control={control})" + ) + else: + raw_reduction = ((control - typed) / control) * 100.0 + if raw_reduction < reduction_min: + failures.append( + f"reduction must be >= {reduction_min:g}% " + f"(observed={raw_reduction:.1f}%)" + ) + if "speedup_min" in spec: + speedup_min = float(spec["speedup_min"]) + thresholds["speedup_min"] = speedup_min + if control <= 0 or typed <= 0: + failures.append( + "positive timing values required for speedup " + f"(control={control}, typed={typed})" + ) + else: + raw_speedup = control / typed + if raw_speedup < speedup_min: + failures.append( + f"speedup must be >= {speedup_min:g}x " + f"(observed={raw_speedup:.3f}x)" + ) + + rows.append( + { + "field": field, + "category": spec["category"], + "source": spec["source"], + "proves": spec["proves"], + "status": "fail" if failures else "pass", + "thresholds": thresholds, + "control": control, + "typed": typed, + "delta": delta.get("delta"), + "delta_pct": delta.get("delta_pct"), + "reduction_pct": delta.get("reduction_pct"), + "speedup": delta.get("speedup"), + "failures": failures, + } + ) + return rows + + def benchmark_deltas(compiler: dict[str, Any], errors: list[str], *, gate: bool) -> dict[str, Any]: workloads = compiler.get("workloads", {}) typed = workloads.get("native_abi_packet_typed", {}) @@ -394,9 +1003,90 @@ def benchmark_deltas(compiler: dict[str, Any], errors: list[str], *, gate: bool) number_value(nested(control, "benchmark", "mean_wall_ms")), number_value(nested(typed, "benchmark", "mean_wall_ms")), ) + fields["p95_wall_ms"] = ratio_delta( + number_value(nested(control, "benchmark", "p95_wall_ms")), + number_value(nested(typed, "benchmark", "p95_wall_ms")), + ) missing = [name for name, delta in fields.items() if delta["typed"] is None or delta["control"] is None] if gate and missing: errors.append(f"benchmark deltas missing values: {missing}") + + material_failures: list[str] = [] + material_passes: list[str] = [] + benchmark_stat_quality = { + "typed": nested(typed, "benchmark", "stat_quality"), + "control": nested(control, "benchmark", "stat_quality"), + } + for role, quality in benchmark_stat_quality.items(): + if quality != MATERIAL_REQUIRED_STAT_QUALITY: + material_failures.append( + f"{role} benchmark stat_quality must be {MATERIAL_REQUIRED_STAT_QUALITY!r} " + f"to prove p95 speedup (observed={quality!r})" + ) + + for field, minimum in MATERIAL_REDUCTION_THRESHOLDS.items(): + delta = fields.get(field, {}) + control_value = delta.get("control") + typed_value = delta.get("typed") + reduction_pct = delta.get("reduction_pct") + if control_value is None or typed_value is None: + continue + if control_value <= 0: + material_failures.append( + f"{field}: control baseline must be positive to prove >={minimum:.0f}% reduction " + f"(control={control_value}, typed={typed_value})" + ) + continue + raw_reduction_pct = ((control_value - typed_value) / control_value) * 100.0 + if raw_reduction_pct < minimum: + material_failures.append( + f"{field}: reduction must be >={minimum:.0f}% " + f"(control={control_value}, typed={typed_value}, reduction_pct={reduction_pct})" + ) + else: + material_passes.append(field) + + for field in MATERIAL_ELIMINATION_FIELDS: + delta = fields.get(field, {}) + control_value = delta.get("control") + typed_value = delta.get("typed") + if control_value is None or typed_value is None: + continue + if control_value <= 0: + material_failures.append( + f"{field}: control baseline must be positive to prove elimination " + f"(control={control_value}, typed={typed_value})" + ) + elif typed_value != 0: + material_failures.append( + f"{field}: typed value must be 0 for 100% elimination " + f"(control={control_value}, typed={typed_value})" + ) + else: + material_passes.append(field) + + for field, minimum in MATERIAL_SPEEDUP_THRESHOLDS.items(): + delta = fields.get(field, {}) + control_value = delta.get("control") + typed_value = delta.get("typed") + speedup = delta.get("speedup") + if control_value is None or typed_value is None: + continue + if control_value <= 0 or typed_value <= 0: + material_failures.append( + f"{field}: positive wall-time values are required to prove >={minimum:g}x speedup " + f"(control={control_value}, typed={typed_value})" + ) + continue + raw_speedup = control_value / typed_value + if raw_speedup < minimum: + material_failures.append( + f"{field}: speedup must be >={minimum:g}x " + f"(control={control_value}, typed={typed_value}, speedup={speedup})" + ) + else: + material_passes.append(field) + non_improving = [] zero_baseline_required_fields = [] positive_required_improvements = [] @@ -426,17 +1116,35 @@ def benchmark_deltas(compiler: dict[str, Any], errors: list[str], *, gate: bool) "at least one required improvement field must have a positive control " "baseline and a lower typed value" ) + material_accounting = material_accounting_rows(fields) + accounting_failures = [ + f"{row['field']}: {failure}" + for row in material_accounting + for failure in row.get("failures", []) + ] + material_failures.extend(accounting_failures) if gate and non_improving: errors.append(f"benchmark deltas missing required improvements: {non_improving}") + if gate and material_failures: + errors.append(f"benchmark deltas miss material performance gate: {material_failures}") return { - "status": "pass" if not missing and not non_improving else "fail", + "status": "pass" if not missing and not non_improving and not material_failures else "fail", "typed_workload": "native_abi_packet_typed", "control_workload": "native_abi_packet_control", "required_improvement_fields": list(REQUIRED_IMPROVEMENT_FIELDS), + "material_contracts": MATERIAL_CONTRACTS, + "material_reduction_thresholds": MATERIAL_REDUCTION_THRESHOLDS, + "material_elimination_fields": list(MATERIAL_ELIMINATION_FIELDS), + "material_speedup_thresholds": MATERIAL_SPEEDUP_THRESHOLDS, + "material_required_stat_quality": MATERIAL_REQUIRED_STAT_QUALITY, + "benchmark_stat_quality": benchmark_stat_quality, + "material_passes": material_passes, + "material_failures": material_failures, "positive_required_improvements": positive_required_improvements, "zero_baseline_required_fields": zero_baseline_required_fields, "missing_values": missing, "non_improving_required_fields": non_improving, + "material_accounting": material_accounting, "fields": fields, } @@ -468,6 +1176,108 @@ def runtime_safety_summary( } +def release_symbol_summary( + root: Path, + metadata: dict[str, Any], + errors: list[str], + *, + gate: bool, +) -> dict[str, Any]: + command = command_entry(metadata, "release", "runtime_symbols") + status = command_status(metadata, "release", "runtime_symbols") + log_path = resolve_path(command.get("log"), root) + log = read_text(log_path) if log_path else "" + missing_tokens = [token for token in REQUIRED_RELEASE_SYMBOL_TOKENS if token not in log] + archive = metadata.get("runtime_archive", "") if isinstance(metadata, dict) else "" + fingerprints = { + key: metadata.get(key, "") if isinstance(metadata, dict) else "" + for key in REQUIRED_RELEASE_FINGERPRINT_FIELDS + } + missing_fingerprints = [key for key, value in fingerprints.items() if not value] + sentinel_counts = release_sentinel_counts(log) + stale_symbol_count = [ + count for count in sentinel_counts if count < REQUIRED_RELEASE_SENTINEL_COUNT + ] + passed = ( + status == "pass" + and bool(log) + and not missing_tokens + and bool(sentinel_counts) + and not stale_symbol_count + and not missing_fingerprints + ) + if gate and status != "pass": + errors.append(f"release:runtime_symbols: command status is {status}") + if gate and not log: + errors.append("release:runtime_symbols: symbol guard log is missing") + if gate and missing_tokens: + errors.append( + "release:runtime_symbols: expected proof tokens missing from log: " + f"{missing_tokens}" + ) + if gate and not sentinel_counts: + errors.append("release:runtime_symbols: sentinel count proof is missing from log") + if gate and stale_symbol_count: + errors.append( + "release:runtime_symbols: sentinel count is below current guard set " + f"(required={REQUIRED_RELEASE_SENTINEL_COUNT}, observed={sentinel_counts})" + ) + if gate and missing_fingerprints: + errors.append( + "release:runtime_symbols: archive/source freshness fingerprints missing: " + f"{missing_fingerprints}" + ) + return { + "status": "pass" if passed else "fail", + "command": command, + "runtime_archive": archive, + "log": str(log_path) if log_path else "", + "required_tokens": list(REQUIRED_RELEASE_SYMBOL_TOKENS), + "missing_tokens": missing_tokens, + "required_sentinel_count": REQUIRED_RELEASE_SENTINEL_COUNT, + "sentinel_counts": sentinel_counts, + "stale_symbol_counts": stale_symbol_count, + "fingerprints": fingerprints, + "missing_fingerprints": missing_fingerprints, + } + + +def all_rows_pass(rows: Any) -> bool: + if not isinstance(rows, dict) or not rows: + return False + return all(isinstance(row, dict) and row.get("status") == "pass" for row in rows.values()) + + +def compiler_matrix_status(compiler: dict[str, Any]) -> str: + workloads = compiler.get("workloads", {}) + if compiler.get("status") != "pass" or not isinstance(workloads, dict) or not workloads: + return "fail" + return "pass" if all(row.get("status") == "pass" for row in workloads.values()) else "fail" + + +def gate_matrix_summary( + correctness: dict[str, Any], + compiler: dict[str, Any], + runtime: dict[str, Any], + release_symbols: dict[str, Any], + deltas: dict[str, Any], +) -> list[dict[str, Any]]: + status_by_area = { + "native_abi_correctness": "pass" if all_rows_pass(correctness) else "fail", + "native_region_artifacts": compiler_matrix_status(compiler), + "explain_lowering_accounting": "pass" if deltas.get("status") == "pass" else "fail", + "runtime_safety": "pass" if runtime.get("status") == "pass" else "fail", + "release_symbols": "pass" if release_symbols.get("status") == "pass" else "fail", + } + return [ + { + **row, + "status": status_by_area.get(str(row["area"]), "fail"), + } + for row in GATE_MATRIX_SPEC + ] + + def build_packet(root: Path, metadata_path: Path, repo_root: Path, *, gate: bool) -> dict[str, Any]: metadata = load_json(metadata_path, {}) errors: list[str] = [] @@ -476,7 +1286,9 @@ def build_packet(root: Path, metadata_path: Path, repo_root: Path, *, gate: bool correctness = correctness_summary(root, metadata, errors, gate=gate) compiler = compiler_output_summary(root, metadata, errors, warnings, gate=gate) runtime = runtime_safety_summary(root, metadata, errors, gate=gate) + release_symbols = release_symbol_summary(root, metadata, errors, gate=gate) deltas = benchmark_deltas(compiler, errors, gate=gate) + gate_matrix = gate_matrix_summary(correctness, compiler, runtime, release_symbols, deltas) commands = metadata.get("commands", {}) if isinstance(metadata, dict) else {} packet = { @@ -497,9 +1309,11 @@ def build_packet(root: Path, metadata_path: Path, repo_root: Path, *, gate: bool "compiler_suite_report": compiler.get("suite_report"), }, "scope": SCOPE, + "gate_matrix": gate_matrix, "correctness": correctness, "native_call_lowering": compiler, "gc_root_safety": runtime, + "release_symbol_guard": release_symbols, "benchmark_deltas": deltas, } return packet @@ -525,6 +1339,16 @@ def markdown_for_packet(packet: dict[str, Any], repo_root: Path) -> str: lines.append("## Gate Failures") lines.extend(f"- {error}" for error in packet["errors"]) + lines.append("") + lines.append("## Gate Matrix") + lines.append("| Area | Status | Gate | Evidence |") + lines.append("|---|---:|---|---|") + for row in packet.get("gate_matrix", []): + lines.append( + f"| {row.get('label', row.get('area', ''))} | `{row.get('status', 'missing')}` | " + f"{row.get('gate', '')} | {row.get('evidence', '')} |" + ) + lines.append("") lines.append("## Correctness Fixtures") for name, row in packet.get("correctness", {}).items(): @@ -538,9 +1362,17 @@ def markdown_for_packet(packet: dict[str, Any], repo_root: Path) -> str: lowering = packet.get("native_call_lowering", {}) lines.append(f"- Suite: `{lowering.get('status', 'missing')}` report=`{lowering.get('suite_report', '')}`") for name, row in lowering.get("workloads", {}).items(): + contract = row.get("packet_contract", {}) + explain = row.get("explain_lowering_accounting", {}) lines.append( f"- `{name}`: `{row.get('status')}`; missing_artifacts={len(row.get('missing_artifacts', []))}; " - f"safety_failures={len(row.get('failing_safety_checks', []))}" + f"safety_failures={len(row.get('failing_safety_checks', []))}; " + f"stdout_missing={len(row.get('missing_stdout_checks', []))}; " + f"stdout_failures={len(row.get('failing_stdout_checks', []))}; " + f"packet_contract=`{contract.get('status', 'skipped')}`; " + f"explain_records={explain.get('record_count', 0)}; " + f"boxes={explain.get('boxes_inserted', 0)}; " + f"dynamic_fallbacks={explain.get('dynamic_fallbacks', 0)}" ) lines.append("") @@ -551,13 +1383,61 @@ def markdown_for_packet(packet: dict[str, Any], repo_root: Path) -> str: f"observed={len(safety.get('observed_tests', []))}/{len(safety.get('required_tests', []))}" ) + lines.append("") + lines.append("## Release / LTO Symbol Guard") + symbols = packet.get("release_symbol_guard", {}) + fingerprints = symbols.get("fingerprints", {}) + lines.append( + f"- Runtime symbol guard: `{symbols.get('status', 'missing')}`; " + f"archive=`{symbols.get('runtime_archive', '')}`; " + f"sentinels={symbols.get('sentinel_counts', [])}/{symbols.get('required_sentinel_count', '')}; " + f"missing_tokens={symbols.get('missing_tokens', [])}; " + f"archive_sha256=`{fingerprints.get('runtime_archive_sha256', '')}`; " + f"source_digest=`{fingerprints.get('runtime_source_digest', '')}`" + ) + lines.append("") lines.append("## Packet Deltas") deltas = packet.get("benchmark_deltas", {}) + material_status = "pass" if deltas.get("status") == "pass" else "fail" + lines.append(f"- Material gate: `{material_status}`") + lines.append( + f"- Timing quality: typed=`{deltas.get('benchmark_stat_quality', {}).get('typed')}` " + f"control=`{deltas.get('benchmark_stat_quality', {}).get('control')}` " + f"required=`{deltas.get('material_required_stat_quality')}`" + ) + contracts = deltas.get("material_contracts", {}) + if contracts: + lines.append( + f"- Contract: reductions={contracts.get('reductions', {})}; " + f"eliminations={contracts.get('eliminations', {})}; " + f"speedups={contracts.get('speedups', {})}; " + f"stat_quality=`{contracts.get('stat_quality', '')}`" + ) + if deltas.get("material_failures"): + lines.extend(f" - {failure}" for failure in deltas.get("material_failures", [])) + if deltas.get("missing_values"): + lines.append(f" - missing_values={deltas.get('missing_values')}") + + lines.append("") + lines.append("## Material Accounting") + lines.append("| Field | Category | Status | Control | Typed | Reduction | Speedup | Thresholds |") + lines.append("|---|---|---:|---:|---:|---:|---:|---|") + for row in deltas.get("material_accounting", []): + thresholds = ", ".join(f"{key}={value}" for key, value in row.get("thresholds", {}).items()) + lines.append( + f"| `{row.get('field')}` | {row.get('category')} | `{row.get('status')}` | " + f"{row.get('control')} | {row.get('typed')} | {row.get('reduction_pct')} | " + f"{row.get('speedup')} | {thresholds} |" + ) + + lines.append("") + lines.append("## Raw Deltas") for field, delta in deltas.get("fields", {}).items(): lines.append( f"- `{field}`: control={delta.get('control')} typed={delta.get('typed')} " - f"delta={delta.get('delta')} delta_pct={delta.get('delta_pct')}" + f"delta={delta.get('delta')} delta_pct={delta.get('delta_pct')} " + f"reduction_pct={delta.get('reduction_pct')} speedup={delta.get('speedup')}" ) return "\n".join(lines) + "\n" diff --git a/tests/test_compiler_output_regression.py b/tests/test_compiler_output_regression.py index 831bdb49ce..25926244b0 100644 --- a/tests/test_compiler_output_regression.py +++ b/tests/test_compiler_output_regression.py @@ -4,6 +4,7 @@ import tempfile import unittest from pathlib import Path +from types import SimpleNamespace if sys.version_info < (3, 11): @@ -21,6 +22,7 @@ sys.modules[SPEC.name] = HARNESS SPEC.loader.exec_module(HARNESS) +from compiler_output_harness import capture as CAPTURE_MODULE from compiler_output_harness.capture import SUITES @@ -1075,6 +1077,25 @@ def test_workload_spec_loads_current_workloads(self): self.assertIn("vectorization", workload, name) self.assertIn("runtime_budgets", workload, name) + def test_native_abi_root_barrier_budgets_are_explicit(self): + spec = HARNESS.load_workload_spec(HARNESS.DEFAULT_SPEC_PATH) + expected = { + "h1_native_rep_equivalence": 1, + "h1_buffer_alias_negative": 3, + "native_owned_typed_views": 1, + "native_pod_layout_constants": 2, + "native_memory_bulk_fill": 2, + "native_memory_fixture": 2, + } + for name, maximum in expected.items(): + self.assertEqual( + spec["workloads"][name]["runtime_budgets"][ + "write_barriers_static" + ], + maximum, + name, + ) + def test_suite_parser_accepts_native_region_proof(self): parser = HARNESS.build_parser() args = parser.parse_args( @@ -1132,11 +1153,70 @@ def test_ci_wires_native_abi_proof_suite(self): workflow = (REPO_ROOT / ".github" / "workflows" / "test.yml").read_text( encoding="utf-8" ) + native_region_block = workflow.split("Gate native-region proof compiler output", 1)[ + 1 + ].split("Gate native-ABI proof compiler output", 1)[0] + native_abi_block = workflow.split("Gate native-ABI proof compiler output", 1)[ + 1 + ].split("Gate typed feedback runtime evidence", 1)[0] + self.assertIn("--gate", native_region_block) + self.assertIn("--gate", native_abi_block) self.assertIn("Gate native-ABI proof compiler output", workflow) self.assertIn("--suite native-abi-proof", workflow) + self.assertIn("Run native ABI evidence report unit tests", workflow) + self.assertIn("tests.test_native_abi_evidence_report", workflow) + self.assertIn("native-abi-evidence-packet:", workflow) + self.assertIn("Gate native ABI evidence packet", workflow) + self.assertIn('RUSTC_WRAPPER: ""', workflow) + self.assertIn("RUSTFLAGS: -Awarnings", workflow) + self.assertIn("tests/test_native_abi_evidence_packet_smoke.sh", workflow) + self.assertIn("target/native-abi-evidence-packet", workflow) self.assertIn("Gate typed feedback runtime evidence", workflow) self.assertIn("tests.test_typed_feedback_runtime_evidence", workflow) + def test_capture_suite_propagates_gate_to_workload_capture(self): + observed_gate_values = [] + original_capture = CAPTURE_MODULE.capture + CAPTURE_MODULE.SUITES["__gate_propagation_test__"] = ["dummy_workload"] + + def fake_capture(args): + observed_gate_values.append(args.gate) + out_dir = Path(args.out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + (out_dir / "structural-report.json").write_text( + json.dumps({"status": "pass", "errors": []}), + encoding="utf-8", + ) + return 0 + + CAPTURE_MODULE.capture = fake_capture + try: + with tempfile.TemporaryDirectory() as temp: + args = SimpleNamespace( + suite="__gate_propagation_test__", + out_dir=temp, + gate=True, + print_summary=False, + ) + self.assertEqual(CAPTURE_MODULE.capture_suite(args), 0) + finally: + CAPTURE_MODULE.capture = original_capture + CAPTURE_MODULE.SUITES.pop("__gate_propagation_test__", None) + + self.assertEqual(observed_gate_values, [True]) + + def test_trace_budget_compile_env_requests_gc_trace(self): + env = CAPTURE_MODULE._compile_env("clang", enable_gc_trace=True) + self.assertEqual(env["PERRY_GC_TRACE"], "1") + self.assertNotIn("PERRY_GC_TRACE", CAPTURE_MODULE._compile_env("clang")) + + def test_auto_optimize_enables_diagnostics_for_gc_trace_evidence(self): + optimized_libs = ( + REPO_ROOT / "crates" / "perry" / "src" / "commands" / "compile" / "optimized_libs.rs" + ).read_text(encoding="utf-8") + self.assertIn('std::env::var("PERRY_GC_TRACE")', optimized_libs) + self.assertIn("perry-runtime/diagnostics", optimized_libs) + def test_release_sweep_wires_native_abi_evidence_packet_smoke(self): release_sweep = (REPO_ROOT / "scripts" / "release_sweep.sh").read_text( encoding="utf-8" @@ -1150,13 +1230,27 @@ def test_release_sweep_wires_native_abi_evidence_packet_smoke(self): smoke = ( REPO_ROOT / "tests" / "test_native_abi_evidence_packet_smoke.sh" ).read_text(encoding="utf-8") + packet = ( + REPO_ROOT / "scripts" / "native_abi_evidence_packet.sh" + ).read_text(encoding="utf-8") self.assertIn( "13|native_abi_evidence|all|native ABI evidence packet smoke gate", release_sweep, ) self.assertIn("tests/test_native_abi_evidence_packet_smoke.sh", tier) self.assertIn("PERRY_BIN", tier) + self.assertIn("--runs 5", smoke) self.assertIn("--gate", smoke) + self.assertIn("scripts/check_runtime_symbols.sh", packet) + self.assertIn('export RUSTC_WRAPPER=""', packet) + self.assertIn('export RUSTFLAGS="-Awarnings"', packet) + self.assertIn('"rustc_wrapper_effective"', packet) + self.assertIn('"rustflags_effective"', packet) + self.assertIn("release_symbol_guard", smoke) + self.assertIn("--gate requires --runs >= 5", packet) + self.assertIn("build_runtime_archive", packet) + self.assertIn("snapshot_tool_artifacts", packet) + self.assertIn("rustc_wrapper_scrubbed", packet) def test_runtime_symbol_guard_roots_numeric_array_helpers(self): guard = (REPO_ROOT / "scripts" / "check_runtime_symbols.sh").read_text( @@ -1232,11 +1326,37 @@ def test_runtime_symbol_guard_roots_map_set_string_lowering_helpers(self): ) for symbol in ( "js_map_set_string_number", + "js_map_set_string_key", + "js_map_set_string_i32", + "js_map_set_string_u32", + "js_map_set_string_f32", + "js_map_set_string_bool", + "js_map_set_string_string", + "js_map_set_number_key", "js_map_get_string_key", + "js_map_get_number_key", "js_map_has_string_key", + "js_map_has_number_key", + "js_map_delete_string_key", + "js_map_delete_number_key", "js_set_add_string", + "js_set_add_number", "js_set_has_string", + "js_set_has_number", "js_set_delete_string", + "js_set_delete_number", + "js_set_add_i32", + "js_set_has_i32", + "js_set_delete_i32", + "js_set_add_u32", + "js_set_has_u32", + "js_set_delete_u32", + "js_set_add_f32", + "js_set_has_f32", + "js_set_delete_f32", + "js_set_add_bool", + "js_set_has_bool", + "js_set_delete_bool", ): self.assertIn(symbol, guard) @@ -1253,8 +1373,10 @@ def test_runtime_symbol_guard_roots_async_control_box_helpers(self): "js_bool_box_set", "js_iter_result_set", "js_iter_result_set_f64", + "js_iter_result_set_i1", "js_iter_result_get_value", "js_iter_result_get_value_f64", + "js_iter_result_get_value_i1", "js_iter_result_get_done", ): self.assertIn(symbol, guard) @@ -1317,7 +1439,15 @@ def test_parse_kept_paths_includes_compile_metadata(self): def test_runtime_counter_summary_combines_static_and_trace_counts(self): counters = HARNESS.structural_counters( GOOD_IR, - GOOD_IR + "\n call double @js_boxed_number_new(double 1.0)\n", + GOOD_IR + + "\n call double @js_boxed_number_new(double 1.0)\n" + + " call double @js_buffer_get(double 1.0, double 0.0)\n" + + " call double @js_typed_array_get(double 1.0, double 0.0)\n" + + " call void @js_write_barrier(i64 1, i64 2)\n" + + " call void @js_write_barrier_slot(i64 1, i64 8, i64 2)\n" + + " call void @js_write_barrier_root_nanbox(i64 2)\n" + + " call void @js_write_barrier_root_heap_word(i64 2)\n" + + " call i32 @js_uint8array_get(i64 1, i32 0)\n", GOOD_ASM, ) summary = HARNESS.runtime_counter_summary( @@ -1337,7 +1467,10 @@ def test_runtime_counter_summary_combines_static_and_trace_counts(self): self.assertEqual(summary["gc_collections_traced"], 2) self.assertEqual(summary["allocations_traced"], 4) self.assertEqual(summary["write_barriers_traced"], 3) + self.assertEqual(summary["write_barriers_static"], 4) self.assertEqual(summary["boxed_number_allocations_static"], 1) + self.assertEqual(summary["buffer_slow_path_accesses_static"], 2) + self.assertEqual(summary["array_slow_path_accesses_static"], 2) def test_trace_runtime_budgets_fail_when_gc_trace_disabled(self): benchmark = { diff --git a/tests/test_native_abi_contract.sh b/tests/test_native_abi_contract.sh index 8fd481441f..5e429a85e0 100755 --- a/tests/test_native_abi_contract.sh +++ b/tests/test_native_abi_contract.sh @@ -318,7 +318,8 @@ write_evidence "compile.log" "$COMPILE_OUTPUT" RUN_OUTPUT=$(./test_bin 2>&1) write_evidence "runtime.stdout" "$RUN_OUTPUT" -if [ "$RUN_OUTPUT" != "PASS" ]; then +RUN_LAST_LINE=$(printf '%s\n' "$RUN_OUTPUT" | tail -n 1) +if [ "$RUN_LAST_LINE" != "PASS" ]; then echo "FAIL: JS-visible native ABI behavior changed" echo "Expected: PASS" echo "Got: $RUN_OUTPUT" diff --git a/tests/test_native_abi_evidence_packet_smoke.sh b/tests/test_native_abi_evidence_packet_smoke.sh index 2ca4e6f7e9..f77f0b7442 100755 --- a/tests/test_native_abi_evidence_packet_smoke.sh +++ b/tests/test_native_abi_evidence_packet_smoke.sh @@ -34,7 +34,7 @@ fi set +e PYTHON="$PYTHON_BIN" "$ROOT/scripts/native_abi_evidence_packet.sh" \ - --runs 1 \ + --runs 5 \ --out "$OUT" \ --gate STATUS=$? @@ -55,7 +55,7 @@ if status == 0: assert packet["status"] == "pass", packet["errors"] else: assert packet["status"] == "fail", packet -for section in ("correctness", "native_call_lowering", "gc_root_safety", "benchmark_deltas"): +for section in ("gate_matrix", "correctness", "native_call_lowering", "gc_root_safety", "release_symbol_guard", "benchmark_deltas"): assert section in packet, packet.keys() PY diff --git a/tests/test_native_abi_evidence_report.py b/tests/test_native_abi_evidence_report.py index 9300861efa..35042454ce 100644 --- a/tests/test_native_abi_evidence_report.py +++ b/tests/test_native_abi_evidence_report.py @@ -36,6 +36,17 @@ def correctness_tokens(tokens): return "\n".join(tokens) + "\n" +def native_record(rep, consumer, *, access_mode="unchecked_native", bounds_state=None, **overrides): + row = { + "native_rep_name": rep, + "consumer": consumer, + "access_mode": access_mode, + "bounds_state": bounds_state or {"proven": {"proof": "loop_guard"}}, + } + row.update(overrides) + return row + + def create_correctness(root): contract = root / "correctness" / "native-abi-contract" pod = root / "correctness" / "c-layout-pod-records" @@ -49,7 +60,15 @@ def create_correctness(root): write_text(pod / "native-reps.txt", correctness_tokens(REPORT.REQUIRED_CORRECTNESS["c_layout_pod_records"]["tokens"])) -def create_workload(suite_root, name, runtime_summary, median): +def create_workload( + suite_root, + name, + runtime_summary, + median, + p95=None, + stat_quality="timing", + native_records=None, +): root = suite_root / name artifacts = { "hir": root / "hir.txt", @@ -62,7 +81,11 @@ def create_workload(suite_root, name, runtime_summary, median): } for path in artifacts.values(): write_text(path) + write_json(artifacts["native_reps"], {"records": native_records or []}) manifest = { + "workload": name, + "workload_kind": name, + "source": f"benchmarks/compiler_output/fixtures/{name}.ts", "artifacts": { "hir": str(artifacts["hir"]), "llvm_before_opt": str(artifacts["llvm_before_opt"]), @@ -82,6 +105,8 @@ def create_workload(suite_root, name, runtime_summary, median): "benchmark": { "median_wall_ms": median, "mean_wall_ms": median, + "p95_wall_ms": median if p95 is None else p95, + "stat_quality": stat_quality, "runs": [{"exit_code": 0}], }, } @@ -89,6 +114,9 @@ def create_workload(suite_root, name, runtime_summary, median): {"name": name, "status": "pass", "detail": ""} for name in REPORT.SAFETY_CHECK_NAMES ] + stdout_check = REPORT.REQUIRED_PACKET_STDOUT_CHECKS.get(name) + if stdout_check: + checks.append({"name": stdout_check, "status": "pass", "detail": ""}) write_json(root / "manifest.json", manifest) write_json(root / "structural-report.json", {"status": "pass", "checks": checks, "errors": []}) return { @@ -109,23 +137,44 @@ def create_compiler_output(root): { "boxed_number_allocations_static": 0, "buffer_slow_path_accesses_static": 0, - "allocations_traced": 1, + "array_slow_path_accesses_static": 0, + "allocations_traced": 5, "write_barriers_static": 0, + "write_barriers_traced": 8, "runtime_calls_static": 2, }, 10.0, + p95=20.0, + native_records=[ + native_record("buffer_view", "BufferView"), + native_record("u8", "u8_load_zext_i32"), + ], ) control = create_workload( suite_root, "native_abi_packet_control", { - "boxed_number_allocations_static": 4, - "buffer_slow_path_accesses_static": 8, - "allocations_traced": 9, + "boxed_number_allocations_static": 64, + "buffer_slow_path_accesses_static": 128, + "array_slow_path_accesses_static": 256, + "allocations_traced": 640, "write_barriers_static": 6, + "write_barriers_traced": 360, "runtime_calls_static": 12, }, 25.0, + p95=32.0, + native_records=[ + native_record( + "js_value", + "js_buffer_get", + access_mode="dynamic_fallback", + bounds_state="unknown", + native_value_state="dynamic_fallback", + materialization_reason="runtime_api", + fallback_reason="runtime_api", + ), + ], ) write_json( suite_root / "suite-report.json", @@ -141,11 +190,19 @@ def create_compiler_output(root): def create_metadata(root): runtime_log = root / "logs" / "native-async.log" + symbol_log = root / "logs" / "runtime-symbols.log" write_text(runtime_log, "\n".join(REPORT.REQUIRED_RUNTIME_TESTS) + "\n") + write_text( + symbol_log, + f"ok: target/debug/libperry_runtime.a defines all {REPORT.REQUIRED_RELEASE_SENTINEL_COUNT} sentinel symbols\n", + ) write_json( root / "metadata.json", { "schema_version": 1, + "runtime_archive": "target/debug/libperry_runtime.a", + "runtime_archive_sha256": "a" * 64, + "runtime_source_digest": "b" * 64, "commands": { "correctness": { "native_abi_contract": command(), @@ -154,6 +211,9 @@ def create_metadata(root): "packet": { "compiler_output": command(), }, + "release": { + "runtime_symbols": command(log=symbol_log), + }, "runtime": { "native_async": command(log=runtime_log), }, @@ -180,6 +240,14 @@ def test_synthetic_packet_passes_gate(self): packet = REPORT.build_packet(root, root / "metadata.json", repo_root, gate=True) self.assertEqual(packet["status"], "pass", packet["errors"]) self.assertEqual(packet["benchmark_deltas"]["status"], "pass") + typed_contract = packet["native_call_lowering"]["workloads"][ + "native_abi_packet_typed" + ]["packet_contract"] + control_contract = packet["native_call_lowering"]["workloads"][ + "native_abi_packet_control" + ]["packet_contract"] + self.assertEqual(typed_contract["status"], "pass", typed_contract) + self.assertEqual(control_contract["status"], "pass", control_contract) self.assertIn("region-local native type lowering", packet["scope"]["summary"]) self.assertIn( "does not claim a general typed function/method/closure ABI", @@ -189,8 +257,28 @@ def test_synthetic_packet_passes_gate(self): markdown = REPORT.markdown_for_packet(packet, repo_root) self.assertIn("# Selected Native / Region-Local Evidence Packet: PASS", markdown) self.assertIn("## Scope", markdown) + self.assertIn("## Gate Matrix", markdown) self.assertIn("typed clones, or generic trampoline dispatch", markdown) + self.assertIn("packet_contract=`pass`", markdown) self.assertIn("## Selected Native / Region-Local Lowering", markdown) + self.assertIn("explain_records=", markdown) + self.assertIn("stdout_missing=0", markdown) + self.assertIn("## Release / LTO Symbol Guard", markdown) + self.assertIn("Runtime symbol guard: `pass`", markdown) + self.assertIn("source_digest=", markdown) + self.assertIn("## Packet Deltas", markdown) + self.assertIn("Contract: reductions=", markdown) + self.assertIn("## Material Accounting", markdown) + self.assertTrue( + all(row["status"] == "pass" for row in packet["gate_matrix"]), + packet["gate_matrix"], + ) + self.assertEqual( + packet["native_call_lowering"]["workloads"]["native_abi_packet_control"][ + "explain_lowering_accounting" + ]["dynamic_fallbacks"], + 1, + ) def test_missing_artifact_fails_gate(self): temp, root, repo_root = self.make_packet() @@ -201,6 +289,109 @@ def test_missing_artifact_fails_gate(self): self.assertEqual(packet["status"], "fail") self.assertTrue(any("native_abi_packet_typed" in error for error in packet["errors"])) + def test_typed_packet_requires_native_rep_evidence(self): + temp, root, repo_root = self.make_packet() + with temp: + reps_path = ( + root + / "compiler-output" + / "native-abi-proof" + / "native_abi_packet_typed" + / "native-reps-0.json" + ) + write_json(reps_path, {"records": []}) + packet = REPORT.build_packet(root, root / "metadata.json", repo_root, gate=True) + self.assertEqual(packet["status"], "fail") + typed_contract = packet["native_call_lowering"]["workloads"][ + "native_abi_packet_typed" + ]["packet_contract"] + self.assertEqual(typed_contract["status"], "fail") + self.assertTrue( + any("typed_unchecked_buffer_view" in error for error in packet["errors"]), + packet["errors"], + ) + + def test_control_packet_requires_positive_static_baseline(self): + temp, root, repo_root = self.make_packet() + with temp: + manifest_path = ( + root + / "compiler-output" + / "native-abi-proof" + / "native_abi_packet_control" + / "manifest.json" + ) + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + manifest["runtime_counter_summary"]["boxed_number_allocations_static"] = 0 + write_json(manifest_path, manifest) + packet = REPORT.build_packet(root, root / "metadata.json", repo_root, gate=True) + self.assertEqual(packet["status"], "fail") + control_contract = packet["native_call_lowering"]["workloads"][ + "native_abi_packet_control" + ]["packet_contract"] + self.assertEqual(control_contract["status"], "fail") + self.assertTrue( + any( + "boxed_number_allocations_static must be positive" in error + for error in packet["errors"] + ), + packet["errors"], + ) + + def test_packet_contract_rejects_swapped_workload_manifest(self): + temp, root, repo_root = self.make_packet() + with temp: + manifest_path = ( + root + / "compiler-output" + / "native-abi-proof" + / "native_abi_packet_typed" + / "manifest.json" + ) + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + manifest["source"] = "benchmarks/compiler_output/fixtures/native_abi_packet_control.ts" + manifest["workload_kind"] = "native_abi_packet_control" + write_json(manifest_path, manifest) + packet = REPORT.build_packet(root, root / "metadata.json", repo_root, gate=True) + self.assertEqual(packet["status"], "fail") + typed_contract = packet["native_call_lowering"]["workloads"][ + "native_abi_packet_typed" + ]["packet_contract"] + self.assertEqual(typed_contract["status"], "fail") + self.assertTrue( + any("manifest source must be" in error for error in packet["errors"]), + packet["errors"], + ) + + def test_missing_packet_stdout_check_fails_gate(self): + temp, root, repo_root = self.make_packet() + with temp: + report_path = ( + root + / "compiler-output" + / "native-abi-proof" + / "native_abi_packet_typed" + / "structural-report.json" + ) + structural = json.loads(report_path.read_text(encoding="utf-8")) + structural["checks"] = [ + check + for check in structural["checks"] + if check["name"] != "native_abi_packet_typed_checksum" + ] + write_json(report_path, structural) + + packet = REPORT.build_packet(root, root / "metadata.json", repo_root, gate=True) + self.assertEqual(packet["status"], "fail") + workload = packet["native_call_lowering"]["workloads"]["native_abi_packet_typed"] + self.assertEqual( + workload["missing_stdout_checks"], + ["native_abi_packet_typed_checksum"], + ) + self.assertTrue( + any("native_abi_packet_typed_checksum" in error for error in packet["errors"]) + ) + def test_command_status_failure_fails_gate(self): temp, root, repo_root = self.make_packet() with temp: @@ -211,15 +402,80 @@ def test_command_status_failure_fails_gate(self): self.assertEqual(packet["status"], "fail") self.assertTrue(any("correctness:native_abi_contract" in error for error in packet["errors"])) + def test_missing_runtime_symbol_proof_fails_gate(self): + temp, root, repo_root = self.make_packet() + with temp: + metadata = json.loads((root / "metadata.json").read_text(encoding="utf-8")) + log_path = Path(metadata["commands"]["release"]["runtime_symbols"]["log"]) + write_text(log_path, "::warning::check_runtime_symbols: no llvm-nm/nm available\n") + + packet = REPORT.build_packet(root, root / "metadata.json", repo_root, gate=True) + self.assertEqual(packet["status"], "fail") + self.assertEqual(packet["release_symbol_guard"]["status"], "fail") + self.assertTrue( + any("release:runtime_symbols" in error for error in packet["errors"]) + ) + + def test_stale_runtime_symbol_count_fails_gate(self): + temp, root, repo_root = self.make_packet() + with temp: + metadata = json.loads((root / "metadata.json").read_text(encoding="utf-8")) + log_path = Path(metadata["commands"]["release"]["runtime_symbols"]["log"]) + write_text( + log_path, + f"ok: target/debug/libperry_runtime.a defines all {REPORT.REQUIRED_RELEASE_SENTINEL_COUNT - 1} sentinel symbols\n", + ) + + packet = REPORT.build_packet(root, root / "metadata.json", repo_root, gate=True) + self.assertEqual(packet["status"], "fail") + self.assertEqual(packet["release_symbol_guard"]["status"], "fail") + self.assertTrue( + any("sentinel count is below" in error for error in packet["errors"]), + packet["errors"], + ) + + def test_missing_runtime_fingerprints_fail_gate(self): + temp, root, repo_root = self.make_packet() + with temp: + metadata = json.loads((root / "metadata.json").read_text(encoding="utf-8")) + metadata.pop("runtime_archive_sha256") + metadata.pop("runtime_source_digest") + write_json(root / "metadata.json", metadata) + + packet = REPORT.build_packet(root, root / "metadata.json", repo_root, gate=True) + self.assertEqual(packet["status"], "fail") + self.assertEqual(packet["release_symbol_guard"]["status"], "fail") + self.assertEqual( + packet["release_symbol_guard"]["missing_fingerprints"], + ["runtime_archive_sha256", "runtime_source_digest"], + ) + def test_benchmark_delta_calculation(self): temp, root, repo_root = self.make_packet() with temp: packet = REPORT.build_packet(root, root / "metadata.json", repo_root, gate=True) fields = packet["benchmark_deltas"]["fields"] - self.assertEqual(fields["buffer_slow_path_accesses_static"]["delta"], -8.0) + self.assertEqual(fields["buffer_slow_path_accesses_static"]["delta"], -128.0) + self.assertEqual(fields["buffer_slow_path_accesses_static"]["reduction_pct"], 100.0) + self.assertEqual(fields["array_slow_path_accesses_static"]["delta"], -256.0) + self.assertEqual(fields["array_slow_path_accesses_static"]["reduction_pct"], 100.0) self.assertEqual(fields["median_wall_ms"]["delta_pct"], -60.0) + self.assertEqual(fields["median_wall_ms"]["speedup"], 2.5) + self.assertEqual(fields["p95_wall_ms"]["speedup"], 1.6) + self.assertIn("write_barriers_traced", fields) + self.assertIn("runtime_calls_static", fields) + accounting = { + row["field"]: row + for row in packet["benchmark_deltas"]["material_accounting"] + } + self.assertEqual(accounting["runtime_calls_static"]["status"], "pass") + self.assertEqual(accounting["write_barriers_static"]["status"], "pass") + self.assertEqual( + packet["benchmark_deltas"]["benchmark_stat_quality"], + {"typed": "timing", "control": "timing"}, + ) - def test_zero_baseline_allocation_delta_is_informational_when_static_fields_improve(self): + def test_zero_baseline_material_delta_fails_gate(self): temp, root, repo_root = self.make_packet() with temp: for workload in ("native_abi_packet_typed", "native_abi_packet_control"): @@ -235,14 +491,9 @@ def test_zero_baseline_allocation_delta_is_informational_when_static_fields_impr write_json(manifest_path, manifest) packet = REPORT.build_packet(root, root / "metadata.json", repo_root, gate=True) - self.assertEqual(packet["status"], "pass", packet["errors"]) - self.assertIn( - "allocations_traced", - packet["benchmark_deltas"]["zero_baseline_required_fields"], - ) - self.assertIn( - "buffer_slow_path_accesses_static", - packet["benchmark_deltas"]["positive_required_improvements"], + self.assertEqual(packet["status"], "fail") + self.assertTrue( + any("material performance gate" in error for error in packet["errors"]) ) def test_required_allocation_deltas_must_improve(self): @@ -267,6 +518,120 @@ def test_required_allocation_deltas_must_improve(self): any("benchmark deltas missing required improvements" in error for error in packet["errors"]) ) + def test_material_reduction_thresholds_must_pass(self): + temp, root, repo_root = self.make_packet() + with temp: + manifest_path = ( + root + / "compiler-output" + / "native-abi-proof" + / "native_abi_packet_typed" + / "manifest.json" + ) + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + manifest["runtime_counter_summary"]["allocations_traced"] = 40 + write_json(manifest_path, manifest) + + packet = REPORT.build_packet(root, root / "metadata.json", repo_root, gate=True) + self.assertEqual(packet["status"], "fail") + failures = packet["benchmark_deltas"]["material_failures"] + self.assertTrue(any("allocations_traced" in failure for failure in failures)) + + def test_material_elimination_thresholds_must_pass(self): + temp, root, repo_root = self.make_packet() + with temp: + manifest_path = ( + root + / "compiler-output" + / "native-abi-proof" + / "native_abi_packet_typed" + / "manifest.json" + ) + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + manifest["runtime_counter_summary"]["boxed_number_allocations_static"] = 1 + write_json(manifest_path, manifest) + + packet = REPORT.build_packet(root, root / "metadata.json", repo_root, gate=True) + self.assertEqual(packet["status"], "fail") + failures = packet["benchmark_deltas"]["material_failures"] + self.assertTrue(any("boxed_number_allocations_static" in failure for failure in failures)) + + def test_material_speedup_thresholds_must_pass(self): + temp, root, repo_root = self.make_packet() + with temp: + manifest_path = ( + root + / "compiler-output" + / "native-abi-proof" + / "native_abi_packet_typed" + / "manifest.json" + ) + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + manifest["benchmark"]["median_wall_ms"] = 20.0 + write_json(manifest_path, manifest) + + packet = REPORT.build_packet(root, root / "metadata.json", repo_root, gate=True) + self.assertEqual(packet["status"], "fail") + failures = packet["benchmark_deltas"]["material_failures"] + self.assertTrue(any("median_wall_ms" in failure for failure in failures)) + + def test_material_runtime_helper_thresholds_must_pass(self): + temp, root, repo_root = self.make_packet() + with temp: + manifest_path = ( + root + / "compiler-output" + / "native-abi-proof" + / "native_abi_packet_typed" + / "manifest.json" + ) + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + manifest["runtime_counter_summary"]["runtime_calls_static"] = 11 + write_json(manifest_path, manifest) + + packet = REPORT.build_packet(root, root / "metadata.json", repo_root, gate=True) + self.assertEqual(packet["status"], "fail") + failures = packet["benchmark_deltas"]["material_failures"] + self.assertTrue(any("runtime_calls_static" in failure for failure in failures)) + + def test_material_static_barrier_thresholds_must_pass(self): + temp, root, repo_root = self.make_packet() + with temp: + manifest_path = ( + root + / "compiler-output" + / "native-abi-proof" + / "native_abi_packet_typed" + / "manifest.json" + ) + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + manifest["runtime_counter_summary"]["write_barriers_static"] = 5 + write_json(manifest_path, manifest) + + packet = REPORT.build_packet(root, root / "metadata.json", repo_root, gate=True) + self.assertEqual(packet["status"], "fail") + failures = packet["benchmark_deltas"]["material_failures"] + self.assertTrue(any("write_barriers_static" in failure for failure in failures)) + + def test_material_speedup_requires_timing_quality(self): + temp, root, repo_root = self.make_packet() + with temp: + manifest_path = ( + root + / "compiler-output" + / "native-abi-proof" + / "native_abi_packet_typed" + / "manifest.json" + ) + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + manifest["benchmark"]["stat_quality"] = "smoke" + write_json(manifest_path, manifest) + + packet = REPORT.build_packet(root, root / "metadata.json", repo_root, gate=True) + self.assertEqual(packet["status"], "fail") + failures = packet["benchmark_deltas"]["material_failures"] + self.assertTrue(any("stat_quality" in failure for failure in failures)) + if __name__ == "__main__": unittest.main()