-
-
Notifications
You must be signed in to change notification settings - Fork 161
perf(codegen): constructed throw operands keep the packed fast path (7.99 → 0.95 ns, 8.1× vs node) #9235
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
perf(codegen): constructed throw operands keep the packed fast path (7.99 → 0.95 ns, 8.1× vs node) #9235
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5147,9 +5147,25 @@ fn stmt_is_packed_f64_loop_safe( | |||||||||||||
| // way `Stmt::Return` checks its value; #9185 admitted any throw at all | ||||||||||||||
| // and leaned on `stmt_array_length_effect` to reject the constructing | ||||||||||||||
| // ones, which is a weaker guarantee than stating the requirement here. | ||||||||||||||
| // The thrown operand is deliberately NOT required to be | ||||||||||||||
| // `expr_is_packed_f64_loop_safe`. That predicate asks "can the clone | ||||||||||||||
| // keep running after this", and after a throw it cannot: control | ||||||||||||||
| // leaves the loop for a landing pad and never returns, so no later | ||||||||||||||
| // iteration can observe a value the operand disturbed. | ||||||||||||||
| // | ||||||||||||||
| // What the operand still must not do is change `arr.length` before the | ||||||||||||||
| // hoisted bound is used, and that IS checked — `stmt_preserves_array_length` | ||||||||||||||
| // walks `Stmt::Throw(e)` into `e`, so `throw arr.pop()` is rejected | ||||||||||||||
| // there. Keeping the two questions in their own predicates is what lets | ||||||||||||||
| // a constructed operand (`new Error(…)`, `"bad " + i`) stay on the fast | ||||||||||||||
| // path; requiring loop-safety here costs 8.4x for no correctness gain. | ||||||||||||||
| // | ||||||||||||||
| // The accumulator writeback this needs is emitted at the throw site by | ||||||||||||||
| // `flush_packed_accumulator_locals`; without it this admission is the | ||||||||||||||
| // #9185 wrong answer. | ||||||||||||||
| Stmt::Throw(value) => { | ||||||||||||||
| packed_loop_abrupt_enabled() | ||||||||||||||
| && expr_is_packed_f64_loop_safe(ctx, value, arr_id, counter_id) | ||||||||||||||
| && throw_operand_cannot_unwind(ctx, value, arr_id, counter_id) | ||||||||||||||
| } | ||||||||||||||
| Stmt::LabeledBreak(_) | ||||||||||||||
| | Stmt::LabeledContinue(_) | ||||||||||||||
|
|
@@ -5287,6 +5303,99 @@ fn local_is_int32_value(ctx: &FnCtx<'_>, local_id: u32) -> bool { | |||||||||||||
| ) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /// Can evaluating `expr` as a thrown operand UNWIND before the throw itself? | ||||||||||||||
| /// | ||||||||||||||
| /// This is the guard on `flush_packed_accumulator_locals`. That flush is | ||||||||||||||
| /// emitted at the throw site, after the operand is lowered — so an operand that | ||||||||||||||
| /// unwinds on its own leaves the loop-carried accumulators stale, reproducing | ||||||||||||||
| /// #9185 one level deeper. `expr_is_packed_f64_loop_safe` is NOT sufficient | ||||||||||||||
| /// here: it accepts a bare `Expr::Binary` over locals, and `+` on an object | ||||||||||||||
| /// dispatches to a user `valueOf`/`toString` that can throw. | ||||||||||||||
| /// | ||||||||||||||
| /// Throwing a value coerces nothing, so a bare local is fine. Anything that | ||||||||||||||
| /// COERCES — arithmetic, concatenation, an Error message being stringified — | ||||||||||||||
| /// must be provably free of user dispatch. | ||||||||||||||
| fn throw_operand_cannot_unwind( | ||||||||||||||
| ctx: &FnCtx<'_>, | ||||||||||||||
| expr: &perry_hir::Expr, | ||||||||||||||
| arr_id: u32, | ||||||||||||||
| counter_id: u32, | ||||||||||||||
| ) -> bool { | ||||||||||||||
| use perry_hir::Expr; | ||||||||||||||
| match expr { | ||||||||||||||
| // Thrown as-is: no conversion, so no user code, whatever it holds. | ||||||||||||||
| Expr::LocalGet(_) | ||||||||||||||
| | Expr::String(_) | ||||||||||||||
| | Expr::Number(_) | ||||||||||||||
| | Expr::Integer(_) | ||||||||||||||
| | Expr::Bool(_) | ||||||||||||||
| | Expr::Null | ||||||||||||||
| | Expr::Undefined => true, | ||||||||||||||
| Expr::ErrorNew(message) => message | ||||||||||||||
| .as_ref() | ||||||||||||||
| .is_none_or(|m| expr_is_coercion_free_primitive(ctx, m, arr_id, counter_id)), | ||||||||||||||
| Expr::TypeErrorNew(message) | ||||||||||||||
| | Expr::RangeErrorNew(message) | ||||||||||||||
| | Expr::ReferenceErrorNew(message) | ||||||||||||||
| | Expr::SyntaxErrorNew(message) => { | ||||||||||||||
| expr_is_coercion_free_primitive(ctx, message, arr_id, counter_id) | ||||||||||||||
| } | ||||||||||||||
| Expr::Binary { left, right, .. } | Expr::Compare { left, right, .. } => { | ||||||||||||||
| expr_is_coercion_free_primitive(ctx, left, arr_id, counter_id) | ||||||||||||||
| && expr_is_coercion_free_primitive(ctx, right, arr_id, counter_id) | ||||||||||||||
| } | ||||||||||||||
| Expr::Unary { operand, .. } => { | ||||||||||||||
| expr_is_coercion_free_primitive(ctx, operand, arr_id, counter_id) | ||||||||||||||
| } | ||||||||||||||
| _ => false, | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /// Can `expr` be stringified WITHOUT running user code? | ||||||||||||||
| /// | ||||||||||||||
| /// Deliberately syntactic and much narrower than `expr_is_packed_f64_loop_safe`: | ||||||||||||||
| /// that predicate accepts a bare `Expr::LocalGet`, which is right for a value | ||||||||||||||
| /// that is merely thrown (throwing coerces nothing) but wrong for one that is | ||||||||||||||
| /// about to be stringified by an Error constructor, since the local may hold an | ||||||||||||||
| /// object with a user `toString`. | ||||||||||||||
| /// | ||||||||||||||
| /// Admitted: literals, the loop counter, a read of the packed array being | ||||||||||||||
| /// iterated (both are numbers by the loop's own guards), and arithmetic or | ||||||||||||||
| /// concatenation over those. Anything else — including any other local — is | ||||||||||||||
| /// rejected, because proving it is not an object is not this predicate's job. | ||||||||||||||
| fn expr_is_coercion_free_primitive( | ||||||||||||||
| ctx: &FnCtx<'_>, | ||||||||||||||
| expr: &perry_hir::Expr, | ||||||||||||||
| arr_id: u32, | ||||||||||||||
| counter_id: u32, | ||||||||||||||
| ) -> bool { | ||||||||||||||
| use perry_hir::Expr; | ||||||||||||||
| match expr { | ||||||||||||||
| Expr::String(_) | ||||||||||||||
| | Expr::Number(_) | ||||||||||||||
| | Expr::Integer(_) | ||||||||||||||
| | Expr::Bool(_) | ||||||||||||||
| | Expr::Null | ||||||||||||||
| | Expr::Undefined => true, | ||||||||||||||
| // The counter is the loop's own i32 induction variable. | ||||||||||||||
| Expr::LocalGet(id) => *id == counter_id, | ||||||||||||||
| // An element of the packed f64 array is a genuine double: the versioned | ||||||||||||||
| // loop only entered the fast clone after proving that layout. | ||||||||||||||
| Expr::IndexGet { object, index } => { | ||||||||||||||
| matches!(object.as_ref(), Expr::LocalGet(id) if *id == arr_id) | ||||||||||||||
| && expr_is_coercion_free_primitive(ctx, index, arr_id, counter_id) | ||||||||||||||
|
Comment on lines
+5384
to
+5386
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Restrict packed-array reads to the proven in-bounds index. This accepts Require the index to be the proven loop counter for this admission. Proposed fix Expr::IndexGet { object, index } => {
matches!(object.as_ref(), Expr::LocalGet(id) if *id == arr_id)
- && expr_is_coercion_free_primitive(ctx, index, arr_id, counter_id)
+ && matches!(index.as_ref(), Expr::LocalGet(id) if *id == counter_id)
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| } | ||||||||||||||
| Expr::Binary { left, right, .. } | Expr::Compare { left, right, .. } => { | ||||||||||||||
| expr_is_coercion_free_primitive(ctx, left, arr_id, counter_id) | ||||||||||||||
| && expr_is_coercion_free_primitive(ctx, right, arr_id, counter_id) | ||||||||||||||
| } | ||||||||||||||
| Expr::Unary { operand, .. } | Expr::NumberCoerce(operand) => { | ||||||||||||||
| expr_is_coercion_free_primitive(ctx, operand, arr_id, counter_id) | ||||||||||||||
| } | ||||||||||||||
| _ => false, | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| fn expr_is_packed_f64_loop_safe( | ||||||||||||||
| ctx: &FnCtx<'_>, | ||||||||||||||
| expr: &perry_hir::Expr, | ||||||||||||||
|
|
@@ -8203,6 +8312,28 @@ pub(crate) fn expr_preserves_array_length( | |||||||||||||
| && walk(object) | ||||||||||||||
| && args.iter().all(&walk) | ||||||||||||||
| } | ||||||||||||||
| // The Error-family construction nodes allocate an error object and | ||||||||||||||
| // store their own message; none of them can reach an unrelated local | ||||||||||||||
| // array, so they preserve `arr.length`. Together with the `Stmt::Throw` | ||||||||||||||
| // admission above this is what keeps `if (bad) throw new Error(…)` — | ||||||||||||||
| // the #9151 shape — on the packed fast path. | ||||||||||||||
| // | ||||||||||||||
| // Identity is not in question at this layer: HIR emits these nodes only | ||||||||||||||
| // for the INTRINSIC constructor (`lower_new` gates them on | ||||||||||||||
| // `!shadowed_by_user_binding`). A user `class Error { … }`, or any other | ||||||||||||||
| // shadowing binding, lowers to `Expr::New`/`NewDynamic` instead and is | ||||||||||||||
| // still rejected by the fallback below — checking the NAME here would be | ||||||||||||||
| // a wrong-answer bug rather than a missed optimisation. The message and | ||||||||||||||
| // options operands are arbitrary expressions and are still walked. | ||||||||||||||
| Expr::ErrorNew(message) => message.as_ref().is_none_or(|m| walk(m)), | ||||||||||||||
| Expr::TypeErrorNew(message) | ||||||||||||||
| | Expr::RangeErrorNew(message) | ||||||||||||||
| | Expr::ReferenceErrorNew(message) | ||||||||||||||
| | Expr::SyntaxErrorNew(message) => walk(message), | ||||||||||||||
| Expr::ErrorNewWithCause { message, cause } => walk(message) && walk(cause), | ||||||||||||||
| Expr::ErrorNewWithOptions { | ||||||||||||||
| message, options, .. | ||||||||||||||
| } => walk(message) && walk(options), | ||||||||||||||
|
Comment on lines
+8334
to
+8336
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- applicable repository knowledge files ---'
find /tmp/coderabbit-repo-knowledge/perryts-perry-d4a878bc -maxdepth 2 -type f -name '*.md' -print
printf '%s\n' '--- convention headers ---'
for f in /tmp/coderabbit-repo-knowledge/perryts-perry-d4a878bc/*/*.md; do
[ -f "$f" ] || continue
head -5 "$f"
done
printf '%s\n' '--- diff stat and targeted diff ---'
git diff --stat -- crates/perry-codegen/src/stmt/loops.rs
git diff -- crates/perry-codegen/src/stmt/loops.rs | sed -n '1,260p'
printf '%s\n' '--- structural outline ---'
ast-grep outline crates/perry-codegen/src/stmt/loops.rs --lang rust | sed -n '1,220p'
printf '%s\n' '--- targeted source ranges ---'
sed -n '5120,5185p' crates/perry-codegen/src/stmt/loops.rs
sed -n '5285,5410p' crates/perry-codegen/src/stmt/loops.rs
sed -n '8280,8350p' crates/perry-codegen/src/stmt/loops.rs
printf '%s\n' '--- relevant symbol references ---'
rg -n -C 3 'expr_preserves_array_length|throw_operand_cannot_unwind|ErrorNewWithOptions|array_length|length_hoist|hoist' crates/perry-codegen/src/stmt/loops.rsRepository: PerryTS/perry Length of output: 50369 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- exact ErrorNewWithOptions references ---'
rg -n -C 5 'ErrorNewWithOptions' crates --glob '*.rs'
printf '%s\n' '--- array-length analysis ---'
sed -n '7480,7805p' crates/perry-codegen/src/stmt/loops.rs
sed -n '7950,8265p' crates/perry-codegen/src/stmt/loops.rs
sed -n '8265,8360p' crates/perry-codegen/src/stmt/loops.rs
printf '%s\n' '--- hoisted-bound lowering ---'
sed -n '5890,6075p' crates/perry-codegen/src/stmt/loops.rs
printf '%s\n' '--- relevant runtime/property helpers ---'
rg -n -C 4 'error.*cause|cause.*error|js_.*error|ErrorNew|new_error|ErrorOptions|options.*cause|property_get' crates/perry-codegen crates/perry-runtime crates/perry-hir --glob '*.rs' | head -500Repository: PerryTS/perry Length of output: 50369 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- native Error lowering ---'
sed -n '360,445p' crates/perry-codegen/src/expr/array_methods.rs
printf '%s\n' '--- HIR contract and lowering ---'
sed -n '1988,2020p' crates/perry-hir/src/ir/expr.rs
sed -n '1160,1210p' crates/perry-hir/src/lower/expr_new.rs
printf '%s\n' '--- error runtime symbols ---'
rg -n -C 6 'js_error_new|error_new|ERROR_KIND|cause' crates/perry-runtime crates/perry-codegen/src --glob '*.rs' | head -350
printf '%s\n' '--- remaining bound setup and loop condition ---'
sed -n '6040,6155p' crates/perry-codegen/src/stmt/loops.rsRepository: PerryTS/perry Length of output: 45819 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- exact native symbol across tracked files ---'
git grep -n -C 8 'js_error_new_kind_with_options_from_value' -- .
printf '%s\n' '--- error implementation files ---'
git ls-files | rg '(^|/)(error|object|value|builtins|stdlib|ffi)([^/]*)\.(rs|c|h|cpp)$' | head -200
printf '%s\n' '--- exact cause/property access in runtime sources ---'
git grep -n -C 5 -E 'cause|js_.*property|get_property|property_get' -- crates/perry-runtime crates/perry-stdlib 2>/dev/null | rg -C 3 'cause|error|property' | head -400Repository: PerryTS/perry Length of output: 50369 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- runtime Error helper ---'
rg -n 'fn apply_cause_from_options|apply_cause_from_options|js_error_new_kind_with_options_from_value' crates/perry-runtime/src/error.rs
sed -n '620,765p' crates/perry-runtime/src/error.rs
printf '%s\n' '--- classifier and preservation call chain ---'
sed -n '4680,4865p' crates/perry-codegen/src/stmt/loops.rs
sed -n '6935,7085p' crates/perry-codegen/src/stmt/loops.rs
sed -n '8195,8385p' crates/perry-codegen/src/stmt/loops.rsRepository: PerryTS/perry Length of output: 26397 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- apply_cause_from_options ---'
sed -n '600,625p' crates/perry-runtime/src/error.rs
printf '%s\n' '--- js_dyn_index_get binding and implementation ---'
git grep -n -C 8 'fn js_dyn_index_get\|js_dyn_index_get(' -- crates/perry-runtime/src crates/perry-codegen/src | head -240Repository: PerryTS/perry Length of output: 22471 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- packed-loop safety admission ---'
sed -n '5065,5180p' crates/perry-codegen/src/stmt/loops.rs
printf '%s\n' '--- Error expression safety and condition cache use ---'
sed -n '5390,5515p' crates/perry-codegen/src/stmt/loops.rs
rg -n -C 8 'cached_lengths|get_cached|cached.*length|hoisted_length_slot|i32_length_slot' crates/perry-codegen/src/stmt/loops.rs crates/perry-codegen/src/expr --glob '*.rs' | head -260Repository: PerryTS/perry Length of output: 35571 Do not mark
🤖 Prompt for AI Agents |
||||||||||||||
| Expr::NativeMethodCall { .. } | Expr::CallSpread { .. } => false, | ||||||||||||||
| Expr::Closure { .. } => false, | ||||||||||||||
| Expr::Binary { left, right, .. } | ||||||||||||||
|
|
||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🔎 Supported by static analysis
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 50369
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 50369
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 50369
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 50369
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 50369
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 50370
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 50369
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 50369
Reject TDZ-sensitive local reads.
Expr::LocalGet(_)can calljs_box_get_bitsfor a TDZ-seeded boxed lexical binding. That call can raiseReferenceErrorbeforeStmt::Throwcallsflush_packed_accumulator_locals, so the catch block can observe stale packed accumulators. Exclude TDZ-sensitive locals fromthrow_operand_cannot_unwind, or flush before lowering the operand.🤖 Prompt for AI Agents