diff --git a/changelog.d/14059-codegen-contents-stable-bindings.md b/changelog.d/14059-codegen-contents-stable-bindings.md new file mode 100644 index 00000000000..20076d9b9b8 --- /dev/null +++ b/changelog.d/14059-codegen-contents-stable-bindings.md @@ -0,0 +1 @@ +A property-type proof about an object literal (or a parameter clone guarded by its contents) is kept only when no code can write that object for the binding's whole life: a delete, a store of another type, a write through an alias, a callee, a closure, Object.assign or defineProperty no longer leaves a stale number proof behind, so such reads match node. diff --git a/crates/perry-codegen/src/codegen/closure.rs b/crates/perry-codegen/src/codegen/closure.rs index bc17c5feb3b..b092b718335 100644 --- a/crates/perry-codegen/src/codegen/closure.rs +++ b/crates/perry-codegen/src/codegen/closure.rs @@ -1331,6 +1331,7 @@ pub(super) fn compile_closure( .compiler_private_async_i1_control_locals, scope_map: &cross_module.scope_map, string_accumulator_locals: &cross_module.string_accumulator_locals, + contents_stable_bindings: &cross_module.contents_stable_bindings, string_length_read_of: None, closure_rest_params, local_closure_func_ids: HashMap::new(), diff --git a/crates/perry-codegen/src/codegen/entry.rs b/crates/perry-codegen/src/codegen/entry.rs index 402d28fb48c..553b83be24a 100644 --- a/crates/perry-codegen/src/codegen/entry.rs +++ b/crates/perry-codegen/src/codegen/entry.rs @@ -918,6 +918,7 @@ pub(super) fn compile_module_entry( .compiler_private_async_i1_control_locals, scope_map: &cross_module.scope_map, string_accumulator_locals: &cross_module.string_accumulator_locals, + contents_stable_bindings: &cross_module.contents_stable_bindings, string_length_read_of: None, closure_rest_params, local_closure_func_ids: HashMap::new(), diff --git a/crates/perry-codegen/src/codegen/entry/module_init.rs b/crates/perry-codegen/src/codegen/entry/module_init.rs index dbfe6cc5fa8..400b5ac4f48 100644 --- a/crates/perry-codegen/src/codegen/entry/module_init.rs +++ b/crates/perry-codegen/src/codegen/entry/module_init.rs @@ -370,6 +370,7 @@ pub(super) fn compile_module_init( .compiler_private_async_i1_control_locals, scope_map: &cross_module.scope_map, string_accumulator_locals: &cross_module.string_accumulator_locals, + contents_stable_bindings: &cross_module.contents_stable_bindings, string_length_read_of: None, closure_rest_params, local_closure_func_ids: HashMap::new(), diff --git a/crates/perry-codegen/src/codegen/function.rs b/crates/perry-codegen/src/codegen/function.rs index bf34128d878..5e244d63ed6 100644 --- a/crates/perry-codegen/src/codegen/function.rs +++ b/crates/perry-codegen/src/codegen/function.rs @@ -1404,6 +1404,7 @@ pub(super) fn compile_function( .compiler_private_async_i1_control_locals, scope_map: &cross_module.scope_map, string_accumulator_locals: &cross_module.string_accumulator_locals, + contents_stable_bindings: &cross_module.contents_stable_bindings, string_length_read_of: None, closure_rest_params, local_closure_func_ids: HashMap::new(), diff --git a/crates/perry-codegen/src/codegen/method.rs b/crates/perry-codegen/src/codegen/method.rs index 9dcf35f9823..5a462a13a51 100644 --- a/crates/perry-codegen/src/codegen/method.rs +++ b/crates/perry-codegen/src/codegen/method.rs @@ -674,6 +674,7 @@ pub(super) fn compile_method( .compiler_private_async_i1_control_locals, scope_map: &cross_module.scope_map, string_accumulator_locals: &cross_module.string_accumulator_locals, + contents_stable_bindings: &cross_module.contents_stable_bindings, string_length_read_of: None, closure_rest_params, local_closure_func_ids: HashMap::new(), diff --git a/crates/perry-codegen/src/codegen/method_static.rs b/crates/perry-codegen/src/codegen/method_static.rs index 627609d0f02..9fe4b6d9038 100644 --- a/crates/perry-codegen/src/codegen/method_static.rs +++ b/crates/perry-codegen/src/codegen/method_static.rs @@ -349,6 +349,7 @@ pub(in crate::codegen) fn compile_static_method( .compiler_private_async_i1_control_locals, scope_map: &cross_module.scope_map, string_accumulator_locals: &cross_module.string_accumulator_locals, + contents_stable_bindings: &cross_module.contents_stable_bindings, string_length_read_of: None, closure_rest_params, local_closure_func_ids: HashMap::new(), diff --git a/crates/perry-codegen/src/codegen/mod.rs b/crates/perry-codegen/src/codegen/mod.rs index c3d1895eed7..b3214de978d 100644 --- a/crates/perry-codegen/src/codegen/mod.rs +++ b/crates/perry-codegen/src/codegen/mod.rs @@ -3182,6 +3182,7 @@ fn lower_module( compiler_private_async_i1_control_locals, scope_map: Default::default(), string_accumulator_locals: Default::default(), + contents_stable_bindings: Default::default(), definite_init: Default::default(), disable_buffer_fast_path, program_shadows_buffer_read_method: @@ -3405,6 +3406,7 @@ fn lower_module( cross_module.scope_map = crate::scope_env::ScopeMap::build(hir, &module_boxed_vars, &module_globals); cross_module.string_accumulator_locals = crate::collectors::string_accumulator_locals(hir); + cross_module.contents_stable_bindings = crate::collectors::contents_stable_bindings(hir); cross_module.capture_type_proofs = crate::collectors::typed_locals::capture_proofs(hir, &module_boxed_vars, &module_globals); cross_module.definite_init = crate::collectors::definite_init::collect(hir); @@ -3846,18 +3848,23 @@ fn lower_module( // on `demoted`, `fill(values: Float64Array, nodes: number)` loses // `fill$spec_ta7x10000_i32` entirely, because writing an element // reads as "mutation" of the typed-array parameter. - let body_calls = crate::collectors::body_contains_call(&f.body); + // + // Both halves are the definition of a contents-stable binding + // (`collectors::contents_stable_bindings`), shared with the + // let-binding proofs: a parameter is an entry-aliased binding, so + // it is stable only when no write reaches it (through it, an + // alias or an escape) and the body runs no unknown code and stores + // through no foreign root. A primitive keeps only the rebinding + // half: no callee can reach the caller's copy. let guard_blocked: Vec = f .params .iter() .map(|p| { - crate::collectors::has_any_mutation(&f.body, p.id) - || (body_calls - && spec_return_proof::is_reference_like( - &cross_module.type_aliases, - &p.ty, - 0, - )) + if spec_return_proof::is_reference_like(&cross_module.type_aliases, &p.ty, 0) { + !cross_module.contents_stable_bindings.contains(&p.id) + } else { + crate::collectors::has_any_mutation(&f.body, p.id) + } }) .collect(); let declaration_guards = param_guard::declaration_guards( diff --git a/crates/perry-codegen/src/codegen/opts.rs b/crates/perry-codegen/src/codegen/opts.rs index e104cab63c6..324296f37b3 100644 --- a/crates/perry-codegen/src/codegen/opts.rs +++ b/crates/perry-codegen/src/codegen/opts.rs @@ -1287,6 +1287,9 @@ pub(crate) struct CrossModuleCtx<'a> { /// Untyped string accumulators, module-wide /// (`collectors::string_accumulator_locals`). pub string_accumulator_locals: std::collections::HashSet, + /// Bindings whose object no code writes for their whole life + /// (`collectors::contents_stable_bindings`), module-wide. + pub contents_stable_bindings: std::collections::HashSet, /// Bindings assigned before any reader can run /// (`collectors::definite_init`), module-wide. pub definite_init: crate::collectors::DefiniteInit, diff --git a/crates/perry-codegen/src/collectors/contents_stable.rs b/crates/perry-codegen/src/collectors/contents_stable.rs new file mode 100644 index 00000000000..43d35a08a19 --- /dev/null +++ b/crates/perry-codegen/src/collectors/contents_stable.rs @@ -0,0 +1,734 @@ +//! Contents-stable bindings: the bindings whose object no code can write for +//! the binding's whole life. +//! +//! A proof about a binding's CONTENTS (an object literal's property types, the +//! descriptor a guarded clone checked at entry) is established once, at the +//! binding. It stays true only while nothing writes the object: a `delete`, a +//! store of another type or a `defineProperty` between the proof and a read +//! changes the property's value (`delete o.y; o.y * 10` is NaN, not the +//! Number the literal was born with). This is the one definition of "nothing +//! writes it", used by the let-binding proofs (`FnCtx::admits_contents_proof`) +//! and by the guarded parameter clones (`guard_blocked` in `compile_module`). +//! +//! The analysis is module-wide because a module-level binding is reachable +//! from every function of the module (local ids are module-unique). Aliases +//! (`const t = o`, `t = o`) form one class; a class is stable when no member +//! is +//! - written: a store, update or delete rooted at it (`o.x = v`, `o[i] = v`, +//! `o.a.b = v`, `delete o.y`, `o.push(v)`), or reassigned; +//! - escaped: used as a value anywhere but a read receiver, an operand of an +//! arithmetic/comparison operator, or `return` from its own frame. A call +//! argument, a method receiver (`this`), a value stored into another object, +//! a closure's value use, `with`, and every expression this walk does not +//! list (`Object.assign`, `defineProperty`, spread, ...) are escapes. The one +//! refinement: an argument to a known function whose parameter is itself +//! contents-stable and never returned (that callee can neither write nor +//! leak it). +//! A read of a member's property used as a value (`g(o.a)`, `const t = o.a`) +//! hands out the child object: it escapes the parent unless the field is +//! primitive-valued in every literal of the class. +//! +//! A class whose every source is a literal construction (or another member) +//! is FRESH: nothing outside can hold its object before the binding. Any other +//! class (a parameter, a call result, a `catch` binding) may already be +//! aliased by the caller, so it is stable only if additionally no body that +//! binds a member runs unknown code ([`body_contains_call`], #8094) or stores +//! through a root that is not a fresh member. +//! +//! Exported module bindings are never stable: another module can write them. + +use std::collections::{HashMap, HashSet}; + +use perry_hir::types::FuncId; +use perry_hir::{Expr, Module, Param, Stmt}; + +use super::mutation::body_contains_call; + +/// Every contents-stable binding id of `hir` (lets and parameters). +pub(crate) fn contents_stable_bindings(hir: &Module) -> HashSet { + let mut scan = Scan::default(); + scan.body(&[], &hir.init); + for f in &hir.functions { + scan.body(&f.params, &f.body); + scan.functions + .insert(f.id, f.params.iter().map(|p| (p.id, p.is_rest)).collect()); + } + for c in &hir.classes { + let fields: Vec = c.fields.iter().map(|f| f.name.clone()).collect(); + scan.class_fields.insert(c.name.clone(), fields); + if let Some(ctor) = &c.constructor { + scan.body(&ctor.params, &ctor.body); + } + for f in c + .methods + .iter() + .chain(c.static_methods.iter()) + .chain(c.getters.iter().map(|(_, f)| f)) + .chain(c.setters.iter().map(|(_, f)| f)) + .chain(c.computed_members.iter().map(|m| &m.function)) + { + scan.body(&f.params, &f.body); + } + for field in c.fields.iter().chain(c.static_fields.iter()) { + let roots: Vec<&Expr> = field.key_expr.iter().chain(field.init.iter()).collect(); + if !roots.is_empty() { + scan.begin_body(true); + for e in roots { + scan.expr(e, Use::Value); + } + scan.end_body(); + } + } + } + let exported: HashSet<&str> = hir + .exports + .iter() + .filter_map(|e| match e { + perry_hir::Export::Named { local, .. } => Some(local.as_str()), + _ => None, + }) + .collect(); + for s in &hir.init { + if let Stmt::Let { id, name, .. } = s { + if exported.contains(name.as_str()) { + scan.written.insert(*id); + } + } + } + scan.resolve() +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum Use { + /// The value is consumed where it stands (an operand, a discarded + /// statement value): it reaches no other binding or heap slot. + Consume, + /// The value can reach code or storage this walk does not follow. + Value, + /// `return `: harmless for a binding of the returning frame. + Return, +} + +#[derive(Default)] +struct Scan { + parent: HashMap, + written: HashSet, + /// Members bound by something other than a literal or another member. + entry_aliased: HashSet, + /// Literal initializers per member. + fresh_inits: HashMap>, + /// (parent member, field read) whose value was handed out; `None` is an + /// index or a deeper path. + child_escapes: Vec<(u32, Option)>, + /// (member, callee, argument index) of a known-function call argument. + call_args: Vec<(u32, FuncId, usize)>, + /// Parameter ids of each known function (`id`, `is_rest`). + functions: HashMap>, + class_fields: HashMap>, + returned: HashSet, + /// (frame binding, field read) handed to the caller by `return o.f` + /// (`None`: an index or a deeper path). A caller argument bound to that + /// parameter has that child handed out. + returned_children: Vec<(u32, Option)>, + /// The body (index) that binds each member. + home: HashMap, + /// Per body: does it run unknown code. + body_calls: Vec, + /// Per body: the roots of its stores (`None`: not a local). + store_roots: Vec>>, + current: usize, + /// Bindings of the innermost function frame (for `return`). + frames: Vec>, +} + +impl Scan { + fn begin_body(&mut self, calls: bool) { + self.current = self.body_calls.len(); + self.body_calls.push(calls); + self.store_roots.push(Vec::new()); + self.frames.push(HashSet::new()); + } + + fn end_body(&mut self) { + self.frames.pop(); + } + + fn body(&mut self, params: &[Param], body: &[Stmt]) { + self.begin_body(body_contains_call(body)); + self.params(params); + self.stmts(body); + self.end_body(); + } + + fn params(&mut self, params: &[Param]) { + for p in params { + self.declare(p.id); + self.entry_aliased.insert(p.id); + if let Some(d) = &p.default { + self.expr(d, Use::Value); + } + } + } + + fn declare(&mut self, id: u32) { + self.home.insert(id, self.current); + if let Some(frame) = self.frames.last_mut() { + frame.insert(id); + } + } + + fn find(&mut self, id: u32) -> u32 { + let mut root = id; + while let Some(&p) = self.parent.get(&root) { + if p == root { + break; + } + root = p; + } + let mut cur = id; + while cur != root { + let next = self.parent.insert(cur, root).unwrap_or(root); + cur = next; + } + root + } + + fn union(&mut self, a: u32, b: u32) { + let (ra, rb) = (self.find(a), self.find(b)); + if ra != rb { + self.parent.insert(ra, rb); + } + } + + fn stmts(&mut self, stmts: &[Stmt]) { + for s in stmts { + self.stmt(s); + } + } + + fn stmt(&mut self, s: &Stmt) { + match s { + Stmt::Let { id, init, .. } => self.bind(*id, init.as_ref()), + Stmt::Expr(e) => self.expr(e, Use::Consume), + Stmt::Throw(e) => self.expr(e, Use::Value), + Stmt::Return(Some(e)) => self.expr(e, Use::Return), + Stmt::Return(None) => {} + Stmt::If { + condition, + then_branch, + else_branch, + } => { + self.expr(condition, Use::Consume); + self.stmts(then_branch); + if let Some(b) = else_branch { + self.stmts(b); + } + } + Stmt::While { condition, body } | Stmt::DoWhile { body, condition } => { + self.expr(condition, Use::Consume); + self.stmts(body); + } + Stmt::For { + init, + condition, + update, + body, + } => { + if let Some(i) = init { + self.stmt(i); + } + if let Some(c) = condition { + self.expr(c, Use::Consume); + } + if let Some(u) = update { + self.expr(u, Use::Consume); + } + self.stmts(body); + } + Stmt::Labeled { body, .. } => self.stmt(body), + Stmt::Try { + body, + catch, + finally, + } => { + self.stmts(body); + if let Some(c) = catch { + if let Some((id, _)) = &c.param { + self.declare(*id); + self.entry_aliased.insert(*id); + } + self.stmts(&c.body); + } + if let Some(f) = finally { + self.stmts(f); + } + } + Stmt::Switch { + discriminant, + cases, + } => { + self.expr(discriminant, Use::Consume); + for c in cases { + if let Some(t) = &c.test { + self.expr(t, Use::Consume); + } + self.stmts(&c.body); + } + } + Stmt::PreallocateBoxes(ids) + | Stmt::PreallocateTdzBoxes(ids) + | Stmt::ReleaseBoxes(ids) => { + // A box starts as `undefined` and is filled by a later + // assignment; the slot itself is not a value source. + let _ = ids; + } + Stmt::Break | Stmt::Continue | Stmt::LabeledBreak(_) | Stmt::LabeledContinue(_) => {} + } + } + + /// `id` is bound to `init` (a `let`, or a scoped temporary). + fn bind(&mut self, id: u32, init: Option<&Expr>) { + self.declare(id); + match init { + Some(Expr::LocalGet(other)) => self.union(id, *other), + Some(e) if is_literal_construction(e) => { + self.fresh_inits.entry(id).or_default().push(e.clone()); + if contains_closure(e) { + // A method or accessor of the literal runs with the + // object as `this`. + self.entry_aliased.insert(id); + } + self.expr_children(e, Use::Value); + } + Some(e) => { + self.entry_aliased.insert(id); + self.expr(e, Use::Value); + } + None => { + self.entry_aliased.insert(id); + } + } + } + + /// A store through `target` (the object whose slot is written). + fn store(&mut self, target: &Expr) { + match chain_root(target) { + Some(root) => { + self.written.insert(root); + // A deeper store (`o.a.b = v`) writes a child, which need not + // be fresh even when `o` is. + let direct = matches!(target, Expr::LocalGet(_)); + self.store_roots[self.current].push(direct.then_some(root)); + self.chain_indices(target); + } + None => { + self.store_roots[self.current].push(None); + self.expr(target, Use::Value); + } + } + } + + /// Index expressions along a read chain are consumed as keys. + fn chain_indices(&mut self, e: &Expr) { + match e { + Expr::PropertyGet { object, .. } => self.chain_indices(object), + Expr::IndexGet { object, index, .. } => { + self.chain_indices(object); + self.expr(index, Use::Consume); + } + _ => {} + } + } + + fn escape(&mut self, id: u32, how: Use) { + if how == Use::Return && self.frames.last().is_some_and(|f| f.contains(&id)) { + self.returned.insert(id); + } else if how != Use::Consume { + self.written.insert(id); + } + } + + /// A read receiver: the object is read, not handed out. + fn receiver(&mut self, object: &Expr) { + match object { + Expr::LocalGet(_) => {} + other => self.expr(other, Use::Consume), + } + } + + fn expr(&mut self, e: &Expr, how: Use) { + match e { + Expr::LocalGet(id) => self.escape(*id, how), + Expr::PropertyGet { + object, property, .. + } => { + self.receiver(object); + if how != Use::Consume { + if let Some(root) = chain_root(object) { + let field = + matches!(object.as_ref(), Expr::LocalGet(_)).then(|| property.clone()); + if how == Use::Return + && self.frames.last().is_some_and(|f| f.contains(&root)) + { + self.returned_children.push((root, field)); + } else { + self.child_escapes.push((root, field)); + } + } + } + } + Expr::IndexGet { object, index, .. } => { + self.receiver(object); + self.expr(index, Use::Consume); + if how != Use::Consume { + if let Some(root) = chain_root(object) { + if how == Use::Return + && self.frames.last().is_some_and(|f| f.contains(&root)) + { + self.returned_children.push((root, None)); + } else { + self.child_escapes.push((root, None)); + } + } + } + } + Expr::LocalSet(id, value) => { + self.written.insert(*id); + match value.as_ref() { + Expr::LocalGet(other) => self.union(*id, *other), + v => self.expr(v, Use::Value), + } + } + Expr::Update { id, .. } => { + self.written.insert(*id); + } + Expr::ScopedTemp { id, value, body } => { + self.bind(*id, Some(value)); + self.expr(body, how); + } + Expr::PropertySet { object, value, .. } => { + self.store(object); + self.expr(value, Use::Value); + } + Expr::IndexSet { + object, + index, + value, + .. + } => { + self.store(object); + self.expr(index, Use::Consume); + self.expr(value, Use::Value); + } + Expr::PropertyUpdate { object, .. } => self.store(object), + Expr::IndexUpdate { object, index, .. } => { + self.store(object); + self.expr(index, Use::Consume); + } + Expr::PutValueSet { + target, + key, + value, + receiver, + .. + } => { + self.store(target); + self.store(receiver); + self.expr(key, Use::Consume); + self.expr(value, Use::Value); + } + Expr::Delete(operand) => match operand.as_ref() { + Expr::PropertyGet { object, .. } => self.store(object), + Expr::IndexGet { object, index, .. } => { + self.store(object); + self.expr(index, Use::Consume); + } + other => self.expr(other, Use::Consume), + }, + Expr::ArrayPush { array_id, .. } + | Expr::ArrayPushSpread { array_id, .. } + | Expr::ArrayUnshift { array_id, .. } + | Expr::ArraySplice { array_id, .. } + | Expr::ArrayCopyWithin { array_id, .. } => { + self.written.insert(*array_id); + self.store_roots[self.current].push(Some(*array_id)); + self.expr_children(e, Use::Value); + } + Expr::ArrayPop(id) | Expr::ArrayShift(id) => { + self.written.insert(*id); + self.store_roots[self.current].push(Some(*id)); + } + Expr::SetAdd { set_id, .. } => { + self.written.insert(*set_id); + self.store_roots[self.current].push(Some(*set_id)); + self.expr_children(e, Use::Value); + } + Expr::Call { callee, args, .. } => { + match callee.as_ref() { + // A method call hands the receiver over as `this`. + Expr::PropertyGet { object, .. } | Expr::IndexGet { object, .. } => { + self.expr(object, Use::Value); + if let Expr::IndexGet { index, .. } = callee.as_ref() { + self.expr(index, Use::Consume); + } + } + other => self.expr(other, Use::Value), + } + let known = match callee.as_ref() { + Expr::FuncRef(fid) => Some(*fid), + _ => None, + }; + for (i, arg) in args.iter().enumerate() { + match (known, arg) { + (Some(fid), Expr::LocalGet(id)) => self.call_args.push((*id, fid, i)), + _ => self.expr(arg, Use::Value), + } + } + } + Expr::Binary { left, right, .. } | Expr::Compare { left, right, .. } => { + self.expr(left, Use::Consume); + self.expr(right, Use::Consume); + } + Expr::Unary { operand, .. } | Expr::TypeOf(operand) | Expr::Void(operand) => { + self.expr(operand, Use::Consume) + } + Expr::Logical { left, right, .. } => { + self.expr(left, how); + self.expr(right, how); + } + Expr::Conditional { + condition, + then_expr, + else_expr, + } => { + self.expr(condition, Use::Consume); + self.expr(then_expr, how); + self.expr(else_expr, how); + } + Expr::Sequence(es) => { + for (i, x) in es.iter().enumerate() { + self.expr(x, if i + 1 == es.len() { how } else { Use::Consume }); + } + } + Expr::Closure { params, body, .. } => { + // A closure body runs whenever it is called: its uses count + // as uses here, and its own bindings form a new frame. + self.frames.push(HashSet::new()); + self.params(params); + self.stmts(body); + self.frames.pop(); + } + _ => self.expr_children(e, Use::Value), + } + } + + fn expr_children(&mut self, e: &Expr, how: Use) { + let mut children = Vec::new(); + perry_hir::walker::walk_expr_children(e, &mut |c| children.push(c)); + for c in children { + self.expr(c, how); + } + } + + fn resolve(mut self) -> HashSet { + let ids: Vec = self.home.keys().copied().collect(); + let mut fresh_class: HashMap = HashMap::new(); + let mut class_inits: HashMap> = HashMap::new(); + for &id in &ids { + let root = self.find(id); + let entry = fresh_class.entry(root).or_insert(true); + if self.entry_aliased.contains(&id) { + *entry = false; + } + if let Some(inits) = self.fresh_inits.get(&id) { + class_inits + .entry(root) + .or_default() + .extend(inits.iter().cloned()); + } + } + // A class needs a literal source to be fresh; an id never bound in + // this module (a capture from elsewhere) is not. + for (root, fresh) in fresh_class.iter_mut() { + if !class_inits.contains_key(root) { + *fresh = false; + } + } + let is_fresh = |s: &mut Self, fresh: &HashMap, id: u32| { + let r = s.find(id); + fresh.get(&r).copied().unwrap_or(false) + }; + + // A handed-out child escapes a fresh parent unless it is a primitive. + // (An entry-aliased class needs a hazard-free body, and a write + // through a handed-out child is a call or a store through a root + // that is not a fresh member: a hazard.) + let child_escapes = std::mem::take(&mut self.child_escapes); + for (id, field) in child_escapes { + let root = self.find(id); + if !is_fresh(&mut self, &fresh_class, id) { + continue; + } + let primitive = class_inits.get(&root).is_some_and(|inits| { + inits.iter().all(|init| { + literal_field_is_primitive(init, field.as_deref(), &self.class_fields) + }) + }); + if !primitive { + self.written.insert(id); + } + } + + // Body hazards for entry-aliased classes: unknown code, or a store + // through a root that is not a fresh member. + let mut hazard: Vec = self.body_calls.clone(); + let store_roots = std::mem::take(&mut self.store_roots); + for (body, roots) in store_roots.iter().enumerate() { + if roots + .iter() + .any(|r| r.is_none_or(|id| !is_fresh(&mut self, &fresh_class, id))) + { + hazard[body] = true; + } + } + + let stable_now = + |s: &mut Self, written_roots: &HashSet, hazard_roots: &HashSet, id: u32| { + let r = s.find(id); + !written_roots.contains(&r) + && (fresh_class.get(&r).copied().unwrap_or(false) || !hazard_roots.contains(&r)) + }; + let mut hazard_roots = HashSet::new(); + for &id in &ids { + if self.home.get(&id).is_some_and(|b| hazard[*b]) { + let r = self.find(id); + hazard_roots.insert(r); + } + } + let class_roots = |s: &mut Self| -> HashSet { + let written: Vec = s.written.iter().copied().collect(); + written.into_iter().map(|w| s.find(w)).collect() + }; + + // Parameters never depend on the call-argument refinement: a body + // that passes one on contains a call, which already blocks it. + let written_roots = class_roots(&mut self); + let call_args = std::mem::take(&mut self.call_args); + let returned_children = std::mem::take(&mut self.returned_children); + for (id, fid, index) in call_args { + let callee_param = self + .functions + .get(&fid) + .and_then(|params| params.get(index)) + .copied(); + let harmless = callee_param.is_some_and(|(param, is_rest)| { + !is_rest + && !self.returned.contains(¶m) + && stable_now(&mut self, &written_roots, &hazard_roots, param) + && returned_children + .iter() + .filter(|(p, _)| *p == param) + .all(|(_, field)| { + // `return p.f` hands the argument's child to the + // caller: harmless only for a primitive child. + let root = self.find(id); + fresh_class.get(&root).copied().unwrap_or(false) + && class_inits.get(&root).is_some_and(|inits| { + inits.iter().all(|init| { + literal_field_is_primitive( + init, + field.as_deref(), + &self.class_fields, + ) + }) + }) + }) + }); + if !harmless { + self.written.insert(id); + } + } + + let written_roots = class_roots(&mut self); + ids.into_iter() + .filter(|&id| stable_now(&mut self, &written_roots, &hazard_roots, id)) + .collect() + } +} + +/// The local a read chain (`o`, `o.a`, `o[i].b`) starts from. +fn chain_root(e: &Expr) -> Option { + match e { + Expr::LocalGet(id) => Some(*id), + Expr::PropertyGet { object, .. } | Expr::IndexGet { object, .. } => chain_root(object), + _ => None, + } +} + +/// A construction that yields a new object no other binding holds. +fn is_literal_construction(e: &Expr) -> bool { + match e { + Expr::Object(_) | Expr::Array(..) => true, + Expr::New { class_name, .. } => class_name.starts_with("__AnonShape_"), + _ => false, + } +} + +fn contains_closure(e: &Expr) -> bool { + if matches!(e, Expr::Closure { .. }) { + return true; + } + let mut found = false; + perry_hir::walker::walk_expr_children(e, &mut |c| found = found || contains_closure(c)); + found +} + +/// A value that can never be an object. +fn is_primitive_valued(e: &Expr) -> bool { + matches!( + e, + Expr::Undefined + | Expr::Null + | Expr::Bool(_) + | Expr::Number(_) + | Expr::Integer(_) + | Expr::BigInt(_) + | Expr::String(_) + | Expr::WtfString(_) + | Expr::Binary { .. } + | Expr::Compare { .. } + | Expr::Unary { .. } + | Expr::TypeOf(_) + ) +} + +/// Is `field` (`None`: any field or element) of the literal `init` a +/// primitive? An absent field reads through the prototype: not known. +fn literal_field_is_primitive( + init: &Expr, + field: Option<&str>, + class_fields: &HashMap>, +) -> bool { + let pairs: Vec<(&str, &Expr)> = match init { + Expr::Object(props) => props.iter().map(|(k, v)| (k.as_str(), v)).collect(), + Expr::New { + class_name, args, .. + } => match class_fields.get(class_name) { + Some(names) if names.len() == args.len() => { + names.iter().map(String::as_str).zip(args.iter()).collect() + } + _ => return false, + }, + Expr::Array(elements, _) => { + return field.is_none() && elements.iter().all(is_primitive_valued); + } + _ => return false, + }; + match field { + Some(name) => pairs + .iter() + .find(|(k, _)| *k == name) + .is_some_and(|(_, v)| is_primitive_valued(v)), + None => pairs.iter().all(|(_, v)| is_primitive_valued(v)), + } +} diff --git a/crates/perry-codegen/src/collectors/contents_stable_tests.rs b/crates/perry-codegen/src/collectors/contents_stable_tests.rs new file mode 100644 index 00000000000..227b137d2f0 --- /dev/null +++ b/crates/perry-codegen/src/collectors/contents_stable_tests.rs @@ -0,0 +1,241 @@ +//! `collectors::contents_stable_bindings`: which bindings may keep a proof +//! about their object's contents. + +use perry_hir::types::Type; +use perry_hir::{Expr, Function, Module, Param, Stmt}; + +use super::contents_stable::contents_stable_bindings; + +fn literal() -> Expr { + Expr::Object(vec![ + ("x".to_string(), Expr::Number(1.0)), + ("y".to_string(), Expr::Number(2.0)), + ]) +} + +fn let_(id: u32, init: Expr) -> Stmt { + Stmt::Let { + id, + name: format!("v{id}"), + ty: Type::Any, + mutable: false, + init: Some(init), + } +} + +fn get(id: u32, property: &str) -> Expr { + Expr::PropertyGet { + byte_offset: 0, + byte_end: 0, + object: Box::new(Expr::LocalGet(id)), + property: property.to_string(), + } +} + +fn set(id: u32, property: &str, value: Expr) -> Stmt { + Stmt::Expr(Expr::PropertySet { + byte_offset: 0, + byte_end: 0, + object: Box::new(Expr::LocalGet(id)), + property: property.to_string(), + value: Box::new(value), + }) +} + +fn call(callee: Expr, args: Vec) -> Expr { + Expr::Call { + byte_offset: 0, + byte_end: 0, + callee: Box::new(callee), + args, + type_args: Vec::new(), + } +} + +fn product(id: u32) -> Stmt { + Stmt::Expr(Expr::Binary { + op: perry_hir::BinaryOp::Mul, + left: Box::new(get(id, "x")), + right: Box::new(get(id, "y")), + }) +} + +fn function(id: u32, params: Vec, body: Vec) -> Function { + Function { + id, + name: format!("f{id}"), + type_params: Vec::new(), + params: params + .into_iter() + .map(|pid| Param { + id: pid, + name: format!("p{pid}"), + ty: Type::Any, + default: None, + decorators: Vec::new(), + is_rest: false, + arguments_object: None, + }) + .collect(), + return_type: Type::Any, + body, + is_async: false, + is_generator: false, + is_strict: true, + is_exported: false, + captures: Vec::new(), + decorators: Vec::new(), + was_plain_async: false, + was_unrolled: false, + } +} + +fn stable(init: Vec, functions: Vec) -> std::collections::HashSet { + let mut module = Module::new("contents_stable.ts"); + module.init = init; + module.functions = functions; + contents_stable_bindings(&module) +} + +#[test] +fn a_literal_nothing_writes_is_stable() { + let ids = stable(vec![let_(1, literal()), product(1)], Vec::new()); + assert!(ids.contains(&1)); +} + +#[test] +fn a_delete_or_a_store_is_a_write() { + let delete = Stmt::Expr(Expr::Delete(Box::new(get(1, "y")))); + assert!(!stable(vec![let_(1, literal()), delete, product(1)], Vec::new()).contains(&1)); + let store = set(1, "y", Expr::String("s".to_string())); + assert!(!stable(vec![let_(1, literal()), store, product(1)], Vec::new()).contains(&1)); +} + +#[test] +fn a_write_through_an_alias_writes_every_alias() { + let ids = stable( + vec![ + let_(1, literal()), + let_(2, Expr::LocalGet(1)), + set(2, "y", Expr::String("s".to_string())), + product(1), + ], + Vec::new(), + ); + assert!(!ids.contains(&1) && !ids.contains(&2)); +} + +#[test] +fn a_child_handed_out_escapes_its_parent_unless_primitive() { + let nested = Expr::Object(vec![("a".to_string(), literal())]); + let ids = stable(vec![let_(1, nested), let_(2, get(1, "a"))], Vec::new()); + assert!(!ids.contains(&1)); + let ids = stable(vec![let_(1, literal()), let_(2, get(1, "x"))], Vec::new()); + assert!(ids.contains(&1)); +} + +#[test] +fn a_call_argument_escapes_unless_the_callee_parameter_is_stable() { + // f10(p11) writes its parameter; f20(p21) only reads it. + let writer = function( + 10, + vec![11], + vec![set(11, "y", Expr::String("s".to_string()))], + ); + let reader = function(20, vec![21], vec![Stmt::Return(Some(get(21, "x")))]); + let ids = stable( + vec![ + let_(1, literal()), + Stmt::Expr(call(Expr::FuncRef(10), vec![Expr::LocalGet(1)])), + let_(2, literal()), + Stmt::Expr(call(Expr::FuncRef(20), vec![Expr::LocalGet(2)])), + let_(3, literal()), + Stmt::Expr(call(Expr::GlobalGet(0), vec![Expr::LocalGet(3)])), + ], + vec![writer, reader], + ); + assert!(!ids.contains(&1), "a writing callee"); + assert!(ids.contains(&2), "a read-only callee"); + assert!(ids.contains(&21), "a read-only parameter"); + assert!(!ids.contains(&3), "an unknown callee"); +} + +#[test] +fn a_parameter_needs_a_body_without_unknown_code_or_foreign_stores() { + let calls = function( + 10, + vec![11], + vec![ + Stmt::Expr(call(Expr::GlobalGet(0), Vec::new())), + Stmt::Return(Some(get(11, "x"))), + ], + ); + // `other.y = v` may be a store through the same object (`f(o, o)`). + let foreign = function( + 20, + vec![21, 22], + vec![set(22, "y", Expr::Number(1.0)), product(21)], + ); + let fresh_store = function( + 30, + vec![31], + vec![ + let_(32, literal()), + set(32, "y", Expr::Number(1.0)), + product(31), + ], + ); + let ids = stable(Vec::new(), vec![calls, foreign, fresh_store]); + assert!(!ids.contains(&11)); + assert!(!ids.contains(&21)); + assert!( + ids.contains(&31), + "a store into a fresh local cannot reach the parameter" + ); +} + +#[test] +fn a_closure_write_is_a_write() { + let closure = Expr::Closure { + func_id: 50, + params: Vec::new(), + return_type: Type::Any, + body: vec![set(1, "y", Expr::String("s".to_string()))], + captures: vec![1], + 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: true, + }; + let ids = stable( + vec![let_(1, literal()), let_(2, closure), product(1)], + Vec::new(), + ); + assert!(!ids.contains(&1)); +} + +#[test] +fn a_callee_returning_an_object_child_hands_it_out() { + // f10(p11) returns p11.a (an object), f20(p21) returns p21.x (a number). + let object_child = function(10, vec![11], vec![Stmt::Return(Some(get(11, "a")))]); + let number_child = function(20, vec![21], vec![Stmt::Return(Some(get(21, "x")))]); + let nested = Expr::Object(vec![ + ("a".to_string(), literal()), + ("x".to_string(), Expr::Number(1.0)), + ]); + let ids = stable( + vec![ + let_(1, nested.clone()), + Stmt::Expr(call(Expr::FuncRef(10), vec![Expr::LocalGet(1)])), + let_(2, nested), + Stmt::Expr(call(Expr::FuncRef(20), vec![Expr::LocalGet(2)])), + ], + vec![object_child, number_child], + ); + assert!(!ids.contains(&1), "an object child handed back"); + assert!(ids.contains(&2), "a number child handed back"); +} diff --git a/crates/perry-codegen/src/collectors/mod.rs b/crates/perry-codegen/src/collectors/mod.rs index 00438a41f5a..9aaebbd776e 100644 --- a/crates/perry-codegen/src/collectors/mod.rs +++ b/crates/perry-codegen/src/collectors/mod.rs @@ -14,6 +14,9 @@ mod cjs_scaffolding; mod clamp_detect; mod class_accessors; mod closures; +pub(crate) mod contents_stable; +#[cfg(test)] +mod contents_stable_tests; pub(crate) mod definite_init; mod escape_arrays; mod escape_check; @@ -81,6 +84,7 @@ pub use clamp_detect::{detect_clamp3, detect_clamp_u8, returns_i32_identity_arg, pub(crate) use byte_read_key::{collect_numeric_typed_locals, uint8array_get_reads_a_byte}; pub(crate) use class_accessors::{is_class_getter, is_class_setter}; pub(crate) use closures::{collect_closures_in_expr, collect_closures_in_stmts}; +pub(crate) use contents_stable::contents_stable_bindings; pub(crate) use definite_init::DefiniteInit; pub(crate) use escape_arrays::{const_index, MAX_SCALAR_OBJECT_FIELDS}; pub(crate) use escape_check::{check_escapes_in_stmts, find_new_candidates}; @@ -108,8 +112,7 @@ pub(crate) use integer_locals::{ pub(crate) use literal_method_home::literal_method_home_classes; pub(crate) use local_refs::{expr_contains_local_get, mark_all_candidate_refs_in_expr}; pub(crate) use mutation::{ - body_contains_call, body_contains_closure, flat_const_array_uses_are_read_only, - has_any_mutation, + body_contains_closure, flat_const_array_uses_are_read_only, has_any_mutation, }; pub(crate) use number_by_construction::collect_number_by_construction_locals; pub(crate) use object_create_proto::{object_create_protos, ObjectCreateProtos}; diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs index a0d31c9b103..41d8153d685 100644 --- a/crates/perry-codegen/src/expr/mod.rs +++ b/crates/perry-codegen/src/expr/mod.rs @@ -826,6 +826,10 @@ pub(crate) struct FnCtx<'a> { /// (`collectors::string_accumulator_locals`): selected for the in-place /// self-append and demoted on ordinary reads, like a declared `string`. pub string_accumulator_locals: &'a std::collections::HashSet, + /// Module-wide contents-stable bindings + /// (`collectors::contents_stable_bindings`): the only bindings that may + /// keep a proof about their object's contents (`admits_contents_proof`). + pub contents_stable_bindings: &'a std::collections::HashSet, /// Set while lowering `.length`: that read of `id` cannot hand the /// string anywhere, so it skips the unique-owner demote. pub string_length_read_of: Option, @@ -2895,6 +2899,42 @@ impl<'a> FnCtx<'a> { /// type after a non-dominating write is a wrong-code bug (#7846). Declared /// annotations are never inserted into this map, so a successful lookup is /// both provenance-checked and write-stable. + /// May binding `id` keep the runtime-derived `proof`? + /// + /// A proof about the CONTENTS of an object (its property types) is + /// established once, at the binding; a later `delete`, store or + /// `defineProperty` changes a property without touching the binding. Such + /// a proof is kept only for a binding whose object nothing writes for its + /// whole life (`collectors::contents_stable_bindings`). A class identity + /// claims no contents: its field reads are re-checked as declared-only. + pub(crate) fn admits_contents_proof(&self, id: u32, proof: &HirType) -> bool { + !self.proof_claims_contents(proof, 0) || self.contents_stable_bindings.contains(&id) + } + + fn proof_claims_contents(&self, ty: &HirType, depth: usize) -> bool { + if depth > 16 { + return true; + } + match ty { + HirType::Object(_) => true, + HirType::Named(name) => { + if let Some(alias) = self.type_aliases.get(name) { + self.proof_claims_contents(alias, depth + 1) + } else { + self.interfaces.contains_key(name) + } + } + HirType::Union(variants) | HirType::Tuple(variants) => variants + .iter() + .any(|v| self.proof_claims_contents(v, depth + 1)), + HirType::Array(element) => self.proof_claims_contents(element, depth + 1), + HirType::Generic { base, type_args } if base == "Array" => type_args + .iter() + .any(|v| self.proof_claims_contents(v, depth + 1)), + _ => false, + } + } + pub(crate) fn stable_local_type_proof(&self, id: &u32) -> Option<&HirType> { if self.reassigned_locals.contains(id) { self.typed_locals.get(id) diff --git a/crates/perry-codegen/src/lower_call/new_ctor_args.rs b/crates/perry-codegen/src/lower_call/new_ctor_args.rs index d71f97452de..4e9eed7f6f4 100644 --- a/crates/perry-codegen/src/lower_call/new_ctor_args.rs +++ b/crates/perry-codegen/src/lower_call/new_ctor_args.rs @@ -93,7 +93,7 @@ pub(crate) fn bind_inline_constructor_params( crate::expr::emit_shadow_slot_bind_for_local(ctx, param.id); ctx.local_types.insert(param.id, param.ty.clone()); ctx.proven_local_types.remove(¶m.id); - if let Some(proof) = proof { + if let Some(proof) = proof.filter(|proof| ctx.admits_contents_proof(param.id, proof)) { ctx.proven_local_types.insert(param.id, proof); } } diff --git a/crates/perry-codegen/src/lower_call/scalar_method.rs b/crates/perry-codegen/src/lower_call/scalar_method.rs index b3ea2496479..604747325e1 100644 --- a/crates/perry-codegen/src/lower_call/scalar_method.rs +++ b/crates/perry-codegen/src/lower_call/scalar_method.rs @@ -329,7 +329,9 @@ fn lower_scalar_method_inline_body( ctx.block().store(DOUBLE, value, &slot); ctx.locals.insert(param.id, slot); ctx.local_types.insert(param.id, param.ty.clone()); - ctx.proven_local_types.insert(param.id, param.ty.clone()); + if ctx.admits_contents_proof(param.id, ¶m.ty) { + ctx.proven_local_types.insert(param.id, param.ty.clone()); + } } ctx.scalar_ctor_target.push(receiver_id); @@ -433,7 +435,9 @@ fn lower_scalar_method_int32_inline_body( ctx.block().store(I32, value, &slot); ctx.i32_counter_slots.insert(param.id, slot); ctx.local_types.insert(param.id, param.ty.clone()); - ctx.proven_local_types.insert(param.id, param.ty.clone()); + if ctx.admits_contents_proof(param.id, ¶m.ty) { + ctx.proven_local_types.insert(param.id, param.ty.clone()); + } } ctx.scalar_ctor_target.push(receiver_id); diff --git a/crates/perry-codegen/src/stmt/contents_stable_ir_tests.rs b/crates/perry-codegen/src/stmt/contents_stable_ir_tests.rs new file mode 100644 index 00000000000..88b0f4597ee --- /dev/null +++ b/crates/perry-codegen/src/stmt/contents_stable_ir_tests.rs @@ -0,0 +1,67 @@ +//! A literal local that nothing writes keeps its contents proof: its +//! property reads multiply as a bare `fmul double`. The same literal after a +//! `delete` must not, or `o.y * 10` reads the deleted slot (F13). Both +//! literals are handed to a read-only callee, so neither is scalar-replaced +//! and only the binding proof decides the product. + +fn compile(source: &str) -> String { + let ast = perry_parser::parse_typescript(source, "contents_stable.ts").unwrap(); + let m = perry_hir::lower_module(&ast, "contents_stable", "contents_stable.ts").unwrap(); + String::from_utf8( + crate::compile_module( + &m, + crate::CompileOptions { + emit_ir_only: true, + ..Default::default() + }, + ) + .unwrap(), + ) + .unwrap() +} + +fn body<'a>(ir: &'a str, name: &str) -> &'a str { + let at = ir + .find(&format!("@perry_fn_contents_stable__{name}(")) + .and_then(|p| { + ir[..p] + .rfind("define ") + .filter(|d| !ir[*d..p].contains('\n')) + }) + .unwrap_or_else(|| panic!("no body for {name}: {ir}")); + let end = ir[at..].find("\n}\n").unwrap() + at; + &ir[at..end] +} + +const SOURCE: &str = r#" + function peek(o: { x: number; y: number }): number { + return o.x; + } + function stable(): number { + const o = { x: 1.5, y: 2.5 }; + peek(o); + return o.x * o.y; + } + function deleted(): number { + const o = { x: 1.5, y: 2.5 }; + peek(o); + delete (o as any).y; + return o.x * o.y; + } + console.log(stable(), deleted()); +"#; + +#[test] +fn a_literal_local_nothing_writes_keeps_its_contents_proof() { + let ir = compile(SOURCE); + let stable = body(&ir, "stable"); + let deleted = body(&ir, "deleted"); + assert!( + stable.contains("fmul double") && !stable.contains("@js_dynamic_mul("), + "a stable literal multiplies its proven slots: {stable}" + ); + assert!( + deleted.contains("@js_dynamic_mul("), + "a deleted property must not keep the number proof: {deleted}" + ); +} diff --git a/crates/perry-codegen/src/stmt/let_scalar_new.rs b/crates/perry-codegen/src/stmt/let_scalar_new.rs index cd500a5be89..7ab2ecdf935 100644 --- a/crates/perry-codegen/src/stmt/let_scalar_new.rs +++ b/crates/perry-codegen/src/stmt/let_scalar_new.rs @@ -238,7 +238,9 @@ pub(super) fn try_lower_scalar_replaced_new( ctx.local_types.insert(param.id, param.ty.clone()); ctx.proven_local_types.remove(¶m.id); if let Some(Some(proof)) = arg_proofs.get(index) { - ctx.proven_local_types.insert(param.id, proof.clone()); + if ctx.admits_contents_proof(param.id, proof) { + ctx.proven_local_types.insert(param.id, proof.clone()); + } } } // #9081: the body is spliced into the enclosing frame, @@ -271,7 +273,9 @@ pub(super) fn try_lower_scalar_replaced_new( ctx.local_types.insert(param.id, param.ty.clone()); ctx.proven_local_types.remove(¶m.id); if let Some(Some(proof)) = arg_proofs.get(i) { - ctx.proven_local_types.insert(param.id, proof.clone()); + if ctx.admits_contents_proof(param.id, proof) { + ctx.proven_local_types.insert(param.id, proof.clone()); + } } } ctx.class_stack.pop(); diff --git a/crates/perry-codegen/src/stmt/let_stmt.rs b/crates/perry-codegen/src/stmt/let_stmt.rs index 572cbac9058..f45e358ca41 100644 --- a/crates/perry-codegen/src/stmt/let_stmt.rs +++ b/crates/perry-codegen/src/stmt/let_stmt.rs @@ -339,7 +339,9 @@ pub(crate) fn lower_let( } else { proven }; - ctx.proven_local_types.insert(id, proven); + if ctx.admits_contents_proof(id, &proven) { + ctx.proven_local_types.insert(id, proven); + } } // #7773/#7506: a numeric local inherits a DECLARED-ONLY proof from its diff --git a/crates/perry-codegen/src/stmt/mod.rs b/crates/perry-codegen/src/stmt/mod.rs index 2805e023f86..67504a77cb4 100644 --- a/crates/perry-codegen/src/stmt/mod.rs +++ b/crates/perry-codegen/src/stmt/mod.rs @@ -28,6 +28,8 @@ mod class_field_loop_tests; mod class_first_loop; #[cfg(test)] mod compound_alias_fold_tests; +#[cfg(test)] +mod contents_stable_ir_tests; mod counter_range; pub(crate) mod eh_actions; mod element_shape_carried; diff --git a/test-files/test_gap_property_value_contents_stable.ts b/test-files/test_gap_property_value_contents_stable.ts new file mode 100644 index 00000000000..1b4e85b7a75 --- /dev/null +++ b/test-files/test_gap_property_value_contents_stable.ts @@ -0,0 +1,118 @@ +// A proof about an object's property types is taken at the binding (an +// object literal's construction, the argument an inlined call binds). It may +// be kept only while nothing can write the object: a `delete`, a store of +// another type, an alias, a callee, a closure or a builtin that writes it +// changes the property between the proof and the read. Every read below must +// see the property as it is now. The last lines are literals nothing writes: +// their reads stay exact. + +const n = { x: 1, y: 2 }; +delete (n as any).y; +console.log(n.y * 10, n.y + 1, -n.y, n.y); + +const s = { x: 1, y: 2 }; +(s as any).y = "s"; +console.log(s.y + 1, s.y * 2, typeof s.y); + +function r(o: { x: number; y: number; z: number }): number { + return o.x * 100 + o.y * 10 + o.z; +} +const h = { x: 1, y: 2, z: 3 }; +delete (h as any).y; +console.log(r(h)); + +const q = { x: 1, y: 2, z: 3 }; +(q as any).x = "x"; +console.log(r(q)); + +const k = { x: 1, y: 2, z: 3 }; +delete (k as any).y; +(k as any).y = 7; +console.log(r(k), Object.keys(k).join()); + +type V = { x: number; y: number }; +const v: V = { x: 1, y: 2 }; +(v as any).y = "s"; +console.log(v.y + 1, v.y * 2); + +const a1 = { x: 1, y: 2 }; +const a2 = a1; +(a2 as any).y = "alias"; +console.log(a1.y + 1); + +function w(o: any) { + o.y = "callee"; +} +const a3 = { x: 1, y: 2 }; +w(a3); +console.log(a3.y + 1); + +const a4 = { x: 1, y: 2 }; +const cl = () => { + (a4 as any).y = "closure"; +}; +cl(); +console.log(a4.y + 1); + +const a5 = { x: 1, y: 2 }; +function g5() { + (a5 as any).y = "module fn"; +} +g5(); +console.log(a5.y + 1); + +const a6 = { x: 1, y: 2 }; +const hold = { r: a6 }; +(hold.r as any).y = "held"; +console.log(a6.y + 1); + +const a7 = { x: 1, y: 2 }; +Object.assign(a7, { y: "assign" }); +console.log(a7.y + 1); + +const a8 = { x: 1, y: 2 }; +Object.defineProperty(a8, "y", { value: "define" }); +console.log(a8.y + 1); + +const a9 = { x: 1, y: 2 }; +const arr9 = [a9]; +(arr9[0] as any).y = "array"; +console.log(a9.y + 1); + +const n1 = { a: { b: 1 } }; +const t1 = n1.a; +(t1 as any).b = "child"; +console.log(n1.a.b + 1); + +function w2(z: any) { + z.b = "child callee"; +} +const n2 = { a: { b: 1 } }; +w2(n2.a); +console.log(n2.a.b + 1); + +function f3() { + const o = { x: 1, y: 2 }; + (o as any).y = "local"; + return o.y + 1; +} +function f4() { + const o = { x: 1, y: 2 }; + delete (o as any).y; + return o.y * 10; +} +console.log(f3(), f4()); + +function f5(p: { x: number; y: number }, other: any) { + other.y = "other param"; + return p.y + 1; +} +const z5 = { x: 1, y: 2 }; +console.log(f5(z5, z5)); + +function readOnly(o: { x: number; y: number }): number { + return o.x * 2 + o.y; +} +const u = { a: 1.5, b: 2 }; +const ro = { x: 3, y: 4 }; +console.log(u.a * u.b, readOnly(ro), ro.x * ro.y);