Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions changelog.d/8012-async-control-cells.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
### Faster compiler-private async control cells (#8008)

Async and generator state machines now access their compiler-minted state,
pending-type, done, and executing cells with direct typed loads and stores.
Those cells are preallocated as single-field `I32Box` or `BoolBox` values, so
their pointer provenance and representation are already proven; routing every
access through the general runtime helpers redundantly checked the same pointer
against a thread-local box registry.

Ordinary user captures keep the checked runtime path, including its invalid-box
protection. Primitive allocation and registry insertion also remain unchanged;
only access to the four private controls bypasses repeated validation.

On the refreshed async probe set this removes 7.08% of retired instructions
from the pure async/await topology, 5.92% when plain objects flow through the
same topology, and 2.10% from the full `asyncpipe` workload. The synchronous
and Promise.all-only controls remain flat. An IR regression locks down both
the narrow eligibility boundary and the direct typed accesses.
48 changes: 39 additions & 9 deletions crates/perry-codegen/src/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2297,6 +2297,40 @@ pub(crate) fn load_boxed_local_pointer(ctx: &mut FnCtx<'_>, id: u32) -> Result<O
Ok(None)
}

/// Load a compiler-private async i32 control cell directly.
///
/// These cells are allocated by `Stmt::PreallocateBoxes` before the generated
/// state-machine closures are created. Unlike a general user capture, the
/// pointer is therefore compiler-minted and its pointee representation is
/// proven: the `I32Box` value is the first (and only) field. Keep ordinary
/// boxes on the checked runtime path; this helper is deliberately reachable
/// only from the `is_compiler_private_async_i32_control_local` arms below.
pub(crate) fn load_async_i32_control_cell(ctx: &mut FnCtx<'_>, cell: &str) -> String {
let ptr = ctx.block().inttoptr(I64, cell);
ctx.block().load(I32, &ptr)
}

/// Store a compiler-private async i32 control cell directly. See
/// `load_async_i32_control_cell` for the allocation/provenance proof.
pub(crate) fn store_async_i32_control_cell(ctx: &mut FnCtx<'_>, cell: &str, value: &str) {
let ptr = ctx.block().inttoptr(I64, cell);
ctx.block().store(I32, value, &ptr);
}

/// Load a compiler-private async boolean control cell directly. `BoolBox`'s
/// value is a Rust `bool`, represented as LLVM i1 at the FFI boundary.
pub(crate) fn load_async_i1_control_cell(ctx: &mut FnCtx<'_>, cell: &str) -> String {
let ptr = ctx.block().inttoptr(I64, cell);
ctx.block().load(I1, &ptr)
}

/// Store a compiler-private async boolean control cell directly. See
/// `load_async_i1_control_cell` for the representation proof.
pub(crate) fn store_async_i1_control_cell(ctx: &mut FnCtx<'_>, cell: &str, value: &str) {
let ptr = ctx.block().inttoptr(I64, cell);
ctx.block().store(I1, value, &ptr);
}

Comment on lines +2300 to +2333

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

ast-grep outline crates/perry-codegen/src/expr/mod.rs \
  --match 'load_async_i32_control_cell' --view expanded
ast-grep outline crates/perry-codegen/src/stmt/mod.rs \
  --match 'emit_preallocate_boxes' --view expanded

rg -n -C 14 \
  'PreallocateBoxes|collect_prealloc_box_ids_in_stmts|compiler_private_async_i32_control_locals|compiler_private_async_i1_control_locals|is_compiler_private_async_i32_control_local|is_compiler_private_async_i1_control_local|load_boxed_local_pointer|load_async_i32_control_cell|store_async_i32_control_cell|load_async_i1_control_cell|store_async_i1_control_cell' \
  crates/perry-codegen crates/perry-transform

Repository: PerryTS/perry

Length of output: 50372


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- expr helpers and predicates ---'
sed -n '2240,2350p' crates/perry-codegen/src/expr/mod.rs

printf '%s\n' '--- statement lowering and preallocation ---'
sed -n '540,690p' crates/perry-codegen/src/stmt/mod.rs

printf '%s\n' '--- direct helper call sites ---'
rg -n -C 8 \
  'load_async_i32_control_cell|store_async_i32_control_cell|load_async_i1_control_cell|store_async_i1_control_cell|is_compiler_private_async_i32_control_local|is_compiler_private_async_i1_control_local' \
  crates/perry-codegen/src

printf '%s\n' '--- all PreallocateBoxes construction sites ---'
rg -n -C 10 \
  'PreallocateBoxes|PreallocateTdzBoxes|emit_preallocate_boxes|collect_prealloc_box_ids_in_stmts' \
  crates/perry-transform/src crates/perry-codegen/src \
  -g '*.rs' \
  | head -n 1400

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

roots = [Path("crates/perry-transform/src"), Path("crates/perry-codegen/src")]
files = [p for root in roots for p in root.rglob("*.rs")]

for p in files:
    text = p.read_text()
    for match in re.finditer(r"PreallocateBoxes|PreallocateTdzBoxes", text):
        line = text.count("\n", 0, match.start()) + 1
        start = max(0, line - 4)
        lines = text.splitlines()
        end = min(len(lines), line + 8)
        print(f"\n--- {p}:{line} ---")
        for n in range(start + 1, end + 1):
            print(f"{n}: {lines[n-1]}")
PY

Repository: PerryTS/perry

Length of output: 50371


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- complete preallocation lowering ---'
sed -n '680,755p' crates/perry-codegen/src/stmt/mod.rs

printf '%s\n' '--- generator body construction ---'
sed -n '620,745p' crates/perry-transform/src/generator/lower.rs

printf '%s\n' '--- async transform control-local setup ---'
rg -n -C 18 \
  'compiler_private_async_i32_control_locals|compiler_private_async_i1_control_locals|async_i32|async_i1|control_local|control cell|PreallocateBoxes' \
  crates/perry-transform/src/async_to_generator.rs \
  crates/perry-transform/src/generator \
  crates/perry-codegen/src/codegen \
  crates/perry-codegen/src/expr \
  -g '*.rs' \
  | head -n 1800

printf '%s\n' '--- all private-set references ---'
rg -n -C 6 \
  'compiler_private_async_i32_control_locals|compiler_private_async_i1_control_locals' \
  crates -g '*.rs'

Repository: PerryTS/perry

Length of output: 50372


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

needles = (
    "compiler_private_async_i32_control_locals",
    "compiler_private_async_i1_control_locals",
)
for p in Path("crates").rglob("*.rs"):
    lines = p.read_text().splitlines()
    for i, line in enumerate(lines):
        if any(n in line for n in needles):
            lo = max(0, i - 20)
            hi = min(len(lines), i + 21)
            print(f"\n--- {p}:{i+1} ---")
            for n in range(lo, hi):
                print(f"{n+1}: {lines[n]}")
PY

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- private-set mutations and definitions ---'
rg -n \
  'compiler_private_async_i32_control_locals|compiler_private_async_i1_control_locals' \
  crates/perry-codegen crates/perry-transform \
  -g '*.rs' \
  | grep -E 'insert|extend|union|HashSet|pub |fn |struct |=' \
  | head -n 500

printf '%s\n' '--- all async control-local identifiers and types ---'
rg -n -C 10 \
  '__gen_state|__gen_done|__gen_executing|__gen_pending_type|__gen_pending_value|pending_type_id|state_id|done_id|executing_id' \
  crates/perry-transform/src/generator \
  -g '*.rs' \
  | head -n 1800

printf '%s\n' '--- function-body lowering order ---'
rg -n -C 18 \
  'lower_stmts|lower_stmt|func.body|collect_prealloc_box_ids_in_stmts|prealloc_boxes' \
  crates/perry-codegen/src/codegen crates/perry-codegen/src/stmt crates/perry-codegen/src/expr \
  -g '*.rs' \
  | head -n 2200

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

for p in Path("crates").rglob("*.rs"):
    text = p.read_text()
    for name in (
        "compiler_private_async_i32_control_locals",
        "compiler_private_async_i1_control_locals",
    ):
        for m in re.finditer(re.escape(name), text):
            line = text.count("\n", 0, m.start()) + 1
            before = text[max(0, m.start()-180):m.start()]
            after = text[m.end():m.end()+220]
            if any(op in before[-80:] + after[:120] for op in ("insert", "extend", "union", "collect", "HashSet", "=")):
                print(f"{p}:{line}: {text.splitlines()[line-1].strip()}")
PY

Repository: PerryTS/perry

Length of output: 2472


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- private-ID scanner population ---'
sed -n '1540,1650p' crates/perry-codegen/src/codegen/mod.rs
sed -n '1700,1790p' crates/perry-codegen/src/codegen/mod.rs

printf '%s\n' '--- scanner definitions and call sites ---'
rg -n -C 24 \
  'collect_compiler_private|compiler_private_async|__gen_state|__gen_done|__gen_executing|__gen_pending' \
  crates/perry-codegen/src/codegen/mod.rs \
  crates/perry-codegen/src \
  -g '*.rs' \
  | head -n 1800

printf '%s\n' '--- function lowering entry and body traversal ---'
rg -n -C 22 \
  'lower_stmts|lower_stmt|f.body|method.body|body.iter|lower_function|lower_method' \
  crates/perry-codegen/src/codegen/function.rs \
  crates/perry-codegen/src/codegen/method.rs \
  crates/perry-codegen/src/codegen/closure.rs \
  crates/perry-codegen/src/stmt/mod.rs \
  -g '*.rs' \
  | head -n 2200

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

p = Path("crates/perry-codegen/src/codegen/mod.rs")
lines = p.read_text().splitlines()
for i, line in enumerate(lines):
    if "compiler_private_async" in line or "collect_compiler_private" in line:
        lo, hi = max(0, i-35), min(len(lines), i+45)
        print(f"\n--- {p}:{i+1} ---")
        for n in range(lo, hi):
            print(f"{n+1}: {lines[n]}")
PY

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- compiler-private collector definition ---'
rg -n 'fn collect_compiler_private_async_control_locals_in_stmts|collect_compiler_private_async_control_locals_in_stmts' \
  crates/perry-codegen/src/boxed_vars.rs
sed -n '1,180p' crates/perry-codegen/src/boxed_vars.rs

printf '%s\n' '--- closure lowering and capture slot setup ---'
rg -n -C 20 \
  'closure_captures|load_boxed_local_pointer|js_closure_get_capture_bits|collect_prealloc_box_ids_in_stmts|PreallocateBoxes' \
  crates/perry-codegen/src/codegen/closure.rs \
  crates/perry-codegen/src/boxed_vars.rs \
  crates/perry-codegen/src/expr/mod.rs \
  -g '*.rs' \
  | head -n 1800

printf '%s\n' '--- codegen body lowering calls ---'
rg -n -C 16 \
  'lower_stmts|lower_stmt|lower_expr|f.body|body.as_slice|body.iter' \
  crates/perry-codegen/src/codegen/function.rs \
  crates/perry-codegen/src/codegen/method.rs \
  crates/perry-codegen/src/codegen/closure.rs \
  -g '*.rs' \
  | head -n 1800

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

p = Path("crates/perry-codegen/src/boxed_vars.rs")
lines = p.read_text().splitlines()
for i, line in enumerate(lines):
    if "collect_compiler_private_async_control_locals_in_stmts" in line:
        lo, hi = max(0, i-8), min(len(lines), i+120)
        print(f"--- {p}:{i+1} ---")
        for n in range(lo, hi):
            print(f"{n+1}: {lines[n]}")
PY

Repository: PerryTS/perry

Length of output: 50369


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- all PreallocateBoxes constructors ---'
rg -n -C 5 \
  'Stmt::PreallocateBoxes\(|Stmt::PreallocateTdzBoxes\(' \
  crates -g '*.rs'

printf '%s\n' '--- internal control names outside generator lowering ---'
rg -n -C 5 \
  '__gen_state|__gen_pending_type|__gen_done|__gen_executing' \
  crates tests test-files 2>/dev/null \
  | head -n 1800

printf '%s\n' '--- tests for private control cells and preallocation ---'
rg -n -C 8 \
  'compiler_private_async|primitive_i32_control_cell|primitive_i1_control_cell|PreallocateBoxes|__gen_state|__gen_done|__gen_executing|__gen_pending_type' \
  crates tests test-files 2>/dev/null \
  | head -n 2200

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

# Read-only structural verifier for the invariant relevant to this review.
files = [p for root in (Path("crates/perry-transform/src"), Path("crates/perry-codegen/src"))
         for p in root.rglob("*.rs")]
constructors = []
for p in files:
    for i, line in enumerate(p.read_text().splitlines(), 1):
        if re.search(r'Stmt::Preallocate(?:Tdz)?Boxes\s*\(', line):
            constructors.append((str(p), i, line.strip()))

print("PREALLOC_CONSTRUCTORS")
for item in constructors:
    print(f"{item[0]}:{item[1]}:{item[2]}")

lower = Path("crates/perry-transform/src/generator/lower.rs").read_text()
print("\nGENERATOR_FIRST_PREALLOC",
      bool(re.search(r'new_body\.push\(Stmt::PreallocateBoxes\(prealloc_ids\)\);', lower)))

collector = Path("crates/perry-codegen/src/boxed_vars.rs").read_text()
rules = {
    "i32_names": bool(re.search(r'"__gen_state"\s*\|\s*"__gen_pending_type"', collector)),
    "i1_names": bool(re.search(r'"__gen_done"\s*\|\s*"__gen_executing"', collector)),
    "requires_preallocated": bool(re.search(r'if preallocated\.contains\(id\)', collector)),
}
print("COLLECTOR_RULES", rules)

for name in ("__gen_state", "__gen_pending_type", "__gen_done", "__gen_executing"):
    occurrences = []
    for p in files:
        for i, line in enumerate(p.read_text().splitlines(), 1):
            if name in line:
                occurrences.append(f"{p}:{i}")
    print(name, len(occurrences), *occurrences[:12], sep=" | ")
PY

Repository: PerryTS/perry

Length of output: 12687


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- all-repository preallocation construction ---'
rg -n -C 8 \
  'PreallocateBoxes|PreallocateTdzBoxes|prealloc.*box|preallocate.*box' \
  . -g '*.rs' \
  | grep -E 'push|insert|emit|lower|Preallocate' \
  | head -n 1800

printf '%s\n' '--- control-value coercion helpers ---'
rg -n -C 18 \
  'lower_i32_control_store_value|lower_i1_control_store_value|compiler_private_async_control' \
  crates/perry-codegen/src/expr/mod.rs \
  crates/perry-codegen/src/stmt/let_stmt.rs \
  -g '*.rs' \
  | head -n 1600

printf '%s\n' '--- generated-name reservation or user-name filtering ---'
rg -n -C 8 \
  '__gen_state|__gen_pending_type|__gen_done|__gen_executing|reserved.*name|internal.*name|name.*reserved' \
  crates/perry-hir crates/perry-transform crates/perry-codegen \
  -g '*.rs' \
  | head -n 1800

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

p = Path("crates/perry-codegen/src/expr/mod.rs")
lines = p.read_text().splitlines()
for i, line in enumerate(lines):
    if "lower_i32_control_store_value" in line or "lower_i1_control_store_value" in line:
        lo, hi = max(0, i-8), min(len(lines), i+65)
        print(f"\n--- {p}:{i+1} ---")
        for n in range(lo, hi):
            print(f"{n+1}: {lines[n]}")
PY

Repository: PerryTS/perry

Length of output: 13622


Avoid name-based classification of user locals

A preallocated user binding named __gen_state, __gen_pending_type, __gen_done, or __gen_executing matches the compiler-private classifier in crates/perry-codegen/src/boxed_vars.rs:1427-1438. Its writes then use primitive-cell lowering, so a Number binding can be truncated to i32. Use compiler-generated IDs or reserve these names. Add a regression with a captured user binding.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/src/expr/mod.rs` around lines 2300 - 2333, Replace the
name-based checks in is_compiler_private_async_i32_control_local and related
classifiers with compiler-generated identity/IDs, or reserve the __gen_state,
__gen_pending_type, __gen_done, and __gen_executing names so user locals cannot
match them. Ensure ordinary captured user bindings continue through
boxed-variable lowering without primitive-cell truncation, and add a regression
covering a captured user binding with one of these names.

Source: Coding guidelines

pub(crate) fn box_i1_for_compat_shadow(ctx: &mut FnCtx<'_>, value: &str) -> String {
let bits = ctx.block().select(
I1,
Expand Down Expand Up @@ -2381,7 +2415,7 @@ fn lower_async_i32_control_const_compare(
let Some(ptr) = load_boxed_local_pointer(ctx, id)? else {
return Ok(None);
};
let value = ctx.block().call(I32, "js_i32_box_get", &[(I64, &ptr)]);
let value = load_async_i32_control_cell(ctx, &ptr);
let constant_s = constant.to_string();
let (lhs, rhs) = if local_on_left {
(value.as_str(), constant_s.as_str())
Expand Down Expand Up @@ -2921,7 +2955,7 @@ pub(crate) fn lower_expr_value(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<Optio
let Some(ptr) = load_boxed_local_pointer(ctx, *id)? else {
return Ok(None);
};
let value = ctx.block().call(I32, "js_i32_box_get", &[(I64, &ptr)]);
let value = load_async_i32_control_cell(ctx, &ptr);
let lowered = LoweredValue::i32(value);
ctx.record_lowered_value(
"LocalGet",
Expand All @@ -2941,8 +2975,7 @@ pub(crate) fn lower_expr_value(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<Optio
let Some(ptr) = load_boxed_local_pointer(ctx, *id)? else {
return Ok(None);
};
let value_i32 = ctx.block().call(I32, "js_bool_box_get", &[(I64, &ptr)]);
let value = ctx.block().icmp_ne(I32, &value_i32, "0");
let value = load_async_i1_control_cell(ctx, &ptr);
let lowered = LoweredValue::i1(value);
ctx.record_lowered_value(
"LocalGet",
Expand Down Expand Up @@ -3009,8 +3042,7 @@ pub(crate) fn lower_expr_value(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<Optio
return Ok(None);
};
let value_i32 = lower_i32_control_store_value(ctx, value)?;
ctx.block()
.call_void("js_i32_box_set", &[(I64, &ptr), (I32, &value_i32)]);
store_async_i32_control_cell(ctx, &ptr, &value_i32);
record_native_arena_owner_assignment(ctx, *id, value.as_ref());
record_int_facts_for_local_set(ctx, *id, value);
let lowered = LoweredValue::i32(value_i32);
Expand All @@ -3035,9 +3067,7 @@ pub(crate) fn lower_expr_value(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<Optio
return Ok(None);
};
let value_i1 = lower_i1_control_store_value(ctx, value)?;
let value_i32 = ctx.block().zext(I1, &value_i1, I32);
ctx.block()
.call_void("js_bool_box_set", &[(I64, &ptr), (I32, &value_i32)]);
store_async_i1_control_cell(ctx, &ptr, &value_i1);
record_native_arena_owner_assignment(ctx, *id, value.as_ref());
let lowered = LoweredValue::i1(value_i1);
ctx.record_lowered_value(
Expand Down
7 changes: 2 additions & 5 deletions crates/perry-codegen/src/stmt/let_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1154,13 +1154,10 @@ pub(crate) fn lower_let(
let bptr = blk.load(I64, &slot_clone);
if crate::expr::is_compiler_private_async_i32_control_local(ctx, id) {
let init_i32 = crate::expr::lower_i32_control_store_value(ctx, init_expr)?;
ctx.block()
.call_void("js_i32_box_set", &[(I64, &bptr), (I32, &init_i32)]);
crate::expr::store_async_i32_control_cell(ctx, &bptr, &init_i32);
} else if crate::expr::is_compiler_private_async_i1_control_local(ctx, id) {
let init_i1 = crate::expr::lower_i1_control_store_value(ctx, init_expr)?;
let init_i32 = ctx.block().zext(I1, &init_i1, I32);
ctx.block()
.call_void("js_bool_box_set", &[(I64, &bptr), (I32, &init_i32)]);
crate::expr::store_async_i1_control_cell(ctx, &bptr, &init_i1);
} else {
let init_val =
lower_expr_with_expected_type(ctx, init_expr, Some(&refined_ty))?;
Expand Down
22 changes: 17 additions & 5 deletions crates/perry-codegen/tests/native_proof_regressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7095,19 +7095,31 @@ fn compiler_private_async_control_cells_use_primitive_heap_boxes() {
compiler_private_async_control_body(),
);

for symbol in [
"call i64 @js_i32_box_alloc",
for symbol in ["call i64 @js_i32_box_alloc", "call i64 @js_bool_box_alloc"] {
assert!(
ir.contains(symbol),
"expected compiler-private control lowering to emit {symbol}:\n{ir}"
);
}
for checked_access in [
"call i32 @js_i32_box_get",
"call void @js_i32_box_set",
"call i64 @js_bool_box_alloc",
"call i32 @js_bool_box_get",
"call void @js_bool_box_set",
] {
assert!(
ir.contains(symbol),
"expected compiler-private control lowering to emit {symbol}:\n{ir}"
!ir.contains(checked_access),
"proven compiler-private control cells must bypass checked box access ({checked_access}):\n{ir}"
);
}
assert!(
ir.contains("inttoptr i64")
&& ir.contains("load i32, ptr")
&& ir.contains("store i32")
&& ir.contains("load i1, ptr")
&& ir.contains("store i1"),
"compiler-private controls should use direct typed cell loads/stores:\n{ir}"
);
assert!(
ir.contains("icmp eq i32"),
"__gen_state constant comparisons should stay as i32 compares:\n{ir}"
Expand Down
Loading