--- a/crates/jrsonnet-evaluator/src/arr/mod.rs +++ b/crates/jrsonnet-evaluator/src/arr/mod.rs @@ -7,7 +7,12 @@ use jrsonnet_gcmodule::{Cc, cc_dyn}; -use crate::{Context, Result, Thunk, Val, analyze::LExpr, function::NativeFn, typed::IntoUntyped}; +use crate::{ + Context, Result, Thunk, Val, + analyze::{ClosureShape, LExpr}, + function::NativeFn, + typed::IntoUntyped, +}; mod spec; pub use spec::{ArrayLike, *}; @@ -36,8 +41,8 @@ Self::new(()) } - pub fn expr(ctx: Context, exprs: Rc>) -> Self { - Self::new(ExprArray::new(ctx, exprs)) + pub fn expr(ctx: Context, shape: &ClosureShape, exprs: Rc>) -> Self { + Self::new(ExprArray::new(ctx, shape, exprs)) } pub fn repeated(data: Self, repeats: u32) -> Option { --- a/crates/jrsonnet-evaluator/src/arr/spec.rs +++ b/crates/jrsonnet-evaluator/src/arr/spec.rs @@ -12,7 +12,7 @@ use super::ArrValue; use crate::{ Context, Error, ObjValue, Result, Thunk, Val, - analyze::LExpr, + analyze::{ClosureShape, LExpr}, error::ErrorKind::InfiniteRecursionDetected, evaluate::evaluate, function::NativeFn, @@ -123,9 +123,9 @@ cached: Cc>>, } impl ExprArray { - pub fn new(ctx: Context, src: Rc>) -> Self { + pub fn new(outer: Context, shape: &ClosureShape, src: Rc>) -> Self { Self { - ctx, + ctx: Context::enter_using(&outer, shape), cached: Cc::new(RefCell::new(vec![ArrayThunk::Waiting; src.len()])), src, } --- a/crates/jrsonnet-evaluator/src/async_import.rs +++ b/crates/jrsonnet-evaluator/src/async_import.rs @@ -4,7 +4,7 @@ use jrsonnet_ir::{IStr, Source, SourcePath, visit::Visitor}; use rustc_hash::FxHashMap; -use crate::{AsPathLike, FileData, ImportResolver, ResolvePathOwned, State}; +use crate::{AsPathLike, FileData, ImportResolver, ResolvePathOwned, Result, State}; pub struct Import { path: ResolvePathOwned, @@ -109,23 +109,23 @@ } } Job::ParseFile(path) => { - if let Some(file) = s.0.file_cache.borrow_mut().get_mut(&path) { - if file.parsed.is_none() { - let Some(code) = file.get_string() else { - continue; - }; - let source = Source::new(path.clone(), code.clone()); - // If failed - then skip import - file.parsed = crate::parse_jsonnet(&code, source).map(Rc::new).ok(); - if let Some(parsed) = &file.parsed { - let mut imports = FoundImports(vec![]); - imports.visit_expr(parsed); - for import in imports.0 { - queue.push(Job::ResolveImport { - from: path.clone(), - import, - }); - } + if let Some(file) = s.0.file_cache.borrow_mut().get_mut(&path) + && file.parsed.is_none() + { + let Some(code) = file.get_string() else { + continue; + }; + let source = Source::new(path.clone(), code.clone()); + // If failed - then skip import + file.parsed = crate::parse_jsonnet(&code, source).map(Rc::new).ok(); + if let Some(parsed) = &file.parsed { + let mut imports = FoundImports(vec![]); + imports.visit_expr(parsed); + for import in imports.0 { + queue.push(Job::ResolveImport { + from: path.clone(), + import, + }); } } } --- a/crates/jrsonnet-evaluator/src/ctx.rs +++ b/crates/jrsonnet-evaluator/src/ctx.rs @@ -1,151 +1,296 @@ -use std::{clone::Clone, fmt::Debug}; +use std::{ + cell::{Cell, OnceCell, RefCell}, + clone::Clone, + fmt::{self, Debug}, +}; use educe::Educe; use jrsonnet_gcmodule::{Cc, Trace}; use jrsonnet_interner::IStr; -use crate::{Pending, Result, SupThis, Thunk, Val, analyze::LocalId, error, error::ErrorKind::*}; +use crate::{ + Result, SupThis, Thunk, Val, + analyze::{CaptureSlot, ClosureShape, LSlot, LocalId, LocalSlot}, + bail, error, + error::ErrorKind::*, +}; #[derive(Debug, Trace, Clone, Educe)] #[educe(PartialEq)] -pub struct Context(#[educe(PartialEq(method = Cc::ptr_eq))] Cc); +pub struct Context(#[educe(PartialEq(method = Cc::ptr_eq))] pub(crate) Cc); -#[derive(Debug, Trace, Clone)] -struct ContextInternal { - sup_this: Option, - /// `bindings[i]` corresponds to `LocalId(offset + i)`. - bindings: Vec>>, - offset: u32, - parent: Option, +#[derive(Trace)] +pub(crate) struct ContextInternal { + /// Immutable, packed at closure-create time. + pub(crate) captures: Cc>>, + /// Filled during closure initialization + pub(crate) locals: Cc, + pub(crate) sup_this: Option, } -impl Context { - pub fn new_future() -> Pending { - Pending::new() +impl Debug for ContextInternal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ContextInternal") + .field("captures", &self.captures.len()) + .field("locals", &self.locals) + .field("sup_this", &self.sup_this.is_some()) + .finish() } +} - pub fn sup_this(&self) -> Option<&SupThis> { - self.0.sup_this.as_ref() +#[derive(Trace, Debug)] +pub(crate) struct IterFrame { + slots: Vec>>>, + captured: Cell, +} +impl IterFrame { + pub fn new(n: u16) -> IterFrame { + let cells: Vec>>> = (0..n).map(|_| RefCell::new(None)).collect(); + IterFrame { + slots: cells, + captured: Cell::new(false), + } } - - pub fn try_sup_this(&self) -> Result { - self.0 - .sup_this - .clone() - .ok_or_else(|| error!(CantUseSelfSupOutsideOfObject)) + pub fn set(&self, slot: LocalSlot, value: Thunk) { + *self.slots[slot.0 as usize].borrow_mut() = Some(value); } - - /// Update binding in `CoW` fashion. Only useful for eager comprehension - /// fast-path, as it requires Cc refcount to be 1; Use `ContextBuilder` otherwise. - pub(crate) fn cow_fill_binding(&mut self, id: LocalId, value: Thunk) { - let mut value = Some(Some(value)); +} - self.0.update_with(|inner| { - let local_idx = (id.0 - inner.offset) as usize; - while inner.bindings.len() <= local_idx { - inner.bindings.push(None); +#[derive(Trace, Debug)] +pub(crate) enum LocalsFrame { + Once1(OnceCell>), + /// Letrec/function/object/for frames - slots are filled during frame setup + Once(Vec>>), + /// Comp-eager fast-path, cells are reset per iteration for the unique frames (i.e for the non-capturing thunks) + Iter(IterFrame), +} +impl LocalsFrame { + pub fn set(&self, slot: LocalSlot, value: Thunk) { + match self { + LocalsFrame::Once1(cell) => { + debug_assert_eq!(slot.0, 0, "Once1 only holds slot 0"); + cell.set(value) + .map_err(|_| ()) + .expect("slot already filled"); } - inner.bindings[local_idx] = value.take().expect("called once"); - }); - } - - pub fn binding(&self, id: LocalId) -> Option> { - let id_num = id.0; - if id_num >= self.0.offset { - let local_idx = (id_num - self.0.offset) as usize; - if let Some(Some(thunk)) = self.0.bindings.get(local_idx) { - return Some(thunk.clone()); + LocalsFrame::Once(cells) => { + cells[slot.0 as usize] + .set(value) + .map_err(|_| ()) + .expect("slot already filled"); } + LocalsFrame::Iter(_) => unreachable!("iter frame has different constructors"), } - if let Some(parent) = &self.0.parent { - return parent.binding(id); + } +} + +impl LocalsFrame { + pub(crate) fn new_once(n: u16) -> Cc { + if n == 1 { + return Cc::new(Self::Once1(OnceCell::new())); } - None + let cells: Vec>> = (0..n).map(|_| OnceCell::new()).collect(); + Cc::new(Self::Once(cells)) } +} - #[must_use] - pub fn into_future(self, ctx: Pending) -> Self { - { - ctx.clone().fill(self); +pub(crate) struct IterContext { + context: Context, +} +impl IterContext { + pub(crate) fn create(&self, build: impl FnOnce(&IterFrame)) -> Result { + if !Cc::is_unique(&self.context.0.locals) { + bail!(EagerCompspecCaptured); + } + let LocalsFrame::Iter(frame) = &*self.context.0.locals else { + unreachable!("IterContext is only created for Iter ctx"); + }; + if frame.captured.get() { + bail!(EagerCompspecCaptured); } - ctx.unwrap() + build(frame); + Ok(self.context.clone()) } } -#[derive(Clone)] -pub struct ContextBuilder { +#[derive(Trace, Clone)] +pub(crate) struct PackedContext { + captures: Cc>>, + n_locals: u16, +} +impl PackedContext { + pub fn enter(self, sup_this: SupThis, build: impl FnOnce(&LocalsFrame, &Context)) -> Context { + let locals = LocalsFrame::new_once(self.n_locals); + let val = Context(Cc::new(ContextInternal { + captures: self.captures.clone(), + locals, + sup_this: Some(sup_this), + })); + build(&val.0.locals, &val); + val + } +} +#[derive(Trace, Clone, Educe, Debug)] +#[educe(PartialEq)] +pub(crate) struct PackedContextSupThis { + #[educe(PartialEq(method = Cc::ptr_eq))] + captures: Cc>>, + n_locals: u16, sup_this: Option, - bindings: Vec>>, - offset: u32, - parent: Option, } +impl PackedContextSupThis { + pub fn enter(self, build: impl FnOnce(&LocalsFrame, &Context)) -> Context { + let locals = LocalsFrame::new_once(self.n_locals); + let val = Context(Cc::new(ContextInternal { + captures: self.captures.clone(), + locals, + sup_this: self.sup_this, + })); + build(&val.0.locals, &val); + val + } +} -impl ContextBuilder { - pub fn new() -> Self { - Self { +impl Context { + #[inline] + pub fn slot(&self, slot: LSlot) -> Thunk { + match slot { + LSlot::Local(i) => self.local(i), + LSlot::Capture(i) => self.capture(i), + } + } + /// Read a local slot from the shared locals frame. + /// + /// # Panics + /// If the slot has not yet been filled. The analyzer guarantees + /// that slot indices are in range and that letrec setup completes + /// before the first read. A panic indicates an analyzer/runtime + /// invariant violation, not a user error. + #[inline] + pub fn local(&self, slot: LocalSlot) -> Thunk { + match &*self.0.locals { + LocalsFrame::Once1(cell) => { + debug_assert_eq!(slot.0, 0, "Once1 only holds slot 0"); + cell.get().expect("local read before letrec init").clone() + } + LocalsFrame::Once(cells) => cells[slot.0 as usize] + .get() + .expect("local read before letrec init") + .clone(), + LocalsFrame::Iter(cells) => cells.slots[slot.0 as usize] + .borrow() + .as_ref() + .expect("iter local read before iteration filled it") + .clone(), + } + } + + /// Read a captured slot from this closure's capture pack. + #[inline] + pub fn capture(&self, slot: CaptureSlot) -> Thunk { + (*self.0.captures)[slot.0 as usize].clone() + } + + pub fn sup_this(&self) -> Option<&SupThis> { + self.0.sup_this.as_ref() + } + + pub fn try_sup_this(&self) -> Result { + self.0 + .sup_this + .clone() + .ok_or_else(|| error!(CantUseSelfSupOutsideOfObject)) + } + + /// Build a root context: empty captures, externals filled into a + /// fresh Once locals frame in declaration order. Used once at + /// program entry to construct the context the analyzed root LIR + /// runs against. + pub(crate) fn root(externals: Vec>) -> Self { + let n: u16 = externals + .len() + .try_into() + .expect("more than u16::MAX externals"); + let cells: Vec>> = externals + .into_iter() + .map(|t| { + let cell = OnceCell::new(); + cell.set(t).map_err(|_| ()).expect("fresh cell"); + cell + }) + .collect(); + debug_assert_eq!(cells.len(), n as usize); + let locals = Cc::new(LocalsFrame::Once(cells)); + Self(Cc::new(ContextInternal { + captures: Cc::new(Vec::new()), + locals, sup_this: None, - bindings: Vec::new(), - offset: 0, - parent: None, - } + })) } - pub(crate) fn extend(parent: Context, capacity: usize) -> Self { - let offset = parent.0.offset + parent.0.bindings.len() as u32; - Self { - sup_this: parent.0.sup_this.clone(), - bindings: Vec::with_capacity(capacity), - offset, - parent: Some(parent), + pub(crate) fn pack_captures(&self, shape: &ClosureShape) -> PackedContext { + PackedContext { + captures: Cc::new(pack_captures(self, &shape.captures)), + n_locals: shape.n_locals, } } - - pub(crate) fn bind(&mut self, id: LocalId, value: Thunk) { - debug_assert!( - id.0 >= self.offset, - "cannot bind {id:?} below offset {}", - self.offset, - ); - let local_idx = (id.0 - self.offset) as usize; - self.bindings.reserve(local_idx); - while self.bindings.len() <= local_idx { - self.bindings.push(None); + pub(crate) fn pack_captures_sup_this(&self, shape: &ClosureShape) -> PackedContextSupThis { + PackedContextSupThis { + captures: Cc::new(pack_captures(self, &shape.captures)), + n_locals: shape.n_locals, + sup_this: self.0.sup_this.clone(), } - self.bindings[local_idx] = Some(value); } - pub(crate) fn build(self) -> Context { - Context(Cc::new(ContextInternal { - sup_this: self.sup_this, - bindings: self.bindings, - offset: self.offset, - parent: self.parent, - })) + pub(crate) fn enter_iter( + parent: &Context, + shape: &ClosureShape, + cb: impl FnOnce(IterContext) -> Result<()>, + ) -> Result<()> { + let captures = Cc::new(pack_captures(parent, &shape.captures)); + let locals = IterFrame::new(shape.n_locals); + cb(IterContext { + context: Self(Cc::new(ContextInternal { + captures, + locals: Cc::new(LocalsFrame::Iter(locals)), + sup_this: parent.0.sup_this.clone(), + })), + }) } - pub(crate) fn build_sup_this(mut self, st: SupThis) -> Context { - self.sup_this = Some(st); - self.build() + pub(crate) fn enter_using(parent: &Context, shape: &ClosureShape) -> Self { + debug_assert_eq!(shape.n_locals, 0); + if shape.captures.is_empty() { + if let LocalsFrame::Iter(i) = &*parent.0.locals { + i.captured.set(true); + } + // Value never uses captures, thus evaluating it against the parent gives the same result + return parent.clone(); + } + let captures = Cc::new(pack_captures(parent, &shape.captures)); + Self(Cc::new(ContextInternal { + captures, + locals: parent.0.locals.clone(), + sup_this: parent.0.sup_this.clone(), + })) } } -impl Default for ContextBuilder { - fn default() -> Self { - Self::new() - } +fn pack_captures(parent: &Context, sources: &[LSlot]) -> Vec> { + sources.iter().map(|src| parent.slot(*src)).collect() } pub struct InitialContextBuilder { - builder: ContextBuilder, externals: Vec<(IStr, LocalId)>, + values: Vec>, next_id: u32, } impl InitialContextBuilder { pub(crate) fn new() -> Self { Self { - builder: ContextBuilder::new(), externals: Vec::new(), + values: Vec::new(), next_id: 0, } } @@ -155,11 +300,11 @@ let id = LocalId(self.next_id); self.next_id += 1; self.externals.push((name, id)); - self.builder.bind(id, value); + self.values.push(value); } - pub(crate) fn build(self) -> (ContextBuilder, Vec<(IStr, LocalId)>) { - (self.builder, self.externals) + pub(crate) fn build(self) -> (Vec<(IStr, LocalId)>, Vec>) { + (self.externals, self.values) } } --- a/crates/jrsonnet-evaluator/src/dynamic.rs +++ b/crates/jrsonnet-evaluator/src/dynamic.rs @@ -1,59 +1,6 @@ -use std::{cell::OnceCell, hash::Hasher, ptr::addr_of}; - -use educe::Educe; -use jrsonnet_gcmodule::{Cc, Trace}; - -use crate::{Result, bail, error::ErrorKind::InfiniteRecursionDetected, val::ThunkValue}; - -#[derive(Trace, Educe)] -#[educe(Clone)] -pub struct Pending(pub Cc>); -impl Pending { - pub fn new() -> Self { - Self(Cc::new(OnceCell::new())) - } - pub fn new_filled(v: T) -> Self { - let cell = OnceCell::new(); - let _ = cell.set(v); - Self(Cc::new(cell)) - } - /// # Panics - /// If wrapper is filled already - pub fn fill(self, value: T) { - self.0 - .set(value) - .map_err(|_| ()) - .expect("wrapper is filled already"); - } -} -impl Pending { - /// # Panics - /// If wrapper is not yet filled - pub fn unwrap(&self) -> T { - self.0.get().cloned().expect("pending was not filled") - } - pub fn try_get(&self) -> Option { - self.0.get().cloned() - } -} +use std::{hash::Hasher, ptr::addr_of}; -impl ThunkValue for Pending { - type Output = T; - - fn get(&self) -> Result { - let Some(value) = self.0.get() else { - // TODO: Other error? - bail!(InfiniteRecursionDetected); - }; - Ok(value.clone()) - } -} - -impl Default for Pending { - fn default() -> Self { - Self::new() - } -} +use jrsonnet_gcmodule::Cc; pub fn identity_hash(v: &Cc, hasher: &mut H) { hasher.write_usize(addr_of!(**v) as usize); --- a/crates/jrsonnet-evaluator/src/error.rs +++ b/crates/jrsonnet-evaluator/src/error.rs @@ -10,6 +10,7 @@ use crate::{ ObjValue, ResolvePathOwned, + analyze::Diagnostic, function::{CallLocation, FunctionSignature, ParamName}, stdlib::format::FormatError, typed::TypeLocError, @@ -112,12 +113,14 @@ CantUseSelfSupOutsideOfObject, #[error("static analysis errors: {}", .0.iter().map(|d| d.message.as_str()).collect::>().join("; "))] - StaticAnalysisError(Vec), + StaticAnalysisError(Vec), #[error("no super found")] NoSuperFound, #[error("for loop can only iterate over arrays")] InComprehensionCanOnlyIterateOverArray, + #[error("(should not be visible) eager compspec evaluation failed due to captured context")] + EagerCompspecCaptured, #[error("array out of bounds: {0} is not within [0,{1})")] ArrayBoundsError(isize, u32), @@ -394,12 +397,5 @@ }; ($l:literal$(, $($tt:tt)*)?) => { <$crate::error::Error as From<$crate::error::ErrorKind>>::from($crate::error::ErrorKind::RuntimeError($crate::jrsonnet_macros::format_istr!($l$(, $($tt)*)?)).into()) - }; -} - -#[macro_export] -macro_rules! runtime_error { - ($l:literal$(, $($tt:tt)*)?) => { - $crate::error::Error::from($crate::error::ErrorKind::RuntimeError($crate::jrsonnet_macros::format_istr!($l$(, $($tt)*)?))) }; } --- a/crates/jrsonnet-evaluator/src/evaluate/compspec.rs +++ b/crates/jrsonnet-evaluator/src/evaluate/compspec.rs @@ -3,16 +3,19 @@ use jrsonnet_types::ValType; use super::{ - destructure::{self, evaluate_locals, evaluate_locals_unbound}, + destructure::{destruct, evaluate_locals_unbound, fill_letrec_binds}, evaluate_field_member_static, evaluate_field_member_unbound, }; use crate::{ - Context, ContextBuilder, ObjValue, ObjValueBuilder, Pending, Result, Thunk, Val, - analyze::{LArrComp, LBind, LCompSpec, LDestruct, LExpr, LFieldMember, LObjComp, LocalId}, + Context, ObjValue, ObjValueBuilder, Result, Thunk, Val, + analyze::{ + ClosureShape, LArrComp, LBind, LCompSpec, LDestruct, LExpr, LFieldMember, LObjComp, + LocalSlot, + }, arr::ArrValue, bail, error::ErrorKind::*, - evaluate::evaluate, + evaluate::{evaluate, evaluate_trivial}, }; trait CompCollector { @@ -22,6 +25,7 @@ struct EagerArrCollector<'a> { out: &'a mut Vec, + value_shape: &'a ClosureShape, value: &'a LExpr, } impl CompCollector for EagerArrCollector<'_> { @@ -29,13 +33,23 @@ self.out.reserve(size_hint); } fn collect(&mut self, ctx: Context) -> Result<()> { - self.out.push(evaluate(ctx, self.value)?); + if let Some(v) = evaluate_trivial(self.value) { + self.out.push(v); + return Ok(()); + } + if let LExpr::Slot(slot) = self.value { + self.out.push(ctx.slot(*slot).evaluate()?); + return Ok(()); + } + let env = Context::enter_using(&ctx, self.value_shape); + self.out.push(evaluate(env, self.value)?); Ok(()) } } struct LazyArrCollector<'a> { out: &'a mut Vec>, + value_shape: &'a ClosureShape, value: &'a Rc, } impl CompCollector for LazyArrCollector<'_> { @@ -43,14 +57,24 @@ self.out.reserve(size_hint); } fn collect(&mut self, ctx: Context) -> Result<()> { + if let Some(v) = evaluate_trivial(self.value) { + self.out.push(Thunk::evaluated(v)); + return Ok(()); + } + if let LExpr::Slot(slot) = self.value.as_ref() { + self.out.push(ctx.slot(*slot)); + return Ok(()); + } + let env = Context::enter_using(&ctx, self.value_shape); let value_expr = self.value.clone(); - self.out.push(Thunk!(move || evaluate(ctx, &value_expr))); + self.out.push(Thunk!(move || evaluate(env, &value_expr))); Ok(()) } } struct ObjCompCollectorStatic<'a> { builder: &'a mut ObjValueBuilder, + frame_shape: &'a ClosureShape, locals: &'a [LBind], field: &'a LFieldMember, } @@ -59,15 +83,23 @@ self.builder.reserve_fields(guaranteed); } fn collect(&mut self, inner_ctx: Context) -> Result<()> { - let value_ctx = evaluate_locals(inner_ctx.clone(), self.locals); + // Build the object's A-frame fresh per iteration: captures from + // the comp's iter ctx, locals = `this` (slot 0, unfilled in the + // static path) + member-locals via letrec. + let value_ctx = inner_ctx + .pack_captures_sup_this(self.frame_shape) + .enter(|fill, ctx| { + fill_letrec_binds(fill, &ctx, self.locals); + }); evaluate_field_member_static(self.builder, inner_ctx, value_ctx, self.field) } } struct ObjCompCollectorUnbound<'a> { builder: &'a mut ObjValueBuilder, + frame_shape: Rc, locals: Rc>, - this_id: Option, + this_slot: Option, field: &'a LFieldMember, } impl CompCollector for ObjCompCollectorUnbound<'_> { @@ -75,7 +107,12 @@ self.builder.reserve_fields(guaranteed); } fn collect(&mut self, inner_ctx: Context) -> Result<()> { - let uctx = evaluate_locals_unbound(inner_ctx.clone(), self.locals.clone(), self.this_id); + let uctx = evaluate_locals_unbound( + &inner_ctx, + &self.frame_shape, + self.this_slot, + self.locals.clone(), + ); evaluate_field_member_unbound(self.builder, inner_ctx, uctx, self.field) } } @@ -100,8 +137,9 @@ 0, &mut ObjCompCollectorUnbound { builder: &mut builder, + frame_shape: comp.frame_shape.clone(), locals: comp.locals.clone(), - this_id: comp.this, + this_slot: comp.this, field: &comp.field, }, )?; @@ -114,6 +152,7 @@ 0, &mut ObjCompCollectorStatic { builder: &mut builder, + frame_shape: &comp.frame_shape, locals: &comp.locals, field: &comp.field, }, @@ -126,7 +165,9 @@ pub fn evaluate_arr_comp(ctx: Context, comp: &LArrComp) -> Result { let cached_overs = cache_overs(&ctx, &comp.compspecs)?; - // In eager evaluation, Context is not captured, thus updates in CoW fashion will likely to success + // Eager fast-path: when the comp has only `if` and `for { destruct: Full(_) }` + // specs, allocate one Iter A-frame per for-spec and re-set the slot + // per iteration as long as the frame's refcount stays at 1. 'eager: { let mut out = Vec::new(); @@ -147,6 +188,7 @@ 0, &mut EagerArrCollector { out: &mut out, + value_shape: &comp.value_shape, value: &comp.value, }, ) @@ -166,6 +208,7 @@ 0, &mut LazyArrCollector { out: &mut items, + value_shape: &comp.value_shape, value: &comp.value, }, )?; @@ -220,7 +263,12 @@ evaluate_compspecs_eager(ctx, specs, cached_overs, idx + 1, 0, collector)?; } } - LCompSpec::For { destruct, over, .. } => { + LCompSpec::For { + frame_shape, + destruct, + over, + .. + } => { let arr = if let Some(cached) = &cached_overs[idx] { cached.clone() } else { @@ -232,21 +280,24 @@ }; let inner_reserve = guaranteed_reserve.max(1) * arr.len() as usize; match destruct { - LDestruct::Full(id) => { - let id = *id; - let mut inner_ctx = ContextBuilder::extend(ctx, 1).build(); - for (i, item) in arr.iter().enumerate() { - // TODO: reuse one ContextBuilder for full evaluate_compspecs pipeline - inner_ctx.cow_fill_binding(id, Thunk::evaluated(item?)); - evaluate_compspecs_eager( - inner_ctx.clone(), - specs, - cached_overs, - idx + 1, - if i == 0 { inner_reserve } else { 0 }, - collector, - )?; - } + LDestruct::Full(slot) => { + Context::enter_iter(&ctx, frame_shape, |it| { + for (i, item) in arr.iter().enumerate() { + let item = item?; + let ctx = it.create(|f| { + f.set(*slot, Thunk::evaluated(item)); + })?; + evaluate_compspecs_eager( + ctx, + specs, + cached_overs, + idx + 1, + if i == 0 { inner_reserve } else { 0 }, + collector, + )?; + } + Ok(()) + })?; } // TODO: Should not be eager? CoW won't work here #[cfg(feature = "exp-destruct")] @@ -283,7 +334,12 @@ evaluate_compspecs(ctx, specs, cached_overs, idx + 1, 0, collector)?; } } - LCompSpec::For { destruct, over, .. } => { + LCompSpec::For { + frame_shape, + destruct: dst, + over, + .. + } => { let arr = if let Some(cached) = &cached_overs[idx] { cached.clone() } else { @@ -295,16 +351,10 @@ }; let inner_reserve = guaranteed_reserve.max(1) * arr.len() as usize; for (i, item) in arr.iter().enumerate() { - let item_val = item?; - let mut inner_builder = ContextBuilder::extend(ctx.clone(), 1); - let fctx = Pending::new(); - destructure::destruct( - destruct, - Thunk::evaluated(item_val), - fctx.clone(), - &mut inner_builder, - ); - let inner_ctx = inner_builder.build().into_future(fctx); + let item = item?; + let inner_ctx = ctx.pack_captures_sup_this(frame_shape).enter(|fill, ctx| { + destruct(dst, fill, Thunk::evaluated(item), &ctx); + }); evaluate_compspecs( inner_ctx, specs, --- a/crates/jrsonnet-evaluator/src/evaluate/destructure.rs +++ b/crates/jrsonnet-evaluator/src/evaluate/destructure.rs @@ -3,8 +3,10 @@ use jrsonnet_gcmodule::Trace; use crate::{ - Context, ContextBuilder, Pending, Result, SupThis, Thunk, Unbound, Val, - analyze::{LBind, LDestruct, LDestructField, LDestructRest, LExpr, LocalId}, + Context, LocalsFrame, PackedContext, Result, SupThis, Thunk, Unbound, Val, + analyze::{ + ClosureShape, LBind, LDestruct, LDestructField, LDestructRest, LExpr, LLocalExpr, LocalSlot, + }, bail, evaluate::evaluate, }; @@ -15,9 +17,9 @@ rest: Option<&LDestructRest>, end: &[LDestruct], + fill: &LocalsFrame, value: Thunk, - fctx: Pending, - builder: &mut ContextBuilder, + a_ctx: &Context, ) { let min_len = start.len() + end.len(); let has_rest = rest.is_some(); @@ -44,19 +46,19 @@ let full = full.clone(); destruct( d, + fill, Thunk!(move || Ok(full.evaluate()?.get(i as u32)?.expect("length is checked"))), - fctx.clone(), - builder, + a_ctx, ); } let start_len = start.len() as u32; let end_len = end.len() as u32; - if let Some(crate::analyze::LDestructRest::Keep(id)) = rest { + if let Some(LDestructRest::Keep(slot)) = rest { let full = full.clone(); - builder.bind( - *id, + fill.set( + *slot, Thunk!(move || { let full = full.evaluate()?; let to = full.len() - end_len; @@ -73,14 +75,14 @@ let full = full.clone(); destruct( d, + fill, Thunk!(move || { let full = full.evaluate()?; Ok(full .get(full.len() - end_len + i as u32)? .expect("length is checked")) }), - fctx.clone(), - builder, + a_ctx, ); } } @@ -90,9 +92,9 @@ fields: &[LDestructField], rest: Option<&LDestructRest>, + fill: &LocalsFrame, value: Thunk, - fctx: Pending, - builder: &mut ContextBuilder, + a_ctx: &Context, ) { use jrsonnet_interner::IStr; use rustc_hash::FxHashSet; @@ -124,10 +126,10 @@ Ok(obj) }); - if let Some(crate::analyze::LDestructRest::Keep(id)) = rest { + if let Some(LDestructRest::Keep(slot)) = rest { let full = full.clone(); - builder.bind( - *id, + fill.set( + *slot, Thunk!(move || { let full = full.evaluate()?; let mut out = ObjValueBuilder::new(); @@ -140,121 +142,100 @@ for field in fields { let field_name = field.name.clone(); - let default: Option<(Pending, Rc)> = - field.default.as_ref().map(|e| (fctx.clone(), e.clone())); + let default_thunk: Option> = field + .default + .as_ref() + .map(|(shape, expr)| build_b_thunk(a_ctx, shape, expr.clone())); + let field_full = full.clone(); let value_thunk = Thunk!(move || { let obj = field_full.evaluate()?; obj.get(field_name)?.map_or_else( - || { - let (fctx, expr) = default.as_ref().expect("shape is checked"); - evaluate(fctx.unwrap(), expr) - }, + || default_thunk.as_ref().expect("shape is checked").evaluate(), Ok, ) }); if let Some(into) = &field.into { - destruct(into, value_thunk, fctx.clone(), builder); + destruct(into, fill, value_thunk, a_ctx); } else { unreachable!("analyzer lowers object-destruct shorthands into `into`"); } } } -/// Bind a pre-built thunk to an [`LDestruct`] pattern, inserting one -/// binding per [`LocalId`] the pattern introduces. -/// -/// `fctx` is needed for object-destruct defaults (feature `exp-destruct`). #[allow(unused_variables)] -pub fn destruct( - d: &LDestruct, - value: Thunk, - fctx: Pending, - builder: &mut ContextBuilder, -) { +pub fn destruct(d: &LDestruct, fill: &LocalsFrame, value: Thunk, a_ctx: &Context) { match d { - LDestruct::Full(id) => builder.bind(*id, value), + LDestruct::Full(slot) => fill.set(*slot, value), #[cfg(feature = "exp-destruct")] LDestruct::Skip => {} #[cfg(feature = "exp-destruct")] LDestruct::Array { start, rest, end } => { - destruct_array(start, rest.as_ref(), end, value, fctx, builder) + destruct_array(start, rest.as_ref(), end, fill, value, a_ctx) } #[cfg(feature = "exp-destruct")] - LDestruct::Object { fields, rest } => { - destruct_object(fields, rest.as_ref(), value, fctx, builder) - } + LDestruct::Object { fields, rest } => destruct_object(fields, rest.as_ref(), fill, value, a_ctx), } } -/// Bind one [`LBind`] as a lazy thunk that evaluates in the given -/// future context. Mirrors the old `evaluate_dest` — one entry per -/// binding in a `local … ;` frame. -pub fn evaluate_dest(bind: &LBind, fctx: Pending, builder: &mut ContextBuilder) { - let value = bind.value.clone(); - let fctx_clone = fctx.clone(); - let thunk = Thunk!(move || { - let ctx = fctx_clone.unwrap(); - evaluate(ctx, &value) - }); - destruct(&bind.destruct, thunk, fctx, builder); +pub fn build_b_thunk(a_ctx: &Context, shape: &ClosureShape, expr: Rc) -> Thunk { + let env = Context::enter_using(a_ctx, shape); + Thunk!(move || evaluate(env, &expr)) +} +pub fn build_b_thunk_uno(a_ctx: &Context, shape: Rc<(ClosureShape, LExpr)>) -> Thunk { + let env = Context::enter_using(a_ctx, &shape.0); + Thunk!(move || evaluate(env, &shape.1)) } -/// Bind each LBind's value as a lazy thunk. Mutually recursive locals -/// resolve lazily through the shared Pending. -pub fn evaluate_locals(parent: Context, binds: &[LBind]) -> Context { - if binds.is_empty() { - return parent; - } - let fctx = Context::new_future(); - let mut builder = - ContextBuilder::extend(parent, binds.iter().map(|b| b.destruct.ids().len()).sum()); +pub fn fill_letrec_binds(fill: &LocalsFrame, ctx: &Context, binds: &[LBind]) { for bind in binds { - evaluate_dest(bind, fctx.clone(), &mut builder); + let value_thunk = build_b_thunk(ctx, &bind.value_shape, bind.value.clone()); + destruct(&bind.destruct, fill, value_thunk, ctx); } - builder.build().into_future(fctx) } +pub fn evaluate_local_expr(parent: Context, l: &LLocalExpr) -> Result { + let ctx = parent + .pack_captures_sup_this(&l.frame_shape) + .enter(|fill, ctx| { + fill_letrec_binds(fill, ctx, &l.binds); + }); + evaluate(ctx, &l.body) +} + pub trait CloneableUnbound: Unbound + Clone {} impl CloneableUnbound for V where V: Unbound + Clone {} pub fn evaluate_locals_unbound( - fctx: Context, + outer: &Context, + frame_shape: &ClosureShape, + this_slot: Option, locals: Rc>, - this_id: Option, ) -> impl CloneableUnbound { #[derive(Trace, Clone)] struct UnboundLocals { - fctx: Context, + captures: PackedContext, + this_slot: Option, locals: Rc>, - this_id: Option, } impl Unbound for UnboundLocals { type Bound = Context; fn bind(&self, sup_this: SupThis) -> Result { - let parent = self.fctx.clone(); - - let fctx = Context::new_future(); - let mut builder = ContextBuilder::extend( - parent, - self.locals.iter().map(|b| b.destruct.ids().len()).sum(), - ); - for b in self.locals.iter() { - evaluate_dest(b, fctx.clone(), &mut builder); - } - if let Some(this_id) = self.this_id { - builder.bind(this_id, Thunk::evaluated(Val::Obj(sup_this.this().clone()))); - } - let ctx = builder.build_sup_this(sup_this).into_future(fctx); - Ok(ctx) + Ok(self.captures.clone().enter(sup_this, |fill, ctx| { + if let Some(slot) = self.this_slot { + let this_obj = ctx.sup_this().expect("sup_this set above").this().clone(); + fill.set(slot, Thunk::evaluated(Val::Obj(this_obj))); + } + fill_letrec_binds(fill, ctx, &self.locals); + })) } } UnboundLocals { - fctx, + captures: outer.pack_captures(frame_shape), + this_slot, locals, - this_id, } } --- a/crates/jrsonnet-evaluator/src/evaluate/mod.rs +++ b/crates/jrsonnet-evaluator/src/evaluate/mod.rs @@ -7,21 +7,22 @@ use self::{ compspec::{evaluate_arr_comp, evaluate_obj_comp}, - destructure::{evaluate_locals, evaluate_locals_unbound}, + destructure::{build_b_thunk_uno, evaluate_local_expr, evaluate_locals_unbound}, operator::evaluate_binary_op_special, }; use crate::{ Context, Error, ObjValue, ObjValueBuilder, ObjectAssertion, Result, ResultExt as _, SupThis, Unbound, Val, analyze::{ - LArgsDesc, LAssertStmt, LExpr, LFieldMember, LFieldName, LFunction, LIndexPart, LObjBody, - LObjMembers, + ClosureShape, LArgsDesc, LAssertStmt, LExpr, LFieldMember, LFieldName, LFunction, + LIndexPart, LObjAsserts, LObjBody, LObjMembers, LSlot, }, - bail, + arr::ArrValue, + bail, error, error::{ErrorKind::*, suggest_object_fields}, - evaluate::operator::evaluate_unary_op, + evaluate::{destructure::fill_letrec_binds, operator::evaluate_unary_op}, function::{CallLocation, FuncDesc, FuncVal, prepared::PreparedFuncVal}, - in_frame, runtime_error, + in_frame, typed::FromUntyped as _, val::{CachedUnbound, Thunk}, with_state, @@ -62,11 +63,10 @@ }) } -/// Evaluate a method definition. pub fn evaluate_method(ctx: Context, name: IStr, func: &Rc) -> Val { Val::Func(FuncVal::Normal(Cc::new(FuncDesc { name, - ctx, + body_captures: ctx.pack_captures_sup_this(&func.body_shape), func: func.clone(), }))) } @@ -91,6 +91,15 @@ } pub fn evaluate_thunk(ctx: Context, expr: Rc, tailstrict: bool) -> Result> { + match &*expr { + LExpr::Slot(LSlot::Local(i)) => return Ok(ctx.local(*i)), + LExpr::Slot(LSlot::Capture(i)) => return Ok(ctx.capture(*i)), + _ => { + if let Some(v) = evaluate_trivial(&expr) { + return Ok(Thunk::evaluated(v)); + } + } + } Ok(if tailstrict { Thunk::evaluated(evaluate(ctx, &expr)?) } else { @@ -106,40 +115,21 @@ } } -pub fn evaluate_named(name: &IStr, ctx: Context, expr: &LExpr) -> Result { - if let LExpr::Function(f) = &expr { - return Ok(evaluate_method( - ctx, - f.name.clone().unwrap_or_else(|| name.clone()), - f, - )); - } - evaluate(ctx, expr) -} - pub fn evaluate(ctx: Context, expr: &LExpr) -> Result { Ok(match expr { LExpr::Null => Val::Null, LExpr::Bool(b) => Val::Bool(*b), LExpr::Str(s) => Val::string(s.clone()), LExpr::Num(n) => Val::Num(*n), - LExpr::Local(id) => { - let Some(thunk) = ctx.binding(*id) else { - bail!("should not happen: unbound local {id:?}"); - }; - thunk.evaluate()? - } + LExpr::Slot(slot) => ctx.slot(*slot).evaluate()?, LExpr::BadLocal(name) => panic!("unresolvable reference: {name}"), - LExpr::Arr(items) => Val::Arr(crate::arr::ArrValue::expr(ctx, items.clone())), + LExpr::Arr { shape, items } => Val::Arr(ArrValue::expr(ctx, shape, items.clone())), LExpr::UnaryOp(op, value) => { let value = evaluate(ctx, value)?; evaluate_unary_op(*op, &value)? } LExpr::BinaryOp { lhs, op, rhs } => evaluate_binary_op_special(ctx, lhs, *op, rhs)?, - LExpr::LocalExpr { binds, body } => { - let ctx = evaluate_locals(ctx, binds); - evaluate(ctx, body)? - } + LExpr::LocalExpr(local_expr) => evaluate_local_expr(ctx, local_expr)?, LExpr::IfElse { cond, cond_then, @@ -176,6 +166,7 @@ func.name.clone().unwrap_or_else(names::anonymous), func, ), + LExpr::IdentityFunction => Val::Func(FuncVal::identity()), LExpr::Apply { applicable, args, @@ -240,8 +231,7 @@ let n = v.as_num().ok_or_else(|| -> crate::Error { TypeMismatch("slice step", vec![ValType::Num], v.value_type()).into() })?; - BoundedUsize::new(n as usize) - .ok_or_else(|| runtime_error!("slice step must be >= 1")) + BoundedUsize::new(n as usize).ok_or_else(|| error!("slice step must be >= 1")) }) .transpose()?; Val::from(indexable.slice(start, end, step)?) @@ -278,7 +268,32 @@ bail!(OnlyFunctionsCanBeCalledGot(func_val.value_type())) }; + if func.is_identity() && args.names.is_empty() && args.unnamed.len() == 1 { + return evaluate_thunk(ctx, args.unnamed[0].clone(), tailstrict)?.evaluate(); + } + let name = func.name(); + + if args.names.is_empty() && args.unnamed.len() == 1 && func.params().len() == 1 { + use crate::function::prepared::PreparedCall; + let prepared_inline = PreparedCall::empty(); + let arg = evaluate_thunk(ctx, args.unnamed[0].clone(), tailstrict)?; + let arg_slice = std::slice::from_ref(&arg); + return in_frame( + loc, + || format!("function <{name}> call"), + || { + func.evaluate_prepared( + &prepared_inline, + CallLocation::native(), + arg_slice, + &[], + tailstrict, + ) + }, + ); + } + let unnamed = args .unnamed .iter() @@ -286,6 +301,26 @@ .map(|e| evaluate_thunk(ctx.clone(), e, tailstrict)) .collect::>>()?; + // Fast path: positional-only multi-arg call fully covering the + // params, no defaults. + if args.names.is_empty() && unnamed.len() == func.params().len() { + use crate::function::prepared::PreparedCall; + let prepared_inline = PreparedCall::empty(); + return in_frame( + loc, + || format!("function <{name}> call"), + || { + func.evaluate_prepared( + &prepared_inline, + CallLocation::native(), + &unnamed, + &[], + tailstrict, + ) + }, + ); + } + let named = args .values .iter() @@ -302,7 +337,7 @@ } fn evaluate_index(ctx: Context, indexable: &LExpr, parts: &[LIndexPart]) -> Result { - let mut value = if let LExpr::Super = indexable { + let mut value = if matches!(indexable, LExpr::Super) { let sup_this = ctx.try_sup_this()?; // First part must be evaluated to get the super field name if parts.is_empty() { @@ -422,13 +457,15 @@ #[derive(Trace)] struct UnboundValue { uctx: B, - value: Rc, + value: Rc<(ClosureShape, LExpr)>, name: IStr, } impl> Unbound for UnboundValue { type Bound = Val; fn bind(&self, sup_this: SupThis) -> Result { - evaluate(self.uctx.bind(sup_this)?, &self.value) + let a_ctx = self.uctx.bind(sup_this)?; + let b_ctx = Context::enter_using(&a_ctx, &self.value.0); + evaluate(b_ctx, &self.value.1) } } @@ -468,12 +505,12 @@ return Ok(()); }; - let value = value.clone(); + let thunk = build_b_thunk_uno(&value_ctx, value.clone()); builder .field(name) .with_add(*plus) .with_visibility(*visibility) - .try_thunk(Thunk!(move || { evaluate(value_ctx, &value) }))?; + .try_thunk(thunk)?; Ok(()) } @@ -491,34 +528,33 @@ if needs_unbound { let uctx = CachedUnbound::new(evaluate_locals_unbound( - ctx.clone(), - members.locals.clone(), + &ctx, + &members.frame_shape, members.this, + members.locals.clone(), )); for field in &members.fields { evaluate_field_member_unbound(&mut builder, ctx.clone(), uctx.clone(), field)?; } - if !members.asserts.is_empty() { + if let Some(asserts_block) = &members.asserts { builder.assert(evaluate_object_assertions_unbound( uctx, - members.asserts.clone(), + asserts_block.clone(), )); } } else { - let field_ctx = ctx; - let value_ctx = evaluate_locals(field_ctx.clone(), &members.locals); + let a_ctx = ctx + .pack_captures_sup_this(&members.frame_shape) + .enter(|fill, ctx| { + fill_letrec_binds(fill, &ctx, &members.locals); + }); for field in &members.fields { - evaluate_field_member_static( - &mut builder, - field_ctx.clone(), - value_ctx.clone(), - field, - )?; + evaluate_field_member_static(&mut builder, ctx.clone(), a_ctx.clone(), field)?; } - if !members.asserts.is_empty() { + if let Some(asserts_block) = &members.asserts { builder.assert(evaluate_object_assertions_static( - value_ctx, - members.asserts.clone(), + a_ctx, + asserts_block.clone(), )); } } @@ -529,7 +565,7 @@ pub fn evaluate_assert(ctx: Context, assertion: &LAssertStmt) -> Result<()> { let LAssertStmt { cond, message } = assertion; let assertion_result = in_frame( - CallLocation::native(), + CallLocation::new(&cond.span), || "assertion condition".to_owned(), || bool::from_untyped(evaluate(ctx.clone(), cond)?), )?; @@ -550,18 +586,19 @@ fn evaluate_object_assertions_unbound>( uctx: B, - asserts: Rc>, + asserts: Rc, ) -> impl ObjectAssertion { #[derive(Trace)] struct ObjectAssert { uctx: B, - asserts: Rc>, + asserts: Rc, } impl> ObjectAssertion for ObjectAssert { fn run(&self, sup_this: SupThis) -> Result<()> { - let ctx = self.uctx.bind(sup_this)?; - for assert in &*self.asserts { - evaluate_assert(ctx.clone(), assert)?; + let a_ctx = self.uctx.bind(sup_this)?; + let assert_env = Context::enter_using(&a_ctx, &self.asserts.shape); + for assert in &self.asserts.asserts { + evaluate_assert(assert_env.clone(), assert)?; } Ok(()) } @@ -569,21 +606,25 @@ ObjectAssert { uctx, asserts } } fn evaluate_object_assertions_static( - ctx: Context, - asserts: Rc>, + a_ctx: Context, + asserts: Rc, ) -> impl ObjectAssertion { #[derive(Trace)] struct ObjectAssert { - ctx: Context, - asserts: Rc>, + assert_env: Context, + asserts: Rc, } impl ObjectAssertion for ObjectAssert { fn run(&self, _sup_this: SupThis) -> Result<()> { - for assert in &*self.asserts { - evaluate_assert(self.ctx.clone(), assert)?; + for assert in &self.asserts.asserts { + evaluate_assert(self.assert_env.clone(), assert)?; } Ok(()) } } - ObjectAssert { ctx, asserts } + let assert_env = Context::enter_using(&a_ctx, &asserts.shape); + ObjectAssert { + assert_env, + asserts, + } } --- a/crates/jrsonnet-evaluator/src/function/mod.rs +++ b/crates/jrsonnet-evaluator/src/function/mod.rs @@ -11,9 +11,12 @@ prepared::{PreparedCall, parse_prepared_builtin_call}, }; use crate::{ - Context, ContextBuilder, Result, Thunk, Val, - analyze::{LDestruct, LExpr, LFunction}, - evaluate::{destructure::destruct, ensure_sufficient_stack, evaluate, evaluate_trivial}, + PackedContextSupThis, Result, Thunk, Val, + analyze::LFunction, + evaluate::{ + destructure::{build_b_thunk, destruct}, + ensure_sufficient_stack, evaluate, evaluate_trivial, + }, function::builtin::BuiltinFunc, }; @@ -56,16 +59,7 @@ /// { a() = ... } /// ``` pub name: IStr, - /// Context, in which this function was evaluated. - /// - /// # Example - /// In - /// ```jsonnet - /// local a = 2; - /// function() ... - /// ``` - /// context will contain `a`. - pub ctx: Context, + pub(crate) body_captures: PackedContextSupThis, #[educe(PartialEq(method = Rc::ptr_eq))] pub func: Rc, @@ -82,44 +76,34 @@ named: &[Thunk], prepared: &PreparedCall, ) -> Result { - let has_defaults = !prepared.defaults().is_empty(); - let mut builder = ContextBuilder::extend(self.ctx.clone(), self.func.params.len()); - - let fctx = Context::new_future(); - for (param_idx, thunk) in unnamed.iter().enumerate() { - destruct( - &self.func.params[param_idx].destruct, - thunk.clone(), - fctx.clone(), - &mut builder, - ); - } - - for &(param_idx, arg_idx) in prepared.named() { - destruct( - &self.func.params[param_idx].destruct, - named[arg_idx].clone(), - fctx.clone(), - &mut builder, - ); - } + let body_ctx = self.body_captures.clone().enter(|fill, ctx| { + // Place each provided arg-thunk into its destructured slots. + for (param_idx, thunk) in unnamed.iter().enumerate() { + destruct( + &self.func.params[param_idx].destruct, + fill, + thunk.clone(), + &ctx, + ); + } + for &(param_idx, arg_idx) in prepared.named() { + destruct( + &self.func.params[param_idx].destruct, + fill, + named[arg_idx].clone(), + &ctx, + ); + } - if has_defaults { for ¶m_idx in prepared.defaults() { let param = &self.func.params[param_idx]; - if let Some(default_expr) = ¶m.default { - let default_expr = default_expr.clone(); - let fctxc = fctx.clone(); - let thunk = Thunk!(move || { - let ctx = fctxc.unwrap(); - evaluate(ctx, &default_expr) - }); - destruct(¶m.destruct, thunk, fctx.clone(), &mut builder); - } + let (shape, expr) = param.default.as_ref().expect("default exists"); + let thunk = build_b_thunk(&ctx, shape, expr.clone()); + destruct(¶m.destruct, fill, thunk, &ctx); } - }; - let ctx = builder.build().into_future(fctx); - ensure_sufficient_stack(|| evaluate(ctx, &self.func.body)) + }); + + ensure_sufficient_stack(|| evaluate(body_ctx, &self.func.body)) } pub fn evaluate_trivial(&self) -> Option { @@ -157,6 +141,10 @@ Self::Builtin(BuiltinFunc::new(builtin)) } + pub fn identity() -> Self { + Self::builtin(builtin_id {}) + } + pub fn params(&self) -> FunctionSignature { match self { Self::Builtin(i) => i.params(), @@ -193,27 +181,12 @@ } /// Is this function an identity function. - /// - /// Currently only works for builtin `std.id`, aka `Self::Id` value, and `function(x) x`. /// /// This function should only be used for optimization, not for the conditional logic, i.e code should work with syntetic identity function too pub fn is_identity(&self) -> bool { match self { Self::Builtin(b) => b.as_any().downcast_ref::().is_some(), - Self::Normal(desc) => { - if desc.func.params.len() != 1 { - return false; - } - let param = &desc.func.params[0]; - if param.default.is_some() { - return false; - } - #[allow(irrefutable_let_patterns, reason = "refutable with exp-destruct")] - let LDestruct::Full(id) = ¶m.destruct else { - return false; - }; - matches!(&*desc.func.body, LExpr::Local(v) if v == id) - } + Self::Normal(_) => false, } } --- a/crates/jrsonnet-evaluator/src/function/prepared.rs +++ b/crates/jrsonnet-evaluator/src/function/prepared.rs @@ -46,6 +46,12 @@ pub fn defaults(&self) -> &[usize] { &self.defaults } + pub const fn empty() -> Self { + Self { + named: Vec::new(), + defaults: Vec::new(), + } + } } pub fn prepare_call( --- a/crates/jrsonnet-evaluator/src/integrations/serde.rs +++ b/crates/jrsonnet-evaluator/src/integrations/serde.rs @@ -12,7 +12,7 @@ }; use crate::{ - Error as JrError, ObjValue, ObjValueBuilder, Result, Val, in_description_frame, runtime_error, + Error as JrError, ObjValue, ObjValueBuilder, Result, Val, error, in_description_frame, }; impl<'de> Deserialize<'de> for Val { @@ -629,6 +629,6 @@ where T: std::fmt::Display, { - runtime_error!("serde: {msg}") + error!("serde: {msg}") } } --- a/crates/jrsonnet-evaluator/src/lib.rs +++ b/crates/jrsonnet-evaluator/src/lib.rs @@ -58,6 +58,7 @@ pub use val::{Thunk, Val}; pub mod analyze; +use self::analyze::{LExpr, analyze_root}; use crate::gc::WithCapacityExt as _; #[allow(clippy::needless_return)] @@ -408,12 +409,15 @@ file.evaluating = true; // Dropping file cache guard here, as evaluation may use this map too drop(file_cache); - let (ctx, externals) = self.create_default_context(file_name.clone()).build(); - let report = analyze::analyze_root(&parsed, externals); + let (externals, thunks) = self.create_default_context(file_name).build(); + let report = analyze_root(&parsed, externals); if report.errored { return Err(StaticAnalysisError(report.diagnostics_list).into()); } - let res = evaluate::evaluate(ctx.build(), &report.lir); + debug_assert_eq!(report.root_shape.n_locals as usize, thunks.len()); + debug_assert!(report.root_shape.captures.is_empty()); + let ctx = Context::root(thunks); + let res = evaluate::evaluate(ctx, &report.lir); let mut file_cache = self.file_cache(); let mut file = file_cache.entry(path); @@ -501,33 +505,66 @@ } } +pub struct PreparedSnippet { + lir: LExpr, + thunks: Vec>, +} + /// Raw methods evaluate passed values but don't perform TLA execution impl State { - /// Parses and evaluates the given snippet - pub fn evaluate_snippet(&self, name: impl Into, code: impl Into) -> Result { - self.evaluate_snippet_with(name, code, &()) - } - /// Parses and evaluates the given snippet with custom context modifier - pub fn evaluate_snippet_with( + /// Parses and analyses the given snippet with a custom context + /// modifier. + pub fn prepare_snippet_with( &self, name: impl Into, code: impl Into, context_initializer: &dyn ContextInitializer, - ) -> Result { + ) -> Result { let code = code.into(); let source = Source::new_virtual(name.into(), code.clone()); let parsed = parse_jsonnet(&code, source.clone()).map_err(|e| ImportSyntaxError { path: source.clone(), error: Box::new(e), })?; - let (ctx, externals) = self - .create_default_context_with(source.clone(), context_initializer) + let (externals, thunks) = self + .create_default_context_with(source, context_initializer) .build(); - let report = analyze::analyze_root(&parsed, externals); + let report = analyze_root(&parsed, externals); if report.errored { return Err(StaticAnalysisError(report.diagnostics_list).into()); } - evaluate::evaluate(ctx.build(), &report.lir) + debug_assert_eq!(report.root_shape.n_locals as usize, thunks.len()); + debug_assert!(report.root_shape.captures.is_empty()); + Ok(PreparedSnippet { + lir: report.lir, + thunks, + }) + } + /// Parses and analyses the given snippet + pub fn prepare_snippet( + &self, + name: impl Into, + code: impl Into, + ) -> Result { + self.prepare_snippet_with(name, code, &()) + } + pub fn evaluate_prepared_snippet(&self, prepared: &PreparedSnippet) -> Result { + let ctx = Context::root(prepared.thunks.clone()); + evaluate::evaluate(ctx, &prepared.lir) + } + /// Parses and evaluates the given snippet with custom context modifier + pub fn evaluate_snippet_with( + &self, + name: impl Into, + code: impl Into, + context_initializer: &dyn ContextInitializer, + ) -> Result { + let prepared = self.prepare_snippet_with(name, code, context_initializer)?; + self.evaluate_prepared_snippet(&prepared) + } + /// Parses and evaluates the given snippet + pub fn evaluate_snippet(&self, name: impl Into, code: impl Into) -> Result { + self.evaluate_snippet_with(name, code, &()) } } --- a/tests/benches/cpp_test_suite.rs +++ b/tests/benches/cpp_test_suite.rs @@ -1,10 +1,8 @@ -use std::{collections::HashMap, fs::read_dir, hint::black_box, path::Path}; +use std::{collections::HashMap, fs, fs::read_dir, hint::black_box, path::Path}; use criterion::{Criterion, criterion_group, criterion_main}; use jrsonnet_evaluator::{ - FileImportResolver, State, apply_tla, - manifest::{BlackBoxFormat, JsonFormat}, - stack::limit_stack_depth, + FileImportResolver, State, apply_tla, manifest::JsonFormat, stack::limit_stack_depth, trace::PathResolver, }; @@ -12,31 +10,37 @@ static GLOBAL: mimallocator::Mimalloc = mimallocator::Mimalloc; fn bench_entry(c: &mut Criterion, path: &Path) { - c.bench_function( - path.file_name() - .expect("file path") - .to_str() - .expect("name is utf-8"), - |b| { - let _stack = limit_stack_depth(200_000); + let name = path + .file_name() + .expect("file path") + .to_str() + .expect("name is utf-8") + .to_owned(); + let code = fs::read_to_string(path).expect("read bench source"); - let mut s = State::builder(); + c.bench_function(&name, |b| { + let _stack = limit_stack_depth(200_000); - s.context_initializer(jrsonnet_stdlib::ContextInitializer::new( - PathResolver::Absolute, - )) - .import_resolver(FileImportResolver::new(vec![])); + let mut s = State::builder(); + s.context_initializer(jrsonnet_stdlib::ContextInitializer::new( + PathResolver::Absolute, + )) + .import_resolver(FileImportResolver::new(vec![])); + let s = s.build(); + let _entered = s.enter(); - let s = s.build(); - let _s = s.enter(); + // Parse + analysis happen once; each iter only measures + // evaluation + manifestation. + let prepared = s + .prepare_snippet(name.clone(), code.clone()) + .expect("prepared"); - b.iter(|| { - let imported = s.import(path).expect("evaluated"); - let res = apply_tla(&HashMap::new(), imported).expect("tla applied"); - black_box(res.manifest(JsonFormat::cli(3)).expect("manifested")); - }); - }, - ); + b.iter(|| { + let imported = s.evaluate_prepared_snippet(&prepared).expect("evaluated"); + let res = apply_tla(&HashMap::new(), imported).expect("tla applied"); + black_box(res.manifest(JsonFormat::cli(3)).expect("manifested")); + }); + }); } fn criterion_benchmark(c: &mut Criterion) { for entry in read_dir("go_builtin_benchmarks").expect("dir exists") {