diff --git a/Cargo.lock b/Cargo.lock index f905aee4..328578fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,6 +13,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + [[package]] name = "annotate-snippets" version = "0.9.1" @@ -370,8 +379,11 @@ dependencies = [ "jrsonnet-gcmodule", "jrsonnet-macros", "jrsonnet-parser", + "lru", "md5", "num-bigint", + "regex", + "rustc-hash", "serde", "serde_json", "serde_yaml_with_quirks", @@ -425,12 +437,27 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "lru" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03f1160296536f10c833a82dca22267d5486734230d47bf00bf435885814ba1e" +dependencies = [ + "hashbrown 0.13.2", +] + [[package]] name = "md5" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + [[package]] name = "mimalloc-sys" version = "0.1.6" @@ -600,6 +627,23 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" + [[package]] name = "rustc-hash" version = "1.1.0" diff --git a/crates/jrsonnet-evaluator/src/evaluate/mod.rs b/crates/jrsonnet-evaluator/src/evaluate/mod.rs index 9ea8f3aa..0addd879 100644 --- a/crates/jrsonnet-evaluator/src/evaluate/mod.rs +++ b/crates/jrsonnet-evaluator/src/evaluate/mod.rs @@ -1,10 +1,11 @@ -use std::rc::Rc; +use std::{fmt, marker::PhantomData, ops::Deref, rc::Rc}; use jrsonnet_gcmodule::{Cc, Trace}; use jrsonnet_interner::IStr; +use jrsonnet_macros::{tco, tcok, tcr}; use jrsonnet_parser::{ - ArgsDesc, AssertStmt, BindSpec, CompSpec, Expr, FieldMember, FieldName, ForSpecData, - IfSpecData, LiteralType, LocExpr, Member, ObjBody, ParamsDesc, + ArgsDesc, AssertStmt, BinaryOpType, BindSpec, CompSpec, Expr, FieldMember, FieldName, + ForSpecData, IfSpecData, LiteralType, LocExpr, Member, ObjBody, ParamsDesc, UnaryOpType, }; use jrsonnet_types::ValType; @@ -13,13 +14,14 @@ use crate::{ arr::ArrValue, destructure::evaluate_dest, error::{suggest_object_fields, ErrorKind::*}, - evaluate::operator::{evaluate_add_op, evaluate_binary_op_special, evaluate_unary_op}, - function::{CallLocation, FuncDesc, FuncVal}, + evaluate::operator::evaluate_unary_op, + function::{parse::parse_function_call, CallLocation, FuncDesc, FuncVal}, + operator::evaluate_binary_op_normal, throw, - typed::Typed, + typed::{CheckType, Typed}, val::{CachedUnbound, IndexableVal, StrValue, Thunk, ThunkValue}, - Context, GcHashMap, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result, State, - Unbound, Val, + Context, GcHashMap, MaybeUnbound, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result, + State, Unbound, Val, }; pub mod destructure; pub mod operator; @@ -411,235 +413,852 @@ pub fn evaluate_named(ctx: Context, expr: &LocExpr, name: IStr) -> Result { }) } -#[allow(clippy::too_many_lines)] -pub fn evaluate(ctx: Context, expr: &LocExpr) -> Result { - use Expr::*; +pub(crate) struct Fifo { + data: Vec<(T, Tag)>, +} +impl Fifo { + fn with_capacity(cap: usize) -> Self { + Self { + data: Vec::with_capacity(cap), + } + } + fn single(cap: usize, data: T, tag: Tag) -> Self { + // eprintln!(">>> {}", tag.0); + let mut out = Self { + data: Vec::with_capacity(cap), + }; + out.push(data, tag); + out + } + pub(crate) fn push(&mut self, data: T, tag: Tag) { + // eprintln!(">>> {}", tag.0); + self.data.push((data, tag)); + } + #[track_caller] + pub(crate) fn pop(&mut self, tag: Tag) -> T { + // eprintln!("<<< {}", tag.0); + let (data, stag) = self + .data + .pop() + .unwrap_or_else(|| panic!("underflow querying for {tag:?}")); + // debug_assert doesn't work here, as it always requires PartialEq + #[cfg(debug_assertions)] + assert_eq!( + stag, tag, + "mismatched expected {tag:?} and actual {stag:?} tags", + ); + data + } + pub(crate) fn is_empty(&self) -> bool { + self.data.is_empty() + } + pub(crate) fn len(&self) -> usize { + self.data.len() + } + pub(crate) fn reserve(&mut self, size: usize) { + self.data.reserve(size) + } +} + +pub(crate) struct Tag { + #[cfg(debug_assertions)] + name: &'static str, + #[cfg(debug_assertions)] + id: u64, + _marker: PhantomData, +} - if let Some(trivial) = evaluate_trivial(expr) { - return Ok(trivial); +#[cfg(debug_assertions)] +impl PartialEq for Tag { + fn eq(&self, other: &Self) -> bool { + self.name == other.name && self.id == other.id } - let LocExpr(expr, loc) = expr; - Ok(match &**expr { - Literal(LiteralType::This) => { - Val::Obj(ctx.this().ok_or(CantUseSelfOutsideOfObject)?.clone()) +} +impl fmt::Debug for Tag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + #[cfg(debug_assertions)] + { + write!(f, "Tag({})", self.name) } - Literal(LiteralType::Super) => Val::Obj( - ctx.super_obj().ok_or(NoSuperFound)?.with_this( - ctx.this() - .expect("if super exists - then this should too") - .clone(), - ), - ), - Literal(LiteralType::Dollar) => { - Val::Obj(ctx.dollar().ok_or(NoTopLevelObjectFound)?.clone()) + #[cfg(not(debug_assertions))] + { + write!(f, "UncheckedTag") } - Literal(LiteralType::True) => Val::Bool(true), - Literal(LiteralType::False) => Val::Bool(false), - Literal(LiteralType::Null) => Val::Null, - Parened(e) => evaluate(ctx, e)?, - Str(v) => Val::Str(StrValue::Flat(v.clone())), - Num(v) => Val::new_checked_num(*v)?, - BinaryOp(v1, o, v2) => evaluate_binary_op_special(ctx, v1, *o, v2)?, - UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?, - Var(name) => State::push( - CallLocation::new(loc), - || format!("variable <{name}> access"), - || ctx.binding(name.clone())?.evaluate(), - )?, - Index(LocExpr(v, _), index) if matches!(&**v, Expr::Literal(LiteralType::Super)) => { - let name = evaluate(ctx.clone(), index)?; - let Val::Str(name) = name else { - throw!(ValueIndexMustBeTypeGot( - ValType::Obj, - ValType::Str, - name.value_type(), - )) - }; - ctx.super_obj() - .expect("no super found") - .get_for(name.into_flat(), ctx.this().expect("no this found").clone())? - .expect("value not found") + } +} +impl Clone for Tag { + fn clone(&self) -> Self { + Self { + #[cfg(debug_assertions)] + name: self.name, + #[cfg(debug_assertions)] + id: self.id.clone(), + _marker: self._marker.clone(), + } + } +} +impl Copy for Tag {} + +#[inline(always)] +pub(crate) fn val_tag(name: &'static str) -> Tag { + #[cfg(debug_assertions)] + { + static ID: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0); + Tag { + name, + id: ID.fetch_add(1, core::sync::atomic::Ordering::SeqCst), + _marker: PhantomData, + } + } + #[cfg(not(debug_assertions))] + { + Tag { + _marker: PhantomData, + } + } +} +#[inline(always)] +pub(crate) fn ctx_tag(name: &'static str) -> Tag { + #[cfg(debug_assertions)] + { + static ID: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0); + Tag { + name, + id: ID.fetch_add(1, core::sync::atomic::Ordering::SeqCst), + _marker: PhantomData, + } + } + #[cfg(not(debug_assertions))] + { + Tag { + _marker: PhantomData, + } + } +} + +#[inline(always)] +pub(crate) fn apply_tag<'a>() -> Tag { + #[cfg(debug_assertions)] + { + Tag { + name: "APPLY", + id: 0, + _marker: PhantomData, } - Index(value, index) => match (evaluate(ctx.clone(), value)?, evaluate(ctx, index)?) { - (Val::Obj(v), Val::Str(key)) => State::push( - CallLocation::new(loc), - || format!("field <{key}> access"), - || match v.get(key.clone().into_flat()) { - Ok(Some(v)) => Ok(v), - Ok(None) => { - let suggestions = suggest_object_fields(&v, key.clone().into_flat()); - - throw!(NoSuchField(key.clone().into_flat(), suggestions)) + } + #[cfg(not(debug_assertions))] + { + Tag { + _marker: PhantomData, + } + } +} + +#[derive(Debug)] +pub(crate) enum TailCallApply { + Eval { + expr: LocExpr, + in_ctx: Tag, + out_val: Tag, + }, + ParseNormalArgs { + params: ParamsDesc, + args: ArgsDesc, + tailstrict: bool, + def_ctx: Tag, + call_ctx: Tag, + out_ctx: Tag, + }, + IfCond { + in_val: Tag, + then_else_ctx: Tag, + then_val: LocExpr, + else_val: Option, + out_val: Tag, + }, + ApplyUnknownFn { + args: ArgsDesc, + location: CallLocation<'static>, + tailstrict: bool, + lhs_val: Tag, + call_ctx: Tag, + out_val: Tag, + }, + ApplyBopSpecial { + and: bool, + a_val: Tag, + b_ctx: Tag, + b_expr: LocExpr, + out_val: Tag, + }, + ApplyBopNormal { + op: BinaryOpType, + a_val: Tag, + b_val: Tag, + out_val: Tag, + }, + ApplyUop { + op: UnaryOpType, + in_val: Tag, + out_val: Tag, + }, + EvalObj { + in_ctx: Tag, + obj: RcMap, + out_val: Tag, + }, + AssertType { + in_val: Tag, + out_val: Tag, + ty: ValType, + }, + EvalIndex { + lhs_val: Tag, + rhs_val: Tag, + out_val: Tag, + }, + Unbound { + invoke: MaybeUnbound, + sup: Option, + this: Option, + out_val: Tag, + }, +} +#[derive(Debug)] +pub(crate) struct RcMap(Rc, fn(&T) -> &U); + +impl RcMap { + fn map(rc: Rc, projection: fn(&T) -> &U) -> Self { + RcMap(rc, projection) + } +} + +impl Deref for RcMap +where + T: ?Sized, + U: ?Sized, +{ + type Target = U; + + fn deref(&self) -> &Self::Target { + (self.1)(&*self.0) + } +} + +pub(crate) struct TcVM { + pub(crate) vals: Fifo, + pub(crate) ctxs: Fifo, + pub(crate) apply: Fifo, + + #[cfg(debug_assertions)] + pub(crate) vals_offset: usize, + #[cfg(debug_assertions)] + pub(crate) ctxs_offset: usize, + pub(crate) apply_offset: usize, +} +impl TcVM { + fn has_apply(&self) -> bool { + self.apply.len() > self.apply_offset + } +} + +#[inline(always)] +fn evaluate_inner( + tcvm: &mut TcVM, + expr: LocExpr, + in_ctx: Tag, + out_val: Tag, +) -> Result<()> { + use Expr::*; + let ctx = tcvm.ctxs.pop(in_ctx); + + if let Some(trivial) = evaluate_trivial(&expr) { + tcvm.vals.push(trivial, out_val); + return Ok(()); + } + let LocExpr(expr, loc) = expr; + tcvm.vals.push( + match *expr { + Literal(LiteralType::This) => { + Val::Obj(ctx.this().ok_or(CantUseSelfOutsideOfObject)?.clone()) + } + Literal(LiteralType::Super) => Val::Obj( + ctx.super_obj().ok_or(NoSuperFound)?.with_this( + ctx.this() + .expect("if super exists - then this should too") + .clone(), + ), + ), + Literal(LiteralType::Dollar) => { + Val::Obj(ctx.dollar().ok_or(NoTopLevelObjectFound)?.clone()) + } + Literal(LiteralType::True) => Val::Bool(true), + Literal(LiteralType::False) => Val::Bool(false), + Literal(LiteralType::Null) => Val::Null, + Parened(ref e) => { + tcr!( + ctx(parened, ctx.clone()), + Eval { + expr: e.clone(), + in_ctx: parened, + out_val } - Err(e) => Err(e), - }, + ); + return Ok(()); + } + Str(ref v) => Val::Str(StrValue::Flat(v.clone())), + Num(v) => Val::new_checked_num(v)?, + BinaryOp(ref v1, op, ref v2) if matches!(op, BinaryOpType::And | BinaryOpType::Or) => { + tcr!( + ctx(in_ctx, ctx.clone()), + Eval { + expr: v1.clone(), + in_ctx, + out_val: val(a_val), + }, + ctx(b_ctx, ctx.clone()), + ApplyBopSpecial { + a_val, + b_ctx, + b_expr: v2.clone(), + and: op == BinaryOpType::And, + out_val, + } + ); + return Ok(()); + } + BinaryOp(ref v1, op, ref v2) => { + // FIXME: short-circuiting binary op + tcr!( + ctx(lhsc, ctx.clone()), + Eval { + expr: v1.clone(), + in_ctx: lhsc, + out_val: val(a_val), + }, + ctx(rhsc, ctx.clone()), + Eval { + expr: v2.clone(), + in_ctx: rhsc, + out_val: val(b_val), + }, + ApplyBopNormal { + op, + a_val, + b_val, + out_val, + } + ); + return Ok(()); + } + UnaryOp(op, ref v) => { + tcr!( + ctx(uop, ctx.clone()), + Eval { + expr: v.clone(), + in_ctx: uop, + out_val: val(in_val), + }, + ApplyUop { + op, + in_val, + out_val, + }, + ); + return Ok(()); + } + Var(ref name) => State::push( + CallLocation::new(&loc), + || format!("variable <{name}> access"), + || ctx.binding(name.clone())?.evaluate(), )?, - (Val::Obj(_), n) => throw!(ValueIndexMustBeTypeGot( - ValType::Obj, - ValType::Str, - n.value_type(), - )), - - (Val::Arr(v), Val::Num(n)) => { - if n.fract() > f64::EPSILON { - throw!(FractionalIndex) + Index(LocExpr(ref v, _), ref index) + if matches!(&**v, Expr::Literal(LiteralType::Super)) => + { + let name = evaluate(ctx.clone(), &index)?; + let Val::Str(name) = name else { + throw!(ValueIndexMustBeTypeGot( + ValType::Obj, + ValType::Str, + name.value_type(), + )) + }; + ctx.super_obj() + .expect("no super found") + .get_for(name.into_flat(), ctx.this().expect("no this found").clone())? + .expect("value not found") + } + Index(ref value, ref index) => { + tcr!( + ctx(lhsc, ctx.clone()), + Eval { + expr: value.clone(), + in_ctx: lhsc, + out_val: val(lhs_val), + }, + ctx(rhsc, ctx.clone()), + Eval { + expr: index.clone(), + in_ctx: rhsc, + out_val: val(rhs_val), + }, + EvalIndex { + lhs_val, + rhs_val, + out_val, + } + ); + return Ok(()); + } + LocalExpr(ref bindings, ref returned) => { + let mut new_bindings: GcHashMap> = + GcHashMap::with_capacity(bindings.iter().map(BindSpec::capacity_hint).sum()); + let fctx = Context::new_future(); + for b in bindings { + evaluate_dest(&b, fctx.clone(), &mut new_bindings)?; } - v.get(n as usize)? - .ok_or_else(|| ArrayBoundsError(n as usize, v.len()))? - } - (Val::Arr(_), Val::Str(n)) => throw!(AttemptedIndexAnArrayWithString(n.into_flat())), - (Val::Arr(_), n) => throw!(ValueIndexMustBeTypeGot( - ValType::Arr, - ValType::Num, - n.value_type(), - )), - - (Val::Str(s), Val::Num(n)) => Val::Str({ - let v: IStr = s - .clone() - .into_flat() - .chars() - .skip(n as usize) - .take(1) - .collect::() - .into(); - if v.is_empty() { - let size = s.into_flat().chars().count(); - throw!(StringBoundsError(n as usize, size)) + let ctx = ctx.extend(new_bindings, None, None, None).into_future(fctx); + evaluate(ctx, &returned.clone())? + } + Arr(ref items) => { + if items.is_empty() { + Val::Arr(ArrValue::empty()) + } else if items.len() == 1 { + #[derive(Trace)] + struct ArrayElement { + ctx: Context, + item: LocExpr, + } + impl ThunkValue for ArrayElement { + type Output = Val; + fn get(self: Box) -> Result { + evaluate(self.ctx, &self.item) + } + } + Val::Arr(ArrValue::lazy(Cc::new(vec![Thunk::new(ArrayElement { + ctx, + item: items[0].clone(), + })]))) + } else { + Val::Arr(ArrValue::expr(ctx, items.iter().cloned())) } - StrValue::Flat(v) - }), - (Val::Str(_), n) => throw!(ValueIndexMustBeTypeGot( - ValType::Str, - ValType::Num, - n.value_type(), - )), - - (v, _) => throw!(CantIndexInto(v.value_type())), - }, - LocalExpr(bindings, returned) => { - let mut new_bindings: GcHashMap> = - GcHashMap::with_capacity(bindings.iter().map(BindSpec::capacity_hint).sum()); - let fctx = Context::new_future(); - for b in bindings { - evaluate_dest(b, fctx.clone(), &mut new_bindings)?; } - let ctx = ctx.extend(new_bindings, None, None, None).into_future(fctx); - evaluate(ctx, &returned.clone())? - } - Arr(items) => { - if items.is_empty() { - Val::Arr(ArrValue::empty()) - } else if items.len() == 1 { - #[derive(Trace)] - struct ArrayElement { - ctx: Context, - item: LocExpr, + ArrComp(ref expr, ref comp_specs) => { + let mut out = Vec::new(); + evaluate_comp(ctx, &comp_specs, &mut |ctx| { + out.push(evaluate(ctx, &expr)?); + Ok(()) + })?; + Val::Arr(ArrValue::eager(out)) + } + Obj(ref body) => Val::Obj(evaluate_object(ctx, &body)?), + ObjExtend(ref a, _) => { + tcr!( + ctx(base_ctx, ctx.clone()), + Eval { + expr: a.clone(), + in_ctx: base_ctx, + out_val: val(lhs), + }, + ctx(obj_ctx, ctx.clone()), + EvalObj { + in_ctx: obj_ctx, + obj: RcMap::map(expr.clone(), |e| { + match e { + ObjExtend(_, v) => v, + _ => unreachable!(), + } + }), + out_val: val(rhs) + }, + ApplyBopNormal { + op: BinaryOpType::Add, + a_val: lhs, + b_val: rhs, + out_val, + } + ); + return Ok(()); + } + Apply(ref value, ref args, tailstrict) => tcok!( + ctx(in_ctx, ctx.clone()), + Eval { + expr: value.clone(), + in_ctx, + out_val: val(lhs_val), + }, + ctx(call_ctx, ctx.clone()), + ApplyUnknownFn { + args: args.clone(), + // TODO: preserve + location: CallLocation::native(), + tailstrict, + call_ctx, + lhs_val, + out_val, + }, + ), + Function(ref params, ref body) => { + evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone()) + } + AssertExpr(ref assert, ref returned) => { + evaluate_assert(ctx.clone(), &assert)?; + evaluate(ctx, &returned)? + } + ErrorStmt(ref e) => State::push( + CallLocation::new(&loc), + || "error statement".to_owned(), + || throw!(RuntimeError(evaluate(ctx, &e)?.to_string()?,)), + )?, + IfElse { + ref cond, + ref cond_then, + ref cond_else, + } => { + tcr!( + ctx(in_ctx, ctx.clone()), + Eval { + expr: cond.0.clone(), + in_ctx, + out_val: val(in_val), + }, + ctx(then_else_ctx, ctx.clone()), + IfCond { + in_val: in_val.clone(), + then_else_ctx, + then_val: cond_then.clone(), + else_val: cond_else.clone(), + out_val + } + ); + return Ok(()); + } + Slice(ref value, ref desc) => { + fn parse_idx( + loc: CallLocation<'_>, + ctx: &Context, + expr: Option<&LocExpr>, + desc: &'static str, + ) -> Result> { + if let Some(value) = expr { + Ok(Some(State::push( + loc, + || format!("slice {desc}"), + || T::from_untyped(evaluate(ctx.clone(), value)?), + )?)) + } else { + Ok(None) + } } - impl ThunkValue for ArrayElement { - type Output = Val; - fn get(self: Box) -> Result { - evaluate(self.ctx, &self.item) + + let indexable = evaluate(ctx.clone(), &value)?; + let loc = CallLocation::new(&loc); + + let start = parse_idx(loc, &ctx, desc.start.as_ref(), "start")?; + let end = parse_idx(loc, &ctx, desc.end.as_ref(), "end")?; + let step = parse_idx(loc, &ctx, desc.step.as_ref(), "step")?; + + IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)? + } + ref i @ (Import(ref path) | ImportStr(ref path) | ImportBin(ref path)) => { + let Expr::Str(path) = &*path.0 else { + throw!("computed imports are not supported") + }; + let tmp = loc.clone().0; + let s = ctx.state(); + let resolved_path = s.resolve_from(tmp.source_path(), path as &str)?; + match i { + Import(_) => State::push( + CallLocation::new(&loc), + || format!("import {:?}", path.clone()), + || s.import_resolved(resolved_path), + )?, + ImportStr(_) => Val::Str(StrValue::Flat(s.import_resolved_str(resolved_path)?)), + ImportBin(_) => { + Val::Arr(ArrValue::bytes(s.import_resolved_bin(resolved_path)?)) } + _ => unreachable!(), } - Val::Arr(ArrValue::lazy(Cc::new(vec![Thunk::new(ArrayElement { - ctx, - item: items[0].clone(), - })]))) - } else { - Val::Arr(ArrValue::expr(ctx, items.iter().cloned())) } - } - ArrComp(expr, comp_specs) => { - let mut out = Vec::new(); - evaluate_comp(ctx, comp_specs, &mut |ctx| { - out.push(evaluate(ctx, expr)?); - Ok(()) - })?; - Val::Arr(ArrValue::eager(out)) - } - Obj(body) => Val::Obj(evaluate_object(ctx, body)?), - ObjExtend(a, b) => evaluate_add_op( - &evaluate(ctx.clone(), a)?, - &Val::Obj(evaluate_object(ctx, b)?), - )?, - Apply(value, args, tailstrict) => { - evaluate_apply(ctx, value, args, CallLocation::new(loc), *tailstrict)? - } - Function(params, body) => { - evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone()) - } - AssertExpr(assert, returned) => { - evaluate_assert(ctx.clone(), assert)?; - evaluate(ctx, returned)? - } - ErrorStmt(e) => State::push( - CallLocation::new(loc), - || "error statement".to_owned(), - || throw!(RuntimeError(evaluate(ctx, e)?.to_string()?,)), - )?, - IfElse { - cond, - cond_then, - cond_else, - } => { - if State::push( - CallLocation::new(loc), - || "if condition".to_owned(), - || bool::from_untyped(evaluate(ctx.clone(), &cond.0)?), - )? { - evaluate(ctx, cond_then)? - } else { - match cond_else { - Some(v) => evaluate(ctx, v)?, - None => Val::Null, + }, + out_val, + ); + Ok(()) +} + +pub fn evaluate_reuse_inlined(tcvm: &mut TcVM, expr: &LocExpr) -> Result<()> { + use TailCallApply::*; + while tcvm.has_apply() { + let op = tcvm.apply.pop(apply_tag()); + match op { + TailCallApply::Eval { + expr, + in_ctx, + out_val, + } => evaluate_inner(&mut tcvm, expr, in_ctx, out_val)?, + TailCallApply::ApplyUnknownFn { + args, + location, + tailstrict, + lhs_val, + call_ctx, + out_val, + } => { + let value = tcvm.vals.pop(lhs_val); + let Val::Func(f) = value else { + throw!(OnlyFunctionsCanBeCalledGot(value.value_type())); + }; + match f { + FuncVal::Normal(desc) => { + tco!( + ctx(def_ctx, desc.ctx.clone()), + ParseNormalArgs { + params: desc.params.clone(), + args: args, + tailstrict, + def_ctx, + call_ctx, + out_ctx: ctx(body_ctx), + }, + Eval { + expr: desc.body.clone(), + in_ctx: body_ctx.clone(), + out_val + } + ); + } + FuncVal::Builtin(_) | FuncVal::StaticBuiltin(_) | FuncVal::Id => { + // TODO: Proper TCO optimization for builtins + let call_ctx = tcvm.ctxs.pop(call_ctx); + let out = f.evaluate(call_ctx, location, &args, tailstrict)?; + tcvm.vals.push(out, out_val) + } } } - } - Slice(value, desc) => { - fn parse_idx( - loc: CallLocation<'_>, - ctx: &Context, - expr: Option<&LocExpr>, - desc: &'static str, - ) -> Result> { - if let Some(value) = expr { - Ok(Some(State::push( - loc, - || format!("slice {desc}"), - || T::from_untyped(evaluate(ctx.clone(), value)?), - )?)) + ParseNormalArgs { + params, + args, + tailstrict, + def_ctx, + call_ctx, + out_ctx, + } => { + let definition_context = tcvm.ctxs.pop(def_ctx); + let lhs_ctx = tcvm.ctxs.pop(call_ctx); + let ctx = + parse_function_call(lhs_ctx, definition_context, ¶ms, &args, tailstrict)?; + tcvm.ctxs.push(ctx, out_ctx); + } + IfCond { + in_val, + then_else_ctx, + then_val, + else_val, + out_val, + } => { + let cond = tcvm.vals.pop(in_val); + let cond = bool::from_untyped(cond)?; + if cond { + tco!(Eval { + expr: then_val, + in_ctx: then_else_ctx, + out_val + }); + } else if let Some(else_val) = else_val { + tco!(Eval { + expr: else_val, + in_ctx: then_else_ctx, + out_val + }); } else { - Ok(None) + tcvm.ctxs.pop(then_else_ctx); + tcvm.vals.push(Val::Null, out_val) } } + ApplyBopNormal { + op, + a_val, + b_val, + out_val, + } => { + let b = tcvm.vals.pop(b_val); + let a = tcvm.vals.pop(a_val); + let v = evaluate_binary_op_normal(&a, op, &b)?; + tcvm.vals.push(v, out_val); + } + ApplyBopSpecial { + and, + a_val, + b_ctx, + b_expr, + out_val, + } => { + let a = tcvm.vals.pop(a_val); + let a = bool::from_untyped(a)?; + let b_ctx = tcvm.ctxs.pop(b_ctx); + match (and, a) { + (true, false) => tcvm.vals.push(Val::Bool(false), out_val), + (false, true) => tcvm.vals.push(Val::Bool(true), out_val), + (true, _) => tco!( + ctx(in_ctx, b_ctx), + Eval { + expr: b_expr, + in_ctx, + out_val: val(res), + }, + AssertType { + in_val: res, + out_val, + ty: ValType::Bool, + } + ), + (false, _) => tco!( + ctx(in_ctx, b_ctx), + Eval { + expr: b_expr, + in_ctx, + out_val: val(res), + }, + AssertType { + in_val: res, + out_val, + ty: ValType::Bool, + }, + ), + } + } + ApplyUop { + op, + in_val, + out_val, + } => { + let v = tcvm.vals.pop(in_val); + let o = evaluate_unary_op(op, &v)?; + tcvm.vals.push(o, out_val); + } + EvalObj { + in_ctx, + obj, + out_val, + } => { + let in_ctx = tcvm.ctxs.pop(in_ctx); + let v = evaluate_object(in_ctx, &obj)?; + tcvm.vals.push(Val::Obj(v), out_val); + } + AssertType { + in_val, + out_val, + ty, + } => { + let val = tcvm.vals.pop(in_val); + ty.check(&val)?; + tcvm.vals.push(val, out_val) + } + EvalIndex { + lhs_val, + rhs_val, + out_val, + } => { + let rhs = tcvm.vals.pop(rhs_val); + let lhs = tcvm.vals.pop(lhs_val); + let v = match (lhs, rhs) { + (Val::Obj(v), Val::Str(key)) => match v.get(key.clone().into_flat()) { + Ok(Some(v)) => v, + Ok(None) => { + let suggestions = suggest_object_fields(&v, key.clone().into_flat()); - let indexable = evaluate(ctx.clone(), value)?; - let loc = CallLocation::new(loc); + throw!(NoSuchField(key.clone().into_flat(), suggestions)) + } + Err(e) => throw!(e), + }, + (Val::Obj(_), n) => throw!(ValueIndexMustBeTypeGot( + ValType::Obj, + ValType::Str, + n.value_type(), + )), - let start = parse_idx(loc, &ctx, desc.start.as_ref(), "start")?; - let end = parse_idx(loc, &ctx, desc.end.as_ref(), "end")?; - let step = parse_idx(loc, &ctx, desc.step.as_ref(), "step")?; + (Val::Arr(v), Val::Num(n)) => { + if n.fract() > f64::EPSILON { + throw!(FractionalIndex) + } + v.get(n as usize)? + .ok_or_else(|| ArrayBoundsError(n as usize, v.len()))? + } + (Val::Arr(_), Val::Str(n)) => { + throw!(AttemptedIndexAnArrayWithString(n.into_flat())) + } + (Val::Arr(_), n) => throw!(ValueIndexMustBeTypeGot( + ValType::Arr, + ValType::Num, + n.value_type(), + )), - IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)? - } - i @ (Import(path) | ImportStr(path) | ImportBin(path)) => { - let Expr::Str(path) = &*path.0 else { - throw!("computed imports are not supported") - }; - let tmp = loc.clone().0; - let s = ctx.state(); - let resolved_path = s.resolve_from(tmp.source_path(), path as &str)?; - match i { - Import(_) => State::push( - CallLocation::new(loc), - || format!("import {:?}", path.clone()), - || s.import_resolved(resolved_path), - )?, - ImportStr(_) => Val::Str(StrValue::Flat(s.import_resolved_str(resolved_path)?)), - ImportBin(_) => Val::Arr(ArrValue::bytes(s.import_resolved_bin(resolved_path)?)), - _ => unreachable!(), + (Val::Str(s), Val::Num(n)) => Val::Str({ + let v: IStr = s + .clone() + .into_flat() + .chars() + .skip(n as usize) + .take(1) + .collect::() + .into(); + if v.is_empty() { + let size = s.into_flat().chars().count(); + throw!(StringBoundsError(n as usize, size)) + } + StrValue::Flat(v) + }), + + (Val::Str(_), n) => throw!(ValueIndexMustBeTypeGot( + ValType::Str, + ValType::Num, + n.value_type(), + )), + + (v, _) => throw!(CantIndexInto(v.value_type())), + }; + tcvm.vals.push(v, out_val) + } + Unbound { + invoke, + sup, + this, + out_val, + } => { + let o = invoke.evaluate(sup, this)?; + tcvm.vals.push(o, out_val) } } - }) + } + Ok(()) +} + +#[allow(clippy::too_many_lines)] +pub fn evaluate(ctx: Context, expr: &LocExpr) -> Result { + let init_ctx = ctx_tag("init"); + let init_val = val_tag("init"); + + let mut tcvm = TcVM { + vals: Fifo::::with_capacity(1), + ctxs: Fifo::single(1, ctx, init_ctx.clone()), + apply: Fifo::single( + 1, + Eval { + expr: expr.clone(), + in_ctx: init_ctx.clone(), + out_val: init_val.clone(), + }, + apply_tag(), + ), + apply_offset: 0, + ctxs_offset: 0, + vals_offset: 0, + }; + + evaluate_reuse_inlined(&mut tcvm, expr); + + debug_assert!(tcvm.ctxs.is_empty(), "ctx remains: {:?}", tcvm.ctxs.data); + debug_assert!(tcvm.apply.is_empty()); + debug_assert!(tcvm.vals.data.len() == 1); + Ok(tcvm.vals.pop(init_val)) } diff --git a/crates/jrsonnet-evaluator/src/function/mod.rs b/crates/jrsonnet-evaluator/src/function/mod.rs index b5d0fd21..cf36a418 100644 --- a/crates/jrsonnet-evaluator/src/function/mod.rs +++ b/crates/jrsonnet-evaluator/src/function/mod.rs @@ -35,6 +35,15 @@ impl CallLocation<'static> { Self(None) } } +impl Debug for CallLocation<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(_v) = self.0 { + write!(f, "Code") + } else { + write!(f, "Native") + } + } +} /// Represents Jsonnet function defined in code. #[derive(Debug, PartialEq, Trace)] diff --git a/crates/jrsonnet-evaluator/src/obj.rs b/crates/jrsonnet-evaluator/src/obj.rs index 52147d8d..ed3b0422 100644 --- a/crates/jrsonnet-evaluator/src/obj.rs +++ b/crates/jrsonnet-evaluator/src/obj.rs @@ -7,6 +7,7 @@ use std::{ use jrsonnet_gcmodule::{Cc, Trace, Weak}; use jrsonnet_interner::IStr; +use jrsonnet_macros::{tco, tcr}; use jrsonnet_parser::{ExprLocation, Visibility}; use rustc_hash::FxHashMap; @@ -15,7 +16,7 @@ use crate::{ function::CallLocation, gc::{GcHashMap, GcHashSet, TraceBox}, operator::evaluate_add_op, - tb, throw, MaybeUnbound, Result, State, Thunk, Unbound, Val, + tb, throw, MaybeUnbound, Result, State, Tag, TailCallApply, TcVM, Thunk, Unbound, Val, }; #[cfg(not(feature = "exp-preserve-order"))] @@ -389,6 +390,28 @@ impl ObjValue { }) } + #[inline(always)] + pub(crate) fn get_tcvm(&self, tcvm: &mut TcVM, key: IStr, out_val: Tag) -> Result<()> { + self.run_assertions()?; + let cache_key = (key.clone(), None); + if let Some(v) = self.0.value_cache.borrow().get(&cache_key) { + match v { + CacheValue::Cached(v) => { + tcr!(val(out_val, v.clone())); + return Ok(()); + } + CacheValue::NotFound => throw!(NoSuchField(key, vec![])), + CacheValue::Pending => throw!(InfiniteRecursionDetected), + CacheValue::Errored(e) => return Err(e.clone()), + }; + } + self.0 + .value_cache + .borrow_mut() + .insert(cache_key.clone(), CacheValue::Pending); + + todo!() + } pub fn get(&self, key: IStr) -> Result> { self.run_assertions()?; let cache_key = (key.clone(), None); @@ -471,6 +494,20 @@ impl ObjValue { (None, None) => Ok(None), } } + fn evaluate_this_tcvm( + &self, + tcvm: &mut TcVM, + v: &ObjMember, + real_this: Self, + out_val: Tag, + ) { + tcr!(Unbound { + invoke: v.invoke.clone(), + sup: self.0.sup.clone(), + this: Some(real_this), + out_val, + }); + } fn evaluate_this(&self, v: &ObjMember, real_this: Self) -> Result { v.invoke.evaluate(self.0.sup.clone(), Some(real_this)) } diff --git a/crates/jrsonnet-evaluator/src/trace/mod.rs b/crates/jrsonnet-evaluator/src/trace/mod.rs index 5a1f9925..84083911 100644 --- a/crates/jrsonnet-evaluator/src/trace/mod.rs +++ b/crates/jrsonnet-evaluator/src/trace/mod.rs @@ -1,6 +1,6 @@ use std::{ any::Any, - path::{Path, PathBuf}, + path::{Path, PathBuf, Component}, }; use jrsonnet_gcmodule::Trace; @@ -36,6 +36,10 @@ impl PathResolver { if from.is_relative() { return from.to_string_lossy().into_owned(); } + // In case of different disks/different root directory - do not try to diff + if base.components().filter(|c| !matches!(c, Component::RootDir)).next() != from.components().filter(|c| !matches!(c, Component::RootDir)).next() { + return from.to_string_lossy().into_owned(); + } pathdiff::diff_paths(from, base) .expect("base is absolute") .to_string_lossy() diff --git a/crates/jrsonnet-evaluator/src/typed/conversions.rs b/crates/jrsonnet-evaluator/src/typed/conversions.rs index 9d9fb0df..8205346e 100644 --- a/crates/jrsonnet-evaluator/src/typed/conversions.rs +++ b/crates/jrsonnet-evaluator/src/typed/conversions.rs @@ -223,6 +223,22 @@ impl Typed for String { } } +impl Typed for StrValue { + const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Str); + + fn into_untyped(value: Self) -> Result { + Ok(Val::Str(value)) + } + + fn from_untyped(value: Val) -> Result { + ::TYPE.check(&value)?; + match value { + Val::Str(s) => Ok(s), + _ => unreachable!(), + } + } +} + impl Typed for char { const TYPE: &'static ComplexValType = &ComplexValType::Char; diff --git a/crates/jrsonnet-macros/Cargo.toml b/crates/jrsonnet-macros/Cargo.toml index 6311d54e..21eed393 100644 --- a/crates/jrsonnet-macros/Cargo.toml +++ b/crates/jrsonnet-macros/Cargo.toml @@ -13,4 +13,4 @@ proc-macro = true [dependencies] proc-macro2 = "1.0" quote = "1.0" -syn = { version = "1.0", features = ["full"] } +syn = { version = "1.0", features = ["full", "visit", "derive", "visit-mut"] } diff --git a/crates/jrsonnet-macros/src/lib.rs b/crates/jrsonnet-macros/src/lib.rs index e6b63dce..3b75a25c 100644 --- a/crates/jrsonnet-macros/src/lib.rs +++ b/crates/jrsonnet-macros/src/lib.rs @@ -2,13 +2,14 @@ use proc_macro2::TokenStream; use quote::quote; use syn::{ parenthesized, - parse::{Parse, ParseStream}, - parse_macro_input, + parse::{self, Parse, ParseStream}, + parse_macro_input, parse_quote, punctuated::Punctuated, spanned::Spanned, token::{self, Comma}, - Attribute, DeriveInput, Error, FnArg, GenericArgument, Ident, ItemFn, LitStr, Pat, Path, - PathArguments, Result, ReturnType, Token, Type, + visit_mut::VisitMut, + Attribute, DeriveInput, Error, Expr, ExprBlock, ExprCall, ExprStruct, FnArg, GenericArgument, + Ident, ItemFn, LitStr, Pat, Path, PathArguments, Result, ReturnType, Token, Type, }; fn parse_attr(attrs: &[Attribute], ident: I) -> Result> @@ -88,6 +89,9 @@ mod kw { syn::custom_keyword!(rename); syn::custom_keyword!(flatten); syn::custom_keyword!(ok); + + syn::custom_keyword!(ctx); + syn::custom_keyword!(val); } struct EmptyAttr; @@ -651,3 +655,225 @@ fn derive_typed_inner(input: DeriveInput) -> Result { }; }) } + +#[derive(Default)] +struct OutCollector { + out_vals: Vec, + out_ctxs: Vec, +} +impl syn::visit_mut::VisitMut for OutCollector { + fn visit_expr_call_mut(&mut self, call: &mut ExprCall) { + if call.args.len() != 1 { + return; + } + let Expr::Path(p) = call.args.iter().next().unwrap() else { + return; + }; + let Some(def) = p.path.get_ident() else { + return; + }; + match &mut *call.func { + Expr::Path(p) if p.path.is_ident("val") => { + self.out_vals.push(def.clone()); + p.path = parse_quote!(core::convert::identity) + } + Expr::Path(p) if p.path.is_ident("ctx") => { + self.out_ctxs.push(def.clone()); + p.path = parse_quote!(core::convert::identity) + } + _ => return, + } + } +} +enum TcoItem { + InsertCtx(Ident, Expr), + InsertVal(Ident, Expr), + Apply { + val: ExprStruct, + out_vals: Vec, + out_ctxs: Vec, + }, +} +impl Parse for TcoItem { + fn parse(input: ParseStream) -> Result { + if input.peek(kw::ctx) { + input.parse::()?; + let item; + parenthesized!(item in input); + let ident = item.parse()?; + item.parse::()?; + let expr = item.parse()?; + Ok(Self::InsertCtx(ident, expr)) + } else if input.peek(kw::val) { + input.parse::()?; + let item; + parenthesized!(item in input); + let ident = item.parse()?; + item.parse::()?; + let expr = item.parse()?; + Ok(Self::InsertVal(ident, expr)) + } else { + let mut val: ExprStruct = input.parse()?; + let mut collector = OutCollector::default(); + collector.visit_expr_struct_mut(&mut val); + let OutCollector { out_vals, out_ctxs } = collector; + Ok(Self::Apply { + val, + out_ctxs, + out_vals, + }) + } + } +} +impl TcoItem { + fn expand_ops_rev(self, init_rev: &mut Vec, out: &mut Vec) { + use TcoOp::*; + match self { + TcoItem::InsertCtx(n, v) => { + init_rev.push(DeclCtx(n.clone())); + init_rev.push(SetCtx(n, v)); + } + TcoItem::InsertVal(n, v) => { + init_rev.push(DeclVal(n.clone())); + init_rev.push(SetVal(n, v)); + } + TcoItem::Apply { + val, + out_vals, + out_ctxs, + } => { + for n in out_vals.iter() { + init_rev.push(DeclVal(n.clone())); + } + for n in out_ctxs.iter() { + init_rev.push(DeclCtx(n.clone())); + } + out.push(AddApply(val)) + } + } + } +} + +enum TcoOp { + DeclVal(Ident), + DeclCtx(Ident), + SetVal(Ident, Expr), + SetCtx(Ident, Expr), + AddApply(ExprStruct), +} +impl TcoOp { + fn expand(&self, out: &mut Vec) { + out.push(match self { + TcoOp::DeclVal(v) => quote! { + let #v = crate::val_tag(stringify!(#v)); + }, + TcoOp::DeclCtx(v) => quote! { + let #v = crate::ctx_tag(stringify!(#v)); + }, + TcoOp::SetVal(v, e) => quote! {{ + tcvm.vals.push(#e, #v.clone()); + }}, + TcoOp::SetCtx(v, e) => quote! {{ + tcvm.ctxs.push(#e, #v.clone()); + }}, + TcoOp::AddApply(apply) => quote! {{ + tcvm.apply.push(crate::TailCallApply::#apply, crate::apply_tag()); + }}, + }) + } +} + +struct TcoInput { + items: Vec, +} +impl Parse for TcoInput { + fn parse(input: ParseStream) -> Result { + let mut items = Vec::new(); + loop { + if input.is_empty() { + break; + } + let i: TcoItem = input.parse()?; + items.push(i); + if input.peek(Token![,]) { + input.parse::()?; + continue; + } + break; + } + if !input.is_empty() { + return Err(Error::new(input.span(), "unknown statement after input")); + } + Ok(TcoInput { items }) + } +} +impl TcoInput { + fn expand(self, cont: TokenStream) -> TokenStream { + let mut init = Vec::new(); + let mut out = Vec::new(); + + for i in self.items.into_iter().rev() { + i.expand_ops_rev(&mut init, &mut out); + } + + let mut vals = 0usize; + let mut ctxs = 0usize; + let mut applys = 0usize; + for v in init.iter().chain(out.iter()) { + match v { + TcoOp::DeclVal(_) => vals += 1, + TcoOp::DeclCtx(_) => ctxs += 1, + TcoOp::SetVal(_, _) => {} + TcoOp::SetCtx(_, _) => {} + TcoOp::AddApply(_) => applys += 1, + } + } + + let mut run = Vec::new(); + if vals != 0 { + run.push(quote! {tcvm.vals.reserve(#vals);}); + } + if ctxs != 0 { + run.push(quote! {tcvm.ctxs.reserve(#ctxs);}); + } + if applys != 0 { + run.push(quote! {tcvm.apply.reserve(#applys);}); + } + for i in init.iter() { + i.expand(&mut run); + } + for i in out.iter() { + i.expand(&mut run); + } + + quote!({ + #(#run;)*; + #cont + }) + } +} + +#[proc_macro] +pub fn tco(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let item: TcoInput = match syn::parse(item) { + Ok(v) => v, + Err(e) => return e.to_compile_error().into(), + }; + item.expand(quote! {continue;}).into() +} +#[proc_macro] +pub fn tcr(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let item: TcoInput = match syn::parse(item) { + Ok(v) => v, + Err(e) => return e.to_compile_error().into(), + }; + item.expand(quote! {}).into() +} +#[proc_macro] +pub fn tcok(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let item: TcoInput = match syn::parse(item) { + Ok(v) => v, + Err(e) => return e.to_compile_error().into(), + }; + item.expand(quote! {return Ok(())}).into() +} diff --git a/crates/jrsonnet-parser/src/expr.rs b/crates/jrsonnet-parser/src/expr.rs index a9abe79f..55bab67d 100644 --- a/crates/jrsonnet-parser/src/expr.rs +++ b/crates/jrsonnet-parser/src/expr.rs @@ -179,7 +179,8 @@ impl Deref for ParamsDesc { #[cfg_attr(feature = "structdump", derive(Codegen))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, PartialEq, Trace)] +// FIXME: remove Clone +#[derive(Debug, PartialEq, Trace, Clone)] pub struct ArgsDesc { pub unnamed: Vec, pub named: Vec<(IStr, LocExpr)>, diff --git a/crates/jrsonnet-stdlib/Cargo.toml b/crates/jrsonnet-stdlib/Cargo.toml index e4592f8f..6e8f2b49 100644 --- a/crates/jrsonnet-stdlib/Cargo.toml +++ b/crates/jrsonnet-stdlib/Cargo.toml @@ -42,6 +42,9 @@ serde_json = "1.0" serde_yaml_with_quirks = "0.8.24" num-bigint = { version = "0.4.3", optional = true } +regex = "1.8.4" +lru = "0.10.0" +rustc-hash = "1.1.0" [build-dependencies] jrsonnet-parser.workspace = true diff --git a/crates/jrsonnet-stdlib/src/lib.rs b/crates/jrsonnet-stdlib/src/lib.rs index f28e6d5c..b1a8cc86 100644 --- a/crates/jrsonnet-stdlib/src/lib.rs +++ b/crates/jrsonnet-stdlib/src/lib.rs @@ -44,6 +44,8 @@ mod sets; pub use sets::*; mod compat; pub use compat::*; +mod regex; +pub use crate::regex::*; pub fn stdlib_uncached(settings: Rc>) -> ObjValue { let mut builder = ObjValueBuilder::new(); @@ -154,6 +156,8 @@ pub fn stdlib_uncached(settings: Rc>) -> ObjValue { // Sets ("setMember", builtin_set_member::INST), ("setInter", builtin_set_inter::INST), + // Regex + ("regexQuoteMeta", builtin_regex_quote_meta::INST), // Compat ("__compare", builtin___compare::INST), ] @@ -187,6 +191,37 @@ pub fn stdlib_uncached(settings: Rc>) -> ObjValue { .value(Val::Func(FuncVal::builtin(builtin_trace { settings }))) .expect("no conflict"); + // Regex + let regex_cache = RegexCache::default(); + builder + .member("regexFullMatch".into()) + .hide() + .value(Val::Func(FuncVal::builtin(builtin_regex_full_match { + cache: regex_cache.clone(), + }))) + .expect("no conflict"); + builder + .member("regexPartialMatch".into()) + .hide() + .value(Val::Func(FuncVal::builtin(builtin_regex_partial_match { + cache: regex_cache.clone(), + }))) + .expect("no conflict"); + builder + .member("regexReplace".into()) + .hide() + .value(Val::Func(FuncVal::builtin(builtin_regex_replace { + cache: regex_cache.clone(), + }))) + .expect("no conflict"); + builder + .member("regexGlobalReplace".into()) + .hide() + .value(Val::Func(FuncVal::builtin(builtin_regex_global_replace { + cache: regex_cache.clone(), + }))) + .expect("no conflict"); + builder .member("id".into()) .hide() diff --git a/crates/jrsonnet-stdlib/src/misc.rs b/crates/jrsonnet-stdlib/src/misc.rs index df11e60e..84158a96 100644 --- a/crates/jrsonnet-stdlib/src/misc.rs +++ b/crates/jrsonnet-stdlib/src/misc.rs @@ -55,10 +55,11 @@ pub fn builtin_native(this: &builtin_native, x: IStr) -> Val { pub fn builtin_trace( this: &builtin_trace, loc: CallLocation, - str: IStr, + str: Val, rest: Thunk, ) -> Result { - this.settings.borrow().trace_printer.print_trace(loc, str); + use jrsonnet_evaluator::error::ResultExt; + this.settings.borrow().trace_printer.print_trace(loc, str.to_string().description("std.trace message toString")?); rest.evaluate() } diff --git a/crates/jrsonnet-stdlib/src/regex.rs b/crates/jrsonnet-stdlib/src/regex.rs new file mode 100644 index 00000000..27482580 --- /dev/null +++ b/crates/jrsonnet-stdlib/src/regex.rs @@ -0,0 +1,134 @@ +use std::{cell::RefCell, hash::BuildHasherDefault, num::NonZeroUsize, rc::Rc}; + +use ::regex::Regex; +use jrsonnet_evaluator::{ + error::{ErrorKind::*, Result}, + val::StrValue, + IStr, ObjValueBuilder, Val, +}; +use jrsonnet_macros::builtin; +use lru::LruCache; +use rustc_hash::FxHasher; + +pub struct RegexCacheInner { + cache: RefCell, BuildHasherDefault>>, +} +impl Default for RegexCacheInner { + fn default() -> Self { + Self { + cache: RefCell::new(LruCache::with_hasher( + NonZeroUsize::new(20).unwrap(), + BuildHasherDefault::default(), + )), + } + } +} +pub type RegexCache = Rc; +impl RegexCacheInner { + fn parse(&self, pattern: IStr) -> Result> { + let mut cache = self.cache.borrow_mut(); + if let Some(found) = cache.get(&pattern) { + return Ok(found.clone()); + } + let regex = Regex::new(&pattern) + .map_err(|e| RuntimeError(format!("regex parse failed: {e}").into()))?; + let regex = Rc::new(regex); + cache.push(pattern, regex.clone()); + Ok(regex) + } +} + +pub fn regex_match_inner(regex: &Regex, str: String) -> Result { + let mut out = ObjValueBuilder::with_capacity(3); + + let mut captures = Vec::with_capacity(regex.captures_len()); + let mut named_captures = ObjValueBuilder::with_capacity(regex.capture_names().len()); + + let Some(captured) = regex.captures(&str) else { + return Ok(Val::Null) + }; + + for ele in captured.iter().skip(1) { + if let Some(ele) = ele { + captures.push(Val::Str(StrValue::Flat(ele.as_str().into()))) + } else { + captures.push(Val::Str(StrValue::Flat(IStr::empty()))) + } + } + for (i, name) in regex + .capture_names() + .skip(1) + .enumerate() + .flat_map(|(i, v)| Some((i, v?))) + { + let capture = captures[i].clone(); + named_captures.member(name.into()).value(capture)?; + } + + out.member("string".into()) + .value_unchecked(Val::Str(captured.get(0).unwrap().as_str().into())); + out.member("captures".into()) + .value_unchecked(Val::Arr(captures.into())); + out.member("namedCaptures".into()) + .value_unchecked(Val::Obj(named_captures.build())); + + Ok(Val::Obj(out.build())) +} + +#[builtin(fields( + cache: RegexCache, +))] +pub fn builtin_regex_partial_match( + this: &builtin_regex_partial_match, + pattern: IStr, + str: String, +) -> Result { + let regex = this.cache.parse(pattern)?; + regex_match_inner(®ex, str) +} + +#[builtin(fields( + cache: RegexCache, +))] +pub fn builtin_regex_full_match( + this: &builtin_regex_full_match, + pattern: StrValue, + str: String, +) -> Result { + let pattern = format!("^{pattern}$").into(); + let regex = this.cache.parse(pattern)?; + regex_match_inner(®ex, str) +} + +#[builtin] +pub fn builtin_regex_quote_meta(pattern: String) -> String { + regex::escape(&pattern) +} + +#[builtin(fields( + cache: RegexCache, +))] +pub fn builtin_regex_replace( + this: &builtin_regex_replace, + str: String, + pattern: IStr, + to: String, +) -> Result { + let regex = this.cache.parse(pattern)?; + let replaced = regex.replace(&str, to); + Ok(replaced.to_string()) +} + +#[builtin(fields( + cache: RegexCache, +))] +pub fn builtin_regex_global_replace( + this: &builtin_regex_global_replace, + str: String, + pattern: IStr, + to: String, +) -> Result { + let regex = this.cache.parse(pattern)?; + let replaced = regex.replace_all(&str, to); + Ok(replaced.to_string()) +} diff --git a/flake.nix b/flake.nix index afae13da..e4d5b0cd 100644 --- a/flake.nix +++ b/flake.nix @@ -73,7 +73,7 @@ quick = true; jrsonnetVariants = [ { drv = jrsonnet; name = "current"; } - { drv = jrsonnet-nightly; name = "current-nightly"; } + # { drv = jrsonnet-nightly; name = "current-nightly"; } { drv = jrsonnet-release; name = "before-str-extend"; } ]; }; diff --git a/nix/jrsonnet-release.nix b/nix/jrsonnet-release.nix index 485c4311..3bc86889 100644 --- a/nix/jrsonnet-release.nix +++ b/nix/jrsonnet-release.nix @@ -8,10 +8,10 @@ rustPlatform.buildRustPackage rec { src = fetchFromGitHub { owner = "CertainLach"; repo = pname; - rev = "ccafbf79faf649e0990e277c061be9a2b62ad84c"; - hash = "sha256-LTDIJY9wfv4h5e3/5bONHHBS0qMLKdY6bk6ajKEjG7A="; + rev = "777cdf5396004dd5e9447da82c9f081066729d91"; + hash = "sha256-xfNKSjOZM77NB3mJkTY9RC+ClX5KLyk/Q774vWK0goc="; }; - cargoHash = "sha256-LBlJWE3LcbOe/uu19TbLhbUhBKy8DzuDCP4XyuAEmUk="; + cargoHash = "sha256-EJQbOmAD6O5l9YKgd/nFD4Df3PfETQ/ffm2YxxxxW1U="; cargoTestFlags = [ "--package=jrsonnet --features=mimalloc,legacy-this-file" ]; cargoBuildFlags = [ "--package=jrsonnet --features=mimalloc,legacy-this-file" ];