git.delta.rocks / jrsonnet / refs/commits / 0f5ef44c0f70

difftreelog

feat experimental null-coaelse operator

Yaroslav Bolyukin2023-07-26parent: #3302e38.patch.diff
in: master

6 files changed

modifiedcrates/jrsonnet-evaluator/Cargo.tomldiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/Cargo.toml
+++ b/crates/jrsonnet-evaluator/Cargo.toml
@@ -24,6 +24,8 @@
 exp-object-iteration = []
 # Bigint type
 exp-bigint = ["num-bigint", "jrsonnet-types/exp-bigint"]
+# obj?.field, obj?.['field']
+exp-null-coaelse = ["jrsonnet-parser/exp-null-coaelse"]
 
 # Improves performance, and implements some useful things using nightly-only features
 nightly = ["hashbrown/nightly"]
modifiedcrates/jrsonnet-evaluator/src/evaluate/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/evaluate/mod.rs
+++ b/crates/jrsonnet-evaluator/src/evaluate/mod.rs
@@ -446,7 +446,12 @@
 			|| format!("variable <{name}> access"),
 			|| ctx.binding(name.clone())?.evaluate(),
 		)?,
-		Index(LocExpr(v, _), index) if matches!(&**v, Expr::Literal(LiteralType::Super)) => {
+		Index {
+			indexable: LocExpr(v, _),
+			index,
+			#[cfg(feature = "exp-null-coaelse")]
+			null_coaelse,
+		} if matches!(&**v, Expr::Literal(LiteralType::Super)) => {
 			let name = evaluate(ctx.clone(), index)?;
 			let Val::Str(name) = name else {
 				throw!(ValueIndexMustBeTypeGot(
@@ -455,17 +460,37 @@
 					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")
+			let Some(super_obj) = ctx.super_obj() else {
+				throw!(NoSuperFound)
+			};
+			let this = ctx
+				.this()
+				.expect("no this found, while super present, should not happen");
+			let key = name.into_flat();
+			match super_obj.get_for(key.clone(), this.clone())? {
+				Some(v) => v,
+				#[cfg(feature = "exp-null-coaelse")]
+				None if *null_coaelse => Val::Null,
+				None => {
+					let suggestions = suggest_object_fields(super_obj, key.clone());
+
+					throw!(NoSuchField(key, suggestions))
+				}
+			}
 		}
-		Index(value, index) => match (evaluate(ctx.clone(), value)?, evaluate(ctx, index)?) {
+		Index {
+			indexable,
+			index,
+			#[cfg(feature = "exp-null-coaelse")]
+			null_coaelse,
+		} => match (evaluate(ctx.clone(), indexable)?, 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),
+					#[cfg(feature = "exp-null-coaelse")]
+					Ok(None) if *null_coaelse => Ok(Val::Null),
 					Ok(None) => {
 						let suggestions = suggest_object_fields(&v, key.clone().into_flat());
 
@@ -514,6 +539,8 @@
 				ValType::Num,
 				n.value_type(),
 			)),
+			#[cfg(feature = "exp-null-coaelse")]
+			(Val::Null, _) if *null_coaelse => Val::Null,
 
 			(v, _) => throw!(CantIndexInto(v.value_type())),
 		},
modifiedcrates/jrsonnet-evaluator/src/evaluate/operator.rsdiffbeforeafterboth
before · crates/jrsonnet-evaluator/src/evaluate/operator.rs
1use std::cmp::Ordering;23use jrsonnet_parser::{BinaryOpType, LocExpr, UnaryOpType};45use crate::{6	arr::ArrValue,7	error::ErrorKind::*,8	evaluate,9	stdlib::std_format,10	throw,11	typed::Typed,12	val::{equals, StrValue},13	Context, Result, Val,14};1516pub fn evaluate_unary_op(op: UnaryOpType, b: &Val) -> Result<Val> {17	use UnaryOpType::*;18	use Val::*;19	Ok(match (op, b) {20		(Not, Bool(v)) => Bool(!v),21		(Minus, Num(n)) => Num(-*n),22		(BitNot, Num(n)) => Num(f64::from(!(*n as i32))),23		(op, o) => throw!(UnaryOperatorDoesNotOperateOnType(op, o.value_type())),24	})25}2627pub fn evaluate_add_op(a: &Val, b: &Val) -> Result<Val> {28	use Val::*;29	Ok(match (a, b) {30		(Str(v1), Str(v2)) => Str(StrValue::concat(v1.clone(), v2.clone())),3132		// Can't use generic json serialization way, because it depends on number to string concatenation (std.jsonnet:890)33		(Num(a), Str(b)) => Str(StrValue::Flat(format!("{a}{b}").into())),34		(Str(a), Num(b)) => Str(StrValue::Flat(format!("{a}{b}").into())),3536		(Str(a), o) | (o, Str(a)) if a.is_empty() => {37			Val::Str(StrValue::Flat(o.clone().to_string()?))38		}39		(Str(a), o) => Str(StrValue::Flat(40			format!("{a}{}", o.clone().to_string()?).into(),41		)),42		(o, Str(a)) => Str(StrValue::Flat(43			format!("{}{a}", o.clone().to_string()?).into(),44		)),4546		(Obj(v1), Obj(v2)) => Obj(v2.extend_from(v1.clone())),47		(Arr(a), Arr(b)) => Val::Arr(ArrValue::extended(a.clone(), b.clone())),4849		(Num(v1), Num(v2)) => Val::new_checked_num(v1 + v2)?,50		#[cfg(feature = "exp-bigint")]51		(BigInt(a), BigInt(b)) => BigInt(Box::new((&**a).clone() + (&**b).clone())),52		_ => throw!(BinaryOperatorDoesNotOperateOnValues(53			BinaryOpType::Add,54			a.value_type(),55			b.value_type(),56		)),57	})58}5960pub fn evaluate_mod_op(a: &Val, b: &Val) -> Result<Val> {61	use Val::*;62	match (a, b) {63		(Num(a), Num(b)) => {64			if *b == 0.0 {65				throw!(DivisionByZero)66			}67			Ok(Num(a % b))68		}69		(Str(str), vals) => {70			String::into_untyped(std_format(&str.clone().into_flat(), vals.clone())?)71		}72		(a, b) => throw!(BinaryOperatorDoesNotOperateOnValues(73			BinaryOpType::Mod,74			a.value_type(),75			b.value_type()76		)),77	}78}7980pub fn evaluate_binary_op_special(81	ctx: Context,82	a: &LocExpr,83	op: BinaryOpType,84	b: &LocExpr,85) -> Result<Val> {86	use BinaryOpType::*;87	use Val::*;88	Ok(match (evaluate(ctx.clone(), a)?, op, b) {89		(Bool(true), Or, _o) => Val::Bool(true),90		(Bool(false), And, _o) => Val::Bool(false),91		(a, op, eb) => evaluate_binary_op_normal(&a, op, &evaluate(ctx, eb)?)?,92	})93}9495pub fn evaluate_compare_op(a: &Val, b: &Val, op: BinaryOpType) -> Result<Ordering> {96	use Val::*;97	Ok(match (a, b) {98		(Str(a), Str(b)) => a.cmp(b),99		(Num(a), Num(b)) => a.partial_cmp(b).expect("jsonnet numbers are non NaN"),100		#[cfg(feature = "exp-bigint")]101		(BigInt(a), BigInt(b)) => a.cmp(b),102		(Arr(a), Arr(b)) => {103			if let (Some(ai), Some(bi)) = (a.iter_cheap(), b.iter_cheap()) {104				for (a, b) in ai.zip(bi) {105					let ord = evaluate_compare_op(&a, &b, op)?;106					if !ord.is_eq() {107						return Ok(ord);108					}109				}110			} else {111				let ai = a.iter();112				let bi = b.iter();113114				for (a, b) in ai.zip(bi) {115					let ord = evaluate_compare_op(&a?, &b?, op)?;116					if !ord.is_eq() {117						return Ok(ord);118					}119				}120			}121			a.len().cmp(&b.len())122		}123		(_, _) => throw!(BinaryOperatorDoesNotOperateOnValues(124			op,125			a.value_type(),126			b.value_type()127		)),128	})129}130131pub fn evaluate_binary_op_normal(a: &Val, op: BinaryOpType, b: &Val) -> Result<Val> {132	use BinaryOpType::*;133	use Val::*;134	Ok(match (a, op, b) {135		(a, Add, b) => evaluate_add_op(a, b)?,136137		(a, Eq, b) => Bool(equals(a, b)?),138		(a, Neq, b) => Bool(!equals(a, b)?),139140		(a, Lt, b) => Bool(evaluate_compare_op(a, b, Lt)?.is_lt()),141		(a, Gt, b) => Bool(evaluate_compare_op(a, b, Gt)?.is_gt()),142		(a, Lte, b) => Bool(evaluate_compare_op(a, b, Lte)?.is_le()),143		(a, Gte, b) => Bool(evaluate_compare_op(a, b, Gte)?.is_ge()),144145		(Str(a), In, Obj(obj)) => Bool(obj.has_field_ex(a.clone().into_flat(), true)),146		(a, Mod, b) => evaluate_mod_op(a, b)?,147148		(Str(v1), Mul, Num(v2)) => Str(StrValue::Flat(v1.to_string().repeat(*v2 as usize).into())),149150		// Bool X Bool151		(Bool(a), And, Bool(b)) => Bool(*a && *b),152		(Bool(a), Or, Bool(b)) => Bool(*a || *b),153154		// Num X Num155		(Num(v1), Mul, Num(v2)) => Val::new_checked_num(v1 * v2)?,156		(Num(v1), Div, Num(v2)) => {157			if *v2 == 0.0 {158				throw!(DivisionByZero)159			}160			Val::new_checked_num(v1 / v2)?161		}162163		(Num(v1), Sub, Num(v2)) => Val::new_checked_num(v1 - v2)?,164165		(Num(v1), BitAnd, Num(v2)) => Num(f64::from((*v1 as i32) & (*v2 as i32))),166		(Num(v1), BitOr, Num(v2)) => Num(f64::from((*v1 as i32) | (*v2 as i32))),167		(Num(v1), BitXor, Num(v2)) => Num(f64::from((*v1 as i32) ^ (*v2 as i32))),168		(Num(v1), Lhs, Num(v2)) => {169			if *v2 < 0.0 {170				throw!("shift by negative exponent")171			}172			Num(f64::from((*v1 as i32) << (*v2 as i32)))173		}174		(Num(v1), Rhs, Num(v2)) => {175			if *v2 < 0.0 {176				throw!("shift by negative exponent")177			}178			Num(f64::from((*v1 as i32) >> (*v2 as i32)))179		}180181		// Bigint X Bigint182		#[cfg(feature = "exp-bigint")]183		(BigInt(a), Mul, BigInt(b)) => BigInt(Box::new((&**a).clone() * (&**b).clone())),184		#[cfg(feature = "exp-bigint")]185		(BigInt(a), Sub, BigInt(b)) => BigInt(Box::new((&**a).clone() - (&**b).clone())),186187		_ => throw!(BinaryOperatorDoesNotOperateOnValues(188			op,189			a.value_type(),190			b.value_type(),191		)),192	})193}
after · crates/jrsonnet-evaluator/src/evaluate/operator.rs
1use std::cmp::Ordering;23use jrsonnet_parser::{BinaryOpType, LocExpr, UnaryOpType};45use crate::{6	arr::ArrValue,7	error::ErrorKind::*,8	evaluate,9	stdlib::std_format,10	throw,11	typed::Typed,12	val::{equals, StrValue},13	Context, Result, Val,14};1516pub fn evaluate_unary_op(op: UnaryOpType, b: &Val) -> Result<Val> {17	use UnaryOpType::*;18	use Val::*;19	Ok(match (op, b) {20		(Not, Bool(v)) => Bool(!v),21		(Minus, Num(n)) => Num(-*n),22		(BitNot, Num(n)) => Num(f64::from(!(*n as i32))),23		(op, o) => throw!(UnaryOperatorDoesNotOperateOnType(op, o.value_type())),24	})25}2627pub fn evaluate_add_op(a: &Val, b: &Val) -> Result<Val> {28	use Val::*;29	Ok(match (a, b) {30		(Str(v1), Str(v2)) => Str(StrValue::concat(v1.clone(), v2.clone())),3132		// Can't use generic json serialization way, because it depends on number to string concatenation (std.jsonnet:890)33		(Num(a), Str(b)) => Str(StrValue::Flat(format!("{a}{b}").into())),34		(Str(a), Num(b)) => Str(StrValue::Flat(format!("{a}{b}").into())),3536		(Str(a), o) | (o, Str(a)) if a.is_empty() => {37			Val::Str(StrValue::Flat(o.clone().to_string()?))38		}39		(Str(a), o) => Str(StrValue::Flat(40			format!("{a}{}", o.clone().to_string()?).into(),41		)),42		(o, Str(a)) => Str(StrValue::Flat(43			format!("{}{a}", o.clone().to_string()?).into(),44		)),4546		(Obj(v1), Obj(v2)) => Obj(v2.extend_from(v1.clone())),47		(Arr(a), Arr(b)) => Val::Arr(ArrValue::extended(a.clone(), b.clone())),4849		(Num(v1), Num(v2)) => Val::new_checked_num(v1 + v2)?,50		#[cfg(feature = "exp-bigint")]51		(BigInt(a), BigInt(b)) => BigInt(Box::new((&**a).clone() + (&**b).clone())),52		_ => throw!(BinaryOperatorDoesNotOperateOnValues(53			BinaryOpType::Add,54			a.value_type(),55			b.value_type(),56		)),57	})58}5960pub fn evaluate_mod_op(a: &Val, b: &Val) -> Result<Val> {61	use Val::*;62	match (a, b) {63		(Num(a), Num(b)) => {64			if *b == 0.0 {65				throw!(DivisionByZero)66			}67			Ok(Num(a % b))68		}69		(Str(str), vals) => {70			String::into_untyped(std_format(&str.clone().into_flat(), vals.clone())?)71		}72		(a, b) => throw!(BinaryOperatorDoesNotOperateOnValues(73			BinaryOpType::Mod,74			a.value_type(),75			b.value_type()76		)),77	}78}7980pub fn evaluate_binary_op_special(81	ctx: Context,82	a: &LocExpr,83	op: BinaryOpType,84	b: &LocExpr,85) -> Result<Val> {86	use BinaryOpType::*;87	use Val::*;88	Ok(match (evaluate(ctx.clone(), a)?, op, b) {89		(Bool(true), Or, _o) => Val::Bool(true),90		(Bool(false), And, _o) => Val::Bool(false),91		#[cfg(feature = "exp-null-coaelse")]92		(Null, NullCoaelse, eb) => evaluate(ctx, eb)?,93		#[cfg(feature = "exp-null-coaelse")]94		(a, NullCoaelse, _o) => a,95		(a, op, eb) => evaluate_binary_op_normal(&a, op, &evaluate(ctx, eb)?)?,96	})97}9899pub fn evaluate_compare_op(a: &Val, b: &Val, op: BinaryOpType) -> Result<Ordering> {100	use Val::*;101	Ok(match (a, b) {102		(Str(a), Str(b)) => a.cmp(b),103		(Num(a), Num(b)) => a.partial_cmp(b).expect("jsonnet numbers are non NaN"),104		#[cfg(feature = "exp-bigint")]105		(BigInt(a), BigInt(b)) => a.cmp(b),106		(Arr(a), Arr(b)) => {107			if let (Some(ai), Some(bi)) = (a.iter_cheap(), b.iter_cheap()) {108				for (a, b) in ai.zip(bi) {109					let ord = evaluate_compare_op(&a, &b, op)?;110					if !ord.is_eq() {111						return Ok(ord);112					}113				}114			} else {115				let ai = a.iter();116				let bi = b.iter();117118				for (a, b) in ai.zip(bi) {119					let ord = evaluate_compare_op(&a?, &b?, op)?;120					if !ord.is_eq() {121						return Ok(ord);122					}123				}124			}125			a.len().cmp(&b.len())126		}127		(_, _) => throw!(BinaryOperatorDoesNotOperateOnValues(128			op,129			a.value_type(),130			b.value_type()131		)),132	})133}134135pub fn evaluate_binary_op_normal(a: &Val, op: BinaryOpType, b: &Val) -> Result<Val> {136	use BinaryOpType::*;137	use Val::*;138	Ok(match (a, op, b) {139		(a, Add, b) => evaluate_add_op(a, b)?,140141		(a, Eq, b) => Bool(equals(a, b)?),142		(a, Neq, b) => Bool(!equals(a, b)?),143144		(a, Lt, b) => Bool(evaluate_compare_op(a, b, Lt)?.is_lt()),145		(a, Gt, b) => Bool(evaluate_compare_op(a, b, Gt)?.is_gt()),146		(a, Lte, b) => Bool(evaluate_compare_op(a, b, Lte)?.is_le()),147		(a, Gte, b) => Bool(evaluate_compare_op(a, b, Gte)?.is_ge()),148149		(Str(a), In, Obj(obj)) => Bool(obj.has_field_ex(a.clone().into_flat(), true)),150		(a, Mod, b) => evaluate_mod_op(a, b)?,151152		(Str(v1), Mul, Num(v2)) => Str(StrValue::Flat(v1.to_string().repeat(*v2 as usize).into())),153154		// Bool X Bool155		(Bool(a), And, Bool(b)) => Bool(*a && *b),156		(Bool(a), Or, Bool(b)) => Bool(*a || *b),157158		// Num X Num159		(Num(v1), Mul, Num(v2)) => Val::new_checked_num(v1 * v2)?,160		(Num(v1), Div, Num(v2)) => {161			if *v2 == 0.0 {162				throw!(DivisionByZero)163			}164			Val::new_checked_num(v1 / v2)?165		}166167		(Num(v1), Sub, Num(v2)) => Val::new_checked_num(v1 - v2)?,168169		(Num(v1), BitAnd, Num(v2)) => Num(f64::from((*v1 as i32) & (*v2 as i32))),170		(Num(v1), BitOr, Num(v2)) => Num(f64::from((*v1 as i32) | (*v2 as i32))),171		(Num(v1), BitXor, Num(v2)) => Num(f64::from((*v1 as i32) ^ (*v2 as i32))),172		(Num(v1), Lhs, Num(v2)) => {173			if *v2 < 0.0 {174				throw!("shift by negative exponent")175			}176			Num(f64::from((*v1 as i32) << (*v2 as i32)))177		}178		(Num(v1), Rhs, Num(v2)) => {179			if *v2 < 0.0 {180				throw!("shift by negative exponent")181			}182			Num(f64::from((*v1 as i32) >> (*v2 as i32)))183		}184185		// Bigint X Bigint186		#[cfg(feature = "exp-bigint")]187		(BigInt(a), Mul, BigInt(b)) => BigInt(Box::new((&**a).clone() * (&**b).clone())),188		#[cfg(feature = "exp-bigint")]189		(BigInt(a), Sub, BigInt(b)) => BigInt(Box::new((&**a).clone() - (&**b).clone())),190191		_ => throw!(BinaryOperatorDoesNotOperateOnValues(192			op,193			a.value_type(),194			b.value_type(),195		)),196	})197}
modifiedcrates/jrsonnet-parser/Cargo.tomldiffbeforeafterboth
--- a/crates/jrsonnet-parser/Cargo.toml
+++ b/crates/jrsonnet-parser/Cargo.toml
@@ -10,6 +10,7 @@
 [features]
 default = []
 exp-destruct = []
+exp-null-coaelse = []
 # Implement serialization of AST using structdump
 #
 # Structdump generates code, which exactly replicated passed AST
modifiedcrates/jrsonnet-parser/src/expr.rsdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/expr.rs
+++ b/crates/jrsonnet-parser/src/expr.rs
@@ -122,6 +122,8 @@
 
 	And,
 	Or,
+	#[cfg(feature = "exp-null-coaelse")]
+	NullCoaelse,
 
 	// Equialent to std.objectHasEx(a, b, true)
 	In,
@@ -153,6 +155,8 @@
 				And => "&&",
 				Or => "||",
 				In => "in",
+				#[cfg(feature = "exp-null-coaelse")]
+				NullCoaelse => "??",
 			}
 		)
 	}
@@ -399,8 +403,13 @@
 	ErrorStmt(LocExpr),
 	/// a(b, c)
 	Apply(LocExpr, ArgsDesc, bool),
-	/// a[b]
-	Index(LocExpr, LocExpr),
+	/// a[b], a.b, a?.b
+	Index {
+		indexable: LocExpr,
+		index: LocExpr,
+		#[cfg(feature = "exp-null-coaelse")]
+		null_coaelse: bool,
+	},
 	/// function(x) x
 	Function(ParamsDesc, LocExpr),
 	/// if true == false then 1 else 2
modifiedcrates/jrsonnet-parser/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/lib.rs
+++ b/crates/jrsonnet-parser/src/lib.rs
@@ -288,7 +288,11 @@
 		rule unaryop(x: rule<()>) -> ()
 			= quiet!{ x() } / expected!("<unary op>")
 
-
+		rule ensure_null_coaelse()
+			= "" {?
+				#[cfg(not(feature = "exp-null-coaelse"))] return Err("!!!experimental null coaelscing was not enabled");
+				#[cfg(feature = "exp-null-coaelse")] Ok(())
+			}
 		use BinaryOpType::*;
 		use UnaryOpType::*;
 		rule expr(s: &ParserSettings) -> LocExpr
@@ -296,6 +300,10 @@
 				start:position!() v:@ end:position!() { LocExpr(Rc::new(v), ExprLocation(s.source.clone(), start as u32, end as u32)) }
 				--
 				a:(@) _ binop(<"||">) _ b:@ {expr_bin!(a Or b)}
+				a:(@) _ binop(<"??">) _ ensure_null_coaelse() b:@ {
+					#[cfg(feature = "exp-null-coaelse")] return expr_bin!(a NullCoaelse b);
+					unreachable!("ensure_null_coaelse will fail if feature is not enabled")
+				}
 				--
 				a:(@) _ binop(<"&&">) _ b:@ {expr_bin!(a And b)}
 				--
@@ -329,8 +337,16 @@
 						unaryop(<"~">) _ b:@ {expr_un!(BitNot b)}
 				--
 				a:(@) _ "[" _ e:slice_desc(s) _ "]" {Expr::Slice(a, e)}
-				a:(@) _ "." _ a:position!() e:id_loc(s) b:position!() {Expr::Index(a, e)}
-				a:(@) _ "[" _ e:expr(s) _ "]" {Expr::Index(a, e)}
+				indexable:(@) _ null_coaelse:("?" _ ensure_null_coaelse())? "."  _ index:id_loc(s) {Expr::Index{
+					indexable, index,
+					#[cfg(feature = "exp-null-coaelse")]
+					null_coaelse: null_coaelse.is_some(),
+				}}
+				indexable:(@) _ null_coaelse:("?" _ "." _ ensure_null_coaelse())? "[" _ index:expr(s) _ "]" {Expr::Index{
+					indexable, index,
+					#[cfg(feature = "exp-null-coaelse")]
+					null_coaelse: null_coaelse.is_some(),
+				}}
 				a:(@) _ "(" _ args:args(s) _ ")" ts:(_ keyword("tailstrict"))? {Expr::Apply(a, args, ts.is_some())}
 				a:(@) _ "{" _ body:objinside(s) _ "}" {Expr::ObjExtend(a, body)}
 				--