From cf33b6edf3e1bb6c42e4484f4ce0bfdcb2f1d8a5 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Sun, 03 May 2026 02:15:08 +0000 Subject: [PATCH] feat(analyze): explicit captures/locals Inspired by GHC closures --- --- a/Cargo.lock +++ b/Cargo.lock @@ -852,18 +852,18 @@ [[package]] name = "jrsonnet-gcmodule" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21dd97b40cbfb2043094219f95d96519858ba1aee4e8260eb048a1774832a517" +checksum = "95f9ce64915cdb0cab5367940a7cc024394fcf4f2608531e49f6dad39e2082d7" dependencies = [ "jrsonnet-gcmodule-derive", ] [[package]] name = "jrsonnet-gcmodule-derive" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede3d0445c2a7d7adab0a3cc33bdb33df78ffebebc21a2848c221526cb1795d4" +checksum = "64364cfb68be0968a940d69ccb651ec445cde47830da5b294d55d2e47eee8708" dependencies = [ "proc-macro2", "quote", --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ [workspace.package] authors = ["Yaroslav Bolyukin "] -edition = "2021" +edition = "2024" license = "MIT" repository = "https://github.com/CertainLach/jrsonnet" version = "0.5.0-pre98" @@ -22,7 +22,7 @@ jrsonnet-cli = { path = "./crates/jrsonnet-cli", version = "0.5.0-pre98" } jrsonnet-types = { path = "./crates/jrsonnet-types", version = "0.5.0-pre98" } jrsonnet-formatter = { path = "./crates/jrsonnet-formatter", version = "0.5.0-pre98" } -jrsonnet-gcmodule = { version = "0.4.4" } +jrsonnet-gcmodule = { version = "0.4.5" } # Diagnostics. # hi-doc is my library, which handles text formatting very well, but isn't polished enough yet # Previous implementation was based on annotate-snippets, which I don't like for many reasons. --- a/crates/jrsonnet-evaluator/src/analyze.rs +++ b/crates/jrsonnet-evaluator/src/analyze.rs @@ -14,10 +14,9 @@ //! } //! ``` -use std::{fmt::Write, rc::Rc}; +use std::rc::Rc; use drop_bomb::DropBomb; -use hi_doc::{Formatting, SnippetBuilder, Text}; use jrsonnet_gcmodule::Acyclic; use jrsonnet_interner::IStr; use jrsonnet_ir::{ @@ -78,6 +77,20 @@ } } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Acyclic)] +pub enum LSlot { + /// Enclosing frame locals (sibling letrec, params, etc.). + Local(LocalSlot), + /// Enclosing closure's capture pack. + Capture(CaptureSlot), +} + +#[derive(Debug, Acyclic)] +pub struct ClosureShape { + pub captures: Box<[LSlot]>, + pub n_locals: u16, +} + struct LocalDefinition { name: IStr, span: Option, @@ -121,12 +134,15 @@ #[derive(Debug, Acyclic)] pub enum LExpr { - Local(LocalId), + Slot(LSlot), Null, Bool(bool), Str(IStr), Num(NumValue), - Arr(Rc>), + Arr { + shape: ClosureShape, + items: Rc>, + }, ArrComp(Box), Obj(LObjBody), ObjExtend(Box, LObjBody), @@ -141,10 +157,7 @@ rest: Box, }, Error(Span, Box), - LocalExpr { - binds: Vec, - body: Box, - }, + LocalExpr(Box), Import { kind: Spanned, kind_span: Span, @@ -160,6 +173,7 @@ parts: Vec, }, Function(Rc), + IdentityFunction, IfElse { cond: Box, cond_then: Box, @@ -174,10 +188,19 @@ } #[derive(Debug, Acyclic)] +pub struct LLocalExpr { + pub frame_shape: ClosureShape, + pub binds: Vec, + pub body: LExpr, +} + +#[derive(Debug, Acyclic)] pub struct LFunction { pub name: Option, pub params: Vec, pub signature: FunctionSignature, + + pub body_shape: ClosureShape, pub body: Rc, } @@ -185,18 +208,25 @@ pub struct LParam { pub name: Option, pub destruct: LDestruct, - pub default: Option>, + + pub default: Option<(ClosureShape, Rc)>, } #[derive(Debug, Acyclic)] pub struct LBind { pub destruct: LDestruct, + pub value_shape: ClosureShape, pub value: Rc, } -#[derive(Debug, Clone, Acyclic)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Acyclic)] +pub struct CaptureSlot(pub(crate) u16); +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Acyclic)] +pub struct LocalSlot(pub(crate) u16); + +#[derive(Debug, Acyclic)] pub enum LDestruct { - Full(LocalId), + Full(LocalSlot), #[cfg(feature = "exp-destruct")] Skip, #[cfg(feature = "exp-destruct")] @@ -214,54 +244,54 @@ #[derive(Debug, Clone, Copy, Acyclic)] pub enum LDestructRest { - Keep(LocalId), + Keep(LocalSlot), Drop, } -#[derive(Debug, Clone, Acyclic)] +#[derive(Debug, Acyclic)] pub struct LDestructField { pub name: IStr, pub into: Option, - pub default: Option>, + pub default: Option<(ClosureShape, Rc)>, } impl LDestruct { - pub fn each_id(&self, f: &mut F) { + pub fn each_slot(&self, f: &mut F) { match self { - Self::Full(id) => f(*id), + Self::Full(s) => f(*s), #[cfg(feature = "exp-destruct")] Self::Skip => {} #[cfg(feature = "exp-destruct")] Self::Array { start, rest, end } => { for d in start { - d.each_id(f); + d.each_slot(f); } - if let Some(LDestructRest::Keep(id)) = rest { - f(*id); + if let Some(LDestructRest::Keep(s)) = rest { + f(*s); } for d in end { - d.each_id(f); + d.each_slot(f); } } #[cfg(feature = "exp-destruct")] Self::Object { fields, rest } => { for field in fields { if let Some(into) = &field.into { - into.each_id(f); + into.each_slot(f); } else { unreachable!("shorthand object destruct must store `into`"); } } - if let Some(LDestructRest::Keep(id)) = rest { - f(*id); + if let Some(LDestructRest::Keep(s)) = rest { + f(*s); } } } } - pub fn ids(&self) -> SmallVec<[LocalId; 1]> { + pub fn slots(&self) -> SmallVec<[LocalSlot; 1]> { let mut out = SmallVec::new(); - self.each_id(&mut |id| out.push(id)); + self.each_slot(&mut |s| out.push(s)); out } } @@ -303,21 +333,24 @@ #[derive(Debug, Acyclic)] pub struct LObjMembers { - /// If current object identity (`super`/`this`/`$`) is used, `this` should be saved to the specified local - pub this: Option, + pub frame_shape: ClosureShape, + /// If current object identity (`super`/`this`/`$`) is used, `this` should + /// be saved to the specified local slot. + pub this: Option, /// Set if dollar should also be assigned to object identity, `this` should also be set (TODO: proper type-level validation) pub set_dollar: bool, /// True iff `super` is referenced by this object's members. pub uses_super: bool, pub locals: Rc>, - pub asserts: Rc>, + pub asserts: Option>, pub fields: Vec, } #[derive(Debug, Acyclic)] pub struct LObjComp { - pub this: Option, + pub frame_shape: Rc, + pub this: Option, pub set_dollar: bool, pub uses_super: bool, @@ -331,7 +364,19 @@ pub name: LFieldName, pub plus: bool, pub visibility: Visibility, - pub value: Rc, + pub value: Rc<(ClosureShape, LExpr)>, +} + +#[derive(Debug, Acyclic)] +pub struct LClosure { + pub shape: ClosureShape, + pub value: T, +} + +#[derive(Debug, Acyclic)] +pub struct LObjAsserts { + pub shape: ClosureShape, + pub asserts: Vec, } #[derive(Debug, Acyclic)] @@ -350,6 +395,7 @@ #[derive(Debug, Acyclic)] pub struct LArrComp { + pub value_shape: ClosureShape, pub value: Rc, pub compspecs: Vec, } @@ -358,6 +404,7 @@ pub enum LCompSpec { If(LExpr), For { + frame_shape: ClosureShape, destruct: LDestruct, over: LExpr, /// Is `over` does not depend on any variable introduced by an earlier for-spec in this comprehension chain @@ -365,23 +412,323 @@ }, } -// TODO: Binding frame state machine: -// Pending => AllocIds => Initialize => Body => Exit +struct FrameAlloc<'s> { + first_in_frame: LocalId, + stack: &'s mut AnalysisStack, + bomb: DropBomb, +} +impl<'s> FrameAlloc<'s> { + fn new(stack: &'s mut AnalysisStack) -> Self { + FrameAlloc { + first_in_frame: stack.next_local_id(), + stack, + bomb: DropBomb::new("binding frame state"), + } + } + + fn push_locals_closure(&mut self) -> ClosureOnStack { + self.stack.push_closure_a(self.first_in_frame) + } + + fn define_local(&mut self, name: IStr, span: Option) -> Option<(LocalId, LocalSlot)> { + let id = self.stack.next_local_id(); + let stack = self.stack.local_by_name.entry(name.clone()).or_default(); + if let Some(&existing) = stack.last() + && !existing.defined_before(self.first_in_frame) + { + self.stack.report_error( + format!("local is already defined in the current frame: {name}"), + span, + ); + return None; + } + stack.push(id); + self.stack.local_defs.push(LocalDefinition { + name, + span, + defined_at_depth: self.stack.depth, + used_at_depth: u32::MAX, + used_by_sibling: false, + analysis: AnalysisResult::default(), + analyzed: false, + scratch_referenced: false, + }); + let def = self.stack.defining_closure_mut(); + Some((id, def.define_local(id))) + } + fn alloc_bind(&mut self, bind: &BindSpec) -> Option { + match bind { + BindSpec::Field { into, .. } => self.alloc_destruct(into), + BindSpec::Function { name, .. } => { + let (_, id) = self.define_local(name.clone(), None)?; + Some(LDestruct::Full(id)) + } + } + } + fn alloc_destruct(&mut self, destruct: &Destruct) -> Option { + Some(match destruct { + Destruct::Full(name) => { + let (_, id) = self.define_local(name.value.clone(), Some(name.span.clone()))?; + LDestruct::Full(id) + } + #[cfg(feature = "exp-destruct")] + Destruct::Skip => LDestruct::Skip, + #[cfg(feature = "exp-destruct")] + Destruct::Array { start, rest, end } => { + let start = start + .iter() + .map(|d| self.alloc_destruct(d)) + .collect::>>()?; + let rest = match rest { + Some(jrsonnet_ir::DestructRest::Keep(name)) => { + let (_, id) = self.define_local(name.clone(), None)?; + Some(LDestructRest::Keep(id)) + } + Some(jrsonnet_ir::DestructRest::Drop) => Some(LDestructRest::Drop), + None => None, + }; + let end = end + .iter() + .map(|d| self.alloc_destruct(d)) + .collect::>>()?; + LDestruct::Array { start, rest, end } + } + #[cfg(feature = "exp-destruct")] + Destruct::Object { fields, rest } => { + let mut l_fields: Vec<(IStr, LDestruct)> = Vec::with_capacity(fields.len()); + // Allocate destruct LocalIds, then analyse defaults + for (name, into, _default) in fields { + let into = if let Some(inner) = into { + self.alloc_destruct(inner)? + } else { + let (_, id) = self.define_local(name.clone(), None)?; + LDestruct::Full(id) + }; + l_fields.push((name.clone(), into)); + } + // All locals exist, so defaults can reference any sibling. + let l_fields: Vec = l_fields + .into_iter() + .zip(fields.iter()) + .map(|((name, into), (_n, _i, default))| { + let default = match default { + Some(e) => { + let mut default_taint = AnalysisResult::default(); + Some(self.stack.in_using_closure(|stack| { + Rc::new(analyze(&e.value, stack, &mut default_taint)) + })) + } + None => None, + }; + LDestructField { + name, + into: Some(into), + default, + } + }) + .collect(); + let rest = match rest { + Some(jrsonnet_ir::DestructRest::Keep(name)) => { + let (_, id) = self.define_local(name.clone(), None)?; + Some(LDestructRest::Keep(id)) + } + Some(jrsonnet_ir::DestructRest::Drop) => Some(LDestructRest::Drop), + None => None, + }; + LDestruct::Object { + fields: l_fields, + rest, + } + } + }) + } + + fn finish(self) -> PendingInit<'s> { + let Self { + first_in_frame, + stack, + bomb, + } = self; + let first_after_frame = stack.next_local_id(); + PendingInit { + first_after_frame, + stack, + closures: Closures { + referenced: vec![], + spec_shapes: vec![], + first_in_frame, + }, + bomb, + } + } +} /// Frame state: `LocalIds` allocated, values not yet analysed. -struct PendingInit { - first_in_frame: LocalId, +struct PendingInit<'s> { first_after_frame: LocalId, + stack: &'s mut AnalysisStack, + closures: Closures, bomb: DropBomb, } +impl<'s> PendingInit<'s> { + /// Record the analysis of a spec's value: stamp every id bound by the + /// spec with `analysis`, collect the spec's same-frame references, and + /// append them to `closures`. + fn record_spec_init(&mut self, destruct: &LDestruct, analysis: AnalysisResult) { + let mut refs: SmallVec<[LocalId; 4]> = SmallVec::new(); + for i in self.closures.first_in_frame.0..self.first_after_frame.0 { + let def = &mut self.stack.local_defs[i as usize]; + if def.scratch_referenced { + refs.push(LocalId(i)); + def.scratch_referenced = false; + } + } + + let mut ids_count = 0; + let first_local = self.stack.top_defining_local(); + destruct.each_slot(&mut |slot| { + ids_count += 1; + let id = LocalId(first_local.0 + u32::from(slot.0)); + let def = &mut self.stack.local_defs[id.idx()]; + debug_assert!(!def.analyzed, "sanity: local {:?} analysed twice", def.name); + def.analysis = analysis; + def.analyzed = true; + }); + self.closures.push_spec(ids_count, &refs); + } + /// After all specs are analysed, propagate dependency information between + /// siblings to a fix-point, then switch to "body" mode. + fn finish(self) -> PendingBody<'s> { + let Self { + first_after_frame, + closures, + stack, + bomb, + } = self; + + debug_assert_eq!( + first_after_frame, + stack.next_local_id(), + "frame initialisation left unfinished locals" + ); + + debug_assert_eq!( + closures.spec_shapes.iter().map(|(_, d)| *d).sum::(), + (first_after_frame.0 - closures.first_in_frame.0) as usize, + "closures destruct-id counts must match frame local count" + ); + + let mut changed = true; + while changed { + changed = false; + for spec in closures.iter_specs() { + for id_raw in spec.ids.clone() { + let user = LocalId(id_raw); + for &used in spec.references { + changed |= stack.propagate_analysis(user, used); + } + } + } + } + + stack.depth += 1; + PendingBody { + first_after_frame, + closures, + stack, + bomb, + } + } +} + /// Frame state: values analysed, body not yet walked. -struct PendingBody { - first_in_frame: LocalId, +struct PendingBody<'s> { first_after_frame: LocalId, closures: Closures, + stack: &'s mut AnalysisStack, bomb: DropBomb, } +impl<'s> PendingBody<'s> { + /// After the body is processed, drop the frame's locals and emit any + /// "unused local" warnings. + fn finish(self) { + let PendingBody { + first_after_frame, + closures, + stack, + mut bomb, + } = self; + bomb.defuse(); + stack.depth -= 1; + + debug_assert_eq!( + first_after_frame, + stack.next_local_id(), + "nested scopes must be popped before outer frames" + ); + + let mut changed = true; + while changed { + changed = false; + for spec in closures.iter_specs() { + // Effective used_at_depth for the spec = min over its ids. + let mut min_used_at = u32::MAX; + for id_raw in spec.ids.clone() { + min_used_at = min_used_at.min(stack.local_defs[id_raw as usize].used_at_depth); + } + if min_used_at == u32::MAX { + continue; + } + for &used in spec.references { + let used_def = &mut stack.local_defs[used.idx()]; + if min_used_at < used_def.used_at_depth { + used_def.used_at_depth = min_used_at; + changed = true; + } + } + } + } + + let drained: Vec = stack + .local_defs + .drain(closures.first_in_frame.idx()..) + .collect(); + for (i, def) in drained.iter().enumerate().rev() { + let id = LocalId(closures.first_in_frame.0 + i as u32); + let stack_locals = stack + .local_by_name + .get_mut(&def.name) + .expect("local must be in name map"); + let popped = stack_locals.pop().expect("name stack should not be empty"); + debug_assert_eq!(popped, id, "name stack integrity"); + if stack_locals.is_empty() { + stack.local_by_name.remove(&def.name); + } + + if def.used_at_depth == u32::MAX { + if def.used_by_sibling { + stack.report_warning( + format!("local is only referenced by unused siblings: {}", def.name), + def.span.clone(), + ); + } else { + stack.report_warning(format!("unused local: {}", def.name), def.span.clone()); + } + } else if def.analysis.local_dependent_depth > def.defined_at_depth + && def.analysis.object_dependent_depth > def.defined_at_depth + && def.defined_at_depth != 0 + { + // The value doesn't depend on anything defined at or inside + // this local's scope - can be hoisted, unfortunately not automatically. + stack.report_warning( + format!("local could be hoisted to an outer scope: {}", def.name), + def.span.clone(), + ); + } + } + } +} struct Closures { /// All the referenced locals, maybe repeated multiple times @@ -451,14 +798,6 @@ } impl Closures { - fn new(first_in_frame: LocalId) -> Self { - Self { - referenced: Vec::new(), - spec_shapes: Vec::new(), - first_in_frame, - } - } - fn push_spec(&mut self, destruct_ids_count: usize, refs: &[LocalId]) { self.referenced.extend_from_slice(refs); self.spec_shapes.push((refs.len(), destruct_ids_count)); @@ -493,6 +832,41 @@ pub span: Option, } +struct DefiningClosure { + first_local: LocalId, + n_locals: u16, +} + +impl DefiningClosure { + fn resolve(&self, target: LocalId) -> Option { + let end = self.first_local.0 + u32::from(self.n_locals); + if target.0 >= self.first_local.0 && target.0 < end { + Some(LocalSlot( + u16::try_from(target.0 - self.first_local.0).expect("local slots overflow"), + )) + } else { + None + } + } + fn define_local(&mut self, local: LocalId) -> LocalSlot { + let slot = self.n_locals; + let id = self.first_local.0 + u32::from(slot); + debug_assert_eq!(local.0, id); + self.n_locals = self.n_locals.checked_add(1).expect("local slots overflow"); + LocalSlot(slot) + } +} + +/// Per-closure capture computation state. +struct ClosureFrame { + /// Closure may allocate locals + defining: Option, + /// `LocalId` => capture index + captures: FxHashMap, + /// Capture sources in insertion order; consumed by `pop_closure_frame`. + capture_sources: Vec, +} + #[allow(clippy::struct_excessive_bools)] pub struct AnalysisStack { local_defs: Vec, @@ -519,11 +893,19 @@ /// True iff `$` has been referenced anywhere since the outermost object's scope was entered. dollar_used: bool, + /// Stack of closure frames (innermost on top). + closure_stack: Vec, + diagnostics: Vec, /// Whenever analysis would be broken due to static analysis error. errored: bool, } +#[must_use] +struct ClosureOnStack { + bomb: DropBomb, +} + impl AnalysisStack { pub fn new() -> Self { Self { @@ -537,11 +919,120 @@ cur_self_used: false, cur_super_used: false, dollar_used: false, + closure_stack: Vec::new(), diagnostics: Vec::new(), errored: false, } } + fn push_root_closure(&mut self, externals: u16) -> ClosureOnStack { + assert!( + self.closure_stack.is_empty(), + "root is only possible with empty stack" + ); + + self.closure_stack.push(ClosureFrame { + defining: Some(DefiningClosure { + first_local: LocalId(0), + n_locals: externals, + }), + captures: FxHashMap::default(), + capture_sources: Vec::new(), + }); + + ClosureOnStack { + bomb: DropBomb::new("root closure"), + } + } + + fn push_closure_a(&mut self, first_local: LocalId) -> ClosureOnStack { + self.closure_stack.push(ClosureFrame { + defining: Some(DefiningClosure { + first_local, + n_locals: 0, + }), + captures: FxHashMap::default(), + capture_sources: Vec::new(), + }); + ClosureOnStack { + bomb: DropBomb::new("closure with locals"), + } + } + + #[inline] + fn in_using_closure( + &mut self, + inner: impl FnOnce(&mut AnalysisStack) -> T, + ) -> (ClosureShape, T) { + fn push_closure_b(stack: &mut AnalysisStack) -> ClosureOnStack { + stack.closure_stack.push(ClosureFrame { + defining: None, + captures: FxHashMap::default(), + capture_sources: Vec::new(), + }); + ClosureOnStack { + bomb: DropBomb::new("closure with locals"), + } + } + let closure = push_closure_b(self); + let v = inner(self); + let shape = self.pop_closure(closure); + (shape, v) + } + + fn pop_closure(&mut self, mut closure: ClosureOnStack) -> ClosureShape { + closure.bomb.defuse(); + let frame = self.closure_stack.pop().expect("closure frame"); + ClosureShape { + captures: frame.capture_sources.into_boxed_slice(), + n_locals: frame.defining.map(|d| d.n_locals).unwrap_or_default(), + } + } + + /// Resolve a `LocalId` reference to an `LSlot` against the innermost + /// closure frame. May insert capture entries up the closure stack as + /// needed. + fn resolve_to_slot(&mut self, target: LocalId) -> LSlot { + let top = self.closure_stack.len(); + debug_assert!(top > 0, "resolve_to_slot called with no closure frame"); + Self::resolve_at(&mut self.closure_stack, top - 1, target) + } + + fn resolve_at(stack: &mut [ClosureFrame], idx: usize, target: LocalId) -> LSlot { + if let Some(def) = &stack[idx].defining { + if let Some(resolved) = def.resolve(target) { + return LSlot::Local(resolved); + } + } else { + // A sibling letrec slot must never be packed as a capture, or + // it would read an empty `OnceCell`. + for j in (0..idx).rev() { + if let Some(def) = &stack[j].defining { + if let Some(resolved) = def.resolve(target) { + return LSlot::Local(resolved); + } + break; + } + } + } + if let Some(&cap_idx) = stack[idx].captures.get(&target) { + return LSlot::Capture(cap_idx); + } + debug_assert!(idx > 0, "no enclosing closure frame for target {target:?}"); + let parent_slot = Self::resolve_at(stack, idx - 1, target); + let frame = &mut stack[idx]; + let cap_idx = CaptureSlot( + frame + .capture_sources + .len() + .try_into() + .expect("frame has more than u16::MAX captures"), + ); + frame.capture_sources.push(parent_slot); + frame.captures.insert(target, cap_idx); + LSlot::Capture(cap_idx) + } + fn next_local_id(&self) -> LocalId { LocalId(self.local_defs.len() as u32) } @@ -562,12 +1053,7 @@ }); } - fn use_local( - &mut self, - name: &IStr, - span: Span, - taint: &mut AnalysisResult, - ) -> Option { + fn use_local(&mut self, name: &IStr, span: Span, taint: &mut AnalysisResult) -> Option { let Some(ids) = self.local_by_name.get(name) else { let names = suggest_names(name, self.local_by_name.keys()); self.report_error( @@ -586,7 +1072,7 @@ } else { def.scratch_referenced = true; } - Some(id) + Some(self.resolve_to_slot(id)) } /// Assign name to the value provided externally, e.g `std`. @@ -613,37 +1099,20 @@ self.local_by_name.entry(name).or_default().push(id); } - /// Define a new local inside a frame currently being built. - fn define_local( - &mut self, - name: IStr, - span: Option, - frame_start: LocalId, - ) -> Option { - let id = self.next_local_id(); - let stack = self.local_by_name.entry(name.clone()).or_default(); - if let Some(&existing) = stack.last() { - if !existing.defined_before(frame_start) { - self.report_error( - format!("local is already defined in the current frame: {name}"), - span, - ); - return None; - } - } - stack.push(id); - self.local_defs.push(LocalDefinition { - name, - span, - defined_at_depth: self.depth, - used_at_depth: u32::MAX, - used_by_sibling: false, - analysis: AnalysisResult::default(), - analyzed: false, - scratch_referenced: false, - }); - Some(id) + fn defining_closure_mut(&mut self) -> &mut DefiningClosure { + self.closure_stack + .iter_mut() + .rev() + .find_map(|c| c.defining.as_mut()) + .expect("no enclosing defining closure frame") } + fn defining_closure(&self) -> &DefiningClosure { + self.closure_stack + .iter() + .rev() + .find_map(|c| c.defining.as_ref()) + .expect("no enclosing defining closure frame") + } } impl Default for AnalysisStack { @@ -653,169 +1122,8 @@ } impl AnalysisStack { - fn alloc_destruct(&mut self, destruct: &Destruct, frame_start: LocalId) -> Option { - match destruct { - Destruct::Full(name) => { - let id = - self.define_local(name.value.clone(), Some(name.span.clone()), frame_start)?; - Some(LDestruct::Full(id)) - } - #[cfg(feature = "exp-destruct")] - Destruct::Skip => Some(LDestruct::Skip), - #[cfg(feature = "exp-destruct")] - Destruct::Array { start, rest, end } => { - let start = start - .iter() - .map(|d| self.alloc_destruct(d, frame_start)) - .collect::>>()?; - let rest = match rest { - Some(jrsonnet_ir::DestructRest::Keep(name)) => { - let id = self.define_local(name.clone(), None, frame_start)?; - Some(LDestructRest::Keep(id)) - } - Some(jrsonnet_ir::DestructRest::Drop) => Some(LDestructRest::Drop), - None => None, - }; - let end = end - .iter() - .map(|d| self.alloc_destruct(d, frame_start)) - .collect::>>()?; - Some(LDestruct::Array { start, rest, end }) - } - #[cfg(feature = "exp-destruct")] - Destruct::Object { fields, rest } => { - let mut l_fields: Vec<(IStr, LDestruct)> = Vec::with_capacity(fields.len()); - // Two passes: first allocate ALL destruct LocalIds, then - // analyse defaults (which may reference later fields). - let mut l_fields: Vec<(IStr, LDestruct)> = Vec::with_capacity(fields.len()); - for (name, into, _default) in fields { - let into = if let Some(inner) = into { - self.alloc_destruct(inner, frame_start)? - } else { - let id = self.define_local(name.clone(), None, frame_start)?; - LDestruct::Full(id) - }; - l_fields.push((name.clone(), into)); - } - // Second pass: all locals exist, so defaults can reference - // any sibling. - let l_fields: Vec = l_fields - .into_iter() - .zip(fields.iter()) - .map(|((name, into), (_n, _i, default))| { - let default = default.as_ref().map(|e| { - let mut default_taint = AnalysisResult::default(); - Rc::new(analyze(&e.value, self, &mut default_taint)) - }); - LDestructField { - name, - into: Some(into), - default, - } - }) - .collect(); - let rest = match rest { - Some(jrsonnet_ir::DestructRest::Keep(name)) => { - let id = self.define_local(name.clone(), None, frame_start)?; - Some(LDestructRest::Keep(id)) - } - Some(jrsonnet_ir::DestructRest::Drop) => Some(LDestructRest::Drop), - None => None, - }; - Some(LDestruct::Object { - fields: l_fields, - rest, - }) - } - } - } - - // TODO: Proper state machine - fn begin_frame_alloc(&mut self) -> LocalId { - self.next_local_id() - } - - fn finish_frame_alloc(&mut self, first_in_frame: LocalId) -> PendingInit { - let first_after_frame = self.next_local_id(); - PendingInit { - first_in_frame, - first_after_frame, - bomb: DropBomb::new("PendingInit must be passed to finish_frame_init"), - } - } - - /// Record the analysis of a spec's value: stamp every id bound by the - /// spec with `analysis`, collect the spec's same-frame references, and - /// append them to `closures`. - fn record_spec_init( - &mut self, - pending: &PendingInit, - destruct: &LDestruct, - analysis: AnalysisResult, - closures: &mut Closures, - ) { - let mut refs: SmallVec<[LocalId; 4]> = SmallVec::new(); - for i in pending.first_in_frame.0..pending.first_after_frame.0 { - let def = &mut self.local_defs[i as usize]; - if def.scratch_referenced { - refs.push(LocalId(i)); - def.scratch_referenced = false; - } - } - - let mut ids_count = 0; - destruct.each_id(&mut |id| { - ids_count += 1; - let def = &mut self.local_defs[id.idx()]; - debug_assert!(!def.analyzed, "sanity: local {:?} analysed twice", def.name); - def.analysis = analysis; - def.analyzed = true; - }); - closures.push_spec(ids_count, &refs); - } - - /// After all specs are analysed, propagate dependency information between - /// siblings to a fix-point, then switch to "body" mode. - fn finish_frame_init(&mut self, pending: PendingInit, closures: Closures) -> PendingBody { - let PendingInit { - first_in_frame, - first_after_frame, - mut bomb, - } = pending; - bomb.defuse(); - - debug_assert_eq!( - first_after_frame, - self.next_local_id(), - "frame initialisation left unfinished locals" - ); - - debug_assert_eq!( - closures.spec_shapes.iter().map(|(_, d)| *d).sum::(), - (first_after_frame.0 - first_in_frame.0) as usize, - "closures destruct-id counts must match frame local count" - ); - - let mut changed = true; - while changed { - changed = false; - for spec in closures.iter_specs() { - for id_raw in spec.ids.clone() { - let user = LocalId(id_raw); - for &used in spec.references { - changed |= self.propagate_analysis(user, used); - } - } - } - } - - self.depth += 1; - PendingBody { - first_in_frame, - first_after_frame, - closures, - bomb: DropBomb::new("PendingBody must be passed to finish_frame_body"), - } + fn top_defining_local(&self) -> LocalId { + self.defining_closure().first_local } /// Merge `used`'s analysis into `user`'s analysis and record that `user` @@ -834,82 +1142,6 @@ before_obj != user_def.analysis.object_dependent_depth || before_loc != user_def.analysis.local_dependent_depth } - - /// After the body is processed, drop the frame's locals and emit any - /// "unused local" warnings. - fn finish_frame_body(&mut self, pending: PendingBody) { - let PendingBody { - first_in_frame, - first_after_frame, - closures, - mut bomb, - } = pending; - bomb.defuse(); - self.depth -= 1; - - debug_assert_eq!( - first_after_frame, - self.next_local_id(), - "nested scopes must be popped before outer frames" - ); - - let mut changed = true; - while changed { - changed = false; - for spec in closures.iter_specs() { - // Effective used_at_depth for the spec = min over its ids. - let mut min_used_at = u32::MAX; - for id_raw in spec.ids.clone() { - min_used_at = min_used_at.min(self.local_defs[id_raw as usize].used_at_depth); - } - if min_used_at == u32::MAX { - continue; - } - for &used in spec.references { - let used_def = &mut self.local_defs[used.idx()]; - if min_used_at < used_def.used_at_depth { - used_def.used_at_depth = min_used_at; - changed = true; - } - } - } - } - - let drained: Vec = self.local_defs.drain(first_in_frame.idx()..).collect(); - for (i, def) in drained.iter().enumerate().rev() { - let id = LocalId(first_in_frame.0 + i as u32); - let stack = self - .local_by_name - .get_mut(&def.name) - .expect("local must be in name map"); - let popped = stack.pop().expect("name stack should not be empty"); - debug_assert_eq!(popped, id, "name stack integrity"); - if stack.is_empty() { - self.local_by_name.remove(&def.name); - } - - if def.used_at_depth == u32::MAX { - if def.used_by_sibling { - self.report_warning( - format!("local is only referenced by unused siblings: {}", def.name), - def.span.clone(), - ); - } else { - self.report_warning(format!("unused local: {}", def.name), def.span.clone()); - } - } else if def.analysis.local_dependent_depth > def.defined_at_depth - && def.analysis.object_dependent_depth > def.defined_at_depth - && def.defined_at_depth != 0 - { - // The value doesn't depend on anything defined at or inside - // this local's scope - can be hoisted, unfortunately not automatically. - self.report_warning( - format!("local could be hoisted to an outer scope: {}", def.name), - def.span.clone(), - ); - } - } - } } mod names { @@ -922,56 +1154,85 @@ // Object scope helpers impl AnalysisStack { - // TODO: proper state machine - fn enter_object_scope(&mut self) -> ObjectScope { - let is_outermost = self.first_object_depth == u32::MAX; - let scope = ObjectScope { - this_id: self.push_pseudo_local(names::this()), - is_outermost, - prev_this_local: self.this_local, - prev_dollar_alias: self.dollar_alias, - prev_cur_self_used: self.cur_self_used, - prev_cur_super_used: self.cur_super_used, - prev_dollar_used: is_outermost.then_some(self.dollar_used), - prev_last_object: self.last_object_depth, - prev_first_object: self.first_object_depth, - }; + #[inline] + fn in_object_scope( + &mut self, + inner: impl FnOnce(&mut AnalysisStack) -> T, + ) -> (ObjectUsage, ClosureShape, T) { + fn enter_object_scope(stack: &mut AnalysisStack) -> ObjectScope { + let is_outermost = stack.first_object_depth == u32::MAX; + let this_id = stack.next_local_id(); + let closure = stack.push_closure_a(this_id); + let pushed = stack.push_pseudo_local(names::this()); + debug_assert_eq!(pushed, this_id, "this pseudo-local id"); + let scope = ObjectScope { + this_id, + is_outermost, + prev_this_local: stack.this_local, + prev_dollar_alias: stack.dollar_alias, + prev_cur_self_used: stack.cur_self_used, + prev_cur_super_used: stack.cur_super_used, + prev_dollar_used: is_outermost.then_some(stack.dollar_used), + prev_last_object: stack.last_object_depth, + prev_first_object: stack.first_object_depth, + closure, + }; - self.this_local = Some(scope.this_id); - if is_outermost { - self.dollar_alias = Some(scope.this_id); - self.first_object_depth = self.depth; - self.dollar_used = false; + stack.this_local = Some(scope.this_id); + if is_outermost { + stack.dollar_alias = Some(scope.this_id); + stack.first_object_depth = stack.depth; + stack.dollar_used = false; + } + stack.last_object_depth = stack.depth; + stack.cur_self_used = false; + stack.cur_super_used = false; + scope } - self.last_object_depth = self.depth; - self.cur_self_used = false; - self.cur_super_used = false; - scope - } - fn leave_object_scope(&mut self, scope: ObjectScope) -> ObjectUsage { - let _ = self.local_defs.pop().expect("this pseudo-local exists"); - debug_assert_eq!(self.local_defs.len(), scope.this_id.0 as usize); + fn leave_object_scope( + stack: &mut AnalysisStack, + scope: ObjectScope, + ) -> (ObjectUsage, ClosureShape) { + let ObjectScope { + this_id, + is_outermost, + prev_this_local, + prev_dollar_alias, + prev_cur_self_used, + prev_cur_super_used, + prev_dollar_used, + prev_last_object, + prev_first_object, + closure, + } = scope; + let _ = stack.local_defs.pop().expect("this pseudo-local exists"); + debug_assert_eq!(stack.local_defs.len(), this_id.0 as usize); - let set_dollar = scope.is_outermost && self.dollar_used; - let usage = ObjectUsage { - this_id: scope.this_id, - this_used: self.cur_self_used || self.cur_super_used || set_dollar, - uses_super: self.cur_super_used, - set_dollar, - }; + let set_dollar = is_outermost && stack.dollar_used; + let usage = ObjectUsage { + this_used: stack.cur_self_used || stack.cur_super_used || set_dollar, + uses_super: stack.cur_super_used, + set_dollar, + }; + + stack.this_local = prev_this_local; + stack.dollar_alias = prev_dollar_alias; + stack.cur_self_used = prev_cur_self_used; + stack.cur_super_used = prev_cur_super_used; + if let Some(prev) = prev_dollar_used { + stack.dollar_used = prev; + } + stack.last_object_depth = prev_last_object; + stack.first_object_depth = prev_first_object; - self.this_local = scope.prev_this_local; - self.dollar_alias = scope.prev_dollar_alias; - self.cur_self_used = scope.prev_cur_self_used; - self.cur_super_used = scope.prev_cur_super_used; - if let Some(prev) = scope.prev_dollar_used { - self.dollar_used = prev; + let frame_shape = stack.pop_closure(closure); + (usage, frame_shape) } - self.last_object_depth = scope.prev_last_object; - self.first_object_depth = scope.prev_first_object; - - usage + let scope = enter_object_scope(self); + let v = inner(self); + let (usage, shape) = leave_object_scope(self, scope); + (usage, shape, v) } fn push_pseudo_local(&mut self, name: IStr) -> LocalId { @@ -986,14 +1247,18 @@ analyzed: true, scratch_referenced: false, }); + { + let def = self.defining_closure_mut(); + let _ = def.define_local(id); + } id } - fn use_this(&mut self, taint: &mut AnalysisResult) -> Option { + fn use_this(&mut self, taint: &mut AnalysisResult) -> Option { let id = self.this_local?; self.cur_self_used = true; self.use_pseudo_local(id, taint); - Some(id) + Some(self.resolve_to_slot(id)) } fn use_super(&mut self, taint: &mut AnalysisResult) -> Option<()> { @@ -1003,11 +1268,11 @@ Some(()) } - fn use_dollar(&mut self, taint: &mut AnalysisResult) -> Option { + fn use_dollar(&mut self, taint: &mut AnalysisResult) -> Option { let id = self.dollar_alias?; self.dollar_used = true; self.use_pseudo_local(id, taint); - Some(id) + Some(self.resolve_to_slot(id)) } // TODO: Dedicated type for object references instead of "pseudo local" BS, idk @@ -1020,6 +1285,7 @@ } } +#[must_use] struct ObjectScope { this_id: LocalId, is_outermost: bool, @@ -1030,10 +1296,10 @@ prev_dollar_used: Option, prev_last_object: u32, prev_first_object: u32, + closure: ClosureOnStack, } struct ObjectUsage { - this_id: LocalId, this_used: bool, uses_super: bool, set_dollar: bool, @@ -1073,7 +1339,7 @@ stack.report_error("`self` used outside of object", None); LExpr::BadLocal("self") }, - LExpr::Local, + LExpr::Slot, ), LiteralType::Super => { if stack.use_super(taint).is_some() { @@ -1088,7 +1354,7 @@ stack.report_error("`$` used outside of object", None); LExpr::BadLocal("$") }, - LExpr::Local, + LExpr::Slot, ), LiteralType::Null => LExpr::Null, LiteralType::True => LExpr::Bool(true), @@ -1098,10 +1364,15 @@ Expr::Num(n) => LExpr::Num(*n), Expr::Var(v) => stack .use_local(&v.value, v.span.clone(), taint) - .map_or_else(|| LExpr::BadLocal("ref"), LExpr::Local), - Expr::Arr(a) => LExpr::Arr(Rc::new( - a.iter().map(|v| analyze(v, stack, taint)).collect(), - )), + .map_or_else(|| LExpr::BadLocal("ref"), LExpr::Slot), + Expr::Arr(a) => { + let (shape, items) = stack + .in_using_closure(|stack| a.iter().map(|v| analyze(v, stack, taint)).collect()); + LExpr::Arr { + shape, + items: Rc::new(items), + } + } Expr::ArrComp(inner, comp) => analyze_arr_comp(inner, comp, stack, taint), Expr::Obj(obj) => LExpr::Obj(analyze_obj_body(obj, stack, taint)), Expr::ObjExtend(base, obj) => LExpr::ObjExtend( @@ -1238,14 +1509,17 @@ if binds.is_empty() { return analyze(body, stack, taint); } - let (_frame_start, l_binds, body_expr) = - process_local_frame(binds, stack, taint, |stack, taint| { - analyze(body, stack, taint) - }); - LExpr::LocalExpr { + let frame_start = stack.next_local_id(); + let closure = stack.push_closure_a(frame_start); + let (l_binds, body_expr) = process_local_frame(binds, stack, taint, |stack, taint| { + analyze(body, stack, taint) + }); + let frame_shape = stack.pop_closure(closure); + LExpr::LocalExpr(Box::new(LLocalExpr { + frame_shape, binds: l_binds, - body: Box::new(body_expr), - } + body: body_expr, + })) } fn analyze_bind_value( @@ -1267,55 +1541,44 @@ } } -fn alloc_bind_destruct( - bind: &BindSpec, - stack: &mut AnalysisStack, - frame_start: LocalId, -) -> Option { - match bind { - BindSpec::Field { into, .. } => stack.alloc_destruct(into, frame_start), - BindSpec::Function { name, .. } => stack - .define_local(name.clone(), None, frame_start) - .map(LDestruct::Full), - } -} - fn process_local_frame( binds: &[BindSpec], stack: &mut AnalysisStack, taint: &mut AnalysisResult, body_fn: impl FnOnce(&mut AnalysisStack, &mut AnalysisResult) -> R, -) -> (LocalId, Vec, R) { - let frame_start = stack.begin_frame_alloc(); +) -> (Vec, R) { + let mut alloc = FrameAlloc::new(stack); let mut destructs: Vec> = Vec::with_capacity(binds.len()); for bind in binds { - destructs.push(alloc_bind_destruct(bind, stack, frame_start)); + destructs.push(alloc.alloc_bind(bind)); } - let pending = stack.finish_frame_alloc(frame_start); + let mut pending = alloc.finish(); - let mut closures = Closures::new(frame_start); let mut l_binds: Vec = Vec::with_capacity(binds.len()); for (bind, destruct) in binds.iter().zip(destructs.into_iter()) { let mut value_taint = AnalysisResult::default(); - let value = analyze_bind_value(bind, stack, &mut value_taint); + let (value_shape, value) = pending + .stack + .in_using_closure(|stack| analyze_bind_value(bind, stack, &mut value_taint)); taint.taint_by(value_taint); if let Some(destruct) = destruct { - stack.record_spec_init(&pending, &destruct, value_taint, &mut closures); + pending.record_spec_init(&destruct, value_taint); l_binds.push(LBind { destruct, + value_shape, value: Rc::new(value), }); } else { - closures.push_spec(0, &[]); + pending.closures.push_spec(0, &[]); } } - let body_frame = stack.finish_frame_init(pending, closures); - let result = body_fn(stack, taint); - stack.finish_frame_body(body_frame); + let body_frame = pending.finish(); + let result = body_fn(body_frame.stack, taint); + body_frame.finish(); - (frame_start, l_binds, result) + (l_binds, result) } fn analyze_function( @@ -1325,23 +1588,29 @@ stack: &mut AnalysisStack, taint: &mut AnalysisResult, ) -> LExpr { - let frame_start = stack.begin_frame_alloc(); + let mut alloc = FrameAlloc::new(stack); + let closure = alloc.push_locals_closure(); let mut param_destructs: Vec> = Vec::with_capacity(params.exprs.len()); for p in ¶ms.exprs { - param_destructs.push(stack.alloc_destruct(&p.destruct, frame_start)); + param_destructs.push(alloc.alloc_destruct(&p.destruct)); } - let pending = stack.finish_frame_alloc(frame_start); + let mut pending = alloc.finish(); - let mut closures = Closures::new(frame_start); let mut l_params: Vec = Vec::with_capacity(params.exprs.len()); for (p, destruct) in params.exprs.iter().zip(param_destructs.into_iter()) { let mut value_taint = AnalysisResult::default(); - let default = p - .default - .as_ref() - .map(|d| Rc::new(analyze(d, stack, &mut value_taint))); + let default = p.default.as_ref().map_or_else( + || None, + |d| { + Some( + pending + .stack + .in_using_closure(|stack| Rc::new(analyze(d, stack, &mut value_taint))), + ) + }, + ); taint.taint_by(value_taint); if let Some(destruct) = destruct { let name = match &p.destruct { @@ -1349,25 +1618,42 @@ #[cfg(feature = "exp-destruct")] _ => None, }; - stack.record_spec_init(&pending, &destruct, value_taint, &mut closures); + pending.record_spec_init(&destruct, value_taint); l_params.push(LParam { name, destruct, default, }); } else { - closures.push_spec(0, &[]); + pending.closures.push_spec(0, &[]); } } - let body_frame = stack.finish_frame_init(pending, closures); - let body_expr = analyze(body, stack, taint); - stack.finish_frame_body(body_frame); + let body_frame = pending.finish(); + let body_expr = analyze(body, body_frame.stack, taint); + body_frame.finish(); + let body_shape = stack.pop_closure(closure); + // function(x) x is an identity function + if l_params.len() == 1 && l_params[0].default.is_none() { + stack.report_warning( + "do not define identity functions manually, use std.id instead", + None, + ); + #[allow(irrefutable_let_patterns, reason = "refutable with exp-destruct")] + if let LDestruct::Full(param_slot) = &l_params[0].destruct + && let LExpr::Slot(LSlot::Local(s)) = &body_expr + && s == param_slot + { + return LExpr::IdentityFunction {}; + } + } + LExpr::Function(Rc::new(LFunction { name, params: l_params, signature: params.signature.clone(), + body_shape, body: Rc::new(body_expr), })) } @@ -1405,38 +1691,55 @@ }) .collect(); - let scope = stack.enter_object_scope(); - let (_frame_start, l_binds, (l_asserts, l_fields)) = - process_local_frame(locals, stack, taint, |stack, taint| { - let mut l_asserts = Vec::with_capacity(asserts.len()); - for a in asserts { - let mut assert_taint = AnalysisResult::default(); - l_asserts.push(analyze_assert(a, stack, &mut assert_taint)); - taint.taint_by(assert_taint); - } - let mut l_fields = Vec::with_capacity(fields.len()); - for (f, name) in fields.iter().zip(field_names) { - let value = if let Some(params) = &f.params { - analyze_function(name.function_name(), params, &f.value, stack, taint) + let (usage, frame_shape, (l_binds, (l_asserts_opt, l_fields))) = + stack.in_object_scope(|stack| { + process_local_frame(locals, stack, taint, |stack, taint| { + let l_asserts_opt = if asserts.is_empty() { + None } else { - analyze(&f.value, stack, taint) + let (shape, l_asserts) = stack.in_using_closure(|stack| { + let mut l_asserts = Vec::with_capacity(asserts.len()); + for a in asserts { + let mut assert_taint = AnalysisResult::default(); + l_asserts.push(analyze_assert(a, stack, &mut assert_taint)); + taint.taint_by(assert_taint); + } + l_asserts + }); + Some(Rc::new(LObjAsserts { + shape, + asserts: l_asserts, + })) }; - l_fields.push(LFieldMember { - name, - plus: f.plus, - visibility: f.visibility, - value: Rc::new(value), - }); - } - (l_asserts, l_fields) + let mut l_fields = Vec::with_capacity(fields.len()); + for (f, name) in fields.iter().zip(field_names) { + let value = stack.in_using_closure(|stack| { + if let Some(params) = &f.params { + analyze_function(name.function_name(), params, &f.value, stack, taint) + } else { + analyze(&f.value, stack, taint) + } + }); + l_fields.push(LFieldMember { + name, + plus: f.plus, + visibility: f.visibility, + value: Rc::new(value), + }); + } + (l_asserts_opt, l_fields) + }) }); - let usage = stack.leave_object_scope(scope); + // `this` was allocated as the first local of the object's frame, + // so its slot is 0 within that frame. + let this_slot = usage.this_used.then_some(LocalSlot(0)); LObjMembers { - this: usage.this_used.then_some(usage.this_id), + frame_shape, + this: this_slot, set_dollar: usage.set_dollar, uses_super: usage.uses_super, locals: Rc::new(l_binds), - asserts: Rc::new(l_asserts), + asserts: l_asserts_opt, fields: l_fields, } } @@ -1452,26 +1755,30 @@ FieldName::Dyn(e) => LFieldName::Dyn(analyze(e, stack, taint)), }; - let scope = stack.enter_object_scope(); - let body = process_local_frame(&comp.locals, stack, taint, |stack, taint| { - let value = if let Some(params) = &comp.field.params { - analyze_function(None, params, &comp.field.value, stack, taint) - } else { - analyze(&comp.field.value, stack, taint) - }; - LFieldMember { - name: field_name, - plus: comp.field.plus, - visibility: comp.field.visibility, - value: Rc::new(value), - } + let (usage, frame_shape, body) = stack.in_object_scope(|stack| { + process_local_frame(&comp.locals, stack, taint, |stack, taint| { + let value = stack.in_using_closure(|stack| { + if let Some(params) = &comp.field.params { + analyze_function(None, params, &comp.field.value, stack, taint) + } else { + analyze(&comp.field.value, stack, taint) + } + }); + LFieldMember { + name: field_name, + plus: comp.field.plus, + visibility: comp.field.visibility, + value: Rc::new(value), + } + }) }); - let usage = stack.leave_object_scope(scope); - (usage, body) + (usage, frame_shape, body) }); - let (usage, (_frame_start, locals, field)) = res.inner; + let (usage, frame_shape, (locals, field)) = res.inner; + let this_slot = usage.this_used.then_some(LocalSlot(0)); LObjComp { - this: usage.this_used.then_some(usage.this_id), + frame_shape: Rc::new(frame_shape), + this: this_slot, set_dollar: usage.set_dollar, uses_super: usage.uses_super, locals: Rc::new(locals), @@ -1487,10 +1794,12 @@ taint: &mut AnalysisResult, ) -> LExpr { let res = analyze_comp_specs(specs, stack, taint, |stack, taint| { - analyze(inner, stack, taint) + stack.in_using_closure(|stack| analyze(inner, stack, taint)) }); + let (value_shape, value) = res.inner; LExpr::ArrComp(Box::new(LArrComp { - value: Rc::new(res.inner), + value_shape, + value: Rc::new(value), compspecs: res.compspecs, })) } @@ -1525,23 +1834,27 @@ let loop_invariant = over_taint.local_dependent_depth > outer_depth; taint.taint_by(over_taint); - let frame_start = stack.begin_frame_alloc(); - let Some(l_destruct) = stack.alloc_destruct(destruct, frame_start) else { + let mut alloc = FrameAlloc::new(stack); + let closure = alloc.push_locals_closure(); + let Some(l_destruct) = alloc.alloc_destruct(destruct) else { + stack.pop_closure(closure); return go(idx + 1, specs, outer_depth, stack, taint, inside); }; - let pending = stack.finish_frame_alloc(frame_start); + let mut pending = alloc.finish(); let var_analysis = AnalysisResult::default(); - let mut closures = Closures::new(frame_start); - stack.record_spec_init(&pending, &l_destruct, var_analysis, &mut closures); + pending.record_spec_init(&l_destruct, var_analysis); - let body_frame = stack.finish_frame_init(pending, closures); - let (r, mut rest) = go(idx + 1, specs, outer_depth, stack, taint, inside); - stack.finish_frame_body(body_frame); + let body_frame = pending.finish(); + let (r, mut rest) = + go(idx + 1, specs, outer_depth, body_frame.stack, taint, inside); + body_frame.finish(); + let frame_shape = stack.pop_closure(closure); rest.insert( 0, LCompSpec::For { + frame_shape, destruct: l_destruct, over: over_l, loop_invariant, @@ -1570,18 +1883,37 @@ stack.define_external_local(name, id); } + let externals_count: u16 = stack + .local_defs + .len() + .try_into() + .expect("more than u16::MAX externals"); + let closure = stack.push_root_closure(externals_count); + let mut taint = AnalysisResult::default(); let lir = analyze(expr, &mut stack, &mut taint); + let root_shape = stack.pop_closure(closure); + debug_assert!( + stack.closure_stack.is_empty(), + "closure stack imbalance after analyze" + ); + AnalysisReport { lir, + root_shape, root_analysis: taint, diagnostics_list: stack.diagnostics, errored: stack.errored, } } +#[cfg(test)] fn render_diagnostics(src: &str, diags: &[Diagnostic]) -> String { + use std::fmt::Write; + + use hi_doc::{Formatting, SnippetBuilder, Text}; + let mut out = String::new(); let mut unspanned = Vec::new(); let mut spanned: Vec<&Diagnostic> = Vec::new(); @@ -1620,6 +1952,7 @@ pub struct AnalysisReport { pub lir: LExpr, + pub root_shape: ClosureShape, pub root_analysis: AnalysisResult, pub diagnostics_list: Vec, pub errored: bool, --- a/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@array_comp.jsonnet.snap +++ b/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@array_comp.jsonnet.snap @@ -1,7 +1,7 @@ --- source: crates/jrsonnet-evaluator/src/analyze.rs expression: rendered -input_file: crates/jrsonnet-evaluator/src/analyze_tests/array_comp.jsonnet +input_file: crates/jrsonnet-evaluator/src/analysis_tests/array_comp.jsonnet --- --- source --- [x * 2 for x in [1, 2, 3] if x > 1] @@ -13,10 +13,16 @@ --- lir --- ArrComp( LArrComp { + value_shape: ClosureShape { + captures: [], + n_locals: 0, + }, value: BinaryOp { - lhs: Local( - LocalId( - 0, + lhs: Slot( + Local( + LocalSlot( + 0, + ), ), ), op: Mul, @@ -26,13 +32,21 @@ }, compspecs: [ For { + frame_shape: ClosureShape { + captures: [], + n_locals: 1, + }, destruct: Full( - LocalId( + LocalSlot( 0, ), ), - over: Arr( - [ + over: Arr { + shape: ClosureShape { + captures: [], + n_locals: 0, + }, + items: [ Num( 1.0, ), @@ -43,14 +57,16 @@ 3.0, ), ], - ), + }, loop_invariant: true, }, If( BinaryOp { - lhs: Local( - LocalId( - 0, + lhs: Slot( + Local( + LocalSlot( + 0, + ), ), ), op: Gt, --- a/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@dollar_deeply_nested.jsonnet.snap +++ b/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@dollar_deeply_nested.jsonnet.snap @@ -1,7 +1,7 @@ --- source: crates/jrsonnet-evaluator/src/analyze.rs expression: rendered -input_file: crates/jrsonnet-evaluator/src/analyze_tests/dollar_deeply_nested.jsonnet +input_file: crates/jrsonnet-evaluator/src/analysis_tests/dollar_deeply_nested.jsonnet --- --- source --- { @@ -22,15 +22,19 @@ Obj( MemberList( LObjMembers { + frame_shape: ClosureShape { + captures: [], + n_locals: 1, + }, this: Some( - LocalId( + LocalSlot( 0, ), ), set_dollar: true, uses_super: false, locals: [], - asserts: [], + asserts: None, fields: [ LFieldMember { name: Fixed( @@ -38,8 +42,14 @@ ), plus: false, visibility: Normal, - value: Str( - "outer", + value: ( + ClosureShape { + captures: [], + n_locals: 0, + }, + Str( + "outer", + ), ), }, LFieldMember { @@ -48,75 +58,135 @@ ), plus: false, visibility: Normal, - value: Obj( - MemberList( - LObjMembers { - this: None, - set_dollar: false, - uses_super: false, - locals: [], - asserts: [], - fields: [ - LFieldMember { - name: Fixed( - "b", - ), - plus: false, - visibility: Normal, - value: Obj( - MemberList( - LObjMembers { - this: Some( - LocalId( - 2, - ), - ), - set_dollar: false, - uses_super: false, - locals: [], - asserts: [], - fields: [ - LFieldMember { - name: Fixed( - "c", + value: ( + ClosureShape { + captures: [], + n_locals: 0, + }, + Obj( + MemberList( + LObjMembers { + frame_shape: ClosureShape { + captures: [ + Local( + LocalSlot( + 0, + ), + ), + ], + n_locals: 1, + }, + this: None, + set_dollar: false, + uses_super: false, + locals: [], + asserts: None, + fields: [ + LFieldMember { + name: Fixed( + "b", + ), + plus: false, + visibility: Normal, + value: ( + ClosureShape { + captures: [ + Capture( + CaptureSlot( + 0, ), - plus: false, - visibility: Normal, - value: Index { - indexable: Local( - LocalId( - 0, + ), + ], + n_locals: 0, + }, + Obj( + MemberList( + LObjMembers { + frame_shape: ClosureShape { + captures: [ + Capture( + CaptureSlot( + 0, + ), ), - ), - parts: [ - LIndexPart { - span: virtual::45-48, - value: Str( - "top", - ), - }, ], + n_locals: 1, }, - }, - LFieldMember { - name: Fixed( - "d", - ), - plus: false, - visibility: Normal, - value: Local( - LocalId( - 2, + this: Some( + LocalSlot( + 0, ), ), + set_dollar: false, + uses_super: false, + locals: [], + asserts: None, + fields: [ + LFieldMember { + name: Fixed( + "c", + ), + plus: false, + visibility: Normal, + value: ( + ClosureShape { + captures: [ + Capture( + CaptureSlot( + 0, + ), + ), + ], + n_locals: 0, + }, + Index { + indexable: Slot( + Capture( + CaptureSlot( + 0, + ), + ), + ), + parts: [ + LIndexPart { + span: virtual::45-48, + value: Str( + "top", + ), + }, + ], + }, + ), + }, + LFieldMember { + name: Fixed( + "d", + ), + plus: false, + visibility: Normal, + value: ( + ClosureShape { + captures: [], + n_locals: 0, + }, + Slot( + Local( + LocalSlot( + 0, + ), + ), + ), + ), + }, + ], }, - ], - }, + ), + ), ), - ), - }, - ], - }, + }, + ], + }, + ), ), ), }, --- a/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@function_def.jsonnet.snap +++ b/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@function_def.jsonnet.snap @@ -11,94 +11,114 @@ errored: false --- diagnostics --- --- lir --- -LocalExpr { - binds: [ - LBind { - destruct: Full( - LocalId( - 0, - ), - ), - value: Function( - LFunction { - name: Some( - "f", +LocalExpr( + LLocalExpr { + frame_shape: ClosureShape { + captures: [], + n_locals: 1, + }, + binds: [ + LBind { + destruct: Full( + LocalSlot( + 0, ), - params: [ - LParam { - name: Some( - "x", - ), - destruct: Full( - LocalId( - 1, - ), - ), - default: None, - }, - LParam { - name: Some( - "y", - ), - destruct: Full( - LocalId( - 2, - ), - ), - default: None, - }, - ], - signature: FunctionSignature( - [ - ParamParse { - name: Named( + ), + value_shape: ClosureShape { + captures: [], + n_locals: 0, + }, + value: Function( + LFunction { + name: Some( + "f", + ), + params: [ + LParam { + name: Some( "x", ), + destruct: Full( + LocalSlot( + 0, + ), + ), default: None, }, - ParamParse { - name: Named( + LParam { + name: Some( "y", ), + destruct: Full( + LocalSlot( + 1, + ), + ), default: None, }, ], - ), - body: BinaryOp { - lhs: Local( - LocalId( - 1, - ), + signature: FunctionSignature( + [ + ParamParse { + name: Named( + "x", + ), + default: None, + }, + ParamParse { + name: Named( + "y", + ), + default: None, + }, + ], ), - op: Add, - rhs: Local( - LocalId( - 2, + body_shape: ClosureShape { + captures: [], + n_locals: 2, + }, + body: BinaryOp { + lhs: Slot( + Local( + LocalSlot( + 0, + ), + ), ), - ), + op: Add, + rhs: Slot( + Local( + LocalSlot( + 1, + ), + ), + ), + }, }, - }, - ), - }, - ], - body: Apply { - applicable: Local( - LocalId( - 0, - ), - ), - args: LArgsDesc { - unnamed: [ - Num( - 1.0, ), - Num( - 2.0, + }, + ], + body: Apply { + applicable: Slot( + Local( + LocalSlot( + 0, + ), ), - ], - names: [], - values: [], - } from virtual::24-30, - tailstrict: false, + ), + args: LArgsDesc { + unnamed: [ + Num( + 1.0, + ), + Num( + 2.0, + ), + ], + names: [], + values: [], + } from virtual::24-30, + tailstrict: false, + }, }, -} +) --- a/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@hoistable_local.jsonnet.snap +++ b/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@hoistable_local.jsonnet.snap @@ -1,7 +1,7 @@ --- source: crates/jrsonnet-evaluator/src/analyze.rs expression: rendered -input_file: crates/jrsonnet-evaluator/src/analyze_tests/hoistable_local.jsonnet +input_file: crates/jrsonnet-evaluator/src/analysis_tests/hoistable_local.jsonnet --- --- source --- local outer = 1; local inner = 10 + 20; outer + inner @@ -14,50 +14,80 @@ 1 │ local outer = 1; local inner = 10 + 20; outer + inner 2 │ --- lir --- -LocalExpr { - binds: [ - LBind { - destruct: Full( - LocalId( - 0, - ), - ), - value: Num( - 1.0, - ), +LocalExpr( + LLocalExpr { + frame_shape: ClosureShape { + captures: [], + n_locals: 1, }, - ], - body: LocalExpr { binds: [ LBind { destruct: Full( - LocalId( - 1, + LocalSlot( + 0, ), ), - value: BinaryOp { - lhs: Num( - 10.0, + value_shape: ClosureShape { + captures: [], + n_locals: 0, + }, + value: Num( + 1.0, + ), + }, + ], + body: LocalExpr( + LLocalExpr { + frame_shape: ClosureShape { + captures: [ + Local( + LocalSlot( + 0, + ), + ), + ], + n_locals: 1, + }, + binds: [ + LBind { + destruct: Full( + LocalSlot( + 0, + ), + ), + value_shape: ClosureShape { + captures: [], + n_locals: 0, + }, + value: BinaryOp { + lhs: Num( + 10.0, + ), + op: Add, + rhs: Num( + 20.0, + ), + }, + }, + ], + body: BinaryOp { + lhs: Slot( + Capture( + CaptureSlot( + 0, + ), + ), ), op: Add, - rhs: Num( - 20.0, + rhs: Slot( + Local( + LocalSlot( + 0, + ), + ), ), }, }, - ], - body: BinaryOp { - lhs: Local( - LocalId( - 0, - ), - ), - op: Add, - rhs: Local( - LocalId( - 1, - ), - ), - }, + ), }, -} +) --- a/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@loop_invariant.jsonnet.snap +++ b/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@loop_invariant.jsonnet.snap @@ -18,23 +18,41 @@ --- lir --- ArrComp( LArrComp { + value_shape: ClosureShape { + captures: [ + Capture( + CaptureSlot( + 0, + ), + ), + ], + n_locals: 0, + }, value: BinaryOp { - lhs: Local( - LocalId( - 0, + lhs: Slot( + Capture( + CaptureSlot( + 0, + ), ), ), op: Lt, - rhs: Local( - LocalId( - 1, + rhs: Slot( + Local( + LocalSlot( + 0, + ), ), ), }, compspecs: [ For { + frame_shape: ClosureShape { + captures: [], + n_locals: 1, + }, destruct: Full( - LocalId( + LocalSlot( 0, ), ), @@ -69,9 +87,19 @@ loop_invariant: true, }, For { + frame_shape: ClosureShape { + captures: [ + Local( + LocalSlot( + 0, + ), + ), + ], + n_locals: 1, + }, destruct: Full( - LocalId( - 1, + LocalSlot( + 0, ), ), over: Apply { --- a/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@mutual_recursion.jsonnet.snap +++ b/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@mutual_recursion.jsonnet.snap @@ -1,7 +1,7 @@ --- source: crates/jrsonnet-evaluator/src/analyze.rs expression: rendered -input_file: crates/jrsonnet-evaluator/src/analyze_tests/mutual_recursion.jsonnet +input_file: crates/jrsonnet-evaluator/src/analysis_tests/mutual_recursion.jsonnet --- --- source --- local a = b, b = 1; a + 2 @@ -11,40 +11,58 @@ errored: false --- diagnostics --- --- lir --- -LocalExpr { - binds: [ - LBind { - destruct: Full( - LocalId( - 0, +LocalExpr( + LLocalExpr { + frame_shape: ClosureShape { + captures: [], + n_locals: 2, + }, + binds: [ + LBind { + destruct: Full( + LocalSlot( + 0, + ), + ), + value_shape: ClosureShape { + captures: [], + n_locals: 0, + }, + value: Slot( + Local( + LocalSlot( + 1, + ), + ), + ), + }, + LBind { + destruct: Full( + LocalSlot( + 1, + ), ), - ), - value: Local( - LocalId( - 1, + value_shape: ClosureShape { + captures: [], + n_locals: 0, + }, + value: Num( + 1.0, ), - ), - }, - LBind { - destruct: Full( - LocalId( - 1, + }, + ], + body: BinaryOp { + lhs: Slot( + Local( + LocalSlot( + 0, + ), ), ), - value: Num( - 1.0, + op: Add, + rhs: Num( + 2.0, ), }, - ], - body: BinaryOp { - lhs: Local( - LocalId( - 0, - ), - ), - op: Add, - rhs: Num( - 2.0, - ), }, -} +) --- a/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@nested_object_independent.jsonnet.snap +++ b/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@nested_object_independent.jsonnet.snap @@ -1,7 +1,7 @@ --- source: crates/jrsonnet-evaluator/src/analyze.rs expression: rendered -input_file: crates/jrsonnet-evaluator/src/analysis_golden/nested_object_independent.jsonnet +input_file: crates/jrsonnet-evaluator/src/analysis_tests/nested_object_independent.jsonnet --- --- source --- { @@ -19,11 +19,15 @@ Obj( MemberList( LObjMembers { + frame_shape: ClosureShape { + captures: [], + n_locals: 1, + }, this: None, set_dollar: false, uses_super: false, locals: [], - asserts: [], + asserts: None, fields: [ LFieldMember { name: Fixed( @@ -31,8 +35,14 @@ ), plus: false, visibility: Normal, - value: Num( - 1.0, + value: ( + ClosureShape { + captures: [], + n_locals: 0, + }, + Num( + 1.0, + ), ), }, LFieldMember { @@ -41,53 +51,77 @@ ), plus: false, visibility: Normal, - value: Obj( - MemberList( - LObjMembers { - this: Some( - LocalId( - 1, - ), - ), - set_dollar: false, - uses_super: false, - locals: [], - asserts: [], - fields: [ - LFieldMember { - name: Fixed( - "c", - ), - plus: false, - visibility: Normal, - value: Num( - 2.0, - ), + value: ( + ClosureShape { + captures: [], + n_locals: 0, + }, + Obj( + MemberList( + LObjMembers { + frame_shape: ClosureShape { + captures: [], + n_locals: 1, }, - LFieldMember { - name: Fixed( - "d", + this: Some( + LocalSlot( + 0, ), - plus: false, - visibility: Normal, - value: Index { - indexable: Local( - LocalId( - 1, + ), + set_dollar: false, + uses_super: false, + locals: [], + asserts: None, + fields: [ + LFieldMember { + name: Fixed( + "c", + ), + plus: false, + visibility: Normal, + value: ( + ClosureShape { + captures: [], + n_locals: 0, + }, + Num( + 2.0, ), ), - parts: [ - LIndexPart { - span: virtual::35-36, - value: Str( - "c", + }, + LFieldMember { + name: Fixed( + "d", + ), + plus: false, + visibility: Normal, + value: ( + ClosureShape { + captures: [], + n_locals: 0, + }, + Index { + indexable: Slot( + Local( + LocalSlot( + 0, + ), + ), ), + parts: [ + LIndexPart { + span: virtual::35-36, + value: Str( + "c", + ), + }, + ], }, - ], + ), }, - }, - ], - }, + ], + }, + ), ), ), }, --- a/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@object_comp.jsonnet.snap +++ b/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@object_comp.jsonnet.snap @@ -1,7 +1,7 @@ --- source: crates/jrsonnet-evaluator/src/analyze.rs expression: rendered -input_file: crates/jrsonnet-evaluator/src/analyze_tests/object_comp.jsonnet +input_file: crates/jrsonnet-evaluator/src/analysis_tests/object_comp.jsonnet --- --- source --- { [k]: k for k in ['a', 'b'] } @@ -14,35 +14,69 @@ Obj( ObjComp( LObjComp { + frame_shape: ClosureShape { + captures: [ + Local( + LocalSlot( + 0, + ), + ), + ], + n_locals: 1, + }, this: None, set_dollar: false, uses_super: false, locals: [], field: LFieldMember { name: Dyn( - Local( - LocalId( - 0, + Slot( + Local( + LocalSlot( + 0, + ), ), ), ), plus: false, visibility: Normal, - value: Local( - LocalId( - 0, + value: ( + ClosureShape { + captures: [ + Capture( + CaptureSlot( + 0, + ), + ), + ], + n_locals: 0, + }, + Slot( + Capture( + CaptureSlot( + 0, + ), + ), ), ), }, compspecs: [ For { + frame_shape: ClosureShape { + captures: [], + n_locals: 1, + }, destruct: Full( - LocalId( + LocalSlot( 0, ), ), - over: Arr( - [ + over: Arr { + shape: ClosureShape { + captures: [], + n_locals: 0, + }, + items: [ Str( "a", ), @@ -50,7 +84,7 @@ "b", ), ], - ), + }, loop_invariant: true, }, ], --- a/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@object_dollar.jsonnet.snap +++ b/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@object_dollar.jsonnet.snap @@ -1,7 +1,7 @@ --- source: crates/jrsonnet-evaluator/src/analyze.rs expression: rendered -input_file: crates/jrsonnet-evaluator/src/analyze_tests/object_dollar.jsonnet +input_file: crates/jrsonnet-evaluator/src/analysis_tests/object_dollar.jsonnet --- --- source --- { a: 1, b: { c: $.a } } @@ -14,15 +14,19 @@ Obj( MemberList( LObjMembers { + frame_shape: ClosureShape { + captures: [], + n_locals: 1, + }, this: Some( - LocalId( + LocalSlot( 0, ), ), set_dollar: true, uses_super: false, locals: [], - asserts: [], + asserts: None, fields: [ LFieldMember { name: Fixed( @@ -30,8 +34,14 @@ ), plus: false, visibility: Normal, - value: Num( - 1.0, + value: ( + ClosureShape { + captures: [], + n_locals: 0, + }, + Num( + 1.0, + ), ), }, LFieldMember { @@ -40,39 +50,69 @@ ), plus: false, visibility: Normal, - value: Obj( - MemberList( - LObjMembers { - this: None, - set_dollar: false, - uses_super: false, - locals: [], - asserts: [], - fields: [ - LFieldMember { - name: Fixed( - "c", - ), - plus: false, - visibility: Normal, - value: Index { - indexable: Local( - LocalId( + value: ( + ClosureShape { + captures: [], + n_locals: 0, + }, + Obj( + MemberList( + LObjMembers { + frame_shape: ClosureShape { + captures: [ + Local( + LocalSlot( 0, ), ), - parts: [ - LIndexPart { - span: virtual::18-19, - value: Str( - "a", + ], + n_locals: 1, + }, + this: None, + set_dollar: false, + uses_super: false, + locals: [], + asserts: None, + fields: [ + LFieldMember { + name: Fixed( + "c", + ), + plus: false, + visibility: Normal, + value: ( + ClosureShape { + captures: [ + Capture( + CaptureSlot( + 0, + ), + ), + ], + n_locals: 0, + }, + Index { + indexable: Slot( + Capture( + CaptureSlot( + 0, + ), + ), ), + parts: [ + LIndexPart { + span: virtual::18-19, + value: Str( + "a", + ), + }, + ], }, - ], + ), }, - }, - ], - }, + ], + }, + ), ), ), }, --- a/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@object_self.jsonnet.snap +++ b/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@object_self.jsonnet.snap @@ -1,7 +1,7 @@ --- source: crates/jrsonnet-evaluator/src/analyze.rs expression: rendered -input_file: crates/jrsonnet-evaluator/src/analyze_tests/object_self.jsonnet +input_file: crates/jrsonnet-evaluator/src/analysis_tests/object_self.jsonnet --- --- source --- { a: 1, b: self.a } @@ -14,15 +14,19 @@ Obj( MemberList( LObjMembers { + frame_shape: ClosureShape { + captures: [], + n_locals: 1, + }, this: Some( - LocalId( + LocalSlot( 0, ), ), set_dollar: false, uses_super: false, locals: [], - asserts: [], + asserts: None, fields: [ LFieldMember { name: Fixed( @@ -30,8 +34,14 @@ ), plus: false, visibility: Normal, - value: Num( - 1.0, + value: ( + ClosureShape { + captures: [], + n_locals: 0, + }, + Num( + 1.0, + ), ), }, LFieldMember { @@ -40,21 +50,29 @@ ), plus: false, visibility: Normal, - value: Index { - indexable: Local( - LocalId( - 0, + value: ( + ClosureShape { + captures: [], + n_locals: 0, + }, + Index { + indexable: Slot( + Local( + LocalSlot( + 0, + ), + ), ), - ), - parts: [ - LIndexPart { - span: virtual::16-17, - value: Str( - "a", - ), - }, - ], - }, + parts: [ + LIndexPart { + span: virtual::16-17, + value: Str( + "a", + ), + }, + ], + }, + ), }, ], }, --- a/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@object_with_locals.jsonnet.snap +++ b/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@object_with_locals.jsonnet.snap @@ -1,7 +1,7 @@ --- source: crates/jrsonnet-evaluator/src/analyze.rs expression: rendered -input_file: crates/jrsonnet-evaluator/src/analyze_tests/object_with_locals.jsonnet +input_file: crates/jrsonnet-evaluator/src/analysis_tests/object_with_locals.jsonnet --- --- source --- { @@ -18,22 +18,30 @@ Obj( MemberList( LObjMembers { + frame_shape: ClosureShape { + captures: [], + n_locals: 2, + }, this: None, set_dollar: false, uses_super: false, locals: [ LBind { destruct: Full( - LocalId( + LocalSlot( 1, ), ), + value_shape: ClosureShape { + captures: [], + n_locals: 0, + }, value: Num( 10.0, ), }, ], - asserts: [], + asserts: None, fields: [ LFieldMember { name: Fixed( @@ -41,9 +49,17 @@ ), plus: false, visibility: Normal, - value: Local( - LocalId( - 1, + value: ( + ClosureShape { + captures: [], + n_locals: 0, + }, + Slot( + Local( + LocalSlot( + 1, + ), + ), ), ), }, @@ -53,17 +69,25 @@ ), plus: false, visibility: Normal, - value: BinaryOp { - lhs: Local( - LocalId( - 1, + value: ( + ClosureShape { + captures: [], + n_locals: 0, + }, + BinaryOp { + lhs: Slot( + Local( + LocalSlot( + 1, + ), + ), + ), + op: Mul, + rhs: Num( + 2.0, ), - ), - op: Mul, - rhs: Num( - 2.0, - ), - }, + }, + ), }, ], }, --- a/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@redeclared_local.jsonnet.snap +++ b/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@redeclared_local.jsonnet.snap @@ -14,22 +14,34 @@ 1 │ local x = 1, x = 2; x 2 │ --- lir --- -LocalExpr { - binds: [ - LBind { - destruct: Full( - LocalId( +LocalExpr( + LLocalExpr { + frame_shape: ClosureShape { + captures: [], + n_locals: 1, + }, + binds: [ + LBind { + destruct: Full( + LocalSlot( + 0, + ), + ), + value_shape: ClosureShape { + captures: [], + n_locals: 0, + }, + value: Num( + 1.0, + ), + }, + ], + body: Slot( + Local( + LocalSlot( 0, ), ), - value: Num( - 1.0, - ), - }, - ], - body: Local( - LocalId( - 0, ), - ), -} + }, +) --- a/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@shadowing.jsonnet.snap +++ b/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@shadowing.jsonnet.snap @@ -1,7 +1,7 @@ --- source: crates/jrsonnet-evaluator/src/analyze.rs expression: rendered -input_file: crates/jrsonnet-evaluator/src/analyze_tests/shadowing.jsonnet +input_file: crates/jrsonnet-evaluator/src/analysis_tests/shadowing.jsonnet --- --- source --- local x = 1; local x = 2; x @@ -15,36 +15,58 @@ · ╰── unused local: x 2 │ --- lir --- -LocalExpr { - binds: [ - LBind { - destruct: Full( - LocalId( - 0, - ), - ), - value: Num( - 1.0, - ), +LocalExpr( + LLocalExpr { + frame_shape: ClosureShape { + captures: [], + n_locals: 1, }, - ], - body: LocalExpr { binds: [ LBind { destruct: Full( - LocalId( - 1, + LocalSlot( + 0, ), ), + value_shape: ClosureShape { + captures: [], + n_locals: 0, + }, value: Num( - 2.0, + 1.0, ), }, ], - body: Local( - LocalId( - 1, - ), + body: LocalExpr( + LLocalExpr { + frame_shape: ClosureShape { + captures: [], + n_locals: 1, + }, + binds: [ + LBind { + destruct: Full( + LocalSlot( + 0, + ), + ), + value_shape: ClosureShape { + captures: [], + n_locals: 0, + }, + value: Num( + 2.0, + ), + }, + ], + body: Slot( + Local( + LocalSlot( + 0, + ), + ), + ), + }, ), }, -} +) --- a/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@simple_local.jsonnet.snap +++ b/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@simple_local.jsonnet.snap @@ -1,7 +1,7 @@ --- source: crates/jrsonnet-evaluator/src/analyze.rs expression: rendered -input_file: crates/jrsonnet-evaluator/src/analyze_tests/simple_local.jsonnet +input_file: crates/jrsonnet-evaluator/src/analysis_tests/simple_local.jsonnet --- --- source --- local x = 1; x + 2 @@ -11,28 +11,40 @@ errored: false --- diagnostics --- --- lir --- -LocalExpr { - binds: [ - LBind { - destruct: Full( - LocalId( - 0, +LocalExpr( + LLocalExpr { + frame_shape: ClosureShape { + captures: [], + n_locals: 1, + }, + binds: [ + LBind { + destruct: Full( + LocalSlot( + 0, + ), + ), + value_shape: ClosureShape { + captures: [], + n_locals: 0, + }, + value: Num( + 1.0, + ), + }, + ], + body: BinaryOp { + lhs: Slot( + Local( + LocalSlot( + 0, + ), ), ), - value: Num( - 1.0, + op: Add, + rhs: Num( + 2.0, ), }, - ], - body: BinaryOp { - lhs: Local( - LocalId( - 0, - ), - ), - op: Add, - rhs: Num( - 2.0, - ), }, -} +) --- a/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@slice.jsonnet.snap +++ b/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@slice.jsonnet.snap @@ -1,7 +1,8 @@ --- source: crates/jrsonnet-evaluator/src/analyze.rs +assertion_line: 2017 expression: rendered -input_file: crates/jrsonnet-evaluator/src/analyze_tests/slice.jsonnet +input_file: crates/jrsonnet-evaluator/src/analysis_tests/slice.jsonnet --- --- source --- [1, 2, 3, 4, 5][1:3] @@ -13,8 +14,12 @@ --- lir --- Slice( LSliceExpr { - value: Arr( - [ + value: Arr { + shape: ClosureShape { + captures: [], + n_locals: 0, + }, + items: [ Num( 1.0, ), @@ -31,7 +36,7 @@ 5.0, ), ], - ), + }, start: Some( Num( 1.0, --- a/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@super_usage.jsonnet.snap +++ b/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@super_usage.jsonnet.snap @@ -15,11 +15,15 @@ lhs: Obj( MemberList( LObjMembers { + frame_shape: ClosureShape { + captures: [], + n_locals: 1, + }, this: None, set_dollar: false, uses_super: false, locals: [], - asserts: [], + asserts: None, fields: [ LFieldMember { name: Fixed( @@ -27,8 +31,14 @@ ), plus: false, visibility: Normal, - value: Num( - 1.0, + value: ( + ClosureShape { + captures: [], + n_locals: 0, + }, + Num( + 1.0, + ), ), }, LFieldMember { @@ -37,8 +47,14 @@ ), plus: false, visibility: Normal, - value: Num( - 2.0, + value: ( + ClosureShape { + captures: [], + n_locals: 0, + }, + Num( + 2.0, + ), ), }, ], @@ -49,15 +65,19 @@ rhs: Obj( MemberList( LObjMembers { + frame_shape: ClosureShape { + captures: [], + n_locals: 1, + }, this: Some( - LocalId( + LocalSlot( 0, ), ), set_dollar: false, uses_super: true, locals: [], - asserts: [], + asserts: None, fields: [ LFieldMember { name: Fixed( @@ -65,23 +85,29 @@ ), plus: false, visibility: Normal, - value: BinaryOp { - lhs: Index { - indexable: Super, - parts: [ - LIndexPart { - span: virtual::28-29, - value: Str( - "a", - ), - }, - ], + value: ( + ClosureShape { + captures: [], + n_locals: 0, + }, + BinaryOp { + lhs: Index { + indexable: Super, + parts: [ + LIndexPart { + span: virtual::28-29, + value: Str( + "a", + ), + }, + ], + }, + op: Add, + rhs: Num( + 10.0, + ), }, - op: Add, - rhs: Num( - 10.0, - ), - }, + ), }, LFieldMember { name: Fixed( @@ -89,21 +115,29 @@ ), plus: false, visibility: Normal, - value: Index { - indexable: Local( - LocalId( - 0, - ), - ), - parts: [ - LIndexPart { - span: virtual::44-45, - value: Str( - "b", + value: ( + ClosureShape { + captures: [], + n_locals: 0, + }, + Index { + indexable: Slot( + Local( + LocalSlot( + 0, + ), ), - }, - ], - }, + ), + parts: [ + LIndexPart { + span: virtual::44-45, + value: Str( + "b", + ), + }, + ], + }, + ), }, ], }, --- a/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@unused_local.jsonnet.snap +++ b/crates/jrsonnet-evaluator/src/snapshots/jrsonnet_evaluator__analyze__tests__snapshots@unused_local.jsonnet.snap @@ -1,7 +1,7 @@ --- source: crates/jrsonnet-evaluator/src/analyze.rs expression: rendered -input_file: crates/jrsonnet-evaluator/src/analyze_tests/unused_local.jsonnet +input_file: crates/jrsonnet-evaluator/src/analysis_tests/unused_local.jsonnet --- --- source --- local unused = 1; 2 @@ -14,20 +14,30 @@ 1 │ local unused = 1; 2 2 │ --- lir --- -LocalExpr { - binds: [ - LBind { - destruct: Full( - LocalId( - 0, +LocalExpr( + LLocalExpr { + frame_shape: ClosureShape { + captures: [], + n_locals: 1, + }, + binds: [ + LBind { + destruct: Full( + LocalSlot( + 0, + ), + ), + value_shape: ClosureShape { + captures: [], + n_locals: 0, + }, + value: Num( + 1.0, ), - ), - value: Num( - 1.0, - ), - }, - ], - body: Num( - 2.0, - ), -} + }, + ], + body: Num( + 2.0, + ), + }, +) --- a/tests/go_testdata_golden_override/bad_function_call.jsonnet.golden +++ b/tests/go_testdata_golden_override/bad_function_call.jsonnet.golden @@ -1,3 +1,3 @@ function argument is not passed: x Function has the following signature: (x) - bad_function_call.jsonnet:1:16-19: function preparation \ No newline at end of file + bad_function_call.jsonnet:1:16-19: function preparation \ No newline at end of file --- a/tests/go_testdata_golden_override/bad_function_call2.jsonnet.golden +++ b/tests/go_testdata_golden_override/bad_function_call2.jsonnet.golden @@ -1,3 +1,3 @@ too many args, function has 1 Function has the following signature: (x) - bad_function_call2.jsonnet:1:16-23: function preparation \ No newline at end of file + bad_function_call2.jsonnet:1:16-23: function preparation \ No newline at end of file --- a/tests/go_testdata_golden_override/bad_function_call_and_error.jsonnet.golden +++ b/tests/go_testdata_golden_override/bad_function_call_and_error.jsonnet.golden @@ -1,3 +1,3 @@ too many args, function has 1 Function has the following signature: (x) - bad_function_call_and_error.jsonnet:1:16-39: function preparation \ No newline at end of file + bad_function_call_and_error.jsonnet:1:16-39: function preparation \ No newline at end of file --- a/tests/go_testdata_golden_override/optional_args9.jsonnet.golden +++ b/tests/go_testdata_golden_override/optional_args9.jsonnet.golden @@ -1,2 +1,2 @@ argument x is already bound - optional_args9.jsonnet:1:16-27: function preparation \ No newline at end of file + optional_args9.jsonnet:1:16-27: function preparation \ No newline at end of file --- a/tests/golden/comp_if_with_multiple_captures.jsonnet +++ /dev/null @@ -1,3 +0,0 @@ -local features = { gc: 'serialgc', libc: 'musl' }; -local order = ['gc', 'libc', 'missing']; -[features[f] for f in order if std.objectHas(features, f)] --- a/tests/golden/object_assert_after_member_local.jsonnet +++ /dev/null @@ -1,7 +0,0 @@ -local outer1 = 'one'; -local outer2 = 'two'; -{ - local member_local = outer1, - assert outer2 == 'two' : 'wrong outer2: ' + outer2, - result: member_local, -}.result --- /dev/null +++ b/tests/suite/comp_eager_array_body_capture.jsonnet @@ -0,0 +1,2 @@ +std.assertEqual([[v] for v in ['a', 'b']], [['a'], ['b']]) +&& std.assertEqual(std.flattenArrays([[{ x: v }] for v in ['a', 'b']]), [{ x: 'a' }, { x: 'b' }]) --- /dev/null +++ b/tests/suite/comp_if_with_multiple_captures.jsonnet @@ -0,0 +1,6 @@ +local features = { gc: 'serialgc', libc: 'musl' }; +local order = ['gc', 'libc', 'missing']; +std.assertEqual( + [features[f] for f in order if std.objectHas(features, f)], + ['serialgc', 'musl'] +) --- /dev/null +++ b/tests/suite/object_assert_after_member_local.jsonnet @@ -0,0 +1,7 @@ +local outer1 = 'one'; +local outer2 = 'two'; +std.assertEqual({ + local member_local = outer1, + assert outer2 == 'two' : 'wrong outer2: ' + outer2, + result: member_local, +}.result, 'one') --- a/tests/tests/common.rs +++ b/tests/tests/common.rs @@ -1,6 +1,6 @@ use jrsonnet_evaluator::{ - ContextBuilder, ContextInitializer as ContextInitializerT, InitialContextBuilder, - ObjValueBuilder, Result, Source, Thunk, Val, bail, + ContextInitializer as ContextInitializerT, InitialContextBuilder, ObjValueBuilder, Result, + Source, Thunk, Val, bail, function::{FuncVal, builtin}, }; use jrsonnet_gcmodule::Trace; @@ -29,7 +29,7 @@ macro_rules! ensure_val_eq { ($a:expr, $b:expr) => {{ if !::jrsonnet_evaluator::val::equals(&$a.clone(), &$b.clone())? { - use ::jrsonnet_evaluator::manifest::JsonFormat; + use jrsonnet_evaluator::manifest::JsonFormat; ::jrsonnet_evaluator::bail!( "assertion failed: a != b\na={:#?}\nb={:#?}", $a.manifest(JsonFormat::default())?, -- gitstuff