difftreelog
feat(evaluator) tailstrict calls
in: master
4 files changed
crates/jsonnet-evaluator/src/error.rsdiffbeforeafterboth--- a/crates/jsonnet-evaluator/src/error.rs
+++ b/crates/jsonnet-evaluator/src/error.rs
@@ -9,6 +9,8 @@
RuntimeError(String),
StackOverflow,
+ FractionalIndex,
+ DivisionByZero,
}
#[derive(Clone, Debug)]
crates/jsonnet-evaluator/src/evaluate.rsdiffbeforeafterboth--- a/crates/jsonnet-evaluator/src/evaluate.rs
+++ b/crates/jsonnet-evaluator/src/evaluate.rs
@@ -135,7 +135,12 @@
// Num X Num
(Val::Num(v1), BinaryOpType::Mul, Val::Num(v2)) => Val::Num(v1 * v2),
- (Val::Num(v1), BinaryOpType::Div, Val::Num(v2)) => Val::Num(v1 / v2),
+ (Val::Num(v1), BinaryOpType::Div, Val::Num(v2)) => {
+ if *v2 <= f64::EPSILON {
+ create_error(crate::Error::DivisionByZero)?
+ }
+ Val::Num(v1 / v2)
+ }
(Val::Num(v1), BinaryOpType::Sub, Val::Num(v2)) => Val::Num(v1 - v2),
@@ -305,6 +310,7 @@
})
}
+#[inline(always)]
pub fn evaluate(context: Context, expr: &LocExpr) -> Result<Val> {
use Expr::*;
let locexpr = expr.clone();
@@ -356,11 +362,15 @@
create_error(crate::Error::NoSuchField(s))?
}
}
- (Val::Arr(v), Val::Num(n)) => v
- .get(n as usize)
- .unwrap_or_else(|| panic!("out of bounds"))
- .clone()
- .unwrap_if_lazy()?,
+ (Val::Arr(v), Val::Num(n)) => {
+ if n.fract() > f64::EPSILON {
+ create_error(crate::Error::FractionalIndex)?
+ }
+ v.get(n as usize)
+ .unwrap_or_else(|| panic!("out of bounds"))
+ .clone()
+ .unwrap_if_lazy()?
+ }
(Val::Str(s), Val::Num(n)) => {
Val::Str(s.chars().skip(n as usize).take(1).collect())
}
@@ -511,29 +521,52 @@
panic!("bad trace call");
}
}
+ ("std", "pow") => {
+ assert_eq!(args.len(), 2);
+ if let (Val::Num(a), Val::Num(b)) = (
+ evaluate(context.clone(), &args[0].1)?,
+ evaluate(context, &args[1].1)?,
+ ) {
+ Val::Num(a.powf(b))
+ } else {
+ panic!("bad pow call");
+ }
+ }
(ns, name) => panic!("Intristic not found: {}.{}", ns, name),
},
- Val::Func(f) => push(locexpr, "function call".to_owned(), || {
- f.evaluate(
- args.clone()
- .into_iter()
- .map(move |a| {
- (
- a.clone().0,
- if *tailstrict {
- Val::Lazy(LazyVal::new_resolved(
- evaluate(context.clone(), &a.1).unwrap(),
+ Val::Func(f) => {
+ let body = #[inline(always)]
+ || {
+ f.evaluate(
+ args.clone()
+ .into_iter()
+ .map(
+ #[inline(always)]
+ move |a| {
+ Ok((
+ a.clone().0,
+ if *tailstrict {
+ Val::Lazy(LazyVal::new_resolved(evaluate(
+ context.clone(),
+ &a.1,
+ )?))
+ } else {
+ Val::Lazy(lazy_val!(
+ closure!(clone context, clone a, || evaluate(context.clone(), &a.clone().1))
+ ))
+ },
))
- } else {
- Val::Lazy(lazy_val!(
- closure!(clone context, clone a, || evaluate(context.clone(), &a.clone().1))
- ))
},
)
- })
- .collect(),
- )
- })?,
+ .collect::<Result<Vec<_>>>()?,
+ )
+ };
+ if *tailstrict {
+ body()?
+ } else {
+ push(locexpr, "function call".to_owned(), body)?
+ }
+ }
_ => panic!("{:?} is not a function", value),
}
}
@@ -578,9 +611,7 @@
)?
} else {
match cond_else {
- Some(v) => push(v.clone(), "if condition 'else' branch".to_owned(), || {
- evaluate(context, v)
- })?,
+ Some(v) => evaluate(context, v)?,
None => Val::Null,
}
}
crates/jsonnet-evaluator/src/lib.rsdiffbeforeafterboth1#![feature(box_syntax, box_patterns)]2#![feature(type_alias_impl_trait)]3#![feature(debug_non_exhaustive)]4#![allow(macro_expanded_macro_exports_accessed_by_absolute_paths)]5mod ctx;6mod dynamic;7mod error;8mod evaluate;9mod function;10mod obj;11mod val;1213pub use ctx::*;14pub use dynamic::*;15pub use error::*;16pub use evaluate::*;17use jsonnet_parser::*;18pub use obj::*;19use std::{cell::RefCell, collections::HashMap, fmt::Debug, path::PathBuf, rc::Rc};20pub use val::*;2122rc_fn_helper!(23 Binding,24 binding,25 dyn Fn(Option<ObjValue>, Option<ObjValue>) -> Result<Val>26);27rc_fn_helper!(FunctionRhs, function_rhs, dyn Fn(Context) -> Result<Val>);28rc_fn_helper!(29 FunctionDefault,30 function_default,31 dyn Fn(Context, LocExpr) -> Result<Val>32);3334#[derive(Clone)]35pub enum LazyBinding {36 Bindable(Rc<dyn Fn(Option<ObjValue>, Option<ObjValue>) -> Result<LazyVal>>),37 Bound(LazyVal),38}3940impl Debug for LazyBinding {41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {42 write!(f, "LazyBinding")43 }44}45impl LazyBinding {46 pub fn evaluate(&self, this: Option<ObjValue>, super_obj: Option<ObjValue>) -> Result<LazyVal> {47 match self {48 LazyBinding::Bindable(v) => v(this, super_obj),49 LazyBinding::Bound(v) => Ok(v.clone()),50 }51 }52}5354pub struct FileData(String, LocExpr, Option<Val>);55#[derive(Default)]56pub struct EvaluationStateInternals {57 /// Used for stack-overflows and stacktraces58 stack: RefCell<Vec<StackTraceElement>>,59 /// Contains file source codes and evaluated results for imports and pretty60 /// printing stacktraces61 files: RefCell<HashMap<PathBuf, FileData>>,62 globals: RefCell<HashMap<String, Val>>,63}6465thread_local! {66 pub static EVAL_STATE: RefCell<Option<EvaluationState>> = RefCell::new(None)67}68pub(crate) fn with_state<T>(f: impl FnOnce(&EvaluationState) -> T) -> T {69 EVAL_STATE.with(|s| f(s.borrow().as_ref().unwrap()))70}71pub(crate) fn create_error<T>(err: Error) -> Result<T> {72 with_state(|s| s.error(err))73}74pub(crate) fn push<T>(e: LocExpr, comment: String, f: impl FnOnce() -> Result<T>) -> Result<T> {75 with_state(|s| s.push(e, comment, f))76}7778#[derive(Default, Clone)]79pub struct EvaluationState(Rc<EvaluationStateInternals>);80impl EvaluationState {81 pub fn add_file(&self, name: PathBuf, code: String) -> std::result::Result<(), ParseError> {82 self.0.files.borrow_mut().insert(83 name.clone(),84 FileData(85 code.clone(),86 parse(87 &code,88 &ParserSettings {89 file_name: name,90 loc_data: true,91 },92 )?,93 None,94 ),95 );9697 Ok(())98 }99 pub fn add_parsed_file(100 &self,101 name: PathBuf,102 code: String,103 parsed: LocExpr,104 ) -> std::result::Result<(), ()> {105 self.0106 .files107 .borrow_mut()108 .insert(name, FileData(code, parsed, None));109110 Ok(())111 }112 pub fn get_source(&self, name: &PathBuf) -> Option<String> {113 let ro_map = self.0.files.borrow();114 ro_map.get(name).map(|value| value.0.clone())115 }116 pub fn evaluate_file(&self, name: &PathBuf) -> Result<Val> {117 self.begin_state();118 let expr: LocExpr = {119 let ro_map = self.0.files.borrow();120 let value = ro_map121 .get(name)122 .unwrap_or_else(|| panic!("file not added: {:?}", name));123 if value.2.is_some() {124 return Ok(value.2.clone().unwrap());125 }126 value.1.clone()127 };128 let value = evaluate(self.create_default_context()?, &expr)?;129 {130 self.0131 .files132 .borrow_mut()133 .get_mut(name)134 .unwrap()135 .2136 .replace(value.clone());137 }138 self.end_state();139 Ok(value)140 }141142 pub fn parse_evaluate_raw(&self, code: &str) -> Result<Val> {143 let parsed = parse(144 &code,145 &ParserSettings {146 file_name: PathBuf::from("raw.jsonnet"),147 loc_data: true,148 },149 );150 self.begin_state();151 let value = evaluate(self.create_default_context()?, &parsed.unwrap());152 self.end_state();153 value154 }155156 pub fn add_global(&self, name: String, value: Val) {157 self.0.globals.borrow_mut().insert(name, value);158 }159160 pub fn add_stdlib(&self) {161 self.begin_state();162 use jsonnet_stdlib::STDLIB_STR;163 if cfg!(feature = "serialized-stdlib") {164 self.add_parsed_file(165 PathBuf::from("std.jsonnet"),166 STDLIB_STR.to_owned(),167 bincode::deserialize(include_bytes!(concat!(env!("OUT_DIR"), "/stdlib.bincode")))168 .expect("deserialize stdlib"),169 )170 .unwrap();171 } else {172 self.add_file(PathBuf::from("std.jsonnet"), STDLIB_STR.to_owned())173 .unwrap();174 }175 let val = self.evaluate_file(&PathBuf::from("std.jsonnet")).unwrap();176 self.add_global("std".to_owned(), val);177 self.end_state();178 }179180 pub fn create_default_context(&self) -> Result<Context> {181 let globals = self.0.globals.borrow();182 let mut new_bindings: HashMap<String, LazyBinding> = HashMap::new();183 for (name, value) in globals.iter() {184 new_bindings.insert(185 name.clone(),186 LazyBinding::Bound(resolved_lazy_val!(value.clone())),187 );188 }189 Context::new().extend(new_bindings, None, None, None)190 }191192 pub fn push<T>(&self, e: LocExpr, comment: String, f: impl FnOnce() -> Result<T>) -> Result<T> {193 {194 let mut stack = self.0.stack.borrow_mut();195 if stack.len() > 500 {196 drop(stack);197 return self.error(Error::StackOverflow);198 } else {199 stack.push(StackTraceElement(e, comment));200 }201 }202 let result = f();203 self.0.stack.borrow_mut().pop();204 result205 }206 pub fn print_stack_trace(&self) {207 for e in self.stack_trace().0 {208 println!("{:?} - {:?}", e.0, e.1)209 }210 }211 pub fn stack_trace(&self) -> StackTrace {212 StackTrace(self.0.stack.borrow().iter().rev().cloned().collect())213 }214 pub fn error<T>(&self, err: Error) -> Result<T> {215 Err(LocError(err, self.stack_trace()))216 }217218 fn begin_state(&self) {219 EVAL_STATE.with(|v| v.borrow_mut().replace(self.clone()));220 }221 fn end_state(&self) {222 EVAL_STATE.with(|v| v.borrow_mut().take());223 }224}225226#[cfg(test)]227pub mod tests {228 use super::Val;229 use crate::EvaluationState;230 use jsonnet_parser::*;231 use std::path::PathBuf;232233 #[test]234 fn eval_state_stacktrace() {235 let state = EvaluationState::default();236 state237 .push(238 loc_expr!(239 Expr::Num(0.0),240 true,241 (PathBuf::from("test1.jsonnet"), 10, 20)242 ),243 "outer".to_owned(),244 || {245 state.push(246 loc_expr!(247 Expr::Num(0.0),248 true,249 (PathBuf::from("test2.jsonnet"), 30, 40)250 ),251 "inner".to_owned(),252 || {253 state.print_stack_trace();254 Ok(())255 },256 )?;257 Ok(())258 },259 )260 .unwrap();261 }262263 #[test]264 fn eval_state_standard() {265 let state = EvaluationState::default();266 state.add_stdlib();267 assert_eq!(268 state269 .parse_evaluate_raw(r#"std.assertEqual(std.base64("test"), "dGVzdA==")"#)270 .unwrap(),271 Val::Bool(true)272 );273 }274275 macro_rules! eval {276 ($str: expr) => {277 evaluate(278 Context::new(),279 EvaluationState::default(),280 &parse(281 $str,282 &ParserSettings {283 loc_data: true,284 file_name: "test.jsonnet".to_owned(),285 },286 )287 .unwrap(),288 )289 };290 }291292 macro_rules! eval_stdlib {293 ($str: expr) => {{294 let std = "local std = ".to_owned() + jsonnet_stdlib::STDLIB_STR + ";";295 evaluate(296 Context::new(),297 EvaluationState::default(),298 &parse(299 &(std + $str),300 &ParserSettings {301 loc_data: true,302 file_name: "test.jsonnet".to_owned(),303 },304 )305 .unwrap(),306 )307 }};308 }309310 macro_rules! assert_eval {311 ($str: expr) => {312 assert_eq!(313 evaluate(314 Context::new(),315 EvaluationState::default(),316 &parse(317 $str,318 &ParserSettings {319 loc_data: true,320 file_name: "test.jsonnet".to_owned(),321 }322 )323 .unwrap()324 ),325 Val::Bool(true)326 )327 };328 }329 macro_rules! assert_json {330 ($str: expr, $out: expr) => {331 assert_eq!(332 format!(333 "{}",334 evaluate(335 Context::new(),336 EvaluationState::default(),337 &parse(338 $str,339 &ParserSettings {340 loc_data: true,341 file_name: "test.jsonnet".to_owned(),342 }343 )344 .unwrap()345 )346 ),347 $out348 )349 };350 }351 macro_rules! assert_json_stdlib {352 ($str: expr, $out: expr) => {353 assert_eq!(format!("{}", eval_stdlib!($str)), $out)354 };355 }356 macro_rules! assert_eval_neg {357 ($str: expr) => {358 assert_eq!(359 evaluate(360 Context::new(),361 EvaluationState::default(),362 &parse(363 $str,364 &ParserSettings {365 loc_data: true,366 file_name: "test.jsonnet".to_owned(),367 }368 )369 .unwrap()370 ),371 Val::Bool(false)372 )373 };374 }375376 /*377 /// Sanity checking, before trusting to another tests378 #[test]379 fn equality_operator() {380 assert_eval!("2 == 2");381 assert_eval_neg!("2 != 2");382 assert_eval!("2 != 3");383 assert_eval_neg!("2 == 3");384 assert_eval!("'Hello' == 'Hello'");385 assert_eval_neg!("'Hello' != 'Hello'");386 assert_eval!("'Hello' != 'World'");387 assert_eval_neg!("'Hello' == 'World'");388 }389390 #[test]391 fn math_evaluation() {392 assert_eval!("2 + 2 * 2 == 6");393 assert_eval!("3 + (2 + 2 * 2) == 9");394 }395396 #[test]397 fn string_concat() {398 assert_eval!("'Hello' + 'World' == 'HelloWorld'");399 assert_eval!("'Hello' * 3 == 'HelloHelloHello'");400 assert_eval!("'Hello' + 'World' * 3 == 'HelloWorldWorldWorld'");401 }402403 #[test]404 fn local() {405 assert_eval!("local a = 2; local b = 3; a + b == 5");406 assert_eval!("local a = 1, b = a + 1; a + b == 3");407 assert_eval!("local a = 1; local a = 2; a == 2");408 }409410 #[test]411 fn object_lazyness() {412 assert_json!("local a = {a:error 'test'}; {}", r#"{}"#);413 }414415 #[test]416 fn object_inheritance() {417 assert_json!("{a: self.b} + {b:3}", r#"{"a":3,"b":3}"#);418 }419420 #[test]421 fn test_object() {422 assert_json!("{a:2}", r#"{"a":2}"#);423 assert_json!("{a:2+2}", r#"{"a":4}"#);424 assert_json!("{a:2}+{b:2}", r#"{"a":2,"b":2}"#);425 assert_json!("{b:3}+{b:2}", r#"{"b":2}"#);426 assert_json!("{b:3}+{b+:2}", r#"{"b":5}"#);427 assert_json!("local test='a'; {[test]:2}", r#"{"a":2}"#);428 assert_json!(429 r#"430 {431 name: "Alice",432 welcome: "Hello " + self.name + "!",433 }434 "#,435 r#"{"name":"Alice","welcome":"Hello Alice!"}"#436 );437 assert_json!(438 r#"439 {440 name: "Alice",441 welcome: "Hello " + self.name + "!",442 } + {443 name: "Bob"444 }445 "#,446 r#"{"name":"Bob","welcome":"Hello Bob!"}"#447 );448 }449450 #[test]451 fn functions() {452 assert_json!(r#"local a = function(b, c = 2) b + c; a(2)"#, "4");453 assert_json!(454 r#"local a = function(b, c = "Dear") b + c + d, d = "World"; a("Hello")"#,455 r#""HelloDearWorld""#456 );457 }458459 #[test]460 fn local_methods() {461 assert_json!(r#"local a(b, c = 2) = b + c; a(2)"#, "4");462 assert_json!(463 r#"local a(b, c = "Dear") = b + c + d, d = "World"; a("Hello")"#,464 r#""HelloDearWorld""#465 );466 }467468 #[test]469 fn object_locals() {470 assert_json!(r#"{local a = 3, b: a}"#, r#"{"b":3}"#);471 assert_json!(r#"{local a = 3, local c = a, b: c}"#, r#"{"b":3}"#);472 assert_json!(473 r#"{local a = function (b) {[b]:4}, test: a("test")}"#,474 r#"{"test":{"test":4}}"#475 );476 }477478 #[test]479 fn direct_self() {480 println!(481 "{:#?}",482 eval!(483 r#"484 {485 local me = self,486 a: 3,487 b(): me.a,488 }489 "#490 )491 );492 }493494 #[test]495 fn indirect_self() {496 // `self` assigned to `me` was lost when being497 // referenced from field498 eval_stdlib!(499 r#"{500 local me = self,501 a: 3,502 b: me.a,503 }.b"#504 );505 }506507 // We can't trust other tests (And official jsonnet testsuite), if assert is not working correctly508 #[test]509 fn std_assert_ok() {510 eval_stdlib!("std.assertEqual(4.5 << 2, 16)");511 }512513 #[test]514 #[should_panic]515 fn std_assert_failure() {516 eval_stdlib!("std.assertEqual(4.5 << 2, 15)");517 }518519 #[test]520 fn string_is_string() {521 assert_eq!(522 eval_stdlib!("local arr = 'hello'; (!std.isArray(arr)) && (!std.isString(arr))"),523 Val::Bool(false)524 );525 }526527 #[test]528 fn base64_works() {529 assert_json_stdlib!(r#"std.base64("test")"#, r#""dGVzdA==""#);530 }531532 #[test]533 fn utf8_chars() {534 assert_json_stdlib!(535 r#"local c="😎";{c:std.codepoint(c),l:std.length(c)}"#,536 r#"{"c":128526,"l":1}"#537 )538 }539540 #[test]541 fn json() {542 assert_json_stdlib!(543 r#"std.manifestJsonEx({a:3, b:4, c:6},"")"#,544 r#""{\n"a": 3,\n"b": 4,\n"c": 6\n}""#545 );546 }547548 #[test]549 fn test() {550 assert_json_stdlib!(551 r#"[[a, b] for a in [1,2,3] for b in [4,5,6]]"#,552 "[[1,4],[1,5],[1,6],[2,4],[2,5],[2,6],[3,4],[3,5],[3,6]]"553 );554 }555556 #[test]557 fn sjsonnet() {558 eval!(559 r#"560 local x0 = {k: 1};561 local x1 = {k: x0.k + x0.k};562 local x2 = {k: x1.k + x1.k};563 local x3 = {k: x2.k + x2.k};564 local x4 = {k: x3.k + x3.k};565 local x5 = {k: x4.k + x4.k};566 local x6 = {k: x5.k + x5.k};567 local x7 = {k: x6.k + x6.k};568 local x8 = {k: x7.k + x7.k};569 local x9 = {k: x8.k + x8.k};570 local x10 = {k: x9.k + x9.k};571 local x11 = {k: x10.k + x10.k};572 local x12 = {k: x11.k + x11.k};573 local x13 = {k: x12.k + x12.k};574 local x14 = {k: x13.k + x13.k};575 local x15 = {k: x14.k + x14.k};576 local x16 = {k: x15.k + x15.k};577 local x17 = {k: x16.k + x16.k};578 local x18 = {k: x17.k + x17.k};579 local x19 = {k: x18.k + x18.k};580 local x20 = {k: x19.k + x19.k};581 local x21 = {k: x20.k + x20.k};582 x21.k583 "#584 );585 }586 */587}crates/jsonnet-evaluator/src/val.rsdiffbeforeafterboth--- a/crates/jsonnet-evaluator/src/val.rs
+++ b/crates/jsonnet-evaluator/src/val.rs
@@ -78,6 +78,8 @@
}
impl FuncDesc {
// TODO: Check for unset variables
+ /// This function is always inlined to make tailstrict work
+ #[inline(always)]
pub fn evaluate(&self, args: Vec<(Option<String>, Val)>) -> Result<Val> {
let mut new_bindings: HashMap<String, LazyBinding> = HashMap::new();
let future_ctx = Context::new_future();