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
6 changes: 3 additions & 3 deletions crates/perry-codegen/src/lower_call/capture_writeback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ pub(crate) fn emit_class_capture_writeback(
let cap_args_start = new_args.len().saturating_sub(cap_params.len());

for (cap_idx, param) in cap_params.iter().enumerate() {
let Some(id_str) = param.name.strip_prefix("__perry_cap_") else {
let Some(name_outer_id) = perry_hir::cap_fields::cap_field_outer_id(&param.name) else {
continue;
};
// Prefer position-based lookup using new_args: the arg at index
Expand All @@ -71,9 +71,9 @@ pub(crate) fn emit_class_capture_writeback(
None
}
})
// Fallback to the numeric suffix from __perry_cap_<id> for
// Fallback to the numeric id parsed from the cap name for
// call sites that don't supply new_args (empty slice).
.or_else(|| id_str.parse::<u32>().ok());
.or(Some(name_outer_id));
let Some(outer_id) = current_scope_id else {
continue;
};
Expand Down
5 changes: 1 addition & 4 deletions crates/perry-codegen/src/lower_call/new_ctor_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,10 +266,7 @@ pub(super) fn new_site_args_carry_appended_caps(class: &perry_hir::Class, args:
let tail = &args[args.len() - cap_params.len()..];
tail.iter().zip(cap_params.iter()).all(|(arg, p)| {
matches!(arg, Expr::LocalGet(id)
if p.name
.strip_prefix("__perry_cap_")
.and_then(|s| s.parse::<u32>().ok())
== Some(*id))
if perry_hir::cap_fields::cap_field_outer_id(&p.name) == Some(*id))
})
}

Expand Down
76 changes: 76 additions & 0 deletions crates/perry-hir/src/cap_fields.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//! Class-capture instance-field naming.
//!
//! A class whose members reference enclosing-scope locals stashes those
//! captured values onto each instance as `this.__perry_cap_*` fields (written
//! by the constructor) and rebinds them in member prologues. The field name
//! must be unique **per defining module**, not just per capture id: local ids
//! restart at 0 in every module, and `super()` runs the parent constructor's
//! stashes on the SHARED instance. With bare `__perry_cap_<id>` names, a
//! derived-class method rebinding its own capture id K read the PARENT
//! MODULE's captured local K whenever the parent chain crossed modules and a
//! parent-side capture happened to share the id — Next.js's
//! `NextNodeServer.getBuildId` (next-server.js) rebound its `_fs` capture and
//! got base-server.js's `_interop_require_wildcard(require("path"))` instead:
//! `_fs.default.readFileSync(...)` dispatched on the `path` namespace,
//! returned undefined, and `.trim()` killed the server at boot.
//!
//! The name therefore embeds a stable per-module salt:
//! `__perry_cap_<id>m<salt-hex>`. Same-module inheritance keeps sharing
//! parent stashes (equal salt ⇒ equal names); cross-module chains are
//! isolated. Sites that need the outer id back (ctor-arg alignment, factory
//! specialization, capture writeback) parse the leading digits via
//! [`cap_field_outer_id`] instead of assuming the whole suffix is numeric.

/// Prefix shared by every class-capture field/param name.
pub const CAP_FIELD_PREFIX: &str = "__perry_cap_";

/// Instance-field / ctor-param name for a class-captured local. `salt` is the
/// defining module's stable salt (`LoweringContext::cap_salt`).
pub fn cap_field_name(salt: u64, id: u32) -> String {
format!("{CAP_FIELD_PREFIX}{id}m{:012x}", salt & 0xFFFF_FFFF_FFFF)
}

/// Parse the outer local id from a cap field/param name. Accepts both the
/// salted `__perry_cap_<id>m<salt>` form and the legacy `__perry_cap_<id>`
/// (still produced by pre-salt HIR in caches/tests).
pub fn cap_field_outer_id(name: &str) -> Option<u32> {
let suffix = name.strip_prefix(CAP_FIELD_PREFIX)?;
let end = suffix
.bytes()
.position(|b| !b.is_ascii_digit())
.unwrap_or(suffix.len());
if end == 0 {
return None;
}
suffix[..end].parse().ok()
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn name_roundtrip() {
let n = cap_field_name(0xDEAD_BEEF_CAFE, 13);
assert!(n.starts_with(CAP_FIELD_PREFIX));
assert_eq!(cap_field_outer_id(&n), Some(13));
}

#[test]
fn legacy_unsalted_parses() {
assert_eq!(cap_field_outer_id("__perry_cap_7"), Some(7));
}

#[test]
fn distinct_modules_distinct_names() {
assert_ne!(cap_field_name(1, 13), cap_field_name(2, 13));
assert_eq!(cap_field_name(9, 4), cap_field_name(9, 4));
}

#[test]
fn non_cap_names_reject() {
assert_eq!(cap_field_outer_id("__perry_capX"), None);
assert_eq!(cap_field_outer_id("__perry_cap_"), None);
assert_eq!(cap_field_outer_id("distDir"), None);
}
}
1 change: 1 addition & 0 deletions crates/perry-hir/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

pub mod analysis;
pub mod audit;
pub mod cap_fields;
pub mod capability;
mod class_accessors;
pub mod deferral;
Expand Down
7 changes: 7 additions & 0 deletions crates/perry-hir/src/lower/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1149,6 +1149,13 @@ impl LoweringContext {
.map(|(_, params, ret)| (params, ret))
}

/// Stable per-module salt for class-capture field names (see
/// `crate::cap_fields`). Reuses the tagged-template site salt — a stable
/// hash of the module's source path.
pub(crate) fn cap_salt(&self) -> u64 {
self.tagged_template_site_salt
}

pub(crate) fn register_native_module(
&mut self,
local_name: String,
Expand Down
50 changes: 27 additions & 23 deletions crates/perry-hir/src/lower/shared_mutable_capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ fn collect_assigned_deep_expr(expr: &Expr, out: &mut HashSet<LocalId>) {
/// Detect shared-mutable class captures and rewrite them to one-element array
/// boxes. A no-op when there are none (the common case), so non-capturing /
/// immutable-capture code is left byte-identical.

/// Does `name` denote a class-capture field/param for one of `ids`?
/// Matches by parsed outer id (see `crate::cap_fields`): the names carry a
/// per-module salt, and these per-module passes only ever compare names
/// minted by this module's own lowering.
fn is_cap_name_of(name: &str, ids: &HashSet<LocalId>) -> bool {
crate::cap_fields::cap_field_outer_id(name).is_some_and(|id| ids.contains(&id))
}

pub(crate) fn desugar_shared_mutable_captures(module: &mut Module) {
// Bisection escape hatch (#5951): disable the desugar to isolate its effect.
if std::env::var("PERRY_NO_5951").is_ok() {
Expand Down Expand Up @@ -152,10 +161,7 @@ pub(crate) fn desugar_shared_mutable_captures(module: &mut Module) {
// Match them BY NAME within each member and rewrite only that member's body
// with its own ids (never the declaring `shared` set — the declaring `Let`
// that gets array-wrapped lives outside the class).
let targets: HashSet<String> = all_shared
.iter()
.map(|id| format!("__perry_cap_{}", id))
.collect();
let targets: &HashSet<LocalId> = &all_shared;
let no_shared: HashSet<LocalId> = HashSet::new();
for c in &mut module.classes {
for m in &mut c.methods {
Expand Down Expand Up @@ -189,7 +195,7 @@ pub(crate) fn desugar_shared_mutable_captures(module: &mut Module) {
collect_let_names_expr(key, &mut names);
}
for (id, n) in names {
if targets.contains(&n) {
if is_cap_name_of(&n, targets) {
ctor_ids.insert(id);
}
}
Expand Down Expand Up @@ -259,9 +265,9 @@ pub(crate) fn desugar_shared_mutable_captures(module: &mut Module) {

/// The ids in `f`'s own scope (params + `Let`s, descending into nested
/// closures) whose NAME is a flagged `__perry_cap_<id>` rebind target.
fn collect_fn_target_ids(f: &Function, targets: &HashSet<String>, out: &mut HashSet<LocalId>) {
fn collect_fn_target_ids(f: &Function, targets: &HashSet<LocalId>, out: &mut HashSet<LocalId>) {
for p in &f.params {
if targets.contains(&p.name) {
if is_cap_name_of(&p.name, targets) {
out.insert(p.id);
}
}
Expand All @@ -270,7 +276,7 @@ fn collect_fn_target_ids(f: &Function, targets: &HashSet<String>, out: &mut Hash
collect_let_names_stmt(s, &mut names);
}
for (id, n) in names {
if targets.contains(&n) {
if is_cap_name_of(&n, targets) {
out.insert(id);
}
}
Expand All @@ -281,7 +287,7 @@ fn collect_fn_target_ids(f: &Function, targets: &HashSet<String>, out: &mut Hash
/// its params and deep `Let`s/closure params; see `retain_unambiguous`).
fn rewrite_member_scoped(
f: &mut Function,
targets: &HashSet<String>,
targets: &HashSet<LocalId>,
no_shared: &HashSet<LocalId>,
) {
let mut ids: HashSet<LocalId> = HashSet::new();
Expand Down Expand Up @@ -350,19 +356,16 @@ fn collect_declared_counts_expr(expr: &Expr, out: &mut HashMap<LocalId, u32>) {
}

fn retype_capture_holders(module: &mut Module, shared: &HashSet<LocalId>) {
let targets: HashSet<String> = shared
.iter()
.map(|id| format!("__perry_cap_{}", id))
.collect();
let targets: &HashSet<LocalId> = shared;
for c in &mut module.classes {
for f in &mut c.fields {
if targets.contains(&f.name) {
if is_cap_name_of(&f.name, targets) {
f.ty = Type::Any;
}
}
let mut retype_fn = |f: &mut Function| {
for p in &mut f.params {
if targets.contains(&p.name) {
if is_cap_name_of(&p.name, targets) {
p.ty = Type::Any;
}
}
Expand All @@ -389,15 +392,15 @@ fn retype_capture_holders(module: &mut Module, shared: &HashSet<LocalId>) {
}
}

fn retype_lets_in_stmts(stmts: &mut [Stmt], targets: &HashSet<String>) {
fn retype_lets_in_stmts(stmts: &mut [Stmt], targets: &HashSet<LocalId>) {
for s in stmts.iter_mut() {
retype_lets_in_stmt(s, targets);
}
}

fn retype_lets_in_stmt(stmt: &mut Stmt, targets: &HashSet<String>) {
fn retype_lets_in_stmt(stmt: &mut Stmt, targets: &HashSet<LocalId>) {
if let Stmt::Let { name, ty, init, .. } = stmt {
if targets.contains(name) {
if is_cap_name_of(name, targets) {
*ty = Type::Any;
}
if let Some(e) = init {
Expand Down Expand Up @@ -446,7 +449,7 @@ fn retype_lets_in_stmt(stmt: &mut Stmt, targets: &HashSet<String>) {
}
}

fn retype_lets_in_expr(expr: &mut Expr, targets: &HashSet<String>) {
fn retype_lets_in_expr(expr: &mut Expr, targets: &HashSet<LocalId>) {
if let Expr::Closure { body, .. } = expr {
retype_lets_in_stmts(body, targets);
}
Expand Down Expand Up @@ -492,12 +495,13 @@ fn detect_shared_in_body(body: &[Stmt], classes: &HashMap<&str, &Class>) -> Hash
}

fn class_mutates_capture(c: &Class, id: LocalId) -> bool {
let target = format!("__perry_cap_{}", id);
let id_name = collect_class_names(c);
let assigned = collect_class_assigned(c);
assigned
.iter()
.any(|aid| id_name.get(aid).is_some_and(|n| *n == target))
assigned.iter().any(|aid| {
id_name
.get(aid)
.is_some_and(|n| crate::cap_fields::cap_field_outer_id(n) == Some(id))
})
}

/// id -> name across a class: every member function's PARAMS (a field-init
Expand Down
23 changes: 13 additions & 10 deletions crates/perry-hir/src/lower_decl/class_captures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub fn synthesize_class_captures(
constructor: &mut Option<Function>,
static_methods: &mut Vec<Function>,
) {
let cap_salt = ctx.cap_salt();
let module_level_ids = ctx.module_level_ids.clone();
let outer_scope_ids: std::collections::HashSet<LocalId> =
ctx.locals.iter().map(|(_, id, _)| *id).collect();
Expand Down Expand Up @@ -140,7 +141,9 @@ pub fn synthesize_class_captures(
let inherited_cap_ids: std::collections::HashSet<LocalId> = captures_vec
.iter()
.copied()
.filter(|cid| inherited_cap_field_names.contains(&format!("__perry_cap_{}", cid)))
.filter(|cid| {
inherited_cap_field_names.contains(&crate::cap_fields::cap_field_name(cap_salt, *cid))
})
.collect();

// 1. Hidden fields keyed by outer id, skipping inherited.
Expand All @@ -149,7 +152,7 @@ pub fn synthesize_class_captures(
continue;
}
fields.push(ClassField {
name: format!("__perry_cap_{}", cid),
name: crate::cap_fields::cap_field_name(cap_salt, cid),
key_expr: None,
ty: Type::Any,
init: None,
Expand All @@ -161,7 +164,7 @@ pub fn synthesize_class_captures(
if let Some(existing) = ctx.lookup_class_field_names(name) {
let mut updated: Vec<String> = existing.to_vec();
for &cid in &captures_vec {
let field_name = format!("__perry_cap_{}", cid);
let field_name = crate::cap_fields::cap_field_name(cap_salt, cid);
if !updated.contains(&field_name) {
updated.push(field_name);
}
Expand Down Expand Up @@ -203,7 +206,7 @@ pub fn synthesize_class_captures(
// enough to defer to a follow-up.
let field_propagation: std::collections::HashMap<LocalId, String> = captures_vec
.iter()
.map(|&cid| (cid, format!("__perry_cap_{}", cid)))
.map(|&cid| (cid, crate::cap_fields::cap_field_name(cap_salt, cid)))
.collect();

// Helper closure: build a fresh-id map for one function's body,
Expand Down Expand Up @@ -234,15 +237,15 @@ pub fn synthesize_class_captures(
// (same machinery as the ctor param rebinds above).
prologue.push(Stmt::Let {
id: new_id,
name: format!("__perry_cap_{}", outer_id),
name: crate::cap_fields::cap_field_name(cap_salt, outer_id),
ty,
mutable: true,
init: Some(Expr::ClassCaptureValue {
class_name: name.to_string(),
index: index as u32,
fallback: Some(Box::new(Expr::PropertyGet {
object: Box::new(Expr::This),
property: format!("__perry_cap_{}", outer_id),
property: crate::cap_fields::cap_field_name(cap_salt, outer_id),
})),
prefer_fallback: true,
}),
Expand Down Expand Up @@ -334,7 +337,7 @@ pub fn synthesize_class_captures(
id_map.insert(outer_id, new_id);
prologue.push(Stmt::Let {
id: new_id,
name: format!("__perry_cap_{}", outer_id),
name: crate::cap_fields::cap_field_name(cap_salt, outer_id),
ty: captured_outer_types
.get(&outer_id)
.cloned()
Expand Down Expand Up @@ -377,7 +380,7 @@ pub fn synthesize_class_captures(
id_map.insert(outer_id, new_id);
prologue.push(Stmt::Let {
id: new_id,
name: format!("__perry_cap_{}", outer_id),
name: crate::cap_fields::cap_field_name(cap_salt, outer_id),
ty: captured_outer_types
.get(&outer_id)
.cloned()
Expand Down Expand Up @@ -494,7 +497,7 @@ pub fn synthesize_class_captures(
.unwrap_or(Type::Any);
ctor.params.push(Param {
id: fresh_param_id,
name: format!("__perry_cap_{}", outer_id),
name: crate::cap_fields::cap_field_name(cap_salt, outer_id),
ty,
default: None,
decorators: Vec::new(),
Expand All @@ -518,7 +521,7 @@ pub fn synthesize_class_captures(
)));
assignment_stmts.push(Stmt::Expr(Expr::PropertySet {
object: Box::new(Expr::This),
property: format!("__perry_cap_{}", outer_id),
property: crate::cap_fields::cap_field_name(cap_salt, outer_id),
value: Box::new(Expr::LocalGet(fresh_param_id)),
}));
}
Expand Down
Loading
Loading