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: 6 additions & 0 deletions changelog.d/argsarrow-mapped-arguments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- Sloppy functions with simple parameters now use the same mapped-arguments parameter cells in their body and in nested arrow captures. The module-wide boxing analysis previously omitted cells allocated by the arguments prologue: capture creation forwarded a cell pointer, while the arrow body treated it as an ordinary value. Record these cells through the existing arguments-elision proof, including function expressions, without materializing read-only arguments objects.
- Add Node-compared CommonJS coverage for both aliasing directions, nested and returned arrows, escaped arguments, compound receiver snapshots, strict and non-simple parameters, missing arguments, changed length, and deleted indices. Unit tests cover shared capture representation, preserved elision, and invalidation of every parameter's compound-assignment shortcut.

- Apply ordinary function strictness and simple-parameter rules to object methods, bind their arguments before parameter defaults, and prevent scope grouping from replacing a parameter's prologue cell when a var declaration reuses its binding. Function-constructor bodies now begin with their own sloppy strict-mode context unless their source contains a strict directive.

Function-expression var redeclarations now reuse their parameter bindings.
10 changes: 8 additions & 2 deletions crates/perry-codegen/src/boxed_vars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,9 @@ pub(crate) fn collect_boxed_vars(stmts: &[perry_hir::Stmt]) -> HashSet<u32> {
///
/// A param is boxed when it is referenced inside some closure in `body`
/// AND mutated — either inside a closure or in the enclosing scope. The
/// synthesized `arguments` param is excluded: it carries its own
/// mapped-box handling (`materialize_arguments_object`).
/// synthesized `arguments` param is excluded, but its mapped parameters
/// share cells even without a syntactic local write. Publish those cells
/// module-wide so capture creation and closure bodies agree on their storage.
pub(crate) fn collect_boxed_param_ids(
params: &[perry_hir::Param],
body: &[perry_hir::Stmt],
Expand All @@ -70,6 +71,7 @@ pub(crate) fn collect_boxed_param_ids(
if params.is_empty() {
return out;
}
crate::codegen::arguments::add_arguments_mapped_boxes(params, Some(body), &mut out);
let mut closure_refs: HashSet<u32> = HashSet::new();
let mut closure_writes: HashSet<u32> = HashSet::new();
collect_closure_refs_and_writes_in_stmts(body, &mut closure_refs, &mut closure_writes);
Expand Down Expand Up @@ -1857,3 +1859,7 @@ mod tests {
assert_eq!(refine_type_from_init_simple(&Expr::ProcessExit(None)), None);
}
}

#[cfg(test)]
#[path = "boxed_vars_mapped_tests.rs"]
mod mapped_arguments_tests;
167 changes: 167 additions & 0 deletions crates/perry-codegen/src/boxed_vars_mapped_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
use super::*;
use perry_hir::types::Type;
use perry_hir::{ArgumentsObjectMeta, Expr, Function, Module, Param, Stmt};

fn params(mapped: bool) -> Vec<Param> {
let param = |id, name: &str| Param {
id,
name: name.into(),
ty: Type::Any,
default: None,
decorators: Vec::new(),
is_rest: false,
arguments_object: None,
};
let mut args = param(3, "arguments");
args.arguments_object = Some(ArgumentsObjectMeta {
strict: !mapped,
simple_parameters: true,
mapped_parameter_ids: if mapped { vec![(0, 1), (1, 2)] } else { vec![] },
restricted_callee: !mapped,
});
vec![param(1, "a"), param(2, "b"), args]
}

fn closure(id: u32, params: Vec<Param>, body: Vec<Stmt>, captures: Vec<u32>) -> Expr {
Expr::Closure {
func_id: id,
params,
return_type: Type::Any,
body,
captures,
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,
}
}

fn write() -> Stmt {
Stmt::Expr(Expr::IndexSet {
object: Box::new(Expr::LocalGet(3)),
index: Box::new(Expr::Integer(0)),
value: Box::new(Expr::LocalGet(2)),
})
}

fn module(params: Vec<Param>, body: Vec<Stmt>) -> Module {
let mut m = Module::new("mapped");
m.functions.push(Function {
id: 10,
name: "outer".into(),
type_params: Vec::new(),
params,
return_type: Type::Any,
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,
});
m
}

#[test]
fn mapped_cells_are_known_to_all_capture_bodies_without_local_writes() {
let body = vec![Stmt::Expr(closure(
11,
vec![],
vec![Stmt::Expr(closure(12, vec![], vec![write()], vec![2, 3]))],
vec![2, 3],
))];
let boxed =
crate::codegen::boxed_locals::collect_module_boxed_vars(&module(params(true), body));
assert_eq!(boxed, HashSet::from([1, 2]));
}

#[test]
fn function_expression_mapped_cells_reach_nested_arrows() {
let outer = closure(
11,
params(true),
vec![Stmt::Expr(closure(12, vec![], vec![write()], vec![2, 3]))],
vec![],
);
let mut m = Module::new("expression");
m.init.push(Stmt::Expr(outer));
assert_eq!(
crate::codegen::boxed_locals::collect_module_boxed_vars(&m),
HashSet::from([1, 2])
);
}

#[test]
fn escaped_arguments_publish_cells_but_unmapped_arguments_do_not() {
let body = vec![Stmt::Return(Some(Expr::LocalGet(3)))];
assert_eq!(
collect_boxed_param_ids(&params(true), &body),
HashSet::from([1, 2])
);
assert!(collect_boxed_param_ids(&params(false), &body).is_empty());
let mut nonsimple = params(false);
let meta = nonsimple[2].arguments_object.as_mut().unwrap();
meta.strict = false;
meta.simple_parameters = false;
assert!(collect_boxed_param_ids(&nonsimple, &body).is_empty());
}

#[test]
fn elided_length_and_index_reads_keep_ordinary_parameter_slots() {
for read in [
Expr::PropertyGet {
object: Box::new(Expr::LocalGet(3)),
property: "length".into(),
byte_offset: 0,
},
Expr::IndexGet {
object: Box::new(Expr::LocalGet(3)),
index: Box::new(Expr::Integer(0)),
},
] {
assert!(collect_boxed_param_ids(&params(true), &[Stmt::Return(Some(read))]).is_empty());
}
}

#[test]
fn redeclared_parameters_keep_prologue_cells_in_named_and_expression_bodies() {
let body = vec![
Stmt::Let {
id: 1,
name: "a".into(),
ty: Type::Any,
mutable: true,
init: Some(Expr::Integer(5)),
},
Stmt::Return(Some(Expr::LocalGet(3))),
];
let mut named = module(params(true), body.clone());
crate::scope_env::group_scope_boxes(&mut named);
assert!(matches!(named.functions[0].body[0], Stmt::Let { .. }));
// Codegen must decline even a preallocation handed in by another pass.
named.functions[0]
.body
.insert(0, Stmt::PreallocateBoxes(vec![1]));
let boxed = crate::codegen::boxed_locals::collect_module_boxed_vars(&named);
assert!(
crate::scope_env::ScopeMap::build(&named, &boxed, &HashMap::new())
.slot(1)
.is_none()
);
let mut expression = Module::new("redeclared_expression");
expression
.init
.push(Stmt::Expr(closure(11, params(true), body, vec![])));
crate::scope_env::group_scope_boxes(&mut expression);
let Stmt::Expr(Expr::Closure { body, .. }) = &expression.init[0] else {
panic!("closure retained")
};
assert!(matches!(body[0], Stmt::Let { .. }));
}
12 changes: 6 additions & 6 deletions crates/perry-codegen/src/scope_env/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

use std::collections::{BTreeSet, HashMap, HashSet};

use perry_hir::{Expr, Module as HirModule, Stmt};
use perry_hir::{Expr, Module as HirModule, Param, Stmt};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum DeclKind {
Expand Down Expand Up @@ -341,25 +341,25 @@ pub(super) fn stmt_exprs<'a>(s: &'a Stmt, f: &mut dyn FnMut(&'a Expr)) {

/// Call `f` with the body of every closure literal directly in this body's
/// statements (not inside another closure's body — the caller recurses).
pub(super) fn for_each_closure_in_stmts(stmts: &[Stmt], f: &mut dyn FnMut(&[Stmt])) {
pub(super) fn for_each_closure_in_stmts(stmts: &[Stmt], f: &mut dyn FnMut(&[Param], &[Stmt])) {
for_each_stmt_shallow(stmts, &mut |s| {
stmt_exprs(s, &mut |e| for_each_closure_in_expr(e, f));
});
}

/// Call `f` with the body of every closure literal in `e`, not descending into
/// the bodies themselves (param defaults of a closure are searched).
pub(super) fn for_each_closure_in_expr(e: &Expr, f: &mut dyn FnMut(&[Stmt])) {
if let Expr::Closure { body, .. } = e {
f(body);
pub(super) fn for_each_closure_in_expr(e: &Expr, f: &mut dyn FnMut(&[Param], &[Stmt])) {
if let Expr::Closure { params, body, .. } = e {
f(params, body);
}
perry_hir::walker::walk_expr_children(e, &mut |child| for_each_closure_in_expr(child, f));
}

/// How many preallocation statements in the whole module name each id.
pub(super) fn prealloc_counts(hir: &HirModule) -> HashMap<u32, u32> {
let mut counts: HashMap<u32, u32> = HashMap::new();
super::for_each_body(hir, &mut |stmts: &[Stmt]| {
super::for_each_body(hir, &mut |_: &[Param], stmts: &[Stmt]| {
for_each_stmt_shallow(stmts, &mut |s| {
if let Stmt::PreallocateBoxes(ids) | Stmt::PreallocateTdzBoxes(ids) = s {
for id in ids {
Expand Down
33 changes: 20 additions & 13 deletions crates/perry-codegen/src/scope_env/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ pub mod pass;

use std::collections::{HashMap, HashSet};

use perry_hir::{Expr, Module as HirModule, Stmt};
use perry_hir::{Expr, Module as HirModule, Param, Stmt};

pub use pass::group_scope_boxes;

Expand Down Expand Up @@ -158,13 +158,14 @@ impl ScopeMap {
return map;
}
let counts = analysis::prealloc_counts(hir);
for_each_body(hir, &mut |stmts: &[Stmt]| {
for_each_body(hir, &mut |params: &[Param], stmts: &[Stmt]| {
let mut interest = HashSet::new();
analysis::collect_shallow_prealloc_ids(stmts, &mut interest);
interest.retain(|id| {
counts.get(id) == Some(&1)
&& module_boxed_vars.contains(id)
&& !module_globals.contains_key(id)
&& !params.iter().any(|p| p.id == *id)
});
if interest.is_empty() {
return;
Expand Down Expand Up @@ -228,11 +229,11 @@ pub(crate) fn compact_root_slots(

/// Visit every function-like body in the module: top-level functions, class
/// members, the module init, and every closure body nested anywhere in them.
pub(crate) fn for_each_body(hir: &HirModule, f: &mut dyn FnMut(&[Stmt])) {
let mut roots: Vec<&[Stmt]> = vec![&hir.init];
pub(crate) fn for_each_body(hir: &HirModule, f: &mut dyn FnMut(&[Param], &[Stmt])) {
let mut roots: Vec<(&[Param], &[Stmt])> = vec![(&[], &hir.init)];
let mut root_exprs: Vec<&Expr> = Vec::new();
for func in &hir.functions {
roots.push(&func.body);
roots.push((&func.params, &func.body));
push_param_defaults(&func.params, &mut root_exprs);
}
for c in &hir.classes {
Expand All @@ -245,7 +246,7 @@ pub(crate) fn for_each_body(hir: &HirModule, f: &mut dyn FnMut(&[Stmt])) {
.chain(c.computed_members.iter().map(|m| &m.function))
.chain(c.constructor.iter())
{
roots.push(&m.body);
roots.push((&m.params, &m.body));
push_param_defaults(&m.params, &mut root_exprs);
}
for field in c.fields.iter().chain(c.static_fields.iter()) {
Expand All @@ -259,18 +260,24 @@ pub(crate) fn for_each_body(hir: &HirModule, f: &mut dyn FnMut(&[Stmt])) {
for g in &hir.globals {
root_exprs.extend(g.init.iter());
}
for stmts in roots {
f(stmts);
analysis::for_each_closure_in_stmts(stmts, &mut |body| for_each_body_in_closure(body, f));
for (params, stmts) in roots {
f(params, stmts);
analysis::for_each_closure_in_stmts(stmts, &mut |params, body| {
for_each_body_in_closure(params, body, f)
});
}
for e in root_exprs {
analysis::for_each_closure_in_expr(e, &mut |body| for_each_body_in_closure(body, f));
analysis::for_each_closure_in_expr(e, &mut |params, body| {
for_each_body_in_closure(params, body, f)
});
}
}

fn for_each_body_in_closure(body: &[Stmt], f: &mut dyn FnMut(&[Stmt])) {
f(body);
analysis::for_each_closure_in_stmts(body, &mut |inner| for_each_body_in_closure(inner, f));
fn for_each_body_in_closure(params: &[Param], body: &[Stmt], f: &mut dyn FnMut(&[Param], &[Stmt])) {
f(params, body);
analysis::for_each_closure_in_stmts(body, &mut |params, inner| {
for_each_body_in_closure(params, inner, f)
});
}

fn push_param_defaults<'a>(params: &'a [perry_hir::Param], out: &mut Vec<&'a Expr>) {
Expand Down
20 changes: 16 additions & 4 deletions crates/perry-codegen/src/scope_env/pass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

use std::collections::{BTreeMap, HashMap, HashSet};

use perry_hir::{Expr, Module as HirModule, Stmt};
use perry_hir::{Expr, Module as HirModule, Param, Stmt};

use super::analysis::{self, DeclKind};

Expand All @@ -34,13 +34,20 @@ pub fn group_scope_boxes(hir: &mut HirModule) {
let mut edits: HashMap<usize, ListEdits> = HashMap::new();
let mut grouped: HashSet<u32> = HashSet::new();
let init_ptr = hir.init.as_ptr() as usize;
super::for_each_body(hir, &mut |stmts: &[Stmt]| {
super::for_each_body(hir, &mut |params: &[Param], stmts: &[Stmt]| {
// Module-scope bindings that closures capture are globalized by
// codegen and already have shared storage; leave the init body alone.
if stmts.as_ptr() as usize == init_ptr {
return;
}
plan_body(stmts, &module_boxed, &counts, &mut edits, &mut grouped);
plan_body(
params,
stmts,
&module_boxed,
&counts,
&mut edits,
&mut grouped,
);
});
if grouped.is_empty() {
return;
Expand All @@ -49,6 +56,7 @@ pub fn group_scope_boxes(hir: &mut HirModule) {
}

fn plan_body(
params: &[Param],
stmts: &[Stmt],
module_boxed: &HashSet<u32>,
counts: &HashMap<u32, u32>,
Expand All @@ -58,7 +66,11 @@ fn plan_body(
let mut declared = HashSet::new();
crate::collectors::collect_let_ids(stmts, &mut declared);
analysis::collect_shallow_prealloc_ids(stmts, &mut declared);
declared.retain(|id| module_boxed.contains(id) && counts.get(id).copied().unwrap_or(0) <= 1);
declared.retain(|id| {
module_boxed.contains(id)
&& counts.get(id).copied().unwrap_or(0) <= 1
&& !params.iter().any(|p| p.id == *id)
});
if declared.is_empty() {
return;
}
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-hir/src/lower/const_fold_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,11 @@ pub(crate) fn try_const_fold_function_construct_kind(

let outer_strict = ctx.current_strict;
ctx.current_strict = false;
// A Function constructor body has its own strict context; it does not
// inherit the enclosing source's strict-mode stack or module default.
ctx.enter_strict_mode(false);
let lowered_result = lower_fn_expr(ctx, fn_expr);
ctx.exit_strict_mode();
ctx.current_strict = outer_strict;
let lowered = match lowered_result {
Ok(l) => l,
Expand Down
Loading
Loading