difftreelog
feat simplify Thunk creation with closure syntax
in: master
9 files changed
crates/jrsonnet-evaluator/src/arr/spec.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/arr/spec.rs
+++ b/crates/jrsonnet-evaluator/src/arr/spec.rs
@@ -7,7 +7,7 @@
use super::ArrValue;
use crate::{
error::ErrorKind::InfiniteRecursionDetected, evaluate, function::FuncVal, typed::Typed,
- val::ThunkValue, Context, Error, ObjValue, Result, Thunk, Val,
+ Context, Error, ObjValue, Result, Thunk, Val,
};
pub trait ArrayLike: Any + Trace + Debug {
@@ -182,23 +182,6 @@
Ok(Some(new_value))
}
fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {
- #[derive(Trace)]
- struct ArrayElement {
- arr_thunk: ExprArray,
- index: usize,
- }
-
- impl ThunkValue for ArrayElement {
- type Output = Val;
-
- fn get(self: Box<Self>) -> Result<Self::Output> {
- self.arr_thunk
- .get(self.index)
- .transpose()
- .expect("index checked")
- }
- }
-
if index >= self.len() {
return None;
}
@@ -208,9 +191,9 @@
ArrayThunk::Waiting(_) | ArrayThunk::Pending => {}
};
- Some(Thunk::new(ArrayElement {
- arr_thunk: self.clone(),
- index,
+ let arr_thunk = self.clone();
+ Some(Thunk!(move || {
+ arr_thunk.get(index).transpose().expect("index checked")
}))
}
fn get_cheap(&self, _index: usize) -> Option<Val> {
@@ -492,23 +475,6 @@
Ok(Some(new_value))
}
fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {
- #[derive(Trace)]
- struct ArrayElement<const WITH_INDEX: bool> {
- arr_thunk: MappedArray<WITH_INDEX>,
- index: usize,
- }
-
- impl<const WITH_INDEX: bool> ThunkValue for ArrayElement<WITH_INDEX> {
- type Output = Val;
-
- fn get(self: Box<Self>) -> Result<Self::Output> {
- self.arr_thunk
- .get(self.index)
- .transpose()
- .expect("index checked")
- }
- }
-
if index >= self.len() {
return None;
}
@@ -518,9 +484,9 @@
ArrayThunk::Waiting(()) | ArrayThunk::Pending => {}
};
- Some(Thunk::new(ArrayElement {
- arr_thunk: self.clone(),
- index,
+ let arr_thunk = self.clone();
+ Some(Thunk!(move || {
+ arr_thunk.get(index).transpose().expect("index checked")
}))
}
crates/jrsonnet-evaluator/src/evaluate/destructure.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/evaluate/destructure.rs
+++ b/crates/jrsonnet-evaluator/src/evaluate/destructure.rs
@@ -1,13 +1,11 @@
-use jrsonnet_gcmodule::Trace;
use jrsonnet_interner::IStr;
-use jrsonnet_parser::{BindSpec, Destruct, LocExpr, ParamsDesc};
+use jrsonnet_parser::{BindSpec, Destruct};
use crate::{
bail,
error::{ErrorKind::*, Result},
evaluate, evaluate_method, evaluate_named,
gc::GcHashMap,
- val::ThunkValue,
Context, Pending, Thunk, Val,
};
@@ -31,65 +29,34 @@
#[cfg(feature = "exp-destruct")]
Destruct::Array { start, rest, end } => {
use jrsonnet_parser::DestructRest;
-
- use crate::arr::ArrValue;
-
- #[derive(Trace)]
- struct DataThunk {
- parent: Thunk<Val>,
- min_len: usize,
- has_rest: bool,
- }
- impl ThunkValue for DataThunk {
- type Output = ArrValue;
- fn get(self: Box<Self>) -> Result<Self::Output> {
- let v = self.parent.evaluate()?;
- let Val::Arr(arr) = v else {
- bail!("expected array");
- };
- if !self.has_rest {
- if arr.len() != self.min_len {
- bail!("expected {} elements, got {}", self.min_len, arr.len())
- }
- } else if arr.len() < self.min_len {
- bail!(
- "expected at least {} elements, but array was only {}",
- self.min_len,
- arr.len()
- )
+ 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())
}
- Ok(arr)
+ } else if arr.len() < min_len {
+ bail!(
+ "expected at least {} elements, but array was only {}",
+ min_len,
+ arr.len()
+ )
}
- }
-
- let full = Thunk::new(DataThunk {
- min_len: start.len() + end.len(),
- has_rest: rest.is_some(),
- parent,
+ Ok(arr)
});
{
- #[derive(Trace)]
- struct BaseThunk {
- full: Thunk<ArrValue>,
- index: usize,
- }
- impl ThunkValue for BaseThunk {
- type Output = Val;
-
- fn get(self: Box<Self>) -> Result<Self::Output> {
- let full = self.full.evaluate()?;
- Ok(full.get(self.index)?.expect("length is checked"))
- }
- }
for (i, d) in start.iter().enumerate() {
+ let full = full.clone();
destruct(
d,
- Thunk::new(BaseThunk {
- full: full.clone(),
- index: i,
- }),
+ Thunk!(move || Ok(full.evaluate()?.get(i)?.expect("length is checked"))),
fctx.clone(),
new_bindings,
)?;
@@ -98,32 +65,19 @@
match rest {
Some(DestructRest::Keep(v)) => {
- #[derive(Trace)]
- struct RestThunk {
- full: Thunk<ArrValue>,
- start: usize,
- end: usize,
- }
- impl ThunkValue for RestThunk {
- type Output = Val;
-
- fn get(self: Box<Self>) -> Result<Self::Output> {
- let full = self.full.evaluate()?;
- let to = full.len() - self.end;
+ 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(self.start as i32),
+ Some(start as i32),
Some(to as i32),
None,
)))
- }
- }
-
- destruct(
- &Destruct::Full(v.clone()),
- Thunk::new(RestThunk {
- full: full.clone(),
- start: start.len(),
- end: end.len(),
}),
fctx.clone(),
new_bindings,
@@ -133,29 +87,14 @@
}
{
- #[derive(Trace)]
- struct EndThunk {
- full: Thunk<ArrValue>,
- index: usize,
- end: usize,
- }
- impl ThunkValue for EndThunk {
- type Output = Val;
-
- fn get(self: Box<Self>) -> Result<Self::Output> {
- let full = self.full.evaluate()?;
- Ok(full
- .get(full.len() - self.end + self.index)?
- .expect("length is checked"))
- }
- }
for (i, d) in end.iter().enumerate() {
+ let full = full.clone();
+ let end = end.len();
destruct(
d,
- Thunk::new(EndThunk {
- full: full.clone(),
- index: i,
- end: end.len(),
+ Thunk!(move || {
+ let full = full.evaluate()?;
+ Ok(full.get(full.len() - end + i)?.expect("length is checked"))
}),
fctx.clone(),
new_bindings,
@@ -165,71 +104,46 @@
}
#[cfg(feature = "exp-destruct")]
Destruct::Object { fields, rest } => {
- use crate::obj::ObjValue;
-
- #[derive(Trace)]
- struct DataThunk {
- parent: Thunk<Val>,
- field_names: Vec<(IStr, bool)>,
- has_rest: bool,
- }
- impl ThunkValue for DataThunk {
- type Output = ObjValue;
-
- fn get(self: Box<Self>) -> Result<Self::Output> {
- let v = self.parent.evaluate()?;
- let Val::Obj(obj) = v else {
- bail!("expected object");
- };
- for (field, has_default) in &self.field_names {
- if !has_default && !obj.has_field_ex(field.clone(), true) {
- bail!("missing field: {field}");
- }
- }
- if !self.has_rest {
- let len = obj.len();
- if len > self.field_names.len() {
- bail!("too many fields, and rest not found");
- }
- }
- Ok(obj)
- }
- }
let field_names: Vec<_> = fields
.iter()
.map(|f| (f.0.clone(), f.2.is_some()))
.collect();
- let full = Thunk::new(DataThunk {
- parent,
- field_names,
- has_rest: rest.is_some(),
+ 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)
});
for (field, d, default) in fields {
- #[derive(Trace)]
- struct FieldThunk {
- full: Thunk<ObjValue>,
- field: IStr,
- default: Option<(Pending<Context>, LocExpr)>,
- }
- impl ThunkValue for FieldThunk {
- type Output = Val;
-
- fn get(self: Box<Self>) -> Result<Self::Output> {
- let full = self.full.evaluate()?;
- if let Some(field) = full.get(self.field)? {
+ 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) = self.default.as_ref().expect("shape is checked");
+ let (fctx, expr) = default.as_ref().expect("shape is checked");
Ok(evaluate(fctx.clone().unwrap(), expr)?)
}
- }
- }
- let value = Thunk::new(FieldThunk {
- full: full.clone(),
- field: field.clone(),
- default: default.clone().map(|e| (fctx.clone(), e)),
- });
+ })
+ };
+
if let Some(d) = d {
destruct(d, value, fctx.clone(), new_bindings)?;
} else {
@@ -253,26 +167,15 @@
) -> Result<()> {
match d {
BindSpec::Field { into, value } => {
- #[derive(Trace)]
- struct EvaluateThunkValue {
- name: Option<IStr>,
- fctx: Pending<Context>,
- expr: LocExpr,
- }
- impl ThunkValue for EvaluateThunkValue {
- type Output = Val;
- fn get(self: Box<Self>) -> Result<Self::Output> {
- self.name.map_or_else(
- || evaluate(self.fctx.unwrap(), &self.expr),
- |name| evaluate_named(self.fctx.unwrap(), &self.expr, name),
- )
- }
- }
- let data = Thunk::new(EvaluateThunkValue {
- name: into.name(),
- fctx: fctx.clone(),
- expr: value.clone(),
- });
+ let name = into.name();
+ let value = value.clone();
+ let data = {
+ let fctx = fctx.clone();
+ Thunk!(move || name.map_or_else(
+ || evaluate(fctx.unwrap(), &value),
+ |name| evaluate_named(fctx.unwrap(), &value, name),
+ ))
+ };
destruct(into, data, fctx, new_bindings)?;
}
BindSpec::Function {
@@ -280,37 +183,15 @@
params,
value,
} => {
- #[derive(Trace)]
- struct MethodThunk {
- fctx: Pending<Context>,
- name: IStr,
- params: ParamsDesc,
- value: LocExpr,
- }
- impl ThunkValue for MethodThunk {
- type Output = Val;
-
- fn get(self: Box<Self>) -> Result<Self::Output> {
- Ok(evaluate_method(
- self.fctx.unwrap(),
- self.name,
- self.params,
- self.value,
- ))
- }
- }
-
- let old = new_bindings.insert(
- name.clone(),
- Thunk::new(MethodThunk {
- fctx,
- name: name.clone(),
- params: params.clone(),
- value: value.clone(),
- }),
- );
+ let params = params.clone();
+ let name = name.clone();
+ let value = value.clone();
+ let old = new_bindings.insert(name.clone(), {
+ let name = name.clone();
+ Thunk!(move || Ok(evaluate_method(fctx.unwrap(), name, params, value)))
+ });
if old.is_some() {
- bail!(DuplicateLocalVar(name.clone()))
+ bail!(DuplicateLocalVar(name))
}
}
}
crates/jrsonnet-evaluator/src/evaluate/mod.rsdiffbeforeafterboth1use std::rc::Rc;23use jrsonnet_gcmodule::{Cc, Trace};4use jrsonnet_interner::IStr;5use jrsonnet_parser::{6 ArgsDesc, AssertStmt, BinaryOpType, BindSpec, CompSpec, Expr, FieldMember, FieldName,7 ForSpecData, IfSpecData, LiteralType, LocExpr, Member, ObjBody, ParamsDesc,8};9use jrsonnet_types::ValType;1011use self::destructure::destruct;12use crate::{13 arr::ArrValue,14 bail,15 destructure::evaluate_dest,16 error::{suggest_object_fields, ErrorKind::*},17 evaluate::operator::{evaluate_add_op, evaluate_binary_op_special, evaluate_unary_op},18 function::{CallLocation, FuncDesc, FuncVal},19 in_frame,20 typed::Typed,21 val::{CachedUnbound, IndexableVal, NumValue, StrValue, Thunk, ThunkValue},22 Context, Error, GcHashMap, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result,23 ResultExt, Unbound, Val,24};25pub mod destructure;26pub mod operator;2728// This is the amount of bytes that need to be left on the stack before increasing the size.29// It must be at least as large as the stack required by any code that does not call30// `ensure_sufficient_stack`.31const RED_ZONE: usize = 100 * 1024; // 100k3233// Only the first stack that is pushed, grows exponentially (2^n * STACK_PER_RECURSION) from then34// on. This flag has performance relevant characteristics. Don't set it too high.35const STACK_PER_RECURSION: usize = 1024 * 1024; // 1MB3637/// Grows the stack on demand to prevent stack overflow. Call this in strategic locations38/// to "break up" recursive calls. E.g. almost any call to `visit_expr` or equivalent can benefit39/// from this.40///41/// Should not be sprinkled around carelessly, as it causes a little bit of overhead.42#[inline]43pub fn ensure_sufficient_stack<R>(f: impl FnOnce() -> R) -> R {44 stacker::maybe_grow(RED_ZONE, STACK_PER_RECURSION, f)45}4647pub fn evaluate_trivial(expr: &LocExpr) -> Option<Val> {48 fn is_trivial(expr: &LocExpr) -> bool {49 match expr.expr() {50 Expr::Str(_)51 | Expr::Num(_)52 | Expr::Literal(LiteralType::False | LiteralType::True | LiteralType::Null) => true,53 Expr::Arr(a) => a.iter().all(is_trivial),54 Expr::Parened(e) => is_trivial(e),55 _ => false,56 }57 }58 Some(match expr.expr() {59 Expr::Str(s) => Val::string(s.clone()),60 Expr::Num(n) => {61 Val::Num(NumValue::new(*n).expect("parser will not allow non-finite values"))62 }63 Expr::Literal(LiteralType::False) => Val::Bool(false),64 Expr::Literal(LiteralType::True) => Val::Bool(true),65 Expr::Literal(LiteralType::Null) => Val::Null,66 Expr::Arr(n) => {67 if n.iter().any(|e| !is_trivial(e)) {68 return None;69 }70 Val::Arr(ArrValue::eager(71 n.iter()72 .map(evaluate_trivial)73 .map(|e| e.expect("checked trivial"))74 .collect(),75 ))76 }77 Expr::Parened(e) => evaluate_trivial(e)?,78 _ => return None,79 })80}8182pub fn evaluate_method(ctx: Context, name: IStr, params: ParamsDesc, body: LocExpr) -> Val {83 Val::Func(FuncVal::Normal(Cc::new(FuncDesc {84 name,85 ctx,86 params,87 body,88 })))89}9091pub fn evaluate_field_name(ctx: Context, field_name: &FieldName) -> Result<Option<IStr>> {92 Ok(match field_name {93 FieldName::Fixed(n) => Some(n.clone()),94 FieldName::Dyn(expr) => in_frame(95 CallLocation::new(&expr.span()),96 || "evaluating field name".to_string(),97 || {98 let value = evaluate(ctx, expr)?;99 if matches!(value, Val::Null) {100 Ok(None)101 } else {102 Ok(Some(IStr::from_untyped(value)?))103 }104 },105 )?,106 })107}108109pub fn evaluate_comp(110 ctx: Context,111 specs: &[CompSpec],112 callback: &mut impl FnMut(Context) -> Result<()>,113) -> Result<()> {114 match specs.first() {115 None => callback(ctx)?,116 Some(CompSpec::IfSpec(IfSpecData(cond))) => {117 if bool::from_untyped(evaluate(ctx.clone(), cond)?)? {118 evaluate_comp(ctx, &specs[1..], callback)?;119 }120 }121 Some(CompSpec::ForSpec(ForSpecData(var, expr))) => match evaluate(ctx.clone(), expr)? {122 Val::Arr(list) => {123 for item in list.iter_lazy() {124 let fctx = Pending::new();125 let mut new_bindings = GcHashMap::with_capacity(var.capacity_hint());126 destruct(var, item, fctx.clone(), &mut new_bindings)?;127 let ctx = ctx128 .clone()129 .extend(new_bindings, None, None, None)130 .into_future(fctx);131132 evaluate_comp(ctx, &specs[1..], callback)?;133 }134 }135 #[cfg(feature = "exp-object-iteration")]136 Val::Obj(obj) => {137 for field in obj.fields(138 // TODO: Should there be ability to preserve iteration order?139 #[cfg(feature = "exp-preserve-order")]140 false,141 ) {142 #[derive(Trace)]143 struct ObjectFieldThunk {144 obj: ObjValue,145 field: IStr,146 }147 impl ThunkValue for ObjectFieldThunk {148 type Output = Val;149150 fn get(self: Box<Self>) -> Result<Self::Output> {151 self.obj.get(self.field).transpose().expect(152 "field exists, as field name was obtained from object.fields()",153 )154 }155 }156157 let fctx = Pending::new();158 let mut new_bindings = GcHashMap::with_capacity(var.capacity_hint());159 let value = Thunk::evaluated(Val::Arr(ArrValue::lazy(vec![160 Thunk::evaluated(Val::string(field.clone())),161 Thunk::new(ObjectFieldThunk {162 field: field.clone(),163 obj: obj.clone(),164 }),165 ])));166 destruct(var, value, fctx.clone(), &mut new_bindings)?;167 let ctx = ctx168 .clone()169 .extend(new_bindings, None, None, None)170 .into_future(fctx);171172 evaluate_comp(ctx, &specs[1..], callback)?;173 }174 }175 _ => bail!(InComprehensionCanOnlyIterateOverArray),176 },177 }178 Ok(())179}180181trait CloneableUnbound<T>: Unbound<Bound = T> + Clone {}182impl<V, T> CloneableUnbound<T> for V where V: Unbound<Bound = T> + Clone {}183184fn evaluate_object_locals(185 fctx: Pending<Context>,186 locals: Rc<Vec<BindSpec>>,187) -> impl CloneableUnbound<Context> {188 #[derive(Trace, Clone)]189 struct UnboundLocals {190 fctx: Pending<Context>,191 locals: Rc<Vec<BindSpec>>,192 }193 impl Unbound for UnboundLocals {194 type Bound = Context;195196 fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Context> {197 let fctx = Context::new_future();198 let mut new_bindings =199 GcHashMap::with_capacity(self.locals.iter().map(BindSpec::capacity_hint).sum());200 for b in self.locals.iter() {201 evaluate_dest(b, fctx.clone(), &mut new_bindings)?;202 }203204 let ctx = self.fctx.unwrap();205 let new_dollar = ctx.dollar().cloned().or_else(|| this.clone());206207 let ctx = ctx208 .extend(new_bindings, new_dollar, sup, this)209 .into_future(fctx);210211 Ok(ctx)212 }213 }214215 UnboundLocals { fctx, locals }216}217218pub fn evaluate_field_member<B: Unbound<Bound = Context> + Clone>(219 builder: &mut ObjValueBuilder,220 ctx: Context,221 uctx: B,222 field: &FieldMember,223) -> Result<()> {224 let name = evaluate_field_name(ctx, &field.name)?;225 let Some(name) = name else {226 return Ok(());227 };228229 match field {230 FieldMember {231 plus,232 params: None,233 visibility,234 value,235 ..236 } => {237 #[derive(Trace)]238 struct UnboundValue<B: Trace> {239 uctx: B,240 value: LocExpr,241 name: IStr,242 }243 impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {244 type Bound = Val;245 fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Val> {246 evaluate_named(self.uctx.bind(sup, this)?, &self.value, self.name.clone())247 }248 }249250 builder251 .field(name.clone())252 .with_add(*plus)253 .with_visibility(*visibility)254 .with_location(value.span())255 .bindable(UnboundValue {256 uctx,257 value: value.clone(),258 name,259 })?;260 }261 FieldMember {262 params: Some(params),263 visibility,264 value,265 ..266 } => {267 #[derive(Trace)]268 struct UnboundMethod<B: Trace> {269 uctx: B,270 value: LocExpr,271 params: ParamsDesc,272 name: IStr,273 }274 impl<B: Unbound<Bound = Context>> Unbound for UnboundMethod<B> {275 type Bound = Val;276 fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Val> {277 Ok(evaluate_method(278 self.uctx.bind(sup, this)?,279 self.name.clone(),280 self.params.clone(),281 self.value.clone(),282 ))283 }284 }285286 builder287 .field(name.clone())288 .with_visibility(*visibility)289 .with_location(value.span())290 .bindable(UnboundMethod {291 uctx,292 value: value.clone(),293 params: params.clone(),294 name,295 })?;296 }297 }298 Ok(())299}300301#[allow(clippy::too_many_lines)]302pub fn evaluate_member_list_object(ctx: Context, members: &[Member]) -> Result<ObjValue> {303 let mut builder = ObjValueBuilder::new();304 let locals = Rc::new(305 members306 .iter()307 .filter_map(|m| match m {308 Member::BindStmt(bind) => Some(bind.clone()),309 _ => None,310 })311 .collect::<Vec<_>>(),312 );313314 let fctx = Context::new_future();315316 // We have single context for all fields, so we can cache binds317 let uctx = CachedUnbound::new(evaluate_object_locals(fctx.clone(), locals));318319 for member in members {320 match member {321 Member::Field(field) => {322 evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;323 }324 Member::AssertStmt(stmt) => {325 #[derive(Trace)]326 struct ObjectAssert<B: Trace> {327 uctx: B,328 assert: AssertStmt,329 }330 impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {331 fn run(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<()> {332 let ctx = self.uctx.bind(sup, this)?;333 evaluate_assert(ctx, &self.assert)334 }335 }336 builder.assert(ObjectAssert {337 uctx: uctx.clone(),338 assert: stmt.clone(),339 });340 }341 Member::BindStmt(_) => {342 // Already handled343 }344 }345 }346 let this = builder.build();347 fctx.fill(ctx.extend(GcHashMap::new(), None, None, Some(this.clone())));348 Ok(this)349}350351pub fn evaluate_object(ctx: Context, object: &ObjBody) -> Result<ObjValue> {352 Ok(match object {353 ObjBody::MemberList(members) => evaluate_member_list_object(ctx, members)?,354 ObjBody::ObjComp(obj) => {355 let mut builder = ObjValueBuilder::new();356 let locals = Rc::new(357 obj.pre_locals358 .iter()359 .chain(obj.post_locals.iter())360 .cloned()361 .collect::<Vec<_>>(),362 );363 let mut ctxs = vec![];364 evaluate_comp(ctx, &obj.compspecs, &mut |ctx| {365 let fctx = Context::new_future();366 ctxs.push((ctx.clone(), fctx.clone()));367 let uctx = evaluate_object_locals(fctx, locals.clone());368369 evaluate_field_member(&mut builder, ctx, uctx, &obj.field)370 })?;371372 let this = builder.build();373 for (ctx, fctx) in ctxs {374 let _ctx = ctx375 .extend(GcHashMap::new(), None, None, Some(this.clone()))376 .into_future(fctx);377 }378 this379 }380 })381}382383pub fn evaluate_apply(384 ctx: Context,385 value: &LocExpr,386 args: &ArgsDesc,387 loc: CallLocation<'_>,388 tailstrict: bool,389) -> Result<Val> {390 let value = evaluate(ctx.clone(), value)?;391 Ok(match value {392 Val::Func(f) => {393 let body = || f.evaluate(ctx, loc, args, tailstrict);394 if tailstrict {395 body()?396 } else {397 in_frame(loc, || format!("function <{}> call", f.name()), body)?398 }399 }400 v => bail!(OnlyFunctionsCanBeCalledGot(v.value_type())),401 })402}403404pub fn evaluate_assert(ctx: Context, assertion: &AssertStmt) -> Result<()> {405 let value = &assertion.0;406 let msg = &assertion.1;407 let assertion_result = in_frame(408 CallLocation::new(&value.span()),409 || "assertion condition".to_owned(),410 || bool::from_untyped(evaluate(ctx.clone(), value)?),411 )?;412 if !assertion_result {413 in_frame(414 CallLocation::new(&value.span()),415 || "assertion failure".to_owned(),416 || {417 if let Some(msg) = msg {418 bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));419 }420 bail!(AssertionFailed(Val::Null.to_string()?));421 },422 )?;423 }424 Ok(())425}426427pub fn evaluate_named(ctx: Context, expr: &LocExpr, name: IStr) -> Result<Val> {428 use Expr::*;429 Ok(match expr.expr() {430 Function(params, body) => evaluate_method(ctx, name, params.clone(), body.clone()),431 _ => evaluate(ctx, expr)?,432 })433}434435#[allow(clippy::too_many_lines)]436pub fn evaluate(ctx: Context, expr: &LocExpr) -> Result<Val> {437 use Expr::*;438439 if let Some(trivial) = evaluate_trivial(expr) {440 return Ok(trivial);441 }442 let loc = expr.span();443 Ok(match expr.expr() {444 Literal(LiteralType::This) => {445 Val::Obj(ctx.this().ok_or(CantUseSelfOutsideOfObject)?.clone())446 }447 Literal(LiteralType::Super) => Val::Obj(448 ctx.super_obj().ok_or(NoSuperFound)?.with_this(449 ctx.this()450 .expect("if super exists - then this should too")451 .clone(),452 ),453 ),454 Literal(LiteralType::Dollar) => {455 Val::Obj(ctx.dollar().ok_or(NoTopLevelObjectFound)?.clone())456 }457 Literal(LiteralType::True) => Val::Bool(true),458 Literal(LiteralType::False) => Val::Bool(false),459 Literal(LiteralType::Null) => Val::Null,460 Parened(e) => evaluate(ctx, e)?,461 Str(v) => Val::string(v.clone()),462 Num(v) => Val::try_num(*v)?,463 // I have tried to remove special behavior from super by implementing standalone-super464 // expresion, but looks like this case still needs special treatment.465 //466 // Note that other jsonnet implementations will fail on `if value in (super)` expression,467 // because the standalone super literal is not supported, that is because in other468 // implementations `in super` treated differently from in `smth_else`.469 BinaryOp(field, BinaryOpType::In, e)470 if matches!(e.expr(), Expr::Literal(LiteralType::Super)) =>471 {472 let Some(super_obj) = ctx.super_obj() else {473 return Ok(Val::Bool(false));474 };475 let field = evaluate(ctx.clone(), field)?;476 Val::Bool(super_obj.has_field_ex(field.to_string()?, true))477 }478 BinaryOp(v1, o, v2) => evaluate_binary_op_special(ctx, v1, *o, v2)?,479 UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?,480 Var(name) => in_frame(481 CallLocation::new(&loc),482 || format!("variable <{name}> access"),483 || ctx.binding(name.clone())?.evaluate(),484 )?,485 Index { indexable, parts } => ensure_sufficient_stack(|| {486 let mut parts = parts.iter();487 let mut indexable = if matches!(indexable.expr(), Expr::Literal(LiteralType::Super)) {488 let part = parts.next().expect("at least part should exist");489 let Some(super_obj) = ctx.super_obj() else {490 #[cfg(feature = "exp-null-coaelse")]491 if part.null_coaelse {492 return Ok(Val::Null);493 }494 bail!(NoSuperFound)495 };496 let name = evaluate(ctx.clone(), &part.value)?;497498 let Val::Str(name) = name else {499 bail!(ValueIndexMustBeTypeGot(500 ValType::Obj,501 ValType::Str,502 name.value_type(),503 ))504 };505506 let this = ctx507 .this()508 .expect("no this found, while super present, should not happen");509 let name = name.into_flat();510 match super_obj511 .get_for(name.clone(), this.clone())512 .with_description_src(&part.value, || format!("field <{name}> access"))?513 {514 Some(v) => v,515 #[cfg(feature = "exp-null-coaelse")]516 None if part.null_coaelse => return Ok(Val::Null),517 None => {518 let suggestions = suggest_object_fields(super_obj, name.clone());519520 bail!(NoSuchField(name, suggestions))521 }522 }523 } else {524 evaluate(ctx.clone(), indexable)?525 };526527 for part in parts {528 indexable = match (indexable, evaluate(ctx.clone(), &part.value)?) {529 (Val::Obj(v), Val::Str(key)) => match v530 .get(key.clone().into_flat())531 .with_description_src(&part.value, || format!("field <{key}> access"))?532 {533 Some(v) => v,534 #[cfg(feature = "exp-null-coaelse")]535 None if part.null_coaelse => return Ok(Val::Null),536 None => {537 let suggestions = suggest_object_fields(&v, key.clone().into_flat());538539 return Err(Error::from(NoSuchField(540 key.clone().into_flat(),541 suggestions,542 )))543 .with_description_src(&part.value, || format!("field <{key}> access"));544 }545 },546 (Val::Obj(_), n) => bail!(ValueIndexMustBeTypeGot(547 ValType::Obj,548 ValType::Str,549 n.value_type(),550 )),551 (Val::Arr(v), Val::Num(n)) => {552 let n = n.get();553 if n.fract() > f64::EPSILON {554 bail!(FractionalIndex)555 }556 if n < 0.0 {557 bail!(ArrayBoundsError(n as isize, v.len()));558 }559 v.get(n as usize)?560 .ok_or_else(|| ArrayBoundsError(n as isize, v.len()))?561 }562 (Val::Arr(_), Val::Str(n)) => {563 bail!(AttemptedIndexAnArrayWithString(n.into_flat()))564 }565 (Val::Arr(_), n) => bail!(ValueIndexMustBeTypeGot(566 ValType::Arr,567 ValType::Num,568 n.value_type(),569 )),570571 (Val::Str(s), Val::Num(n)) => Val::Str({572 let v: IStr = s573 .clone()574 .into_flat()575 .chars()576 .skip(n.get() as usize)577 .take(1)578 .collect::<String>()579 .into();580 if v.is_empty() {581 let size = s.into_flat().chars().count();582 bail!(StringBoundsError(n.get() as usize, size))583 }584 StrValue::Flat(v)585 }),586 (Val::Str(_), n) => bail!(ValueIndexMustBeTypeGot(587 ValType::Str,588 ValType::Num,589 n.value_type(),590 )),591 #[cfg(feature = "exp-null-coaelse")]592 (Val::Null, _) if part.null_coaelse => return Ok(Val::Null),593 (v, _) => bail!(CantIndexInto(v.value_type())),594 };595 }596 Ok(indexable)597 })?,598 LocalExpr(bindings, returned) => {599 let mut new_bindings: GcHashMap<IStr, Thunk<Val>> =600 GcHashMap::with_capacity(bindings.iter().map(BindSpec::capacity_hint).sum());601 let fctx = Context::new_future();602 for b in bindings {603 evaluate_dest(b, fctx.clone(), &mut new_bindings)?;604 }605 let ctx = ctx.extend(new_bindings, None, None, None).into_future(fctx);606 evaluate(ctx, &returned.clone())?607 }608 Arr(items) => {609 if items.is_empty() {610 Val::Arr(ArrValue::empty())611 } else if items.len() == 1 {612 #[derive(Trace)]613 struct ArrayElement {614 ctx: Context,615 item: LocExpr,616 }617 impl ThunkValue for ArrayElement {618 type Output = Val;619 fn get(self: Box<Self>) -> Result<Val> {620 evaluate(self.ctx, &self.item)621 }622 }623 Val::Arr(ArrValue::lazy(vec![Thunk::new(ArrayElement {624 ctx,625 item: items[0].clone(),626 })]))627 } else {628 Val::Arr(ArrValue::expr(ctx, items.iter().cloned()))629 }630 }631 ArrComp(expr, comp_specs) => {632 let mut out = Vec::new();633 evaluate_comp(ctx, comp_specs, &mut |ctx| {634 #[derive(Trace)]635 struct EvaluateThunk {636 ctx: Context,637 expr: LocExpr,638 }639 impl ThunkValue for EvaluateThunk {640 type Output = Val;641 fn get(self: Box<Self>) -> Result<Val> {642 evaluate(self.ctx, &self.expr)643 }644 }645 out.push(Thunk::new(EvaluateThunk {646 ctx,647 expr: expr.clone(),648 }));649 Ok(())650 })?;651 Val::Arr(ArrValue::lazy(out))652 }653 Obj(body) => Val::Obj(evaluate_object(ctx, body)?),654 ObjExtend(a, b) => evaluate_add_op(655 &evaluate(ctx.clone(), a)?,656 &Val::Obj(evaluate_object(ctx, b)?),657 )?,658 Apply(value, args, tailstrict) => ensure_sufficient_stack(|| {659 evaluate_apply(ctx, value, args, CallLocation::new(&loc), *tailstrict)660 })?,661 Function(params, body) => {662 evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone())663 }664 AssertExpr(assert, returned) => {665 evaluate_assert(ctx.clone(), assert)?;666 evaluate(ctx, returned)?667 }668 ErrorStmt(e) => in_frame(669 CallLocation::new(&loc),670 || "error statement".to_owned(),671 || bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),672 )?,673 IfElse {674 cond,675 cond_then,676 cond_else,677 } => {678 if in_frame(679 CallLocation::new(&loc),680 || "if condition".to_owned(),681 || bool::from_untyped(evaluate(ctx.clone(), &cond.0)?),682 )? {683 evaluate(ctx, cond_then)?684 } else {685 match cond_else {686 Some(v) => evaluate(ctx, v)?,687 None => Val::Null,688 }689 }690 }691 Slice(value, desc) => {692 fn parse_idx<T: Typed>(693 loc: CallLocation<'_>,694 ctx: &Context,695 expr: Option<&LocExpr>,696 desc: &'static str,697 ) -> Result<Option<T>> {698 if let Some(value) = expr {699 Ok(Some(in_frame(700 loc,701 || format!("slice {desc}"),702 || T::from_untyped(evaluate(ctx.clone(), value)?),703 )?))704 } else {705 Ok(None)706 }707 }708709 let indexable = evaluate(ctx.clone(), value)?;710 let loc = CallLocation::new(&loc);711712 let start = parse_idx(loc, &ctx, desc.start.as_ref(), "start")?;713 let end = parse_idx(loc, &ctx, desc.end.as_ref(), "end")?;714 let step = parse_idx(loc, &ctx, desc.step.as_ref(), "step")?;715716 IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)?717 }718 i @ (Import(path) | ImportStr(path) | ImportBin(path)) => {719 let Expr::Str(path) = &path.expr() else {720 bail!("computed imports are not supported")721 };722 let tmp = loc.clone().0;723 let s = ctx.state();724 let resolved_path = s.resolve_from(tmp.source_path(), path as &str)?;725 match i {726 Import(_) => in_frame(727 CallLocation::new(&loc),728 || format!("import {:?}", path.clone()),729 || s.import_resolved(resolved_path),730 )?,731 ImportStr(_) => Val::string(s.import_resolved_str(resolved_path)?),732 ImportBin(_) => Val::Arr(ArrValue::bytes(s.import_resolved_bin(resolved_path)?)),733 _ => unreachable!(),734 }735 }736 })737}1use std::rc::Rc;23use jrsonnet_gcmodule::{Cc, Trace};4use jrsonnet_interner::IStr;5use jrsonnet_parser::{6 ArgsDesc, AssertStmt, BinaryOpType, BindSpec, CompSpec, Expr, FieldMember, FieldName,7 ForSpecData, IfSpecData, LiteralType, LocExpr, Member, ObjBody, ParamsDesc,8};9use jrsonnet_types::ValType;1011use self::destructure::destruct;12use crate::{13 arr::ArrValue,14 bail,15 destructure::evaluate_dest,16 error::{suggest_object_fields, ErrorKind::*},17 evaluate::operator::{evaluate_add_op, evaluate_binary_op_special, evaluate_unary_op},18 function::{CallLocation, FuncDesc, FuncVal},19 in_frame,20 typed::Typed,21 val::{CachedUnbound, IndexableVal, NumValue, StrValue, Thunk},22 Context, Error, GcHashMap, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result,23 ResultExt, Unbound, Val,24};25pub mod destructure;26pub mod operator;2728// This is the amount of bytes that need to be left on the stack before increasing the size.29// It must be at least as large as the stack required by any code that does not call30// `ensure_sufficient_stack`.31const RED_ZONE: usize = 100 * 1024; // 100k3233// Only the first stack that is pushed, grows exponentially (2^n * STACK_PER_RECURSION) from then34// on. This flag has performance relevant characteristics. Don't set it too high.35const STACK_PER_RECURSION: usize = 1024 * 1024; // 1MB3637/// Grows the stack on demand to prevent stack overflow. Call this in strategic locations38/// to "break up" recursive calls. E.g. almost any call to `visit_expr` or equivalent can benefit39/// from this.40///41/// Should not be sprinkled around carelessly, as it causes a little bit of overhead.42#[inline]43pub fn ensure_sufficient_stack<R>(f: impl FnOnce() -> R) -> R {44 stacker::maybe_grow(RED_ZONE, STACK_PER_RECURSION, f)45}4647pub fn evaluate_trivial(expr: &LocExpr) -> Option<Val> {48 fn is_trivial(expr: &LocExpr) -> bool {49 match expr.expr() {50 Expr::Str(_)51 | Expr::Num(_)52 | Expr::Literal(LiteralType::False | LiteralType::True | LiteralType::Null) => true,53 Expr::Arr(a) => a.iter().all(is_trivial),54 Expr::Parened(e) => is_trivial(e),55 _ => false,56 }57 }58 Some(match expr.expr() {59 Expr::Str(s) => Val::string(s.clone()),60 Expr::Num(n) => {61 Val::Num(NumValue::new(*n).expect("parser will not allow non-finite values"))62 }63 Expr::Literal(LiteralType::False) => Val::Bool(false),64 Expr::Literal(LiteralType::True) => Val::Bool(true),65 Expr::Literal(LiteralType::Null) => Val::Null,66 Expr::Arr(n) => {67 if n.iter().any(|e| !is_trivial(e)) {68 return None;69 }70 Val::Arr(ArrValue::eager(71 n.iter()72 .map(evaluate_trivial)73 .map(|e| e.expect("checked trivial"))74 .collect(),75 ))76 }77 Expr::Parened(e) => evaluate_trivial(e)?,78 _ => return None,79 })80}8182pub fn evaluate_method(ctx: Context, name: IStr, params: ParamsDesc, body: LocExpr) -> Val {83 Val::Func(FuncVal::Normal(Cc::new(FuncDesc {84 name,85 ctx,86 params,87 body,88 })))89}9091pub fn evaluate_field_name(ctx: Context, field_name: &FieldName) -> Result<Option<IStr>> {92 Ok(match field_name {93 FieldName::Fixed(n) => Some(n.clone()),94 FieldName::Dyn(expr) => in_frame(95 CallLocation::new(&expr.span()),96 || "evaluating field name".to_string(),97 || {98 let value = evaluate(ctx, expr)?;99 if matches!(value, Val::Null) {100 Ok(None)101 } else {102 Ok(Some(IStr::from_untyped(value)?))103 }104 },105 )?,106 })107}108109pub fn evaluate_comp(110 ctx: Context,111 specs: &[CompSpec],112 callback: &mut impl FnMut(Context) -> Result<()>,113) -> Result<()> {114 match specs.first() {115 None => callback(ctx)?,116 Some(CompSpec::IfSpec(IfSpecData(cond))) => {117 if bool::from_untyped(evaluate(ctx.clone(), cond)?)? {118 evaluate_comp(ctx, &specs[1..], callback)?;119 }120 }121 Some(CompSpec::ForSpec(ForSpecData(var, expr))) => match evaluate(ctx.clone(), expr)? {122 Val::Arr(list) => {123 for item in list.iter_lazy() {124 let fctx = Pending::new();125 let mut new_bindings = GcHashMap::with_capacity(var.capacity_hint());126 destruct(var, item, fctx.clone(), &mut new_bindings)?;127 let ctx = ctx128 .clone()129 .extend(new_bindings, None, None, None)130 .into_future(fctx);131132 evaluate_comp(ctx, &specs[1..], callback)?;133 }134 }135 #[cfg(feature = "exp-object-iteration")]136 Val::Obj(obj) => {137 for field in obj.fields(138 // TODO: Should there be ability to preserve iteration order?139 #[cfg(feature = "exp-preserve-order")]140 false,141 ) {142 let fctx = Pending::new();143 let mut new_bindings = GcHashMap::with_capacity(var.capacity_hint());144 let obj = obj.clone();145 let value = Thunk::evaluated(Val::Arr(ArrValue::lazy(vec![146 Thunk::evaluated(Val::string(field.clone())),147 Thunk!(move || obj.get(field).transpose().expect(148 "field exists, as field name was obtained from object.fields()",149 )),150 ])));151 destruct(var, value, fctx.clone(), &mut new_bindings)?;152 let ctx = ctx153 .clone()154 .extend(new_bindings, None, None, None)155 .into_future(fctx);156157 evaluate_comp(ctx, &specs[1..], callback)?;158 }159 }160 _ => bail!(InComprehensionCanOnlyIterateOverArray),161 },162 }163 Ok(())164}165166trait CloneableUnbound<T>: Unbound<Bound = T> + Clone {}167impl<V, T> CloneableUnbound<T> for V where V: Unbound<Bound = T> + Clone {}168169fn evaluate_object_locals(170 fctx: Pending<Context>,171 locals: Rc<Vec<BindSpec>>,172) -> impl CloneableUnbound<Context> {173 #[derive(Trace, Clone)]174 struct UnboundLocals {175 fctx: Pending<Context>,176 locals: Rc<Vec<BindSpec>>,177 }178 impl Unbound for UnboundLocals {179 type Bound = Context;180181 fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Context> {182 let fctx = Context::new_future();183 let mut new_bindings =184 GcHashMap::with_capacity(self.locals.iter().map(BindSpec::capacity_hint).sum());185 for b in self.locals.iter() {186 evaluate_dest(b, fctx.clone(), &mut new_bindings)?;187 }188189 let ctx = self.fctx.unwrap();190 let new_dollar = ctx.dollar().cloned().or_else(|| this.clone());191192 let ctx = ctx193 .extend(new_bindings, new_dollar, sup, this)194 .into_future(fctx);195196 Ok(ctx)197 }198 }199200 UnboundLocals { fctx, locals }201}202203pub fn evaluate_field_member<B: Unbound<Bound = Context> + Clone>(204 builder: &mut ObjValueBuilder,205 ctx: Context,206 uctx: B,207 field: &FieldMember,208) -> Result<()> {209 let name = evaluate_field_name(ctx, &field.name)?;210 let Some(name) = name else {211 return Ok(());212 };213214 match field {215 FieldMember {216 plus,217 params: None,218 visibility,219 value,220 ..221 } => {222 #[derive(Trace)]223 struct UnboundValue<B: Trace> {224 uctx: B,225 value: LocExpr,226 name: IStr,227 }228 impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {229 type Bound = Val;230 fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Val> {231 evaluate_named(self.uctx.bind(sup, this)?, &self.value, self.name.clone())232 }233 }234235 builder236 .field(name.clone())237 .with_add(*plus)238 .with_visibility(*visibility)239 .with_location(value.span())240 .bindable(UnboundValue {241 uctx,242 value: value.clone(),243 name,244 })?;245 }246 FieldMember {247 params: Some(params),248 visibility,249 value,250 ..251 } => {252 #[derive(Trace)]253 struct UnboundMethod<B: Trace> {254 uctx: B,255 value: LocExpr,256 params: ParamsDesc,257 name: IStr,258 }259 impl<B: Unbound<Bound = Context>> Unbound for UnboundMethod<B> {260 type Bound = Val;261 fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Val> {262 Ok(evaluate_method(263 self.uctx.bind(sup, this)?,264 self.name.clone(),265 self.params.clone(),266 self.value.clone(),267 ))268 }269 }270271 builder272 .field(name.clone())273 .with_visibility(*visibility)274 .with_location(value.span())275 .bindable(UnboundMethod {276 uctx,277 value: value.clone(),278 params: params.clone(),279 name,280 })?;281 }282 }283 Ok(())284}285286#[allow(clippy::too_many_lines)]287pub fn evaluate_member_list_object(ctx: Context, members: &[Member]) -> Result<ObjValue> {288 let mut builder = ObjValueBuilder::new();289 let locals = Rc::new(290 members291 .iter()292 .filter_map(|m| match m {293 Member::BindStmt(bind) => Some(bind.clone()),294 _ => None,295 })296 .collect::<Vec<_>>(),297 );298299 let fctx = Context::new_future();300301 // We have single context for all fields, so we can cache binds302 let uctx = CachedUnbound::new(evaluate_object_locals(fctx.clone(), locals));303304 for member in members {305 match member {306 Member::Field(field) => {307 evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;308 }309 Member::AssertStmt(stmt) => {310 #[derive(Trace)]311 struct ObjectAssert<B: Trace> {312 uctx: B,313 assert: AssertStmt,314 }315 impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {316 fn run(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<()> {317 let ctx = self.uctx.bind(sup, this)?;318 evaluate_assert(ctx, &self.assert)319 }320 }321 builder.assert(ObjectAssert {322 uctx: uctx.clone(),323 assert: stmt.clone(),324 });325 }326 Member::BindStmt(_) => {327 // Already handled328 }329 }330 }331 let this = builder.build();332 fctx.fill(ctx.extend(GcHashMap::new(), None, None, Some(this.clone())));333 Ok(this)334}335336pub fn evaluate_object(ctx: Context, object: &ObjBody) -> Result<ObjValue> {337 Ok(match object {338 ObjBody::MemberList(members) => evaluate_member_list_object(ctx, members)?,339 ObjBody::ObjComp(obj) => {340 let mut builder = ObjValueBuilder::new();341 let locals = Rc::new(342 obj.pre_locals343 .iter()344 .chain(obj.post_locals.iter())345 .cloned()346 .collect::<Vec<_>>(),347 );348 let mut ctxs = vec![];349 evaluate_comp(ctx, &obj.compspecs, &mut |ctx| {350 let fctx = Context::new_future();351 ctxs.push((ctx.clone(), fctx.clone()));352 let uctx = evaluate_object_locals(fctx, locals.clone());353354 evaluate_field_member(&mut builder, ctx, uctx, &obj.field)355 })?;356357 let this = builder.build();358 for (ctx, fctx) in ctxs {359 let _ctx = ctx360 .extend(GcHashMap::new(), None, None, Some(this.clone()))361 .into_future(fctx);362 }363 this364 }365 })366}367368pub fn evaluate_apply(369 ctx: Context,370 value: &LocExpr,371 args: &ArgsDesc,372 loc: CallLocation<'_>,373 tailstrict: bool,374) -> Result<Val> {375 let value = evaluate(ctx.clone(), value)?;376 Ok(match value {377 Val::Func(f) => {378 let body = || f.evaluate(ctx, loc, args, tailstrict);379 if tailstrict {380 body()?381 } else {382 in_frame(loc, || format!("function <{}> call", f.name()), body)?383 }384 }385 v => bail!(OnlyFunctionsCanBeCalledGot(v.value_type())),386 })387}388389pub fn evaluate_assert(ctx: Context, assertion: &AssertStmt) -> Result<()> {390 let value = &assertion.0;391 let msg = &assertion.1;392 let assertion_result = in_frame(393 CallLocation::new(&value.span()),394 || "assertion condition".to_owned(),395 || bool::from_untyped(evaluate(ctx.clone(), value)?),396 )?;397 if !assertion_result {398 in_frame(399 CallLocation::new(&value.span()),400 || "assertion failure".to_owned(),401 || {402 if let Some(msg) = msg {403 bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));404 }405 bail!(AssertionFailed(Val::Null.to_string()?));406 },407 )?;408 }409 Ok(())410}411412pub fn evaluate_named(ctx: Context, expr: &LocExpr, name: IStr) -> Result<Val> {413 use Expr::*;414 Ok(match expr.expr() {415 Function(params, body) => evaluate_method(ctx, name, params.clone(), body.clone()),416 _ => evaluate(ctx, expr)?,417 })418}419420#[allow(clippy::too_many_lines)]421pub fn evaluate(ctx: Context, expr: &LocExpr) -> Result<Val> {422 use Expr::*;423424 if let Some(trivial) = evaluate_trivial(expr) {425 return Ok(trivial);426 }427 let loc = expr.span();428 Ok(match expr.expr() {429 Literal(LiteralType::This) => {430 Val::Obj(ctx.this().ok_or(CantUseSelfOutsideOfObject)?.clone())431 }432 Literal(LiteralType::Super) => Val::Obj(433 ctx.super_obj().ok_or(NoSuperFound)?.with_this(434 ctx.this()435 .expect("if super exists - then this should too")436 .clone(),437 ),438 ),439 Literal(LiteralType::Dollar) => {440 Val::Obj(ctx.dollar().ok_or(NoTopLevelObjectFound)?.clone())441 }442 Literal(LiteralType::True) => Val::Bool(true),443 Literal(LiteralType::False) => Val::Bool(false),444 Literal(LiteralType::Null) => Val::Null,445 Parened(e) => evaluate(ctx, e)?,446 Str(v) => Val::string(v.clone()),447 Num(v) => Val::try_num(*v)?,448 // I have tried to remove special behavior from super by implementing standalone-super449 // expresion, but looks like this case still needs special treatment.450 //451 // Note that other jsonnet implementations will fail on `if value in (super)` expression,452 // because the standalone super literal is not supported, that is because in other453 // implementations `in super` treated differently from in `smth_else`.454 BinaryOp(field, BinaryOpType::In, e)455 if matches!(e.expr(), Expr::Literal(LiteralType::Super)) =>456 {457 let Some(super_obj) = ctx.super_obj() else {458 return Ok(Val::Bool(false));459 };460 let field = evaluate(ctx.clone(), field)?;461 Val::Bool(super_obj.has_field_ex(field.to_string()?, true))462 }463 BinaryOp(v1, o, v2) => evaluate_binary_op_special(ctx, v1, *o, v2)?,464 UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?,465 Var(name) => in_frame(466 CallLocation::new(&loc),467 || format!("variable <{name}> access"),468 || ctx.binding(name.clone())?.evaluate(),469 )?,470 Index { indexable, parts } => ensure_sufficient_stack(|| {471 let mut parts = parts.iter();472 let mut indexable = if matches!(indexable.expr(), Expr::Literal(LiteralType::Super)) {473 let part = parts.next().expect("at least part should exist");474 let Some(super_obj) = ctx.super_obj() else {475 #[cfg(feature = "exp-null-coaelse")]476 if part.null_coaelse {477 return Ok(Val::Null);478 }479 bail!(NoSuperFound)480 };481 let name = evaluate(ctx.clone(), &part.value)?;482483 let Val::Str(name) = name else {484 bail!(ValueIndexMustBeTypeGot(485 ValType::Obj,486 ValType::Str,487 name.value_type(),488 ))489 };490491 let this = ctx492 .this()493 .expect("no this found, while super present, should not happen");494 let name = name.into_flat();495 match super_obj496 .get_for(name.clone(), this.clone())497 .with_description_src(&part.value, || format!("field <{name}> access"))?498 {499 Some(v) => v,500 #[cfg(feature = "exp-null-coaelse")]501 None if part.null_coaelse => return Ok(Val::Null),502 None => {503 let suggestions = suggest_object_fields(super_obj, name.clone());504505 bail!(NoSuchField(name, suggestions))506 }507 }508 } else {509 evaluate(ctx.clone(), indexable)?510 };511512 for part in parts {513 indexable = match (indexable, evaluate(ctx.clone(), &part.value)?) {514 (Val::Obj(v), Val::Str(key)) => match v515 .get(key.clone().into_flat())516 .with_description_src(&part.value, || format!("field <{key}> access"))?517 {518 Some(v) => v,519 #[cfg(feature = "exp-null-coaelse")]520 None if part.null_coaelse => return Ok(Val::Null),521 None => {522 let suggestions = suggest_object_fields(&v, key.clone().into_flat());523524 return Err(Error::from(NoSuchField(525 key.clone().into_flat(),526 suggestions,527 )))528 .with_description_src(&part.value, || format!("field <{key}> access"));529 }530 },531 (Val::Obj(_), n) => bail!(ValueIndexMustBeTypeGot(532 ValType::Obj,533 ValType::Str,534 n.value_type(),535 )),536 (Val::Arr(v), Val::Num(n)) => {537 let n = n.get();538 if n.fract() > f64::EPSILON {539 bail!(FractionalIndex)540 }541 if n < 0.0 {542 bail!(ArrayBoundsError(n as isize, v.len()));543 }544 v.get(n as usize)?545 .ok_or_else(|| ArrayBoundsError(n as isize, v.len()))?546 }547 (Val::Arr(_), Val::Str(n)) => {548 bail!(AttemptedIndexAnArrayWithString(n.into_flat()))549 }550 (Val::Arr(_), n) => bail!(ValueIndexMustBeTypeGot(551 ValType::Arr,552 ValType::Num,553 n.value_type(),554 )),555556 (Val::Str(s), Val::Num(n)) => Val::Str({557 let v: IStr = s558 .clone()559 .into_flat()560 .chars()561 .skip(n.get() as usize)562 .take(1)563 .collect::<String>()564 .into();565 if v.is_empty() {566 let size = s.into_flat().chars().count();567 bail!(StringBoundsError(n.get() as usize, size))568 }569 StrValue::Flat(v)570 }),571 (Val::Str(_), n) => bail!(ValueIndexMustBeTypeGot(572 ValType::Str,573 ValType::Num,574 n.value_type(),575 )),576 #[cfg(feature = "exp-null-coaelse")]577 (Val::Null, _) if part.null_coaelse => return Ok(Val::Null),578 (v, _) => bail!(CantIndexInto(v.value_type())),579 };580 }581 Ok(indexable)582 })?,583 LocalExpr(bindings, returned) => {584 let mut new_bindings: GcHashMap<IStr, Thunk<Val>> =585 GcHashMap::with_capacity(bindings.iter().map(BindSpec::capacity_hint).sum());586 let fctx = Context::new_future();587 for b in bindings {588 evaluate_dest(b, fctx.clone(), &mut new_bindings)?;589 }590 let ctx = ctx.extend(new_bindings, None, None, None).into_future(fctx);591 evaluate(ctx, &returned.clone())?592 }593 Arr(items) => {594 if items.is_empty() {595 Val::Arr(ArrValue::empty())596 } else if items.len() == 1 {597 let item = items[0].clone();598 Val::Arr(ArrValue::lazy(vec![Thunk!(move || evaluate(ctx, &item))]))599 } else {600 Val::Arr(ArrValue::expr(ctx, items.iter().cloned()))601 }602 }603 ArrComp(expr, comp_specs) => {604 let mut out = Vec::new();605 evaluate_comp(ctx, comp_specs, &mut |ctx| {606 let expr = expr.clone();607 out.push(Thunk!(move || evaluate(ctx, &expr)));608 Ok(())609 })?;610 Val::Arr(ArrValue::lazy(out))611 }612 Obj(body) => Val::Obj(evaluate_object(ctx, body)?),613 ObjExtend(a, b) => evaluate_add_op(614 &evaluate(ctx.clone(), a)?,615 &Val::Obj(evaluate_object(ctx, b)?),616 )?,617 Apply(value, args, tailstrict) => ensure_sufficient_stack(|| {618 evaluate_apply(ctx, value, args, CallLocation::new(&loc), *tailstrict)619 })?,620 Function(params, body) => {621 evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone())622 }623 AssertExpr(assert, returned) => {624 evaluate_assert(ctx.clone(), assert)?;625 evaluate(ctx, returned)?626 }627 ErrorStmt(e) => in_frame(628 CallLocation::new(&loc),629 || "error statement".to_owned(),630 || bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),631 )?,632 IfElse {633 cond,634 cond_then,635 cond_else,636 } => {637 if in_frame(638 CallLocation::new(&loc),639 || "if condition".to_owned(),640 || bool::from_untyped(evaluate(ctx.clone(), &cond.0)?),641 )? {642 evaluate(ctx, cond_then)?643 } else {644 match cond_else {645 Some(v) => evaluate(ctx, v)?,646 None => Val::Null,647 }648 }649 }650 Slice(value, desc) => {651 fn parse_idx<T: Typed>(652 loc: CallLocation<'_>,653 ctx: &Context,654 expr: Option<&LocExpr>,655 desc: &'static str,656 ) -> Result<Option<T>> {657 if let Some(value) = expr {658 Ok(Some(in_frame(659 loc,660 || format!("slice {desc}"),661 || T::from_untyped(evaluate(ctx.clone(), value)?),662 )?))663 } else {664 Ok(None)665 }666 }667668 let indexable = evaluate(ctx.clone(), value)?;669 let loc = CallLocation::new(&loc);670671 let start = parse_idx(loc, &ctx, desc.start.as_ref(), "start")?;672 let end = parse_idx(loc, &ctx, desc.end.as_ref(), "end")?;673 let step = parse_idx(loc, &ctx, desc.step.as_ref(), "step")?;674675 IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)?676 }677 i @ (Import(path) | ImportStr(path) | ImportBin(path)) => {678 let Expr::Str(path) = &path.expr() else {679 bail!("computed imports are not supported")680 };681 let tmp = loc.clone().0;682 let s = ctx.state();683 let resolved_path = s.resolve_from(tmp.source_path(), path as &str)?;684 match i {685 Import(_) => in_frame(686 CallLocation::new(&loc),687 || format!("import {:?}", path.clone()),688 || s.import_resolved(resolved_path),689 )?,690 ImportStr(_) => Val::string(s.import_resolved_str(resolved_path)?),691 ImportBin(_) => Val::Arr(ArrValue::bytes(s.import_resolved_bin(resolved_path)?)),692 _ => unreachable!(),693 }694 }695 })696}crates/jrsonnet-evaluator/src/function/parse.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/function/parse.rs
+++ b/crates/jrsonnet-evaluator/src/function/parse.rs
@@ -90,7 +90,8 @@
let fctx = Context::new_future();
let mut defaults = GcHashMap::with_capacity(
params.iter().map(|p| p.0.capacity_hint()).sum::<usize>()
- - filled_named - filled_positionals,
+ - filled_named
+ - filled_positionals,
);
for (idx, param) in params.iter().enumerate().filter(|p| p.1 .1.is_some()) {
@@ -232,22 +233,6 @@
/// 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: &ParamsDesc) -> Result<Context> {
- #[derive(Trace)]
- struct DependsOnUnbound(IStr, ParamsDesc);
- impl ThunkValue for DependsOnUnbound {
- type Output = Val;
- fn get(self: Box<Self>) -> Result<Val> {
- Err(FunctionParameterNotBoundInCall(
- Some(self.0.clone()),
- self.1
- .iter()
- .map(|p| (p.0.name(), ParamDefault::exists(p.1.is_some())))
- .collect(),
- )
- .into())
- }
- }
-
let fctx = Context::new_future();
let mut bindings = GcHashMap::with_capacity(params.iter().map(|p| p.0.capacity_hint()).sum());
@@ -267,10 +252,18 @@
} else {
destruct(
¶m.0,
- Thunk::new(DependsOnUnbound(
- param.0.name().unwrap_or_else(|| "<destruct>".into()),
- params.clone(),
- )),
+ {
+ let param_name = param.0.name().unwrap_or_else(|| "<destruct>".into());
+ let params = params.clone();
+ Thunk!(move || Err(FunctionParameterNotBoundInCall(
+ Some(param_name),
+ params
+ .iter()
+ .map(|p| (p.0.name(), ParamDefault::exists(p.1.is_some())))
+ .collect(),
+ )
+ .into()))
+ },
fctx.clone(),
&mut bindings,
)?;
crates/jrsonnet-evaluator/src/gc.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/gc.rs
+++ b/crates/jrsonnet-evaluator/src/gc.rs
@@ -158,3 +158,5 @@
Self::new()
}
}
+
+pub fn assert_trace<T: Trace>(_v: &T) {}
crates/jrsonnet-evaluator/src/obj.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/obj.rs
+++ b/crates/jrsonnet-evaluator/src/obj.rs
@@ -20,7 +20,7 @@
in_frame,
operator::evaluate_add_op,
tb,
- val::{ArrValue, ThunkValue},
+ val::ArrValue,
MaybeUnbound, Result, Thunk, Unbound, Val,
};
@@ -444,45 +444,16 @@
})
}
pub fn get_lazy(&self, key: IStr) -> Option<Thunk<Val>> {
- #[derive(Trace)]
- struct ThunkGet {
- obj: ObjValue,
- key: IStr,
- }
- impl ThunkValue for ThunkGet {
- type Output = Val;
-
- fn get(self: Box<Self>) -> Result<Self::Output> {
- Ok(self.obj.get(self.key)?.expect("field exists"))
- }
- }
-
if !self.has_field_ex(key.clone(), true) {
return None;
}
- Some(Thunk::new(ThunkGet {
- obj: self.clone(),
- key,
- }))
+ let obj = self.clone();
+
+ Some(Thunk!(move || Ok(obj.get(key)?.expect("field exists"))))
}
pub fn get_lazy_or_bail(&self, key: IStr) -> Thunk<Val> {
- #[derive(Trace)]
- struct ThunkGet {
- obj: ObjValue,
- key: IStr,
- }
- impl ThunkValue for ThunkGet {
- type Output = Val;
-
- fn get(self: Box<Self>) -> Result<Self::Output> {
- self.obj.get_or_bail(self.key)
- }
- }
-
- Thunk::new(ThunkGet {
- obj: self.clone(),
- key,
- })
+ let obj = self.clone();
+ Thunk!(move || obj.get_or_bail(key))
}
pub fn ptr_eq(a: &Self, b: &Self) -> bool {
Cc::ptr_eq(&a.0, &b.0)
@@ -733,11 +704,10 @@
self.value_cache
.borrow_mut()
.insert(cache_key.clone(), CacheValue::Pending);
- let value = self.get_for_uncached(key, this).map_err(|e| {
+ let value = self.get_for_uncached(key, this).inspect_err(|e| {
self.value_cache
.borrow_mut()
.insert(cache_key.clone(), CacheValue::Errored(e.clone()));
- e
})?;
self.value_cache.borrow_mut().insert(
cache_key,
crates/jrsonnet-evaluator/src/val.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/val.rs
+++ b/crates/jrsonnet-evaluator/src/val.rs
@@ -11,6 +11,7 @@
use derivative::Derivative;
use jrsonnet_gcmodule::{Cc, Trace};
use jrsonnet_interner::IStr;
+pub use jrsonnet_macros::Thunk;
use jrsonnet_types::ValType;
use thiserror::Error;
@@ -32,6 +33,27 @@
}
#[derive(Trace)]
+pub struct ThunkValueClosure<D: Trace, O: 'static> {
+ env: D,
+ // Carries no data, as it is not a real closure, all the
+ // captured environment is stored in `env` field.
+ #[trace(skip)]
+ closure: fn(D) -> Result<O>,
+}
+impl<D: Trace, O: 'static> ThunkValueClosure<D, O> {
+ pub fn new(env: D, closure: fn(D) -> Result<O>) -> Self {
+ Self { env, closure }
+ }
+}
+impl<D: Trace, O: 'static> ThunkValue for ThunkValueClosure<D, O> {
+ type Output = O;
+
+ fn get(self: Box<Self>) -> Result<Self::Output> {
+ (self.closure)(self.env)
+ }
+}
+
+#[derive(Trace)]
enum ThunkInner<T: Trace> {
Computed(T),
Errored(Error),
@@ -113,28 +135,11 @@
M: ThunkMapper<Input>,
M::Output: Trace,
{
- #[derive(Trace)]
- struct Mapped<Input: Trace, Mapper: Trace> {
- inner: Thunk<Input>,
- mapper: Mapper,
- }
- impl<Input, Mapper> ThunkValue for Mapped<Input, Mapper>
- where
- Input: Trace + Clone,
- Mapper: ThunkMapper<Input>,
- {
- type Output = Mapper::Output;
-
- fn get(self: Box<Self>) -> Result<Self::Output> {
- let value = self.inner.evaluate()?;
- let mapped = self.mapper.map(value)?;
- Ok(mapped)
- }
- }
-
- Thunk::new(Mapped::<Input, M> {
- inner: self,
- mapper,
+ let inner = self;
+ Thunk!(move || {
+ let value = inner.evaluate()?;
+ let mapped = mapper.map(value)?;
+ Ok(mapped)
})
}
}
crates/jrsonnet-macros/Cargo.tomldiffbeforeafterboth--- a/crates/jrsonnet-macros/Cargo.toml
+++ b/crates/jrsonnet-macros/Cargo.toml
@@ -17,3 +17,4 @@
proc-macro2.workspace = true
quote.workspace = true
syn = { workspace = true, features = ["full"] }
+syn-dissect-closure.workspace = true
crates/jrsonnet-macros/src/lib.rsdiffbeforeafterboth--- a/crates/jrsonnet-macros/src/lib.rs
+++ b/crates/jrsonnet-macros/src/lib.rs
@@ -1,7 +1,7 @@
use std::string::String;
use proc_macro2::TokenStream;
-use quote::quote;
+use quote::{quote, quote_spanned};
use syn::{
parenthesized,
parse::{Parse, ParseStream},
@@ -9,8 +9,8 @@
punctuated::Punctuated,
spanned::Spanned,
token::{self, Comma},
- Attribute, DeriveInput, Error, Expr, FnArg, GenericArgument, Ident, ItemFn, LitStr, Pat, Path,
- PathArguments, Result, ReturnType, Token, Type,
+ Attribute, DeriveInput, Error, Expr, ExprClosure, FnArg, GenericArgument, Ident, ItemFn,
+ LitStr, Pat, Path, PathArguments, Result, ReturnType, Token, Type,
};
fn parse_attr<A: Parse, I>(attrs: &[Attribute], ident: I) -> Result<Option<A>>
@@ -815,3 +815,30 @@
let input = parse_macro_input!(input as FormatInput);
input.expand().into()
}
+
+/// Create Thunk using closure syntax
+#[proc_macro]
+#[allow(non_snake_case)]
+pub fn Thunk(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
+ let input = parse_macro_input!(input as ExprClosure);
+
+ let span = input.inputs.span();
+ let move_check = input.capture.is_none().then(|| {
+ quote_spanned! {span => {
+ compile_error!("Thunk! needs to be called with move closure");
+ }}
+ });
+
+ let (env, closure, args) = syn_dissect_closure::split_env(input);
+
+ let trace_check = args.iter().map(|el| {
+ let span = el.span();
+ quote_spanned! {span => ::jrsonnet_evaluator::gc::assert_trace(&#el);}
+ });
+
+ quote! {{
+ #move_check
+ #(#trace_check)*
+ ::jrsonnet_evaluator::Thunk::new(::jrsonnet_evaluator::val::ThunkValueClosure::new(#env, #closure))
+ }}.into()
+}