From d5225b820ddcee5a432905c10d02f01ffad258d0 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Sun, 05 Apr 2026 19:25:11 +0000 Subject: [PATCH] refactor(evaluator): use static analysis --- --- a/Cargo.lock +++ b/Cargo.lock @@ -815,9 +815,11 @@ dependencies = [ "annotate-snippets", "anyhow", + "drop_bomb", "educe", "hi-doc", "im-rc", + "insta", "jrsonnet-gcmodule", "jrsonnet-interner", "jrsonnet-ir", @@ -830,8 +832,10 @@ "rustc-hash 2.1.2", "rustversion", "serde", + "smallvec 1.15.1", "stacker", "static_assertions", + "strip-ansi-escapes", "strsim", "thiserror", ] @@ -899,6 +903,7 @@ "jrsonnet-interner", "peg", "static_assertions", + "thiserror", ] [[package]] @@ -960,7 +965,6 @@ "base64", "jrsonnet-evaluator", "jrsonnet-gcmodule", - "jrsonnet-ir", "jrsonnet-macros", "lru", "md5", --- a/bindings/jsonnet/src/val_modify.rs +++ b/bindings/jsonnet/src/val_modify.rs @@ -4,7 +4,7 @@ use std::{ffi::CStr, os::raw::c_char}; -use jrsonnet_evaluator::{Thunk, Val, val::ArrValue}; +use jrsonnet_evaluator::{Thunk, Val}; use crate::VM; --- a/crates/jrsonnet-evaluator/Cargo.toml +++ b/crates/jrsonnet-evaluator/Cargo.toml @@ -77,6 +77,12 @@ "PartialEq", ] } im-rc = { version = "15.1.0", features = ["pool"] } +smallvec = "1.15.1" +drop_bomb.workspace = true [build-dependencies] rustversion = "1.0.22" + +[dev-dependencies] +insta.workspace = true +strip-ansi-escapes = "0.2.1" --- a/crates/jrsonnet-evaluator/src/arr/mod.rs +++ b/crates/jrsonnet-evaluator/src/arr/mod.rs @@ -5,10 +5,9 @@ rc::Rc, }; -use jrsonnet_gcmodule::{Cc, cc_dyn}; -use jrsonnet_ir::Expr; +use jrsonnet_gcmodule::{cc_dyn, Cc}; -use crate::{Context, Result, Thunk, Val, function::NativeFn, typed::IntoUntyped}; +use crate::{analyze::LExpr, function::NativeFn, typed::IntoUntyped, Context, Result, Thunk, Val}; mod spec; pub use spec::{ArrayLike, *}; @@ -37,14 +36,18 @@ Self::new(()) } - pub fn expr(ctx: Context, exprs: Rc>) -> Self { + pub fn expr(ctx: Context, exprs: Rc>) -> Self { Self::new(ExprArray::new(ctx, exprs)) } - pub fn repeated(data: Self, repeats: usize) -> Option { + pub fn repeated(data: Self, repeats: u32) -> Option { Some(Self::new(RepeatedArray::new(data, repeats)?)) } + pub fn make(len: u32, cb: NativeFn!((u32,)->Val)) -> Self { + Self::new(MakeArray::new(len, cb)) + } + #[must_use] pub fn map(self, mapper: NativeFn!((Val) -> Val)) -> Self { Self::new(::new(self, ArrayMapper::Plain(mapper))) @@ -79,14 +82,14 @@ Ok(Self::new(out)) } - pub fn extended(a: Self, b: Self) -> Self { - if a.is_empty() { + pub fn extended(a: Self, b: Self) -> Option { + Some(if a.is_empty() { b } else if b.is_empty() { a } else { - Self::new(ExtendedArray::new(a, b)) - } + Self::new(ExtendedArray::new(a, b)?) + }) } pub fn range_exclusive(a: i32, b: i32) -> Self { @@ -98,14 +101,14 @@ #[must_use] pub fn slice(self, index: Option, end: Option, step: Option) -> Self { - let get_idx = |pos: Option, len: usize, default| match pos { + let get_idx = |pos: Option, len: u32, default| match pos { #[expect( clippy::cast_sign_loss, reason = "abs value is used, len is limited to u31" )] - Some(v) if v < 0 => len.saturating_sub((-v) as usize), + Some(v) if v < 0 => len.saturating_add_signed(v), #[expect(clippy::cast_sign_loss, reason = "abs value is used")] - Some(v) => (v as usize).min(len), + Some(v) => (v as u32).min(len), None => default, }; let index = get_idx(index, self.len(), 0); @@ -127,7 +130,7 @@ } /// Array length. - pub fn len(&self) -> usize { + pub fn len(&self) -> u32 { self.0.len() } @@ -143,14 +146,14 @@ /// Get array element by index, evaluating it, if it is lazy. /// /// Returns `None` on out-of-bounds condition. - pub fn get(&self, index: usize) -> Result> { + pub fn get(&self, index: u32) -> Result> { self.0.get(index) } /// Get array element by index, without evaluation. /// /// Returns `None` on out-of-bounds condition. - pub fn get_lazy(&self, index: usize) -> Option> { + pub fn get_lazy(&self, index: u32) -> Option> { self.0.get_lazy(index) } --- a/crates/jrsonnet-evaluator/src/arr/spec.rs +++ b/crates/jrsonnet-evaluator/src/arr/spec.rs @@ -8,47 +8,47 @@ use jrsonnet_gcmodule::{Cc, Trace}; use jrsonnet_interner::{IBytes, IStr}; -use jrsonnet_ir::Expr; use super::ArrValue; use crate::{ - Context, Error, ObjValue, Result, Thunk, Val, + analyze::LExpr, error::ErrorKind::InfiniteRecursionDetected, - evaluate, + evaluate::evaluate, function::NativeFn, typed::{IntoUntyped, Typed}, val::ThunkValue, + Context, Error, ObjValue, Result, Thunk, Val, }; pub trait ArrayLike: Any + Trace + Debug { - fn len(&self) -> usize; + fn len(&self) -> u32; fn is_empty(&self) -> bool { self.len() == 0 } - fn get(&self, index: usize) -> Result>; - fn get_lazy(&self, index: usize) -> Option>; + fn get(&self, index: u32) -> Result>; + fn get_lazy(&self, index: u32) -> Option>; fn is_cheap(&self) -> bool { false } } trait ArrayCheap { - fn get(&self, index: usize) -> Option; - fn len(&self) -> usize; + fn get(&self, index: u32) -> Option; + fn len(&self) -> u32; } impl ArrayLike for T where T: Any + Trace + Debug + ArrayCheap, { - fn len(&self) -> usize { + fn len(&self) -> u32 { ::len(self) } - fn get(&self, index: usize) -> Result> { + fn get(&self, index: u32) -> Result> { Ok(::get(self, index)) } - fn get_lazy(&self, index: usize) -> Option> { + fn get_lazy(&self, index: u32) -> Option> { ::get(self, index).map(Thunk::evaluated) } @@ -58,10 +58,10 @@ } impl ArrayCheap for () { - fn len(&self) -> usize { + fn len(&self) -> u32 { 0 } - fn get(&self, _index: usize) -> Option { + fn get(&self, _index: u32) -> Option { None } } @@ -75,20 +75,20 @@ } impl SliceArray { - fn map_idx(&self, index: usize) -> usize { - self.from as usize + self.step as usize * index + fn map_idx(&self, index: u32) -> u32 { + self.from + self.step * index } } impl ArrayLike for SliceArray { - fn len(&self) -> usize { - (self.to - self.from).div_ceil(self.step) as usize + fn len(&self) -> u32 { + (self.to - self.from).div_ceil(self.step) } - fn get(&self, index: usize) -> Result> { + fn get(&self, index: u32) -> Result> { self.inner.get(self.map_idx(index)) } - fn get_lazy(&self, index: usize) -> Option> { + fn get_lazy(&self, index: u32) -> Option> { self.inner.get_lazy(self.map_idx(index)) } @@ -98,11 +98,13 @@ } impl ArrayCheap for IBytes { - fn len(&self) -> usize { - self.as_slice().len() + fn len(&self) -> u32 { + self.as_slice().len() as u32 } - fn get(&self, index: usize) -> Option { - self.as_slice().get(index).map(|v| Val::Num((*v).into())) + fn get(&self, index: u32) -> Option { + self.as_slice() + .get(index as usize) + .map(|v| Val::Num((*v).into())) } } @@ -117,11 +119,11 @@ #[derive(Debug, Trace, Clone)] pub struct ExprArray { ctx: Context, - src: Rc>, + src: Rc>, cached: Cc>>, } impl ExprArray { - pub fn new(ctx: Context, src: Rc>) -> Self { + pub fn new(ctx: Context, src: Rc>) -> Self { Self { ctx, cached: Cc::new(RefCell::new(vec![ArrayThunk::Waiting; src.len()])), @@ -130,41 +132,36 @@ } } impl ArrayLike for ExprArray { - fn len(&self) -> usize { - self.cached.borrow().len() + fn len(&self) -> u32 { + self.cached.borrow().len() as u32 } - fn get(&self, index: usize) -> Result> { + fn get(&self, index: u32) -> Result> { if index >= self.len() { return Ok(None); } - match &self.cached.borrow()[index] { + match &self.cached.borrow()[index as usize] { ArrayThunk::Computed(c) => return Ok(Some(c.clone())), ArrayThunk::Errored(e) => return Err(e.clone()), ArrayThunk::Pending => return Err(InfiniteRecursionDetected.into()), ArrayThunk::Waiting => {} } - let ArrayThunk::Waiting = - replace(&mut self.cached.borrow_mut()[index], ArrayThunk::Pending) - else { + let ArrayThunk::Waiting = replace( + &mut self.cached.borrow_mut()[index as usize], + ArrayThunk::Pending, + ) else { unreachable!() }; - let new_value = match evaluate(self.ctx.clone(), &self.src[index]) { - Ok(v) => v, - Err(e) => { - self.cached.borrow_mut()[index] = ArrayThunk::Errored(e.clone()); - return Err(e); - } - }; - self.cached.borrow_mut()[index] = ArrayThunk::Computed(new_value.clone()); + let new_value: Val = evaluate(self.ctx.clone(), &self.src[index as usize])?; + self.cached.borrow_mut()[index as usize] = ArrayThunk::Computed(new_value.clone()); Ok(Some(new_value)) } - fn get_lazy(&self, index: usize) -> Option> { + fn get_lazy(&self, index: u32) -> Option> { #[derive(Trace)] struct ExprArrThunk { expr: ExprArray, - index: usize, + index: u32, } impl ThunkValue for ExprArrThunk { type Output = Val; @@ -180,7 +177,7 @@ if index >= self.len() { return None; } - match &self.cached.borrow()[index] { + match &self.cached.borrow()[index as usize] { ArrayThunk::Computed(c) => return Some(Thunk::evaluated(c.clone())), ArrayThunk::Errored(e) => return Some(Thunk::errored(e.clone())), ArrayThunk::Waiting | ArrayThunk::Pending => {} @@ -200,19 +197,20 @@ pub struct ExtendedArray { pub a: ArrValue, pub b: ArrValue, - split: usize, - len: usize, + split: u32, + len: u32, } impl ExtendedArray { - pub fn new(a: ArrValue, b: ArrValue) -> Self { + pub fn new(a: ArrValue, b: ArrValue) -> Option { let a_len = a.len(); let b_len = b.len(); - Self { + let len = a_len.checked_add(b_len)?; + Some(Self { a, b, split: a_len, - len: a_len.checked_add(b_len).expect("too large array value"), - } + len, + }) } } @@ -253,14 +251,14 @@ } } impl ArrayLike for ExtendedArray { - fn get(&self, index: usize) -> Result> { + fn get(&self, index: u32) -> Result> { if self.split > index { self.a.get(index) } else { self.b.get(index - self.split) } } - fn get_lazy(&self, index: usize) -> Option> { + fn get_lazy(&self, index: u32) -> Option> { if self.split > index { self.a.get_lazy(index) } else { @@ -268,7 +266,7 @@ } } - fn len(&self) -> usize { + fn len(&self) -> u32 { self.len } @@ -282,19 +280,19 @@ T: IntoUntyped + Trace + fmt::Debug, for<'a> &'a T: IntoUntyped, { - fn len(&self) -> usize { - self.as_slice().len() + fn len(&self) -> u32 { + self.as_slice().len().try_into().unwrap_or(u32::MAX) } - fn get(&self, index: usize) -> Result> { - let Some(elem) = self.as_slice().get(index) else { + fn get(&self, index: u32) -> Result> { + let Some(elem) = self.as_slice().get(index as usize) else { return Ok(None); }; IntoUntyped::into_untyped(elem).map(Some) } - fn get_lazy(&self, index: usize) -> Option> { - let elem = self.as_slice().get(index)?; + fn get_lazy(&self, index: u32) -> Option> { + let elem = self.as_slice().get(index as usize)?; Some(IntoUntyped::into_lazy_untyped(elem)) } @@ -324,20 +322,20 @@ clippy::cast_sign_loss, reason = "the math is valid with wrapping, sign loss works as intended" )] - fn size(&self) -> usize { - (self.end as usize) - .wrapping_sub(self.start as usize) + fn size(&self) -> u32 { + (self.end as u32) + .wrapping_sub(self.start as u32) .wrapping_add(1) } fn range(&self) -> impl ExactSizeIterator + DoubleEndedIterator { - WithExactSize(self.start..=self.end, self.size()) + WithExactSize(self.start..=self.end, self.size() as usize) } } impl ArrayCheap for RangeArray { - fn get(&self, index: usize) -> Option { - self.range().nth(index).map(|i| Val::Num(i.into())) + fn get(&self, index: u32) -> Option { + self.range().nth(index as usize).map(|i| Val::Num(i.into())) } - fn len(&self) -> usize { + fn len(&self) -> u32 { self.size() } } @@ -345,15 +343,15 @@ #[derive(Debug, Trace)] pub struct ReverseArray(pub ArrValue); impl ArrayLike for ReverseArray { - fn len(&self) -> usize { + fn len(&self) -> u32 { self.0.len() } - fn get(&self, index: usize) -> Result> { + fn get(&self, index: u32) -> Result> { self.0.get(self.0.len() - index - 1) } - fn get_lazy(&self, index: usize) -> Option> { + fn get_lazy(&self, index: u32) -> Option> { self.0.get_lazy(self.0.len() - index - 1) } @@ -379,40 +377,37 @@ let len = inner.len(); Self { inner, - cached: Cc::new(RefCell::new(vec![ArrayThunk::Waiting; len])), + cached: Cc::new(RefCell::new(vec![ArrayThunk::Waiting; len as usize])), mapper, } } - fn evaluate(&self, index: usize, value: Val) -> Result { + fn evaluate(&self, index: u32, value: Val) -> Result { match &self.mapper { ArrayMapper::Plain(f) => f.call(value), - #[expect( - clippy::cast_possible_truncation, - reason = "array len is limited to u31" - )] - ArrayMapper::WithIndex(f) => f.call(index as u32, value), + ArrayMapper::WithIndex(f) => f.call(index, value), } } } impl ArrayLike for MappedArray { - fn len(&self) -> usize { - self.cached.borrow().len() + fn len(&self) -> u32 { + self.cached.borrow().len() as u32 } - fn get(&self, index: usize) -> Result> { + fn get(&self, index: u32) -> Result> { if index >= self.len() { return Ok(None); } - match &self.cached.borrow()[index] { + match &self.cached.borrow()[index as usize] { ArrayThunk::Computed(c) => return Ok(Some(c.clone())), ArrayThunk::Errored(e) => return Err(e.clone()), ArrayThunk::Pending => return Err(InfiniteRecursionDetected.into()), ArrayThunk::Waiting => {} } - let ArrayThunk::Waiting = - replace(&mut self.cached.borrow_mut()[index], ArrayThunk::Pending) - else { + let ArrayThunk::Waiting = replace( + &mut self.cached.borrow_mut()[index as usize], + ArrayThunk::Pending, + ) else { unreachable!() }; @@ -426,18 +421,18 @@ let new_value = match val { Ok(v) => v, Err(e) => { - self.cached.borrow_mut()[index] = ArrayThunk::Errored(e.clone()); + self.cached.borrow_mut()[index as usize] = ArrayThunk::Errored(e.clone()); return Err(e); } }; - self.cached.borrow_mut()[index] = ArrayThunk::Computed(new_value.clone()); + self.cached.borrow_mut()[index as usize] = ArrayThunk::Computed(new_value.clone()); Ok(Some(new_value)) } - fn get_lazy(&self, index: usize) -> Option> { + fn get_lazy(&self, index: u32) -> Option> { #[derive(Trace)] struct MappedArrayThunk { arr: MappedArray, - index: usize, + index: u32, } impl ThunkValue for MappedArrayThunk { type Output = Val; @@ -450,7 +445,7 @@ if index >= self.len() { return None; } - match &self.cached.borrow()[index] { + match &self.cached.borrow()[index as usize] { ArrayThunk::Computed(c) => return Some(Thunk::evaluated(c.clone())), ArrayThunk::Errored(e) => return Some(Thunk::errored(e.clone())), ArrayThunk::Waiting | ArrayThunk::Pending => {} @@ -462,15 +457,92 @@ })) } } +#[derive(Trace, Debug, Clone)] +pub struct MakeArray { + cached: Cc>>, + mapper: NativeFn!((u32,)->Val), +} +impl MakeArray { + pub fn new(len: u32, mapper: NativeFn!((u32)->Val)) -> Self { + Self { + cached: Cc::new(RefCell::new(vec![ArrayThunk::Waiting; len as usize])), + mapper, + } + } +} +impl ArrayLike for MakeArray { + fn len(&self) -> u32 { + self.cached.borrow().len() as u32 + } + + fn get(&self, index: u32) -> Result> { + if index >= self.len() { + return Ok(None); + } + match &self.cached.borrow()[index as usize] { + ArrayThunk::Computed(c) => return Ok(Some(c.clone())), + ArrayThunk::Errored(e) => return Err(e.clone()), + ArrayThunk::Pending => return Err(InfiniteRecursionDetected.into()), + ArrayThunk::Waiting => {} + } + + let ArrayThunk::Waiting = replace( + &mut self.cached.borrow_mut()[index as usize], + ArrayThunk::Pending, + ) else { + unreachable!() + }; + + let val = self.mapper.call(index as u32); + + let new_value = match val { + Ok(v) => v, + Err(e) => { + self.cached.borrow_mut()[index as usize] = ArrayThunk::Errored(e.clone()); + return Err(e); + } + }; + self.cached.borrow_mut()[index as usize] = ArrayThunk::Computed(new_value.clone()); + Ok(Some(new_value)) + } + fn get_lazy(&self, index: u32) -> Option> { + #[derive(Trace)] + struct MakeArrayThunk { + arr: MakeArray, + index: u32, + } + impl ThunkValue for MakeArrayThunk { + type Output = Val; + + fn get(&self) -> Result { + self.arr.get(self.index).transpose().expect("index checked") + } + } + + if index >= self.len() { + return None; + } + match &self.cached.borrow()[index as usize] { + ArrayThunk::Computed(c) => return Some(Thunk::evaluated(c.clone())), + ArrayThunk::Errored(e) => return Some(Thunk::errored(e.clone())), + ArrayThunk::Waiting | ArrayThunk::Pending => {} + } + + Some(Thunk::new(MakeArrayThunk { + arr: self.clone(), + index, + })) + } +} #[derive(Trace, Debug)] pub struct RepeatedArray { data: ArrValue, - repeats: usize, - total_len: usize, + repeats: u32, + total_len: u32, } impl RepeatedArray { - pub fn new(data: ArrValue, repeats: usize) -> Option { + pub fn new(data: ArrValue, repeats: u32) -> Option { let total_len = data.len().checked_mul(repeats)?; Some(Self { data, @@ -478,7 +550,7 @@ total_len, }) } - fn map_idx(&self, index: usize) -> Option { + fn map_idx(&self, index: u32) -> Option { if index > self.total_len { return None; } @@ -487,18 +559,18 @@ } impl ArrayLike for RepeatedArray { - fn len(&self) -> usize { + fn len(&self) -> u32 { self.total_len } - fn get(&self, index: usize) -> Result> { + fn get(&self, index: u32) -> Result> { let Some(idx) = self.map_idx(index) else { return Ok(None); }; self.data.get(idx) } - fn get_lazy(&self, index: usize) -> Option> { + fn get_lazy(&self, index: u32) -> Option> { let idx = self.map_idx(index)?; self.data.get_lazy(idx) } @@ -521,19 +593,19 @@ } impl ArrayLike for PickObjectValues { - fn len(&self) -> usize { - self.keys.len() + fn len(&self) -> u32 { + self.keys.len() as u32 } - fn get(&self, index: usize) -> Result> { - let Some(key) = self.keys.as_slice().get(index) else { + fn get(&self, index: u32) -> Result> { + let Some(key) = self.keys.as_slice().get(index as usize) else { return Ok(None); }; Ok(Some(self.obj.get_or_bail(key.clone())?)) } - fn get_lazy(&self, index: usize) -> Option> { - let key = self.keys.as_slice().get(index)?; + fn get_lazy(&self, index: u32) -> Option> { + let key = self.keys.as_slice().get(index as usize)?; Some(self.obj.get_lazy_or_bail(key.clone())) } @@ -561,12 +633,12 @@ } impl ArrayLike for PickObjectKeyValues { - fn len(&self) -> usize { - self.keys.len() + fn len(&self) -> u32 { + self.keys.len() as u32 } - fn get(&self, index: usize) -> Result> { - let Some(key) = self.keys.as_slice().get(index) else { + fn get(&self, index: u32) -> Result> { + let Some(key) = self.keys.as_slice().get(index as usize) else { return Ok(None); }; Ok(Some( @@ -578,8 +650,8 @@ )) } - fn get_lazy(&self, index: usize) -> Option> { - let key = self.keys.as_slice().get(index)?; + fn get_lazy(&self, index: u32) -> Option> { + let key = self.keys.as_slice().get(index as usize)?; // Nothing can fail in the key part, yet value is still // lazy-evaluated Some(Thunk::evaluated( --- a/crates/jrsonnet-evaluator/src/ctx.rs +++ b/crates/jrsonnet-evaluator/src/ctx.rs @@ -1,58 +1,29 @@ -use std::fmt::Debug; +use std::{clone::Clone, fmt::Debug}; use educe::Educe; use jrsonnet_gcmodule::{Cc, Trace}; use jrsonnet_interner::IStr; -use rustc_hash::{FxHashMap, FxHashSet}; -use crate::{ - ObjValue, Pending, Result, SupThis, Thunk, Val, bail, error::ErrorKind::*, - gc::WithCapacityExt as _, -}; -/// Context keeps information about current lexical code location -/// -/// This information includes local variables, top-level object (`$`), current object (`this`), and super object (`super`) +use crate::{analyze::LocalId, error, error::ErrorKind::*, Pending, Result, SupThis, Thunk, Val}; + #[derive(Debug, Trace, Clone, Educe)] #[educe(PartialEq)] pub struct Context(#[educe(PartialEq(method = Cc::ptr_eq))] Cc); -#[derive(Debug, Trace)] +#[derive(Debug, Trace, Clone)] struct ContextInternal { - dollar: Option, sup_this: Option, - bindings: FxHashMap>, - - branch_point: Option, + /// `bindings[i]` corresponds to `LocalId(offset + i)`. + bindings: Vec>>, + offset: u32, + parent: Option, } + impl Context { pub fn new_future() -> Pending { Pending::new() - } - - pub fn dollar(&self) -> Option<&ObjValue> { - self.0.dollar.as_ref() } - pub fn try_dollar(&self) -> Result { - self.0 - .dollar - .clone() - .ok_or_else(|| CantUseSelfSupOutsideOfObject.into()) - } - - pub fn this(&self) -> Option<&ObjValue> { - self.0.sup_this.as_ref().map(SupThis::this) - } - - pub fn try_this(&self) -> Result { - self.0 - .sup_this - .as_ref() - .ok_or_else(|| CantUseSelfSupOutsideOfObject.into()) - .map(SupThis::this) - .cloned() - } - pub fn sup_this(&self) -> Option<&SupThis> { self.0.sup_this.as_ref() } @@ -61,127 +32,98 @@ self.0 .sup_this .clone() - .ok_or_else(|| CantUseSelfSupOutsideOfObject.into()) + .ok_or_else(|| error!(CantUseSelfSupOutsideOfObject)) } - pub fn binding(&self, name: IStr) -> Result> { - use std::cmp::Ordering; - - use crate::bail; + /// 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)); - if let Some(val) = self.0.bindings.get(&name).cloned() { - return Ok(val); - } - - if let Some(branch_point) = &self.0.branch_point { - return branch_point.binding(name); - } + self.0.update_with(|inner| { + let local_idx = (id.0 - inner.offset) as usize; + while inner.bindings.len() <= local_idx { + inner.bindings.push(None); + } + inner.bindings[local_idx] = value.take().expect("called once"); + }); + } - let mut heap = Vec::new(); - for k in self.0.bindings.keys() { - let conf = strsim::jaro_winkler(k as &str, &name as &str); - if conf < 0.8 { - continue; + 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()); } - heap.push((conf, k.clone())); } - heap.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(Ordering::Equal)); + if let Some(parent) = &self.0.parent { + return parent.binding(id); + } + None + } - bail!(VariableIsNotDefined( - name, - heap.into_iter().map(|(_, k)| k).collect() - )) - } - pub fn contains_binding(&self, name: IStr) -> bool { - self.0.bindings.contains_key(&name) - } #[must_use] pub fn into_future(self, ctx: Pending) -> Self { { ctx.clone().fill(self); } ctx.unwrap() - } - - #[must_use] - pub fn branch_point(self) -> Self { - if self.0.bindings.is_empty() { - self - } else { - ContextBuilder::extend(self).build() - } } } #[derive(Clone)] pub struct ContextBuilder { - dollar: Option, sup_this: Option, - bindings: FxHashMap>, - filled: FxHashSet, - branch_point: Option, + bindings: Vec>>, + offset: u32, + parent: Option, } impl ContextBuilder { pub fn new() -> Self { Self { - dollar: None, sup_this: None, - bindings: FxHashMap::new(), - filled: FxHashSet::new(), - branch_point: None, + bindings: Vec::new(), + offset: 0, + parent: None, } } - pub fn extend_fast(parent: Context) -> Self { + pub(crate) fn extend(parent: Context, capacity: usize) -> Self { + let offset = parent.0.offset + parent.0.bindings.len() as u32; Self { - dollar: parent.0.dollar.clone(), sup_this: parent.0.sup_this.clone(), - bindings: parent.0.bindings.clone(), - filled: FxHashSet::new(), - branch_point: parent.0.branch_point.clone(), + bindings: Vec::with_capacity(capacity), + offset, + parent: Some(parent), } } - pub fn extend(parent: Context) -> Self { - Self { - dollar: parent.0.dollar.clone(), - sup_this: parent.0.sup_this.clone(), - bindings: FxHashMap::new(), - filled: FxHashSet::new(), - branch_point: Some(parent.clone()), + 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); } + self.bindings[local_idx] = Some(value); } - pub fn bind(&mut self, name: impl Into, value: Thunk) { - let _ = self.bindings.insert(name.into(), value); - } - /// After commit, binds would shadow the previous declarations - #[must_use] - pub fn commit(mut self) -> Self { - self.filled.clear(); - self - } - pub fn try_bind(&mut self, name: impl Into, value: Thunk) -> Result<()> { - let name = name.into(); - if !self.filled.insert(name.clone()) { - bail!(DuplicateLocalVar(name)) - } - self.bind(name, value); - Ok(()) - } - pub fn build(self) -> Context { + pub(crate) fn build(self) -> Context { Context(Cc::new(ContextInternal { - dollar: self.dollar, sup_this: self.sup_this, bindings: self.bindings, - branch_point: self.branch_point, + offset: self.offset, + parent: self.parent, })) } - pub fn build_sup_this(mut self, st: SupThis) -> Context { - if self.dollar.is_none() { - self.dollar = Some(st.this().clone()); - } + + pub(crate) fn build_sup_this(mut self, st: SupThis) -> Context { self.sup_this = Some(st); self.build() } @@ -192,3 +134,37 @@ Self::new() } } + +pub struct InitialContextBuilder { + builder: ContextBuilder, + externals: Vec<(IStr, LocalId)>, + next_id: u32, +} + +impl InitialContextBuilder { + pub(crate) fn new() -> Self { + Self { + builder: ContextBuilder::new(), + externals: Vec::new(), + next_id: 0, + } + } + + pub fn bind(&mut self, name: impl Into, value: Thunk) { + let name = name.into(); + let id = LocalId(self.next_id); + self.next_id += 1; + self.externals.push((name, id)); + self.builder.bind(id, value); + } + + pub(crate) fn build(self) -> (ContextBuilder, Vec<(IStr, LocalId)>) { + (self.builder, self.externals) + } +} + +impl Default for InitialContextBuilder { + fn default() -> Self { + Self::new() + } +} --- a/crates/jrsonnet-evaluator/src/error.rs +++ b/crates/jrsonnet-evaluator/src/error.rs @@ -9,16 +9,16 @@ use thiserror::Error; use crate::{ - ObjValue, ResolvePathOwned, function::{CallLocation, FunctionSignature, ParamName}, stdlib::format::FormatError, typed::TypeLocError, + ObjValue, ResolvePathOwned, }; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Acyclic)] pub struct SyntaxError { pub message: String, - pub location: (u32, u32), + pub location: Span, } impl fmt::Display for SyntaxError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -63,26 +63,36 @@ } } +pub(crate) fn suggest_names<'a, 'b>( + name: &'a IStr, + names: impl IntoIterator, +) -> Vec { + let mut heap: Vec<(f64, IStr)> = names + .into_iter() + .filter_map(|def| { + let conf = strsim::jaro_winkler(def.as_str(), name.as_str()); + if conf < 0.8 { + return None; + } + debug_assert!( + def.as_str() != name.as_str(), + "string pooling failure: look for DOC(string-pooling) comment in jrsonnet-interner" + ); + + Some((conf, def.clone())) + }) + .collect(); + heap.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(Ordering::Equal)); + heap.into_iter().map(|v| v.1).collect() +} + pub(crate) fn suggest_object_fields(v: &ObjValue, key: IStr) -> Vec { - let mut heap = Vec::new(); - for field in v.fields_ex( + let fields = v.fields_ex( true, #[cfg(feature = "exp-preserve-order")] false, - ) { - let conf = strsim::jaro_winkler(field.as_str(), key.as_str()); - if conf < 0.8 { - continue; - } - assert!( - field.as_str() != key.as_str(), - "looks like string pooling failure, please write any info regarding this crash to https://github.com/CertainLach/jrsonnet/issues/113, thanks!" - ); - - heap.push((conf, field)); - } - heap.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(Ordering::Equal)); - heap.into_iter().map(|v| v.1).collect() + ); + suggest_names(&key, &fields) } /// Possible errors @@ -100,6 +110,9 @@ #[error("self/super/$ are only usable inside objects")] CantUseSelfSupOutsideOfObject, + + #[error("static analysis errors: {}", .0.iter().map(|d| d.message.as_str()).collect::>().join("; "))] + StaticAnalysisError(Vec), #[error("no super found")] NoSuperFound, @@ -107,17 +120,12 @@ InComprehensionCanOnlyIterateOverArray, #[error("array out of bounds: {0} is not within [0,{1})")] - ArrayBoundsError(isize, usize), + ArrayBoundsError(isize, u32), #[error("string out of bounds: {0} is not within [0,{1})")] StringBoundsError(usize, usize), #[error("assert failed: {}", format_empty_str(.0))] AssertionFailed(IStr), - - #[error("local is not defined: {0}{found}", found = format_found(.1, "local"))] - VariableIsNotDefined(IStr, Vec), - #[error("duplicate local var: {0}")] - DuplicateLocalVar(IStr), #[error("type mismatch: expected {expected}, got {2} {0}", expected = .1.iter().map(|e| format!("{e}")).collect::>().join(", "))] TypeMismatch(&'static str, Vec, ValType), @@ -172,7 +180,6 @@ #[error("syntax error: {error}")] ImportSyntaxError { path: Source, - #[trace(skip)] error: Box, }, @@ -279,11 +286,11 @@ } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f, "{}", self.0.0)?; - for el in &self.0.1.0 { + writeln!(f, "{}", self.0 .0)?; + for el in &self.0 .1 .0 { write!(f, "\t{}", el.desc)?; if let Some(loc) = &el.location { - write!(f, "at {}", loc.0.0.0)?; + write!(f, "at {}", loc.0 .0 .0)?; loc.0.map_source_locations(&[loc.1, loc.2]); } writeln!(f)?; @@ -377,6 +384,18 @@ return Err($crate::error::ErrorKind::RuntimeError($crate::jrsonnet_macros::format_istr!($l$(, $($tt)*)?)).into()) }; } +#[macro_export] +macro_rules! error { + ($w:ident$(::$i:ident)*$(($($tt:tt)*))?) => { + $crate::error::Error::from($w$(::$i)*$(($($tt)*))?) + }; + ($w:ident$(::$i:ident)*$({$($tt:tt)*})?) => { + $crate::error::Error::from($w$(::$i)*$({$($tt)*})?) + }; + ($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 { --- /dev/null +++ b/crates/jrsonnet-evaluator/src/evaluate/compspec.rs @@ -0,0 +1,330 @@ +use std::rc::Rc; + +use jrsonnet_types::ValType; + +use super::{ + destructure::{self, evaluate_locals, evaluate_locals_unbound}, + evaluate_field_member_static, evaluate_field_member_unbound, +}; +use crate::{ + analyze::{LArrComp, LBind, LCompSpec, LDestruct, LExpr, LFieldMember, LObjComp, LocalId}, + arr::ArrValue, + bail, + error::ErrorKind::*, + evaluate::evaluate, + Context, ContextBuilder, ObjValue, ObjValueBuilder, Pending, Result, Thunk, Val, +}; + +trait CompCollector { + fn reserve(&mut self, _guaranteed: usize) {} + fn collect(&mut self, ctx: Context) -> Result<()>; +} + +struct EagerArrCollector<'a> { + out: &'a mut Vec, + value: &'a LExpr, +} +impl CompCollector for EagerArrCollector<'_> { + fn reserve(&mut self, size_hint: usize) { + self.out.reserve(size_hint); + } + fn collect(&mut self, ctx: Context) -> Result<()> { + self.out.push(evaluate(ctx, self.value)?); + Ok(()) + } +} + +struct LazyArrCollector<'a> { + out: &'a mut Vec>, + value: &'a Rc, +} +impl CompCollector for LazyArrCollector<'_> { + fn reserve(&mut self, size_hint: usize) { + self.out.reserve(size_hint); + } + fn collect(&mut self, ctx: Context) -> Result<()> { + let value_expr = self.value.clone(); + self.out.push(Thunk!(move || evaluate(ctx, &value_expr))); + Ok(()) + } +} + +struct ObjCompCollectorStatic<'a> { + builder: &'a mut ObjValueBuilder, + locals: &'a [LBind], + field: &'a LFieldMember, +} +impl CompCollector for ObjCompCollectorStatic<'_> { + fn reserve(&mut self, guaranteed: usize) { + self.builder.reserve_fields(guaranteed); + } + fn collect(&mut self, inner_ctx: Context) -> Result<()> { + let value_ctx = evaluate_locals(inner_ctx.clone(), self.locals); + evaluate_field_member_static(self.builder, inner_ctx, value_ctx, self.field) + } +} + +struct ObjCompCollectorUnbound<'a> { + builder: &'a mut ObjValueBuilder, + locals: Rc>, + this_id: Option, + field: &'a LFieldMember, +} +impl CompCollector for ObjCompCollectorUnbound<'_> { + fn reserve(&mut self, guaranteed: usize) { + 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); + evaluate_field_member_unbound(self.builder, inner_ctx, uctx, self.field) + } +} + +pub fn evaluate_obj_comp( + super_obj: Option, + ctx: Context, + comp: &LObjComp, +) -> Result { + let mut builder = ObjValueBuilder::new(); + if let Some(super_obj) = super_obj { + builder.with_super(super_obj); + } + + let cached_overs = cache_overs(&ctx, &comp.compspecs)?; + if comp.this.is_some() || comp.uses_super { + evaluate_compspecs( + ctx, + &comp.compspecs, + &cached_overs, + 0, + 0, + &mut ObjCompCollectorUnbound { + builder: &mut builder, + locals: comp.locals.clone(), + this_id: comp.this, + field: &comp.field, + }, + )?; + } else { + evaluate_compspecs( + ctx, + &comp.compspecs, + &cached_overs, + 0, + 0, + &mut ObjCompCollectorStatic { + builder: &mut builder, + locals: &comp.locals, + field: &comp.field, + }, + )?; + } + + Ok(Val::Obj(builder.build())) +} + +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: { + let mut out = Vec::new(); + + if evaluate_compspecs_eager( + ctx.clone(), + &comp.compspecs, + &cached_overs, + 0, + 0, + &mut EagerArrCollector { + out: &mut out, + value: &comp.value, + }, + ) + .is_err() + { + break 'eager; + } + return Ok(Val::arr(out)); + } + + let mut items: Vec> = Vec::new(); + evaluate_compspecs( + ctx, + &comp.compspecs, + &cached_overs, + 0, + 0, + &mut LazyArrCollector { + out: &mut items, + value: &comp.value, + }, + )?; + Ok(Val::arr(items)) +} + +fn cache_overs(ctx: &Context, specs: &[LCompSpec]) -> Result>> { + specs + .iter() + .map(|spec| { + Ok(match spec { + LCompSpec::For { + over, + loop_invariant: true, + .. + } => { + let val = evaluate(ctx.clone(), over)?; + let Val::Arr(arr) = val else { + bail!(InComprehensionCanOnlyIterateOverArray) + }; + Some(arr) + } + _ => None, + }) + }) + .collect::>() +} + +fn evaluate_compspecs_eager( + ctx: Context, + specs: &[LCompSpec], + cached_overs: &[Option], + idx: usize, + guaranteed_reserve: usize, + collector: &mut dyn CompCollector, +) -> Result<()> { + if idx >= specs.len() { + collector.reserve(guaranteed_reserve); + return collector.collect(ctx.clone()); + } + match &specs[idx] { + LCompSpec::If(cond) => { + let val = evaluate(ctx.clone(), cond)?; + let Val::Bool(b) = val else { + bail!(TypeMismatch( + "if spec condition", + vec![ValType::Bool], + val.value_type() + )) + }; + if b { + evaluate_compspecs_eager(ctx, specs, cached_overs, idx + 1, 0, collector)?; + } + } + LCompSpec::For { destruct, over, .. } => { + let arr = if let Some(cached) = &cached_overs[idx] { + cached.clone() + } else { + let arr_val = evaluate(ctx.clone(), over)?; + let Val::Arr(arr) = arr_val else { + bail!(InComprehensionCanOnlyIterateOverArray) + }; + arr + }; + 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, + )?; + } + } + #[cfg(feature = "exp-destruct")] + _ => { + for (i, item) in arr.iter().enumerate() { + let item_val = item?; + let mut inner_builder = ContextBuilder::extend(ctx.clone(), 1); + destructure::destruct( + destruct, + Thunk::evaluated(item_val), + None, + &mut inner_builder, + ); + let inner_ctx = inner_builder.build(); + evaluate_compspecs_eager( + inner_ctx, + specs, + cached_overs, + idx + 1, + if i == 0 { inner_reserve } else { 0 }, + collector, + )?; + } + } + } + } + } + Ok(()) +} + +fn evaluate_compspecs( + ctx: Context, + specs: &[LCompSpec], + cached_overs: &[Option], + idx: usize, + guaranteed_reserve: usize, + collector: &mut dyn CompCollector, +) -> Result<()> { + if idx >= specs.len() { + collector.reserve(guaranteed_reserve); + return collector.collect(ctx); + } + match &specs[idx] { + LCompSpec::If(cond) => { + let val = evaluate(ctx.clone(), cond)?; + let Val::Bool(b) = val else { + bail!(TypeMismatch( + "if spec condition", + vec![ValType::Bool], + val.value_type() + )) + }; + if b { + evaluate_compspecs(ctx, specs, cached_overs, idx + 1, 0, collector)?; + } + } + LCompSpec::For { destruct, over, .. } => { + let arr = if let Some(cached) = &cached_overs[idx] { + cached.clone() + } else { + let arr_val = evaluate(ctx.clone(), over)?; + let Val::Arr(arr) = arr_val else { + bail!(InComprehensionCanOnlyIterateOverArray) + }; + arr + }; + 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); + evaluate_compspecs( + inner_ctx, + specs, + cached_overs, + idx + 1, + if i == 0 { inner_reserve } else { 0 }, + collector, + )?; + } + } + } + Ok(()) +} --- a/crates/jrsonnet-evaluator/src/evaluate/destructure.rs +++ b/crates/jrsonnet-evaluator/src/evaluate/destructure.rs @@ -1,213 +1,256 @@ -use jrsonnet_ir::{BindSpec, Destruct}; +use std::rc::Rc; + +use jrsonnet_gcmodule::Trace; use crate::{ - Context, ContextBuilder, Pending, Thunk, Val, error::Result, evaluate_method, - evaluate_named_param, + analyze::{LBind, LDestruct, LDestructField, LDestructRest, LExpr, LocalId}, + bail, + evaluate::evaluate, + Context, ContextBuilder, Pending, Result, SupThis, Thunk, Unbound, Val, }; -#[allow(clippy::too_many_lines)] -#[allow(unused_variables)] -pub fn destruct( - d: &Destruct, - parent: Thunk, +#[allow(dead_code, reason = "not dead in exp-destruct")] +fn destruct_array( + start: &[LDestruct], + rest: Option, + end: &[LDestruct], + + value: Thunk, fctx: Pending, - new_bindings: &mut ContextBuilder, -) -> Result<()> { - match d { - Destruct::Full(v) => { - new_bindings.try_bind(v.clone(), parent)?; + builder: &mut ContextBuilder, +) { + let min_len = start.len() + end.len(); + let has_rest = rest.is_some(); + let full = Thunk!(move || { + let v = value.evaluate()?; + let Val::Arr(arr) = v else { + bail!("expected array"); + }; + if !has_rest { + if arr.len() as usize != min_len { + bail!("expected {} elements, got {}", min_len, arr.len()) + } + } else if (arr.len() as usize) < min_len { + bail!( + "expected at least {} elements, but array was only {}", + min_len, + arr.len() + ) } - #[cfg(feature = "exp-destruct")] - Destruct::Skip => {} - #[cfg(feature = "exp-destruct")] - Destruct::Array { start, rest, end } => { - use jrsonnet_ir::DestructRest; + Ok(arr) + }); - use crate::bail; + for (i, d) in start.iter().enumerate() { + let full = full.clone(); + destruct( + d, + Thunk!(move || Ok(full.evaluate()?.get(i as u32)?.expect("length is checked"))), + fctx.clone(), + builder, + ); + } - let min_len = start.len() + end.len(); - let has_rest = rest.is_some(); - let full = Thunk!(move || { - let v = parent.evaluate()?; - let Val::Arr(arr) = v else { - bail!("expected array"); - }; - if !has_rest { - if arr.len() != min_len { - bail!("expected {} elements, got {}", min_len, arr.len()) - } - } else if arr.len() < min_len { - bail!( - "expected at least {} elements, but array was only {}", - min_len, - arr.len() - ) - } - Ok(arr) - }); + let start_len = start.len() as u32; + let end_len = end.len() as u32; - { - for (i, d) in start.iter().enumerate() { - let full = full.clone(); - destruct( - d, - Thunk!(move || Ok(full.evaluate()?.get(i)?.expect("length is checked"))), - fctx.clone(), - new_bindings, - )?; - } - } + if let Some(crate::analyze::LDestructRest::Keep(id)) = rest { + let full = full.clone(); + builder.bind( + id, + Thunk!(move || { + let full = full.evaluate()?; + let to = full.len() - end_len; + Ok(Val::Arr(full.slice( + Some(start_len as i32), + Some(to as i32), + None, + ))) + }), + ); + } - match rest { - Some(DestructRest::Keep(v)) => { - let start = start.len(); - let end = end.len(); - let full = full.clone(); - destruct( - &Destruct::Full(v.clone()), - Thunk!(move || { - let full = full.evaluate()?; - let to = full.len() - end; - Ok(Val::Arr(full.slice( - Some(start as i32), - Some(to as i32), - None, - ))) - }), - fctx.clone(), - new_bindings, - )?; - } - Some(DestructRest::Drop) | None => {} - } + for (i, d) in end.iter().enumerate() { + let full = full.clone(); + destruct( + d, + Thunk!(move || { + let full = full.evaluate()?; + Ok(full + .get(full.len() - end_len + i as u32)? + .expect("length is checked")) + }), + fctx.clone(), + builder, + ); + } +} - { - for (i, d) in end.iter().enumerate() { - let full = full.clone(); - let end = end.len(); - destruct( - d, - Thunk!(move || { - let full = full.evaluate()?; - Ok(full.get(full.len() - end + i)?.expect("length is checked")) - }), - fctx.clone(), - new_bindings, - )?; - } - } - } - #[cfg(feature = "exp-destruct")] - Destruct::Object { fields, rest } => { - use jrsonnet_ir::DestructRest; - use rustc_hash::FxHashSet; +#[allow(dead_code, reason = "not dead in exp-destruct")] +fn destruct_object( + fields: &[LDestructField], + rest: Option, - use crate::{ObjValueBuilder, bail}; + value: Thunk, + fctx: Pending, + builder: &mut ContextBuilder, +) { + use jrsonnet_interner::IStr; + use rustc_hash::FxHashSet; - let captured_fields: FxHashSet<_> = fields.iter().map(|f| f.0.clone()).collect(); - let field_names: Vec<_> = fields - .iter() - .map(|f| (f.0.clone(), f.2.is_some())) - .collect(); - let has_rest = rest.is_some(); - let full = Thunk!(move || { - let v = parent.evaluate()?; - let Val::Obj(obj) = v else { - bail!("expected object"); - }; - for (field, has_default) in &field_names { - if !has_default && !obj.has_field_ex(field.clone(), true) { - bail!("missing field: {field}"); - } - } - if !has_rest { - let len = obj.len(); - if len > field_names.len() { - bail!("too many fields, and rest not found"); - } - } - Ok(obj) - }); + use crate::{bail, ObjValueBuilder}; - match rest { - Some(DestructRest::Keep(v)) => { - let full = full.clone(); - destruct( - &Destruct::Full(v.clone()), - Thunk!(move || { - let full = full.evaluate()?; - let mut builder = ObjValueBuilder::new(); - builder.extend_with_core(full.as_standalone()); - builder.with_fields_omitted(captured_fields); - Ok(Val::Obj(builder.build())) - }), - fctx.clone(), - new_bindings, - )?; - } - Some(DestructRest::Drop) | None => {} + let captured_fields: FxHashSet = fields.iter().map(|f| f.name.clone()).collect(); + let field_names: Vec<(IStr, bool)> = fields + .iter() + .map(|f| (f.name.clone(), f.default.is_some())) + .collect(); + let has_rest = rest.is_some(); + let full = Thunk!(move || { + let v = value.evaluate()?; + let Val::Obj(obj) = v else { + bail!("expected object"); + }; + for (field, has_default) in &field_names { + if !has_default && !obj.has_field_ex(field.clone(), true) { + bail!("missing field: {field}"); + } + } + if !has_rest { + let len = obj.len(); + if len as usize > field_names.len() { + bail!("too many fields, and rest not found"); } + } + Ok(obj) + }); + + if let Some(crate::analyze::LDestructRest::Keep(id)) = rest { + let full = full.clone(); + builder.bind( + id, + Thunk!(move || { + let full = full.evaluate()?; + let mut out = ObjValueBuilder::new(); + out.extend_with_core(full.as_standalone()); + out.with_fields_omitted(captured_fields); + Ok(Val::Obj(out.build())) + }), + ); + } - for (field, d, default) in fields { - let default = default.clone().map(|e| (fctx.clone(), e)); - let value = { - let field = field.clone(); - let full = full.clone(); - Thunk!(move || { - let full = full.evaluate()?; - if let Some(field) = full.get(field)? { - Ok(field) - } else { - let (fctx, expr) = default.as_ref().expect("shape is checked"); - Ok(crate::evaluate(fctx.clone().unwrap(), expr)?) - } - }) - }; + 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 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) + }, + Ok, + ) + }); - if let Some(d) = d { - destruct(d, value, fctx.clone(), new_bindings)?; - } else { - destruct( - &Destruct::Full(field.clone()), - value, - fctx.clone(), - new_bindings, - )?; - } - } + if let Some(into) = &field.into { + destruct(into, value_thunk, fctx.clone(), builder); + } else { + unreachable!("analyzer lowers object-destruct shorthands into `into`"); } } - Ok(()) } -pub fn evaluate_dest( - d: &BindSpec, +/// 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, - new_bindings: &mut ContextBuilder, -) -> Result<()> { + builder: &mut ContextBuilder, +) { match d { - BindSpec::Field { into, value } => { - let name = into.name(); - let value = value.clone(); - let data = { - let fctx = fctx.clone(); - Thunk!(move || evaluate_named_param(fctx.unwrap(), &value, name)) - }; - destruct(into, data, fctx, new_bindings)?; + LDestruct::Full(id) => builder.bind(*id, value), + #[cfg(feature = "exp-destruct")] + LDestruct::Skip => {} + #[cfg(feature = "exp-destruct")] + LDestruct::Array { start, rest, end } => destruct_array(start, rest, end, value, fctx, builder), + #[cfg(feature = "exp-destruct")] + LDestruct::Object { fields, rest } => destruct_object(fields, rest, value, fctx, builder), + } +} + +/// 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); +} + +/// 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()); + for bind in binds { + evaluate_dest(bind, fctx.clone(), &mut builder); + } + builder.build().into_future(fctx) +} + +pub trait CloneableUnbound: Unbound + Clone {} +impl CloneableUnbound for V where V: Unbound + Clone {} + +pub fn evaluate_locals_unbound( + fctx: Context, + locals: Rc>, + this_id: Option, +) -> impl CloneableUnbound { + #[derive(Trace, Clone)] + struct UnboundLocals { + fctx: Context, + 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) } - BindSpec::Function { - name, - params, - value, - } => { - let params = params.clone(); - let name = name.clone(); - let value = value.clone(); - new_bindings.try_bind( - name.clone(), - Thunk!(move || Ok(evaluate_method(fctx.unwrap(), name, params, value))), - )?; - } } - Ok(()) + + UnboundLocals { + fctx, + locals, + this_id, + } } --- a/crates/jrsonnet-evaluator/src/evaluate/mod.rs +++ b/crates/jrsonnet-evaluator/src/evaluate/mod.rs @@ -2,39 +2,42 @@ use jrsonnet_gcmodule::{Cc, Trace}; use jrsonnet_interner::IStr; -use jrsonnet_ir::{ - ArgsDesc, AssertStmt, BinaryOpType, BindSpec, CompSpec, Expr, ExprParams, FieldMember, - FieldName, ForSpecData, IfSpecData, ImportKind, LiteralType, ObjBody, ObjMembers, Spanned, - function::ParamName, -}; +use jrsonnet_ir::ImportKind; use jrsonnet_types::ValType; -use self::destructure::destruct; +use self::{ + compspec::{evaluate_arr_comp, evaluate_obj_comp}, + destructure::{evaluate_locals, evaluate_locals_unbound}, + operator::evaluate_binary_op_special, +}; use crate::{ - Context, ContextBuilder, Error, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result, - ResultExt, SupThis, Unbound, Val, - arr::ArrValue, + analyze::{ + LArgsDesc, LAssertStmt, LExpr, LFieldMember, LFieldName, LFunction, LIndexPart, LObjBody, + LObjMembers, + }, bail, - destructure::evaluate_dest, - error::{ErrorKind::*, suggest_object_fields}, - evaluate::operator::{evaluate_binary_op_special, evaluate_unary_op}, - function::{CallLocation, FuncDesc, FuncVal, PreparedFuncVal}, - in_frame, - typed::{FromUntyped, IntoUntyped as _, Typed}, - val::{CachedUnbound, IndexableVal, StrValue, Thunk}, - with_state, + error::{suggest_object_fields, ErrorKind::*}, + evaluate::operator::evaluate_unary_op, + function::{prepared::PreparedFuncVal, CallLocation, FuncDesc, FuncVal}, + in_frame, runtime_error, + typed::FromUntyped as _, + val::{CachedUnbound, Thunk}, + with_state, Context, Error, ObjValue, ObjValueBuilder, ObjectAssertion, Result, ResultExt as _, + SupThis, Unbound, Val, }; + +pub mod compspec; pub mod destructure; pub mod operator; // This is the amount of bytes that need to be left on the stack before increasing the size. // It must be at least as large as the stack required by any code that does not call // `ensure_sufficient_stack`. -const RED_ZONE: usize = 100 * 1024; // 100k +const RED_ZONE: usize = 100 * 1024; // Only the first stack that is pushed, grows exponentially (2^n * STACK_PER_RECURSION) from then // on. This flag has performance relevant characteristics. Don't set it too high. -const STACK_PER_RECURSION: usize = 1024 * 1024; // 1MB +const STACK_PER_RECURSION: usize = 1024 * 1024; /// Grows the stack on demand to prevent stack overflow. Call this in strategic locations /// to "break up" recursive calls. E.g. almost any call to `visit_expr` or equivalent can benefit @@ -46,54 +49,36 @@ stacker::maybe_grow(RED_ZONE, STACK_PER_RECURSION, f) } -pub fn evaluate_trivial(expr: &Expr) -> Option { - fn is_trivial(expr: &Expr) -> bool { - match expr { - Expr::Str(_) - | Expr::Num(_) - | Expr::Literal(LiteralType::False | LiteralType::True | LiteralType::Null) => true, - Expr::Arr(a) => a.iter().all(is_trivial), - _ => false, - } - } +pub fn evaluate_trivial(expr: &LExpr) -> Option { + // TODO: Eager trivial array Some(match expr { - Expr::Str(s) => Val::string(s.clone()), - Expr::Num(n) => Val::Num(*n), - Expr::Literal(LiteralType::False) => Val::Bool(false), - Expr::Literal(LiteralType::True) => Val::Bool(true), - Expr::Literal(LiteralType::Null) => Val::Null, - Expr::Arr(n) => { - if n.iter().any(|e| !is_trivial(e)) { - return None; - } - Val::Arr( - n.iter() - .map(evaluate_trivial) - .map(|e| e.expect("checked trivial")) - .collect(), - ) - } + LExpr::Str(s) => Val::string(s.clone()), + LExpr::Num(n) => Val::Num(*n), + LExpr::Bool(false) => Val::Bool(false), + LExpr::Bool(true) => Val::Bool(true), + LExpr::Null => Val::Null, _ => return None, }) } -pub fn evaluate_method(ctx: Context, name: IStr, params: ExprParams, body: Rc) -> Val { +/// Evaluate a method definition. +pub fn evaluate_method(ctx: Context, name: IStr, func: &Rc) -> Val { Val::Func(FuncVal::Normal(Cc::new(FuncDesc { name, ctx, - params, - body, + func: func.clone(), }))) } -pub fn evaluate_field_name(ctx: Context, field_name: &Spanned) -> Result> { - Ok(match &field_name.value { - FieldName::Fixed(n) => Some(n.clone()), - FieldName::Dyn(expr) => in_frame( - CallLocation::new(&field_name.span), +pub fn evaluate_field_name(ctx: Context, field_name: &LFieldName) -> Result> { + Ok(match field_name { + LFieldName::Fixed(n) => Some(n.clone()), + LFieldName::Dyn(expr) => in_frame( + // TODO: Spanned + CallLocation::native(), || "evaluating field name".to_string(), || { - let v = evaluate(ctx, expr)?; + let v = evaluate(ctx.clone(), expr)?; Ok(if matches!(v, Val::Null) { None } else { @@ -104,371 +89,452 @@ }) } -pub fn evaluate_comp( - ctx: Context, - specs: &[CompSpec], - mut guaranteed_reserve: usize, - callback: &mut impl FnMut(Context, usize) -> Result<()>, -) -> Result<()> { - match specs.first() { - None => callback(ctx, guaranteed_reserve)?, - Some(CompSpec::IfSpec(IfSpecData { cond, span: _ })) => { - if bool::from_untyped(evaluate(ctx.clone(), cond)?)? { - evaluate_comp(ctx, &specs[1..], 0, callback)?; - } - } - Some(CompSpec::ForSpec(ForSpecData { - destruct: into, - over, - })) => { - match evaluate(ctx.clone(), over)? { - Val::Arr(list) => { - guaranteed_reserve = guaranteed_reserve.max(1) * list.len(); - for (i, item) in list.iter_lazy().enumerate() { - let fctx = Pending::new(); - let mut ctx = ContextBuilder::extend_fast(ctx.clone()); - destruct(into, item, fctx.clone(), &mut ctx)?; - let ctx = ctx.build().into_future(fctx); +pub fn evaluate_thunk(ctx: Context, expr: Rc, tailstrict: bool) -> Result> { + Ok(if tailstrict { + Thunk::evaluated(evaluate(ctx, &expr)?) + } else { + Thunk!(move || { evaluate(ctx, &expr) }) + }) +} - let specs = &specs[1..]; - evaluate_comp( - ctx, - specs, - if i == 0 || !specs.is_empty() { - guaranteed_reserve - } else { - 0 - }, - callback, - )?; - } - } - Val::Obj(obj) if cfg!(feature = "exp-object-iteration") => { - let fields = obj.fields( - // TODO: Should there be ability to preserve iteration order? - #[cfg(feature = "exp-preserve-order")] - false, - ); - guaranteed_reserve = guaranteed_reserve.max(1) * fields.len(); - for (i, field) in fields.into_iter().enumerate() { - let fctx = Pending::new(); - let mut ctx = ContextBuilder::extend_fast(ctx.clone()); - let obj = obj.clone(); - let value = Thunk::evaluated(Val::arr(vec![ - Thunk::evaluated(Val::string(field.clone())), - obj.get_lazy(field).expect( - "field exists, as field name was obtained from object.fields()", - ), - ])); - destruct(into, value, fctx.clone(), &mut ctx)?; - let ctx = ctx.build().into_future(fctx); +mod names { + use crate::names; - evaluate_comp( - ctx, - &specs[1..], - if i == 0 || !specs.is_empty() { - guaranteed_reserve - } else { - 0 - }, - callback, - )?; - } - } - _ => bail!(InComprehensionCanOnlyIterateOverArray), - } - } + names! { + anonymous: "anonymous", } - Ok(()) } -fn evaluate_arr_comp(ctx: Context, expr: &Rc, comp_specs: &[CompSpec]) -> Result { - let ctx = ctx.branch_point(); - 'eager: { - let mut out = Vec::new(); +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) +} - if evaluate_comp(ctx.clone(), comp_specs, 0, &mut |ctx, reserve| { - if reserve != 0 { - out.reserve(reserve); +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::BadLocal(name) => panic!("unresolvable reference: {name}"), + LExpr::Arr(items) => Val::Arr(crate::arr::ArrValue::expr(ctx, 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::IfElse { + cond, + cond_then, + cond_else, + } => { + let cond_val = evaluate(ctx.clone(), cond)?; + let Val::Bool(b) = cond_val else { + bail!(TypeMismatch( + "if condition", + vec![ValType::Bool], + cond_val.value_type() + )) + }; + if b { + evaluate(ctx, cond_then)? + } else if let Some(e) = cond_else { + evaluate(ctx, e)? + } else { + Val::Null } - out.push(evaluate(ctx, expr)?); - Ok(()) - }) - .is_err() - { - break 'eager; } + LExpr::Error(s, e) => in_frame( + CallLocation::new(s), + || "error statement".to_owned(), + || bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)), + )?, + LExpr::AssertExpr { assert, rest } => { + evaluate_assert(ctx.clone(), assert)?; + evaluate(ctx, rest)? + } - return Ok(ArrValue::new(out)); - }; - let mut out = Vec::new(); - evaluate_comp(ctx, comp_specs, 0, &mut |ctx, reserve| { - if reserve != 0 { - out.reserve(reserve); + LExpr::Function(func) => evaluate_method( + ctx, + func.name.clone().unwrap_or_else(names::anonymous), + func, + ), + LExpr::Apply { + applicable, + args, + tailstrict, + } => evaluate_apply( + ctx, + applicable, + args, + CallLocation::new(&args.span), + *tailstrict, + )?, + LExpr::Index { indexable, parts } => evaluate_index(ctx, indexable, parts)?, + LExpr::Obj(body) => evaluate_obj_body(None, ctx, body)?, + LExpr::ObjExtend(lhs, body) => { + let lhs_val = evaluate(ctx.clone(), lhs)?; + let Val::Obj(lhs_obj) = lhs_val else { + bail!(TypeMismatch( + "object extend lhs", + vec![ValType::Obj], + lhs_val.value_type(), + )) + }; + evaluate_obj_body(Some(lhs_obj), ctx, body)? + } + LExpr::ArrComp(comp) => evaluate_arr_comp(ctx, comp)?, + LExpr::Slice(slice) => { + use crate::typed::BoundedUsize; + let val = evaluate(ctx.clone(), &slice.value)?; + let indexable = val.into_indexable()?; + let start = slice + .start + .as_ref() + .map(|e| evaluate(ctx.clone(), e)) + .transpose()? + .map(|v| -> Result { + v.as_num() + .ok_or_else(|| { + TypeMismatch("slice start", vec![ValType::Num], v.value_type()).into() + }) + .map(|n| n as i32) + }) + .transpose()?; + let end = slice + .end + .as_ref() + .map(|e| evaluate(ctx.clone(), e)) + .transpose()? + .map(|v| -> Result { + v.as_num() + .ok_or_else(|| { + TypeMismatch("slice end", vec![ValType::Num], v.value_type()).into() + }) + .map(|n| n as i32) + }) + .transpose()?; + let step = slice + .step + .as_ref() + .map(|e| evaluate(ctx, e)) + .transpose()? + .map(|v| -> Result> { + 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")) + }) + .transpose()?; + Val::from(indexable.slice(start, end, step)?) } - let expr = expr.clone(); - out.push(Thunk!(move || evaluate(ctx, &expr))); - Ok(()) - })?; - Ok(ArrValue::new(out)) + LExpr::Super => Val::Obj(ctx.try_sup_this()?.standalone_super()?), + LExpr::Import { + kind, + kind_span, + path, + } => with_state(|state| { + let resolved = state.resolve_from(kind_span.0.source_path(), &path.clone())?; + Ok::<_, Error>(match kind.value { + ImportKind::Normal => in_frame( + CallLocation::new(&kind.span), + || "import".to_string(), + || state.import_resolved(resolved), + )?, + ImportKind::Str => Val::string(state.import_resolved_str(resolved)?), + ImportKind::Bin => Val::arr(state.import_resolved_bin(resolved)?), + }) + })?, + }) } -trait CloneableUnbound: Unbound + Clone {} -impl CloneableUnbound for V where V: Unbound + Clone {} +fn evaluate_apply( + ctx: Context, + applicable: &LExpr, + args: &LArgsDesc, + loc: CallLocation<'_>, + tailstrict: bool, +) -> Result { + let func_val = evaluate(ctx.clone(), applicable)?; + let Val::Func(func) = func_val else { + bail!(OnlyFunctionsCanBeCalledGot(func_val.value_type())) + }; -fn evaluate_object_locals( - fctx: Context, - locals: Rc>, -) -> impl CloneableUnbound { - #[derive(Trace, Clone)] - struct UnboundLocals { - fctx: Context, - locals: Rc>, - } - impl Unbound for UnboundLocals { - type Bound = Context; + let name = func.name(); + let unnamed = args + .unnamed + .iter() + .cloned() + .map(|e| evaluate_thunk(ctx.clone(), e, tailstrict)) + .collect::>>()?; - fn bind(&self, sup_this: SupThis) -> Result { - let fctx = Context::new_future(); - let ctx = self.fctx.clone(); - let mut ctx = ContextBuilder::extend(ctx); - for b in self.locals.iter() { - evaluate_dest(b, fctx.clone(), &mut ctx)?; - } - - let ctx = ctx.build_sup_this(sup_this).into_future(fctx); + let named = args + .values + .iter() + .cloned() + .map(|e| evaluate_thunk(ctx.clone(), e, tailstrict)) + .collect::>>()?; + let prepare = PreparedFuncVal::new(func, unnamed.len(), &args.names) + .with_description_src(loc, || format!("function <{name}> preparation"))?; + in_frame( + loc, + || format!("function <{name}> call"), + || prepare.call(CallLocation::native(), &unnamed, &named), + ) +} - Ok(ctx) +fn evaluate_index(ctx: Context, indexable: &LExpr, parts: &[LIndexPart]) -> Result { + let mut value = if let LExpr::Super = indexable { + let sup_this = ctx.try_sup_this()?; + // First part must be evaluated to get the super field name + if parts.is_empty() { + bail!(RuntimeError("super requires an index".into())) } - } + let key_val = evaluate(ctx.clone(), &parts[0].value)?; + let Val::Str(key) = &key_val else { + bail!(ValueIndexMustBeTypeGot( + ValType::Obj, + ValType::Str, + key_val.value_type(), + )) + }; + let field = key.clone().into_flat(); + if let Some(v) = sup_this.get_super(field.clone())? { + // Continue with remaining parts + let mut value = v; + for part in &parts[1..] { + value = index_val(ctx.clone(), CallLocation::new(&part.span), value, part)?; + } + return Ok(value); + } + let suggestions = suggest_object_fields(sup_this.this(), field.clone()); + bail!(NoSuchField(field, suggestions)) + } else { + evaluate(ctx.clone(), indexable)? + }; - UnboundLocals { fctx, locals } + for part in parts { + value = index_val(ctx.clone(), CallLocation::new(&part.span), value, part)?; + } + Ok(value) } -pub fn evaluate_field_member + Clone>( - builder: &mut ObjValueBuilder, - ctx: Context, - uctx: B, - field: &FieldMember, -) -> Result<()> { - let name = evaluate_field_name(ctx, &field.name)?; - let Some(name) = name else { - return Ok(()); - }; - - match field { - FieldMember { - plus, - params: None, - visibility, - value, - .. - } => { - #[derive(Trace)] - struct UnboundValue { - uctx: B, - value: Rc, - name: IStr, +fn index_val(ctx: Context, loc: CallLocation<'_>, value: Val, part: &LIndexPart) -> Result { + let key_val = evaluate(ctx, &part.value)?; + Ok(match (&value, &key_val) { + (Val::Obj(obj), Val::Str(key)) => { + let field = key.clone().into_flat(); + if let Some(v) = obj + .get(field.clone()) + .with_description_src(loc, || format!("field <{field}> access"))? + { + v + } else { + bail!(NoSuchField( + field.clone(), + suggest_object_fields(obj, field) + )) } - impl> Unbound for UnboundValue { - type Bound = Val; - fn bind(&self, sup_this: SupThis) -> Result { - evaluate_named(self.uctx.bind(sup_this)?, &self.value, self.name.clone()) - } + } + (Val::Arr(arr), Val::Num(idx)) => { + let n = idx.get(); + if n.fract() > f64::EPSILON { + bail!(FractionalIndex) } - - builder - .field(name.clone()) - .with_add(*plus) - .with_visibility(*visibility) - .with_location(field.name.span.clone()) - .bindable(UnboundValue { - uctx, - value: value.clone(), - name, - })?; + if n < 0.0 { + bail!(ArrayBoundsError( + n as isize, // truncation is fine for error display + arr.len() + )); + } + #[expect( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + reason = "n is checked positive" + )] + let i = n as u32; + arr.get(i) + .with_description_src(loc, || format!("element <{i}> access"))? + .ok_or_else(|| ArrayBoundsError(i as isize, arr.len()))? } - FieldMember { - params: Some(params), - visibility, - value, - .. - } => { - #[derive(Trace)] - struct UnboundMethod { - uctx: B, - value: Rc, - params: ExprParams, - name: IStr, + (Val::Str(s), Val::Num(idx)) => { + let n = idx.get(); + if n.fract() > f64::EPSILON { + bail!(FractionalIndex) } - impl> Unbound for UnboundMethod { - type Bound = Val; - fn bind(&self, sup_this: SupThis) -> Result { - Ok(evaluate_method( - self.uctx.bind(sup_this)?, - self.name.clone(), - self.params.clone(), - self.value.clone(), - )) - } + let flat = s.clone().into_flat(); + if n < 0.0 { + bail!(ArrayBoundsError( + n as isize, // truncation is fine for error display + flat.chars().count() as u32 + )); } - - builder - .field(name.clone()) - .with_visibility(*visibility) - // .with_location(value.span()) - .bindable(UnboundMethod { - uctx, - value: value.clone(), - params: params.clone(), - name, - })?; + #[expect( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + reason = "n is checked positive, overflow will truncate as expected" + )] + let i = n as usize; + let Some(char) = flat.chars().nth(i) else { + bail!(StringBoundsError(i, flat.chars().count())) + }; + Val::string(char) } - } - Ok(()) + _ => bail!(ValueIndexMustBeTypeGot( + value.value_type(), + ValType::Str, + key_val.value_type() + )), + }) } -#[derive(Trace, Clone)] -struct DirectUnbound(Context); -impl Unbound for DirectUnbound { - type Bound = Context; - fn bind(&self, sup_this: SupThis) -> Result { - Ok(ContextBuilder::extend(self.0.clone()).build_sup_this(sup_this)) +fn evaluate_obj_body(super_obj: Option, ctx: Context, body: &LObjBody) -> Result { + match body { + LObjBody::MemberList(members) => evaluate_obj_members(super_obj, ctx, members), + LObjBody::ObjComp(comp) => evaluate_obj_comp(super_obj, ctx, comp), } } -#[allow(clippy::too_many_lines)] -pub fn evaluate_member_list_object( - super_obj: Option, +pub fn evaluate_field_member_unbound + Clone>( + builder: &mut ObjValueBuilder, ctx: Context, - members: &ObjMembers, -) -> Result { + uctx: B, + field: &LFieldMember, +) -> Result<()> { #[derive(Trace)] - struct ObjectAssert { + struct UnboundValue { uctx: B, - asserts: Rc>, + value: Rc, + name: IStr, } - 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)?; - } - Ok(()) + impl> Unbound for UnboundValue { + type Bound = Val; + fn bind(&self, sup_this: SupThis) -> Result { + evaluate(self.uctx.bind(sup_this)?, &self.value) } } - let mut builder = ObjValueBuilder::new(); - if let Some(super_obj) = super_obj { - builder.with_super(super_obj); + let LFieldMember { + name, + plus, + visibility, + value, + } = field; + let Some(name) = evaluate_field_name(ctx, name)? else { + return Ok(()); + }; + + builder + .field(name.clone()) + .with_add(*plus) + .with_visibility(*visibility) + .bindable(UnboundValue { + uctx, + value: value.clone(), + name, + }) +} +pub fn evaluate_field_member_static( + builder: &mut ObjValueBuilder, + field_ctx: Context, + value_ctx: Context, + field: &LFieldMember, +) -> Result<()> { + let LFieldMember { + name, + plus, + visibility, + value, + } = field; + let Some(name) = evaluate_field_name(field_ctx, name)? else { + return Ok(()); + }; + + let value = value.clone(); + builder + .field(name) + .with_add(*plus) + .with_visibility(*visibility) + .try_thunk(Thunk!(move || { evaluate(value_ctx, &value) }))?; + Ok(()) +} + +fn evaluate_obj_members( + super_obj: Option, + ctx: Context, + members: &LObjMembers, +) -> Result { + let mut builder = ObjValueBuilder::with_capacity(members.fields.len()); + if let Some(sup) = super_obj { + builder.with_super(sup); } - if members.locals.is_empty() { - // We can use the same context for all field evaluation, it doesn't depends on locals, only on this/super - let uctx = DirectUnbound(ctx.clone()); + let needs_unbound = members.this.is_some() || members.uses_super; + + if needs_unbound { + let uctx = CachedUnbound::new(evaluate_locals_unbound( + ctx.clone(), + members.locals.clone(), + members.this, + )); for field in &members.fields { - evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?; + evaluate_field_member_unbound(&mut builder, ctx.clone(), uctx.clone(), field)?; } if !members.asserts.is_empty() { - builder.assert(ObjectAssert { + builder.assert(evaluate_object_assertions_unbound( uctx, - asserts: members.asserts.clone(), - }); + members.asserts.clone(), + )); } } else { - let locals = members.locals.clone(); - // We have single context for all fields, so we can cache them together - let uctx = CachedUnbound::new(evaluate_object_locals(ctx.clone(), locals)); + let field_ctx = ctx; + let value_ctx = evaluate_locals(field_ctx.clone(), &members.locals); for field in &members.fields { - evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?; + evaluate_field_member_static( + &mut builder, + field_ctx.clone(), + value_ctx.clone(), + field, + )?; } if !members.asserts.is_empty() { - builder.assert(ObjectAssert { - uctx, - asserts: members.asserts.clone(), - }); + builder.assert(evaluate_object_assertions_static( + value_ctx, + members.asserts.clone(), + )); } } - Ok(builder.build()) + Ok(Val::Obj(builder.build())) } -pub fn evaluate_object( - super_obj: Option, - ctx: Context, - object: &ObjBody, -) -> Result { - Ok(match object { - ObjBody::MemberList(members) => evaluate_member_list_object(super_obj, ctx, members)?, - ObjBody::ObjComp(obj) => { - let mut builder = ObjValueBuilder::new(); - if let Some(super_obj) = super_obj { - builder.with_super(super_obj); - } - let locals = obj.locals.clone(); - evaluate_comp( - ctx.branch_point(), - &obj.compspecs, - 0, - &mut |ctx, reserve| { - let uctx = evaluate_object_locals(ctx.clone(), locals.clone()); - builder.reserve_fields(reserve); - - evaluate_field_member(&mut builder, ctx, uctx, &obj.field) - }, - )?; - - builder.build() - } - }) -} - -pub fn evaluate_apply( - ctx: Context, - value: &Expr, - args: &ArgsDesc, - loc: CallLocation<'_>, - tailstrict: bool, -) -> Result { - let value = evaluate(ctx.clone(), value)?; - Ok(match value { - Val::Func(f) => { - let name = f.name(); - let unnamed = args - .unnamed - .iter() - .cloned() - .map(|un| evaluate_thunk(ctx.clone(), un, tailstrict)) - .collect::>>()?; - let named = args - .values - .iter() - .cloned() - .map(|un| evaluate_thunk(ctx.clone(), un, tailstrict)) - .collect::>>()?; - let prepare = PreparedFuncVal::new(f, args.unnamed.len(), &args.names) - .with_description_src(loc, || format!("function <{name}> call"))?; - let body = || prepare.call(loc, &unnamed, &named); - if tailstrict { - body()? - } else { - in_frame(loc, || format!("function <{name}> call"), body)? - } - } - v => bail!(OnlyFunctionsCanBeCalledGot(v.value_type())), - }) -} - -pub fn evaluate_assert(ctx: Context, assertion: &AssertStmt) -> Result<()> { - let AssertStmt { assertion, message } = assertion; +pub fn evaluate_assert(ctx: Context, assertion: &LAssertStmt) -> Result<()> { + let LAssertStmt { cond, message } = assertion; let assertion_result = in_frame( - CallLocation::new(&assertion.span), + CallLocation::native(), || "assertion condition".to_owned(), - || bool::from_untyped(evaluate(ctx.clone(), assertion)?), + || bool::from_untyped(evaluate(ctx.clone(), cond)?), )?; if !assertion_result { in_frame( - CallLocation::new(&assertion.span), + CallLocation::new(&cond.span), || "assertion failure".to_owned(), || { if let Some(msg) = message { @@ -481,306 +547,42 @@ Ok(()) } -pub fn evaluate_named_param(ctx: Context, expr: &Expr, name: ParamName) -> Result { - match name { - ParamName::Named(name) => evaluate_named(ctx, expr, name), - ParamName::Unnamed => evaluate(ctx, expr), +fn evaluate_object_assertions_unbound>( + uctx: B, + asserts: Rc>, +) -> impl ObjectAssertion { + #[derive(Trace)] + struct ObjectAssert { + uctx: B, + asserts: Rc>, } -} - -pub fn evaluate_named(ctx: Context, expr: &Expr, name: IStr) -> Result { - use Expr::*; - Ok(match expr { - Function(params, body) => evaluate_method(ctx, name, params.clone(), body.clone()), - _ => evaluate(ctx, expr)?, - }) -} - -pub fn evaluate_thunk(ctx: Context, expr: Rc, tailstrict: bool) -> Result> { - Ok(if tailstrict { - Thunk::evaluated(evaluate(ctx, &expr)?) - } else { - Thunk!(move || { evaluate(ctx, &expr) }) - }) -} -#[allow(clippy::too_many_lines)] -pub fn evaluate(ctx: Context, expr: &Expr) -> Result { - use Expr::*; - - Ok(match expr { - Literal(LiteralType::This) => Val::Obj(ctx.try_this()?), - Literal(LiteralType::Super) => Val::Obj(ctx.try_sup_this()?.standalone_super()?), - Literal(LiteralType::Dollar) => Val::Obj(ctx.try_dollar()?), - Literal(LiteralType::True) => Val::Bool(true), - Literal(LiteralType::False) => Val::Bool(false), - Literal(LiteralType::Null) => Val::Null, - Str(v) => Val::string(v.clone()), - Num(v) => Val::try_num(*v)?, - // I have tried to remove special behavior from super by implementing standalone-super - // expresion, but looks like this case still needs special treatment. - // - // Note that other jsonnet implementations will fail on `if value in (super)` expression, - // because the standalone super literal is not supported, that is because in other - // implementations `in super` treated differently from `in smth_else`. - BinaryOp(bin) - if matches!(&bin.rhs, Expr::Literal(LiteralType::Super)) - && bin.op == BinaryOpType::In => - { - let sup_this = ctx.try_sup_this()?; - // In jsonnet, "field" in e is eager, LHS expression is always executed regardless of super existence. - // In jrsonnet, however, this wasn't true, this was kept here for compatibility. - if !sup_this.has_super() { - return Ok(Val::Bool(false)); - } - let field = evaluate(ctx, &bin.lhs)?; - Val::Bool(sup_this.field_in_super(field.to_string()?)) - } - BinaryOp(bin) => evaluate_binary_op_special(ctx, &bin.lhs, bin.op, &bin.rhs)?, - UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?, - Var(name) => in_frame( - CallLocation::new(&name.span), - || format!("local <{}> access", &**name), - || ctx.binding((**name).clone())?.evaluate(), - )?, - Index { indexable, parts } => ensure_sufficient_stack(|| { - let mut parts = parts.iter(); - let mut indexable = if matches!(&**indexable, Expr::Literal(LiteralType::Super)) { - let part = parts.next().expect("at least part should exist"); - // sup_this existence check might also be skipped here for null-coalesce... - // But I believe this might cause errors. - let sup_this = ctx.try_sup_this()?; - if !sup_this.has_super() { - #[cfg(feature = "exp-null-coaelse")] - if part.null_coaelse { - return Ok(Val::Null); - } - bail!(NoSuperFound) - } - let name = evaluate(ctx.clone(), &part.value)?; - - let Val::Str(name) = name else { - bail!(ValueIndexMustBeTypeGot( - ValType::Obj, - ValType::Str, - name.value_type(), - )) - }; - - let name = name.into_flat(); - match sup_this - .get_super(name.clone()) - .with_description_src(&part.span, || format!("field <{name}> access"))? - { - Some(v) => v, - #[cfg(feature = "exp-null-coaelse")] - None if part.null_coaelse => return Ok(Val::Null), - None => { - let suggestions = suggest_object_fields( - &sup_this.standalone_super().expect("super exists"), - name.clone(), - ); - - bail!(NoSuchField(name, suggestions)) - } - } - } else { - evaluate(ctx.clone(), indexable)? - }; - - for part in parts { - indexable = match (indexable, evaluate(ctx.clone(), &part.value)?) { - (Val::Obj(v), Val::Str(key)) => match v - .get(key.clone().into_flat()) - .with_description_src(&part.span, || format!("field <{key}> access"))? - { - Some(v) => v, - #[cfg(feature = "exp-null-coaelse")] - None if part.null_coaelse => return Ok(Val::Null), - None => { - let suggestions = suggest_object_fields(&v, key.into_flat()); - - return Err(Error::from(NoSuchField( - key.clone().into_flat(), - suggestions, - ))) - .with_description_src(&part.span, || format!("field <{key}> access")); - } - }, - (Val::Obj(_), n) => bail!(ValueIndexMustBeTypeGot( - ValType::Obj, - ValType::Str, - n.value_type(), - )), - (Val::Arr(v), Val::Num(n)) => { - let n = n.get(); - if n.fract() > f64::EPSILON { - bail!(FractionalIndex) - } - if n < 0.0 { - #[expect( - clippy::cast_possible_truncation, - reason = "it would be truncated anyway" - )] - let n = n as isize; - bail!(ArrayBoundsError(n, v.len())); - } - #[expect( - clippy::cast_possible_truncation, - clippy::cast_sign_loss, - reason = "n is checked postive" - )] - v.get(n as usize)? - .ok_or_else(|| ArrayBoundsError(n as isize, v.len()))? - } - (Val::Arr(_), Val::Str(n)) => { - bail!(AttemptedIndexAnArrayWithString(n.into_flat())) - } - (Val::Arr(_), n) => bail!(ValueIndexMustBeTypeGot( - ValType::Arr, - ValType::Num, - n.value_type(), - )), - - (Val::Str(s), Val::Num(n)) => Val::Str({ - let n = n.get(); - if n.fract() > f64::EPSILON { - bail!(FractionalIndex) - } - if n < 0.0 { - #[expect( - clippy::cast_possible_truncation, - reason = "it would be truncated anyway" - )] - let n = n as isize; - bail!(ArrayBoundsError(n, s.into_flat().chars().count())); - } - #[expect( - clippy::cast_sign_loss, - clippy::cast_possible_truncation, - reason = "n is positive, overflow will truncate as expected" - )] - let n = n as usize; - let v: IStr = s - .clone() - .into_flat() - .chars() - .skip(n) - .take(1) - .collect::() - .into(); - if v.is_empty() { - bail!(StringBoundsError(n, s.into_flat().chars().count())) - } - StrValue::Flat(v) - }), - (Val::Str(_), n) => bail!(ValueIndexMustBeTypeGot( - ValType::Str, - ValType::Num, - n.value_type(), - )), - #[cfg(feature = "exp-null-coaelse")] - (Val::Null, _) if part.null_coaelse => return Ok(Val::Null), - (v, _) => bail!(CantIndexInto(v.value_type())), - }; - } - Ok(indexable) - })?, - LocalExpr(bindings, returned) => { - let fctx = Context::new_future(); - let mut ctx = ContextBuilder::extend(ctx); - for b in bindings { - evaluate_dest(b, fctx.clone(), &mut ctx)?; - } - let ctx = ctx.build().into_future(fctx); - evaluate(ctx, returned)? - } - Arr(items) => { - if items.is_empty() { - Val::arr(()) - } else { - Val::Arr(ArrValue::expr(ctx, items.clone())) - } - } - ArrComp(expr, comp_specs) => Val::Arr(evaluate_arr_comp(ctx, expr, comp_specs)?), - Obj(body) => Val::Obj(evaluate_object(None, ctx, body)?), - ObjExtend(a, b) => { - let base = evaluate(ctx.clone(), a)?; - match base { - Val::Obj(base_obj) => Val::Obj(evaluate_object(Some(base_obj), ctx, b)?), - _ => bail!("ObjExtend lhs should be an object value"), - } - } - Apply(value, args, tailstrict) => ensure_sufficient_stack(|| { - evaluate_apply(ctx, value, args, CallLocation::new(&args.span), *tailstrict) - })?, - Function(params, body) => { - evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone()) - } - AssertExpr(assert) => { - evaluate_assert(ctx.clone(), &assert.assert)?; - evaluate(ctx, &assert.rest)? - } - ErrorStmt(s, e) => in_frame( - CallLocation::new(s), - || "error statement".to_owned(), - || bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)), - )?, - IfElse(if_else) => { - if in_frame( - CallLocation::new(&if_else.cond.span), - || "if condition".to_owned(), - || bool::from_untyped(evaluate(ctx.clone(), &if_else.cond.cond)?), - )? { - evaluate(ctx, &if_else.cond_then)? - } else { - match &if_else.cond_else { - Some(v) => evaluate(ctx, v)?, - None => Val::Null, - } + 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)?; } + Ok(()) } - Slice(slice) => { - fn parse_idx( - ctx: Context, - expr: Option<&Spanned>, - desc: &'static str, - ) -> Result> { - if let Some(value) = expr { - Ok(in_frame( - CallLocation::new(&value.span), - || format!("slice {desc}"), - || >::from_untyped(evaluate(ctx, value)?), - )?) - } else { - Ok(None) - } + } + ObjectAssert { uctx, asserts } +} +fn evaluate_object_assertions_static( + ctx: Context, + asserts: Rc>, +) -> impl ObjectAssertion { + #[derive(Trace)] + struct ObjectAssert { + ctx: 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)?; } - - let indexable = evaluate(ctx.clone(), &slice.value)?; - - let start = parse_idx(ctx.clone(), slice.slice.start.as_ref(), "start")?; - let end = parse_idx(ctx.clone(), slice.slice.end.as_ref(), "end")?; - let step = parse_idx(ctx, slice.slice.step.as_ref(), "step")?; - - IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)? + Ok(()) } - Import(kind, path) => { - let Expr::Str(path) = &**path else { - bail!("computed imports are not supported") - }; - with_state(|s| { - let span = &kind.span; - let resolved_path = s.resolve_from(span.0.source_path(), path)?; - Ok(match &**kind { - ImportKind::Normal => in_frame( - CallLocation::new(span), - || format!("import {:?}", path.clone()), - || s.import_resolved(resolved_path), - )?, - ImportKind::Str => Val::string(s.import_resolved_str(resolved_path)?), - ImportKind::Bin => Val::arr(s.import_resolved_bin(resolved_path)?), - }) as Result - })? - } - }) + } + ObjectAssert { ctx, asserts } } --- a/crates/jrsonnet-evaluator/src/evaluate/operator.rs +++ b/crates/jrsonnet-evaluator/src/evaluate/operator.rs @@ -1,16 +1,17 @@ use std::cmp::Ordering; -use jrsonnet_ir::{BinaryOpType, Expr, UnaryOpType}; +use jrsonnet_ir::{BinaryOpType, UnaryOpType}; use crate::{ - Context, Result, Val, + analyze::LExpr, arr::ArrValue, - bail, + bail, error, error::ErrorKind::*, - evaluate, + evaluate::evaluate, stdlib::std_format, typed::IntoUntyped as _, - val::{StrValue, equals}, + val::{equals, StrValue}, + Context, Result, Val, }; pub fn evaluate_unary_op(op: UnaryOpType, b: &Val) -> Result { @@ -39,7 +40,9 @@ (o, Str(a)) => Val::string(format!("{}{a}", o.clone().to_string()?)), (Obj(v1), Obj(v2)) => Obj(v2.extend_from(v1.clone())), - (Arr(a), Arr(b)) => Val::Arr(ArrValue::extended(a.clone(), b.clone())), + (Arr(a), Arr(b)) => Val::Arr( + ArrValue::extended(a.clone(), b.clone()).ok_or_else(|| error!("array is too large"))?, + ), (Num(v1), Num(v2)) => Val::try_num(v1.get() + v2.get())?, @@ -158,19 +161,27 @@ pub fn evaluate_binary_op_special( ctx: Context, - a: &Expr, + a: &LExpr, op: BinaryOpType, - b: &Expr, + b: &LExpr, ) -> Result { use BinaryOpType::*; use Val::*; + Ok(match (evaluate(ctx.clone(), a)?, op, b) { - (Bool(true), Or, _o) => Val::Bool(true), - (Bool(false), And, _o) => Val::Bool(false), + (Bool(true), Or, _) => Val::Bool(true), + (Bool(false), And, _) => Val::Bool(false), #[cfg(feature = "exp-null-coaelse")] (Null, NullCoaelse, eb) => evaluate(ctx, eb)?, #[cfg(feature = "exp-null-coaelse")] - (a, NullCoaelse, _o) => a, + (a, NullCoaelse, _) => a, + (a, In, LExpr::Super) => { + let sup_this = ctx.try_sup_this()?; + if !sup_this.has_super() { + return Ok(Val::Bool(false)); + } + return Ok(Val::Bool(sup_this.field_in_super(a.to_string()?))); + } (a, op, eb) => evaluate_binary_op_normal(&a, op, &evaluate(ctx, eb)?)?, }) } --- a/crates/jrsonnet-evaluator/src/function/builtin.rs +++ b/crates/jrsonnet-evaluator/src/function/builtin.rs @@ -19,6 +19,23 @@ }; } +#[macro_export] +macro_rules! names { + ($($name:ident: $val:literal),* $(,)?) => { + struct Names { + $($name: $crate::IStr,)* + } + thread_local! { + static NAMES: Names = Names { + $($name: $crate::IStr::from($val)),* + }; + } + $(pub fn $name() -> $crate::IStr { + NAMES.with(|n| n.$name.clone()) + })* + } +} + cc_dyn!( #[derive(Clone)] BuiltinFunc, --- a/crates/jrsonnet-evaluator/src/function/mod.rs +++ b/crates/jrsonnet-evaluator/src/function/mod.rs @@ -3,22 +3,24 @@ use educe::Educe; use jrsonnet_gcmodule::{Cc, Trace}; use jrsonnet_interner::IStr; -use jrsonnet_ir::{Destruct, Expr, ExprParams, Span}; +use jrsonnet_ir::Span; pub use jrsonnet_macros::builtin; use self::{ builtin::Builtin, - parse::parse_default_function_call, - prepared::{PreparedCall, parse_prepared_builtin_call, parse_prepared_function_call}, + prepared::{parse_prepared_builtin_call, PreparedCall}, }; use crate::{ - Context, Result, Thunk, Val, evaluate, evaluate_trivial, function::builtin::BuiltinFunc, + analyze::{LDestruct, LExpr, LFunction}, + evaluate::{destructure::destruct, ensure_sufficient_stack, evaluate, evaluate_trivial}, + function::builtin::BuiltinFunc, + Context, ContextBuilder, Result, Thunk, Val, }; pub mod builtin; mod native; mod parse; -mod prepared; +pub(crate) mod prepared; pub use jrsonnet_ir::function::*; pub use native::NativeFn; @@ -66,19 +68,63 @@ /// context will contain `a`. pub ctx: Context, - /// Function parameter definition - pub params: ExprParams, - /// Function body - pub body: Rc, + #[educe(PartialEq(method = Rc::ptr_eq))] + pub func: Rc, } + impl FuncDesc { - /// Create body context, but fill arguments without defaults with lazy error - pub fn default_body_context(&self) -> Result { - parse_default_function_call(self.ctx.clone(), &self.params) + pub fn signature(&self) -> FunctionSignature { + self.func.signature.clone() } + pub fn call( + &self, + unnamed: &[Thunk], + 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, + ); + } + + 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 ctx = builder.build().into_future(fctx); + ensure_sufficient_stack(|| evaluate(ctx, &self.func.body)) + } + pub fn evaluate_trivial(&self) -> Option { - evaluate_trivial(&self.body) + evaluate_trivial(&self.func.body) } } @@ -115,12 +161,12 @@ pub fn params(&self) -> FunctionSignature { match self { Self::Builtin(i) => i.params(), - Self::Normal(p) => p.params.signature.clone(), + Self::Normal(p) => p.signature(), } } /// Amount of non-default required arguments - pub fn params_len(&self) -> usize { - self.params().iter().filter(|p| !p.has_default()).count() + pub fn params_len(&self) -> u32 { + self.params().iter().filter(|p| !p.has_default()).count() as u32 } /// Function name, as defined in code. pub fn name(&self) -> IStr { @@ -139,16 +185,7 @@ _tailstrict: bool, ) -> Result { match self { - FuncVal::Normal(func) => { - let body_ctx = parse_prepared_function_call( - func.ctx.clone(), - prepared, - &func.params, - unnamed, - named, - )?; - evaluate(body_ctx, &func.body) - } + FuncVal::Normal(func) => func.call(unnamed, named, prepared), FuncVal::Builtin(b) => { let args = parse_prepared_builtin_call(prepared, b.params(), unnamed, named); b.call(loc, &args) @@ -156,7 +193,7 @@ } } - /// Is this function an indentity function. + /// Is this function an identity function. /// /// Currently only works for builtin `std.id`, aka `Self::Id` value, and `function(x) x`. /// @@ -165,21 +202,19 @@ match self { Self::Builtin(b) => b.as_any().downcast_ref::().is_some(), Self::Normal(desc) => { - if desc.params.len() != 1 { + if desc.func.params.len() != 1 { return false; } - let param = &desc.params.exprs[0]; + let param = &desc.func.params[0]; if param.default.is_some() { return false; } - - #[allow(clippy::infallible_destructuring_match)] - let id = match ¶m.destruct { - Destruct::Full(id) => id, - #[cfg(feature = "exp-destruct")] - _ => return false, + #[allow(irrefutable_let_patterns, reason = "refutable with exp-destruct")] + let LDestruct::Full(id) = ¶m.destruct + else { + return false; }; - matches!(&*desc.body, Expr::Var(v) if &**v == id) + matches!(&*desc.func.body, LExpr::Local(v) if v == id) } } } --- a/crates/jrsonnet-evaluator/src/function/parse.rs +++ b/crates/jrsonnet-evaluator/src/function/parse.rs @@ -1,49 +1,39 @@ -use jrsonnet_ir::ExprParams; +use std::rc::Rc; use crate::{ - Context, ContextBuilder, Thunk, - destructure::destruct, - error::{ErrorKind::*, Result}, - evaluate_named_param, + analyze::LFunction, + evaluate::{destructure::destruct, evaluate}, + Context, ContextBuilder, Result, Thunk, }; -/// Creates Context, which has all argument default values applied -/// and with unbound values causing error to be returned -pub fn parse_default_function_call(body_ctx: Context, params: &ExprParams) -> Result { +/// Creates Context with all argument default values applied +/// and with unbound values causing error to be returned. +pub fn parse_default_function_call(body_ctx: Context, func: &Rc) -> Result { let fctx = Context::new_future(); - - let mut ctx = ContextBuilder::extend(body_ctx); + let mut builder = ContextBuilder::extend(body_ctx, func.params.len()); - for param in params.exprs.iter() { - if let Some(v) = ¶m.default { - destruct( - ¶m.destruct.clone(), - { - let ctx = fctx.clone(); - let name = param.destruct.name(); - let value = v.clone(); - Thunk!(move || evaluate_named_param(ctx.unwrap(), &value, name)) - }, - fctx.clone(), - &mut ctx, - )?; + for param in &func.params { + 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); } else { - destruct( - ¶m.destruct, - { - let param_name = param.destruct.name(); - let params = params.clone(); - Thunk!(move || Err(FunctionParameterNotBoundInCall( - param_name, - params.signature - ) - .into())) - }, - fctx.clone(), - &mut ctx, - )?; + let name = param.name.clone().unwrap_or_else(|| "".into()); + let thunk = Thunk::errored( + crate::error::ErrorKind::FunctionParameterNotBoundInCall( + jrsonnet_ir::function::ParamName::Named(name), + jrsonnet_ir::function::FunctionSignature::empty(), + ) + .into(), + ); + destruct(¶m.destruct, thunk, fctx.clone(), &mut builder); } } - Ok(ctx.build().into_future(fctx)) + let ctx = builder.build().into_future(fctx); + Ok(ctx) } --- a/crates/jrsonnet-evaluator/src/function/prepared.rs +++ b/crates/jrsonnet-evaluator/src/function/prepared.rs @@ -1,13 +1,13 @@ use std::rc::Rc; use jrsonnet_gcmodule::{Acyclic, Trace}; -use jrsonnet_ir::{ExprParams, IStr, function::FunctionSignature}; +use jrsonnet_ir::{IStr, function::FunctionSignature}; use rustc_hash::FxHashSet; use super::{CallLocation, FuncVal}; use crate::{ - Context, ContextBuilder, Pending, Result, Thunk, Val, bail, destructure::destruct, - error::ErrorKind::*, evaluate_named_param, + Result, Thunk, Val, bail, + error::ErrorKind::*, }; #[derive(Debug, Trace, Clone)] @@ -42,6 +42,15 @@ defaults: Vec, } +impl PreparedCall { + pub fn named(&self) -> &[(usize, usize)] { + &self.named + } + pub fn defaults(&self) -> &[usize] { + &self.defaults + } +} + pub fn prepare_call( params: FunctionSignature, unnamed: usize, @@ -51,6 +60,25 @@ bail!(TooManyArgsFunctionHas(params.len(), params)) } + // Fast path: positional-only (no named args). Avoids HashMap entirely. + if named.is_empty() { + let mut defaults = Vec::new(); + for (param_id, param) in params.iter().enumerate().skip(unnamed) { + if param.has_default() { + defaults.push(param_id); + } else { + bail!(FunctionParameterNotBoundInCall( + param.name().clone(), + params.clone(), + )) + } + } + return Ok(PreparedCall { + named: Vec::new(), + defaults, + }); + } + let expected_defaults = (params.len() - unnamed).saturating_sub(named.len()); let mut ops = PreparedCall { named: Vec::with_capacity(named.len()), @@ -110,63 +138,6 @@ } Ok(ops) -} -pub fn parse_prepared_function_call( - body_ctx: Context, - prepared: &PreparedCall, - params: &ExprParams, - unnamed: &[Thunk], - named: &[Thunk], -) -> Result { - let mut ctx = ContextBuilder::extend(body_ctx); - - let destruct_ctx = Pending::new(); - - for (param_idx, unnamed) in unnamed.iter().enumerate() { - destruct( - ¶ms.exprs[param_idx].destruct, - unnamed.clone(), - destruct_ctx.clone(), - &mut ctx, - )?; - } - - for (param_idx, arg_idx) in prepared.named.iter().copied() { - destruct( - ¶ms.exprs[param_idx].destruct, - named[arg_idx].clone(), - destruct_ctx.clone(), - &mut ctx, - )?; - } - - if prepared.defaults.is_empty() { - let body_ctx = ctx.build().into_future(destruct_ctx); - Ok(body_ctx) - } else { - let fctx = Context::new_future(); - let mut ctx = ctx.commit(); - for param_idx in prepared.defaults.iter().copied() { - // let param = params.0.rc_idx(param_idx); - destruct( - ¶ms.exprs[param_idx].destruct, - { - let ctx = fctx.clone(); - let params = params.clone(); - Thunk!(move || { - let param = ¶ms.exprs[param_idx]; - let name = param.destruct.name(); - let value = param.default.as_ref().expect("default exists"); - evaluate_named_param(ctx.unwrap(), value, name) - }) - }, - fctx.clone(), - &mut ctx, - )?; - } - - Ok(ctx.build().into_future(fctx).into_future(destruct_ctx)) - } } pub fn parse_prepared_builtin_call( prepared: &PreparedCall, --- a/crates/jrsonnet-evaluator/src/integrations/serde.rs +++ b/crates/jrsonnet-evaluator/src/integrations/serde.rs @@ -3,16 +3,16 @@ use jrsonnet_interner::{IBytes, IStr}; use jrsonnet_ir::NumValue; use serde::{ - Deserialize, Serialize, Serializer, de::{self, Visitor}, ser::{ Error, SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant, SerializeTuple, SerializeTupleStruct, SerializeTupleVariant, }, + Deserialize, Serialize, Serializer, }; use crate::{ - Error as JrError, ObjValue, ObjValueBuilder, Result, Val, in_description_frame, runtime_error, + in_description_frame, runtime_error, Error as JrError, ObjValue, ObjValueBuilder, Result, Val, }; impl<'de> Deserialize<'de> for Val { @@ -182,7 +182,7 @@ #[cfg(feature = "exp-bigint")] Self::BigInt(b) => b.serialize(serializer), Self::Arr(arr) => { - let mut seq = serializer.serialize_seq(Some(arr.len()))?; + let mut seq = serializer.serialize_seq(Some(arr.len() as usize))?; for (i, element) in arr.iter().enumerate() { let mut serde_error = None; in_description_frame( @@ -203,7 +203,7 @@ seq.end() } Self::Obj(obj) => { - let mut map = serializer.serialize_map(Some(obj.len()))?; + let mut map = serializer.serialize_map(Some(obj.len() as usize))?; for (field, value) in obj.iter( #[cfg(feature = "exp-preserve-order")] true, --- a/crates/jrsonnet-evaluator/src/lib.rs +++ b/crates/jrsonnet-evaluator/src/lib.rs @@ -36,14 +36,13 @@ pub use ctx::*; pub use dynamic::*; pub use error::{Error, ErrorKind::*, Result, ResultExt}; -pub use evaluate::*; +pub use evaluate::ensure_sufficient_stack; use function::CallLocation; pub use import::*; -use jrsonnet_gcmodule::{Cc, Trace, cc_dyn}; +use jrsonnet_gcmodule::{cc_dyn, Cc, Trace}; pub use jrsonnet_interner::{IBytes, IStr}; -pub use jrsonnet_ir as parser; -pub use jrsonnet_ir::NumValue; -use jrsonnet_ir::{Expr, Source, SourcePath}; +use jrsonnet_ir::Expr; +pub use jrsonnet_ir::{NumValue, Source, SourcePath, Span}; #[doc(hidden)] pub use jrsonnet_macros; @@ -58,6 +57,7 @@ pub use tla::apply_tla; pub use val::{Thunk, Val}; +pub mod analyze; use crate::gc::WithCapacityExt as _; #[allow(clippy::needless_return)] @@ -87,7 +87,7 @@ jrsonnet_ir_parser::parse(code, &jrsonnet_ir_parser::ParserSettings { source }).map_err(|e| { SyntaxError { message: e.message, - location: (e.location.0, e.location.1), + location: e.location, } }) } @@ -165,7 +165,7 @@ pub trait ContextInitializer { /// For composability: extend builder. May panic if this initialization is not supported, /// and the context may only be created via `initialize`. - fn populate(&self, for_file: Source, builder: &mut ContextBuilder); + fn populate(&self, for_file: Source, builder: &mut InitialContextBuilder); /// Allows upcasting from abstract to concrete context initializer. /// jrsonnet by itself doesn't use this method, it is allowed for it to panic. fn as_any(&self) -> &dyn Any; @@ -174,7 +174,7 @@ where T: ContextInitializer, { - fn populate(&self, for_file: Source, builder: &mut ContextBuilder) { + fn populate(&self, for_file: Source, builder: &mut InitialContextBuilder) { (*self).populate(for_file, builder); } @@ -185,7 +185,7 @@ /// Context initializer which adds nothing. impl ContextInitializer for () { - fn populate(&self, _for_file: Source, _builder: &mut ContextBuilder) {} + fn populate(&self, _for_file: Source, _builder: &mut InitialContextBuilder) {} fn as_any(&self) -> &dyn Any { self } @@ -195,7 +195,7 @@ where T: ContextInitializer + 'static, { - fn populate(&self, for_file: Source, builder: &mut ContextBuilder) { + fn populate(&self, for_file: Source, builder: &mut InitialContextBuilder) { if let Some(ctx) = self { ctx.populate(for_file, builder); } @@ -210,7 +210,7 @@ ($($gen:ident)*) => { #[allow(non_snake_case)] impl<$($gen: ContextInitializer + Trace,)*> ContextInitializer for ($($gen,)*) { - fn populate(&self, for_file: Source, builder: &mut ContextBuilder) { + fn populate(&self, for_file: Source, builder: &mut InitialContextBuilder) { let ($($gen,)*) = self; $($gen.populate(for_file.clone(), builder);)* } @@ -408,7 +408,12 @@ file.evaluating = true; // Dropping file cache guard here, as evaluation may use this map too drop(file_cache); - let res = evaluate(self.create_default_context(file_name), &parsed); + let (ctx, externals) = self.create_default_context(file_name.clone()).build(); + let report = analyze::analyze_root(&parsed, externals); + if report.errored { + return Err(StaticAnalysisError(report.diagnostics_list).into()); + } + let res = evaluate::evaluate(ctx.build(), &report.lir); let mut file_cache = self.file_cache(); let mut file = file_cache.entry(path); @@ -438,7 +443,7 @@ } /// Creates context with all passed global variables - pub fn create_default_context(&self, source: Source) -> Context { + pub fn create_default_context(&self, source: Source) -> InitialContextBuilder { self.create_default_context_with(source, &()) } @@ -447,13 +452,13 @@ &self, source: Source, context_initializer: &dyn ContextInitializer, - ) -> Context { + ) -> InitialContextBuilder { let default_initializer = self.context_initializer(); - let mut builder = ContextBuilder::new(); + let mut builder = InitialContextBuilder::new(); default_initializer.populate(source.clone(), &mut builder); context_initializer.populate(source, &mut builder); - builder.build() + builder } } @@ -487,7 +492,7 @@ #[derive(Trace)] pub struct InitialUnderscore(pub Thunk); impl ContextInitializer for InitialUnderscore { - fn populate(&self, _for_file: Source, builder: &mut ContextBuilder) { + fn populate(&self, _for_file: Source, builder: &mut InitialContextBuilder) { builder.bind("_", self.0.clone()); } @@ -515,10 +520,14 @@ path: source.clone(), error: Box::new(e), })?; - evaluate( - self.create_default_context_with(source, context_initializer), - &parsed, - ) + let (ctx, externals) = self + .create_default_context_with(source.clone(), context_initializer) + .build(); + let report = analyze::analyze_root(&parsed, externals); + if report.errored { + return Err(StaticAnalysisError(report.diagnostics_list).into()); + } + evaluate::evaluate(ctx.build(), &report.lir) } } --- a/crates/jrsonnet-evaluator/src/obj/mod.rs +++ b/crates/jrsonnet-evaluator/src/obj/mod.rs @@ -11,8 +11,8 @@ }; use educe::Educe; -use im_rc::{Vector, vector}; -use jrsonnet_gcmodule::{Acyclic, Cc, Trace, Weak, cc_dyn}; +use im_rc::{vector, Vector}; +use jrsonnet_gcmodule::{cc_dyn, Acyclic, Cc, Trace, Weak}; use jrsonnet_interner::IStr; use jrsonnet_ir::Span; use rustc_hash::{FxHashMap, FxHashSet}; @@ -23,13 +23,13 @@ pub use oop::ObjValueBuilder; use crate::{ - CcUnbound, MaybeUnbound, Result, Thunk, Unbound, Val, arr::{PickObjectKeyValues, PickObjectValues}, bail, - error::{ErrorKind::*, suggest_object_fields}, + error::{suggest_object_fields, ErrorKind::*}, + evaluate::operator::evaluate_add_op, identity_hash, - operator::evaluate_add_op, val::{ArrValue, ThunkValue}, + CcUnbound, MaybeUnbound, Result, Thunk, Unbound, Val, }; #[cfg(not(feature = "exp-preserve-order"))] @@ -400,6 +400,15 @@ this: ObjValue, } impl SupThis { + /// Create a `SupThis` for a freshly constructed object (no super). + pub fn new(this: ObjValue) -> Self { + Self { + sup: CoreIdx { + idx: this.0.cores.len(), + }, + this, + } + } pub fn has_super(&self) -> bool { self.sup.super_exists() } @@ -501,11 +510,11 @@ // } /// Returns amount of visible object fields /// If object only contains hidden fields - may return zero. - pub fn len(&self) -> usize { + pub fn len(&self) -> u32 { self.fields_visibility() .values() .filter(|d| d.visible()) - .count() + .count() as u32 } /// For each field, calls callback. /// If callback returns false - ends iteration prematurely. --- a/crates/jrsonnet-evaluator/src/trace/mod.rs +++ b/crates/jrsonnet-evaluator/src/trace/mod.rs @@ -10,7 +10,7 @@ #[cfg(feature = "explaining-traces")] use jrsonnet_ir::Span; -use crate::{Error, error::ErrorKind}; +use crate::{error::ErrorKind, Error}; /// The way paths should be displayed #[derive(Clone, Trace)] @@ -122,7 +122,7 @@ || path.source_path().to_string(), |r| self.resolver.resolve(r), ); - let mut offset = error.location.0 as usize; + let mut offset = error.location.1 as usize; let is_eof = if offset >= path.code().len() { offset = path.code().len().saturating_sub(1); true @@ -259,25 +259,64 @@ struct ResetData { loc: Span, } - use hi_doc::{Formatting, SnippetBuilder, Text, source_to_ansi}; + use hi_doc::{source_to_ansi, Formatting, SnippetBuilder, Text}; write!(out, "{}", error.error())?; if let ErrorKind::ImportSyntaxError { path, error } = error.error() { writeln!(out)?; - let mut offset = error.location; - // To inclusive range - if offset.1 > offset.0 { - offset.1 -= 1; - } let mut builder = SnippetBuilder::new(path.code()); builder .error(Text::fragment("syntax error", Formatting::default())) - .range(offset.0 as usize..=offset.1 as usize) + .range(error.location.range()) .build(); let source = builder.build(); let ansi = source_to_ansi(&source); write!(out, "{ansi}")?; } + if let ErrorKind::StaticAnalysisError(diagnostics) = error.error() { + use crate::analyze::DiagLevel; + let mut builder: Option = None; + let mut current_src: Option<&str> = None; + let flush = + |builder: Option, out: &mut dyn std::fmt::Write| -> Result<(), std::fmt::Error> { + if let Some(b) = builder { + let ansi = source_to_ansi(&b.build()); + write!(out, "\n{}", ansi.trim_end())?; + } + Ok(()) + }; + for diag in diagnostics { + if let Some(span) = &diag.span { + let src = span.0.code(); + if current_src != Some(src) { + flush(builder.take(), out)?; + builder = Some(SnippetBuilder::new(src)); + current_src = Some(src); + } + let b = builder.as_mut().unwrap(); + let ab = match diag.level { + DiagLevel::Error => b.error(Text::fragment( + diag.message.clone(), + Formatting::default(), + )), + DiagLevel::Warning => b.warning(Text::fragment( + diag.message.clone(), + Formatting::default(), + )), + }; + ab.range(span.range()).build(); + } else { + flush(builder.take(), out)?; + current_src = None; + let prefix = match diag.level { + DiagLevel::Error => "error", + DiagLevel::Warning => "warning", + }; + write!(out, "\n{prefix}: {}", diag.message)?; + } + } + flush(builder, out)?; + } let trace = &error.trace(); let snippet_builder: RefCell> = RefCell::new(None); let mut last_location: Option = None; --- a/crates/jrsonnet-evaluator/src/typed/conversions.rs +++ b/crates/jrsonnet-evaluator/src/typed/conversions.rs @@ -637,7 +637,7 @@ } ::TYPE.check(&value)?; // Any::downcast_ref::(&a); - let mut out = Vec::with_capacity(a.len()); + let mut out = Vec::with_capacity(a.len() as usize); for e in a.iter() { let r = e?; out.push(u8::from_untyped(r)?); --- a/crates/jrsonnet-interner/src/lib.rs +++ b/crates/jrsonnet-interner/src/lib.rs @@ -171,6 +171,7 @@ let mut pool = pool.borrow_mut(); if pool.remove(inner).is_none() { + // DOC(string-pooling) // On some platforms (i.e i686-windows), try_with will not fail after TLS // destructor is called, but instead re-initialize the TLS with the empty pool. // Allow non-pooled Drop in this case. --- a/crates/jrsonnet-macros/src/lib.rs +++ b/crates/jrsonnet-macros/src/lib.rs @@ -3,13 +3,14 @@ use proc_macro2::TokenStream; use quote::{quote, quote_spanned}; use syn::{ - Attribute, DeriveInput, Error, Expr, ExprClosure, FnArg, GenericArgument, Ident, ItemFn, - LitStr, Meta, Pat, Path, PathArguments, Result, ReturnType, Token, Type, parenthesized, + parenthesized, parse::{Parse, ParseStream}, parse_macro_input, punctuated::Punctuated, spanned::Spanned, token::Comma, + Attribute, DeriveInput, Error, Expr, ExprClosure, FnArg, GenericArgument, Ident, ItemFn, + LitStr, Meta, Pat, Path, PathArguments, Result, ReturnType, Token, Type, }; use self::typed::{derive_from_untyped_inner, derive_into_untyped_inner, derive_typed_inner}; @@ -402,7 +403,7 @@ State, Val, function::{builtin::Builtin, FunctionSignature, ParamParse, ParamName, ParamDefault, CallLocation}, Result, Context, typed::{Typed, FromUntyped, IntoUntypedResult}, - parser::Span, params, Thunk, + Span, params, Thunk, }; params!( #(#params_desc)* --- a/crates/jrsonnet-stdlib/src/arrays.rs +++ b/crates/jrsonnet-stdlib/src/arrays.rs @@ -1,11 +1,12 @@ #![allow(non_snake_case)] use jrsonnet_evaluator::{ - Either, IStr, ObjValue, ObjValueBuilder, Result, ResultExt, Thunk, Val, bail, - function::{FuncVal, NativeFn, builtin}, + bail, error, + function::{builtin, NativeFn}, runtime_error, - typed::{BoundedI32, BoundedUsize, Either2, FromUntyped}, - val::{ArrValue, IndexableVal, equals}, + typed::{BoundedUsize, Either2, FromUntyped}, + val::{equals, ArrValue, IndexableVal}, + Either, IStr, ObjValue, ObjValueBuilder, Result, ResultExt, Thunk, Val, }; pub fn eval_on_empty(on_empty: Option>) -> Result { @@ -17,32 +18,28 @@ } #[builtin] -pub fn builtin_make_array( - // Can't use usize because range_exclusive is over i32 - sz: BoundedI32<0, { i32::MAX }>, - func: FuncVal, -) -> Result { - if *sz == 0 { +pub fn builtin_make_array(sz: u32, func: NativeFn!((u32,) -> Val)) -> Result { + if sz == 0 { return Ok(ArrValue::empty()); } - func.evaluate_trivial().map_or_else( - // TODO: Different mapped array impl avoiding allocating unnecessary vals - || Ok(ArrValue::range_exclusive(0, *sz).map(FromUntyped::from_untyped(Val::Func(func))?)), - |trivial| { - #[expect(clippy::cast_sign_loss, reason = "sz is bounded to be larger than 0")] - let mut out = Vec::with_capacity(*sz as usize); - for _ in 0..*sz { - out.push(trivial.clone()); + // Try eager evaluation: call func(i) immediately for each element. + 'eager: { + let mut out = Vec::with_capacity(sz as usize); + for i in 0..sz { + match func.call(i) { + Ok(v) => out.push(v), + Err(_) => break 'eager, } - Ok(ArrValue::new(out)) - }, - ) + } + return Ok(ArrValue::new(out)); + } + Ok(ArrValue::make(sz, func)) } #[builtin] -pub fn builtin_repeat(what: Either![IStr, ArrValue], count: usize) -> Result { +pub fn builtin_repeat(what: Either![IStr, ArrValue], count: u32) -> Result { Ok(match what { - Either2::A(s) => Val::string(s.repeat(count)), + Either2::A(s) => Val::string(s.repeat(count as usize)), Either2::B(arr) => Val::Arr( ArrValue::repeated(arr, count) .ok_or_else(|| runtime_error!("repeated length overflow"))?, @@ -210,14 +207,14 @@ let item = item?.clone(); if let Val::Arr(items) = item { if !first { - out.reserve(joiner_items.len()); + out.reserve(joiner_items.len() as usize); // TODO: extend for item in joiner_items.iter() { out.push(item?); } } first = false; - out.reserve(items.len()); + out.reserve(items.len() as usize); for item in items.iter() { out.push(item?); } @@ -256,7 +253,8 @@ pub fn builtin_lines(arr: ArrValue) -> Result { builtin_join( IndexableVal::Str("\n".into()), - ArrValue::extended(arr, ArrValue::new(vec![Val::string("")])), + ArrValue::extended(arr, ArrValue::new(vec![Val::string("")])) + .ok_or_else(|| error!("array is too large"))?, ) } @@ -380,7 +378,7 @@ let newArrLeft = arr.clone().slice(None, Some(at), None); let newArrRight = arr.slice(Some(at + 1), None, None); - Ok(ArrValue::extended(newArrLeft, newArrRight)) + Ok(ArrValue::extended(newArrLeft, newArrRight).ok_or_else(|| error!("array is too large"))?) } #[builtin] @@ -399,20 +397,22 @@ } #[builtin] -pub fn builtin_flatten_arrays(arrs: Vec) -> ArrValue { - pub fn flatten_inner(values: &[ArrValue]) -> ArrValue { +pub fn builtin_flatten_arrays(arrs: Vec) -> Result { + pub fn flatten_inner(values: &[ArrValue]) -> Result { if values.len() == 1 { - return values[0].clone(); + return Ok(values[0].clone()); } else if values.len() == 2 { - return ArrValue::extended(values[0].clone(), values[1].clone()); + return ArrValue::extended(values[0].clone(), values[1].clone()) + .ok_or_else(|| error!("array is too large")); } let (a, b) = values.split_at(values.len() / 2); - ArrValue::extended(flatten_inner(a), flatten_inner(b)) + ArrValue::extended(flatten_inner(a)?, flatten_inner(b)?) + .ok_or_else(|| error!("array is too large")) } if arrs.is_empty() { - return ArrValue::empty(); + return Ok(ArrValue::empty()); } else if arrs.len() == 1 { - return arrs.into_iter().next().expect("single"); + return Ok(arrs.into_iter().next().expect("single")); } flatten_inner(&arrs) } --- a/crates/jrsonnet-stdlib/src/lib.rs +++ b/crates/jrsonnet-stdlib/src/lib.rs @@ -12,15 +12,9 @@ pub use encoding::*; pub use hash::*; use jrsonnet_evaluator::{ - ContextBuilder, IStr, NumValue, ObjValue, ObjValueBuilder, Thunk, Val, - error::Result, - function::{CallLocation, FuncVal, builtin_id}, - tla::TlaArg, - trace::PathResolver, - typed::SerializeTypedObj as _, + IStr, InitialContextBuilder, NumValue, ObjValue, ObjValueBuilder, Source, Thunk, Val, error::Result, function::{CallLocation, FuncVal, builtin_id}, tla::TlaArg, trace::PathResolver, typed::SerializeTypedObj as _ }; use jrsonnet_gcmodule::{Acyclic, Cc, Trace}; -use jrsonnet_ir::Source; use jrsonnet_macros::{IntoUntyped, Typed}; pub use manifest::*; pub use math::*; @@ -544,7 +538,7 @@ } } impl jrsonnet_evaluator::ContextInitializer for ContextInitializer { - fn populate(&self, source: Source, builder: &mut ContextBuilder) { + fn populate(&self, source: Source, builder: &mut InitialContextBuilder) { let mut std = ObjValueBuilder::new(); std.with_super(self.stdlib_obj.clone()); std.field("thisFile").hide().value({ --- a/crates/jrsonnet-stdlib/src/misc.rs +++ b/crates/jrsonnet-stdlib/src/misc.rs @@ -1,22 +1,23 @@ use std::{cell::RefCell, collections::BTreeSet}; use jrsonnet_evaluator::{ - Either, IStr, ObjValue, ObjValueBuilder, ResultExt, Thunk, Val, bail, + bail, error::{ErrorKind::*, Result}, - function::{CallLocation, FuncVal, builtin}, + function::{builtin, CallLocation, FuncVal}, manifest::JsonFormat, typed::{Either2, Either4}, - val::{ArrValue, equals}, + val::{equals, ArrValue}, + Either, IStr, ObjValue, ObjValueBuilder, ResultExt, Thunk, Val, }; use jrsonnet_gcmodule::Cc; use crate::Settings; #[builtin] -pub fn builtin_length(x: Either![IStr, ArrValue, ObjValue, FuncVal]) -> usize { +pub fn builtin_length(x: Either![IStr, ArrValue, ObjValue, FuncVal]) -> u32 { use Either4::*; match x { - A(x) => x.chars().count(), + A(x) => x.chars().count() as u32, B(x) => x.len(), C(x) => x.len(), D(f) => f.params_len(), @@ -102,7 +103,7 @@ } else if b.len() == a.len() { return equals(&Val::Arr(a), &Val::Arr(b)); } - for (a, b) in a.iter().take(b.len()).zip(b.iter()) { + for (a, b) in a.iter().take(b.len() as usize).zip(b.iter()) { let a = a?; let b = b?; if !equals(&a, &b)? { @@ -127,7 +128,7 @@ return equals(&Val::Arr(a), &Val::Arr(b)); } let a_len = a.len(); - for (a, b) in a.iter().skip(a_len - b.len()).zip(b.iter()) { + for (a, b) in a.iter().skip((a_len - b.len()) as usize).zip(b.iter()) { let a = a?; let b = b?; if !equals(&a, &b)? { --- a/tests/tests/builtin.rs +++ b/tests/tests/builtin.rs @@ -1,11 +1,7 @@ mod common; use jrsonnet_evaluator::{ - ContextBuilder, ContextInitializer, FileImportResolver, Result, State, Thunk, Val, - function::{CallLocation, FuncVal, builtin, builtin::Builtin}, - parser::Source, - trace::PathResolver, - typed::FromUntyped, + ContextInitializer, FileImportResolver, InitialContextBuilder, Result, Source, State, Thunk, Val, function::{CallLocation, FuncVal, builtin, builtin::{Builtin}}, trace::PathResolver, typed::FromUntyped }; use jrsonnet_gcmodule::Trace; use jrsonnet_stdlib::ContextInitializer as StdContextInitializer; @@ -31,7 +27,7 @@ #[derive(Trace)] struct NativeAddContextInitializer; impl ContextInitializer for NativeAddContextInitializer { - fn populate(&self, _for_file: Source, builder: &mut ContextBuilder) { + fn populate(&self, _for_file: Source, builder: &mut InitialContextBuilder) { builder.bind("nativeAdd", Thunk::evaluated(Val::function(native_add {}))); } @@ -76,7 +72,7 @@ #[derive(Trace)] struct CurryAddContextInitializer; impl ContextInitializer for CurryAddContextInitializer { - fn populate(&self, _for_file: Source, builder: &mut ContextBuilder) { + fn populate(&self, _for_file: Source, builder: &mut InitialContextBuilder) { builder.bind("curryAdd", Thunk::evaluated(Val::function(curry_add {}))); } --- a/tests/tests/common.rs +++ b/tests/tests/common.rs @@ -1,8 +1,5 @@ use jrsonnet_evaluator::{ - ContextBuilder, ContextInitializer as ContextInitializerT, ObjValueBuilder, Result, Thunk, Val, - bail, - function::{FuncVal, builtin}, - parser::Source, + ContextBuilder, ContextInitializer as ContextInitializerT, InitialContextBuilder, ObjValueBuilder, Result, Thunk, Val, bail, function::{FuncVal, builtin}, Source }; use jrsonnet_gcmodule::Trace; @@ -68,7 +65,7 @@ #[allow(dead_code)] pub struct ContextInitializer; impl ContextInitializerT for ContextInitializer { - fn populate(&self, _for_file: Source, builder: &mut ContextBuilder) { + fn populate(&self, _for_file: Source, builder: &mut InitialContextBuilder) { let mut bobj = ObjValueBuilder::new(); bobj.method("assertThrow", assert_throw {}); bobj.method("paramNames", param_names {}); --- a/tests/tests/cpp_test_suite.rs +++ b/tests/tests/cpp_test_suite.rs @@ -9,9 +9,11 @@ gc::WithCapacityExt as _, manifest::JsonFormat, rustc_hash::FxHashMap, + stack::limit_stack_depth, tla::TlaArg, trace::{CompactFormat, PathResolver, TraceFormat}, }; +use jrsonnet_gcmodule::ObjectSpace; use jrsonnet_stdlib::ContextInitializer; mod common; use common::ContextInitializer as TestContextInitializer; @@ -179,6 +181,12 @@ continue; } + let _stack = if entry.path().file_stem().is_some_and(|e| e == "recursive_function" || e == "tailstrict"|| e == "tailstrict5") { + Some(limit_stack_depth(100_000)) + } else { + None + }; + if entry .path() .file_name() @@ -188,7 +196,7 @@ continue; } - println!("test: {}", entry.path().display()); + eprintln!("test: {}", entry.path().display()); let result = run(&entry.path(), &root); @@ -212,25 +220,10 @@ if let Some(golden_path) = read_file(&golden_override)? { golden = Some(golden_path); } - - // ir-parser has its own override layer - #[cfg(feature = "ir-parser")] - let ir_parser_override_path = { - let p = root_tests - .join(format!("{root_dir}_golden_override_ir_parser")) - .join(golden_path.file_name().expect("file has basename")); - if let Some(golden_path) = read_file(&p)? { - golden = Some(golden_path); - } - p - }; // Otherwise assume test should just not fail and return true. let golden = golden.unwrap_or_else(|| "true".to_owned()); - #[cfg(feature = "ir-parser")] - let update_golden_path = &ir_parser_override_path; - #[cfg(not(feature = "ir-parser"))] let update_golden_path = &golden_override; match (serde_json::from_str::(&result), serde_json::from_str::(&golden)) { @@ -270,8 +263,11 @@ } } } + println!("done!"); } } + jrsonnet_gcmodule::with_thread_object_space(ObjectSpace::leak); + Ok(()) } --- a/tests/tests/snapshots/golden__golden@issue172.jsonnet.snap +++ b/tests/tests/snapshots/golden__golden@issue172.jsonnet.snap @@ -3,7 +3,4 @@ expression: result input_file: tests/golden/issue172.jsonnet --- -local is not defined: b - issue172.jsonnet:1:45-47: local access - issue172.jsonnet:1:4-10: field access - elem <0> evaluation +static analysis errors: undefined local: b --- a/tests/tests/snapshots/golden__golden@issue23.jsonnet.snap +++ b/tests/tests/snapshots/golden__golden@issue23.jsonnet.snap @@ -4,4 +4,4 @@ input_file: tests/golden/issue23.jsonnet --- infinite recursion detected - issue23.jsonnet:1:1-8: import "issue23.jsonnet" + issue23.jsonnet:1:1-8: import --- a/tests/tests/snapshots/golden__golden@missing_binding.jsonnet.snap +++ b/tests/tests/snapshots/golden__golden@missing_binding.jsonnet.snap @@ -3,6 +3,5 @@ expression: result input_file: tests/golden/missing_binding.jsonnet --- -local is not defined: sta +static analysis errors: undefined local: sta There is a local with similar name present: std - missing_binding.jsonnet:1:1-5: local access -- gitstuff