git.delta.rocks / jrsonnet / refs/commits / ecde57a8973c

difftreelog

feat parse new object iteration syntax

tvmprlwuYaroslav Bolyukin2026-05-06parent: #0f5424f.patch.diff
in: master

10 files changed

modifiedcmds/jrsonnet/Cargo.tomldiffbeforeafterboth
--- a/cmds/jrsonnet/Cargo.toml
+++ b/cmds/jrsonnet/Cargo.toml
@@ -31,7 +31,7 @@
 ]
 # Destructuring of locals
 exp-destruct = ["jrsonnet-evaluator/exp-destruct"]
-# Iteration over objects yields [key, value] elements
+# Iteration over objects using [key]: value syntax
 exp-object-iteration = ["jrsonnet-evaluator/exp-object-iteration"]
 # Bigint type
 exp-bigint = ["jrsonnet-evaluator/exp-bigint", "jrsonnet-cli/exp-bigint"]
modifiedcrates/jrsonnet-evaluator/Cargo.tomldiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/Cargo.toml
+++ b/crates/jrsonnet-evaluator/Cargo.toml
@@ -40,8 +40,12 @@
   "jrsonnet-peg-parser?/exp-destruct",
   "jrsonnet-ir-parser?/exp-destruct",
 ]
-# Iteration over objects yields [key, value] elements
-exp-object-iteration = []
+# Iteration over objects using [key]: value syntax
+exp-object-iteration = [
+  "jrsonnet-ir/exp-object-iteration",
+  "jrsonnet-peg-parser?/exp-object-iteration",
+  "jrsonnet-ir-parser?/exp-object-iteration",
+]
 # Bigint type
 exp-bigint = ["num-bigint", "jrsonnet-types/exp-bigint"]
 # obj?.field, obj?.['field']
modifiedcrates/jrsonnet-evaluator/src/analyze.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/analyze.rs
+++ b/crates/jrsonnet-evaluator/src/analyze.rs
@@ -1862,6 +1862,8 @@
 				);
 				(r, rest)
 			}
+			#[cfg(feature = "exp-object-iteration")]
+			CompSpec::ForObjSpec(_) => todo!(),
 		}
 	}
 	let outer_depth = stack.depth;
modifiedcrates/jrsonnet-ir-parser/Cargo.tomldiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/Cargo.toml
+++ b/crates/jrsonnet-ir-parser/Cargo.toml
@@ -11,9 +11,10 @@
 
 [features]
 default = []
-experimental = ["exp-null-coaelse", "exp-destruct"]
+experimental = ["exp-null-coaelse", "exp-destruct", "exp-object-iteration"]
 exp-null-coaelse = ["jrsonnet-ir/exp-null-coaelse"]
 exp-destruct = ["jrsonnet-ir/exp-destruct"]
+exp-object-iteration = ["jrsonnet-ir/exp-object-iteration"]
 
 [dependencies]
 insta.workspace = true
modifiedcrates/jrsonnet-ir-parser/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/lib.rs
+++ b/crates/jrsonnet-ir-parser/src/lib.rs
@@ -66,6 +66,13 @@
 		!self.at_eof() && self.peek() == kind
 	}
 
+	#[allow(dead_code)]
+	fn nth(&self, n: usize) -> SyntaxKind {
+		self.lexemes
+			.get(self.offset + n)
+			.map_or(SyntaxKind::EOF, |l| l.kind)
+	}
+
 	fn eat_any(&mut self) {
 		self.offset += 1;
 	}
@@ -561,20 +568,36 @@
 	}
 }
 
-fn for_spec(p: &mut Parser<'_>) -> Result<ForSpecData> {
+fn for_spec(p: &mut Parser<'_>) -> Result<CompSpec> {
 	p.eat(T![for])?;
+	#[cfg(feature = "exp-object-iteration")]
+	if p.at(T!['[']) && p.nth(1) == SyntaxKind::IDENT && p.nth(2) == T![']'] && p.nth(3) == T![:] {
+		p.eat(T!['['])?;
+		let key = ident(p)?;
+		p.eat(T![']'])?;
+		let visibility = visibility(p)?;
+		let value = destruct(p)?;
+		p.eat(T![in])?;
+		let over = expr(p)?;
+		return Ok(CompSpec::ForObjSpec(jrsonnet_ir::ForObjSpecData {
+			key,
+			visibility,
+			value,
+			over,
+		}));
+	}
 	let d = destruct(p)?;
 	p.eat(T![in])?;
 	let over = expr(p)?;
-	Ok(ForSpecData { destruct: d, over })
+	Ok(CompSpec::ForSpec(ForSpecData { destruct: d, over }))
 }
 
 fn compspecs(p: &mut Parser<'_>) -> Result<Vec<CompSpec>> {
 	let mut specs = Vec::new();
-	specs.push(CompSpec::ForSpec(for_spec(p)?));
+	specs.push(for_spec(p)?);
 	loop {
 		if p.at(T![for]) {
-			specs.push(CompSpec::ForSpec(for_spec(p)?));
+			specs.push(for_spec(p)?);
 		} else if p.at(T![if]) {
 			let isd = if_spec_data(p)?;
 			specs.push(CompSpec::IfSpec(isd));
modifiedcrates/jrsonnet-ir/Cargo.tomldiffbeforeafterboth
--- a/crates/jrsonnet-ir/Cargo.toml
+++ b/crates/jrsonnet-ir/Cargo.toml
@@ -12,9 +12,10 @@
 
 [features]
 default = []
-experimental = ["exp-destruct", "exp-null-coaelse"]
+experimental = ["exp-destruct", "exp-null-coaelse", "exp-object-iteration"]
 exp-destruct = []
 exp-null-coaelse = []
+exp-object-iteration = []
 
 [dependencies]
 jrsonnet-interner.workspace = true
modifiedcrates/jrsonnet-ir/src/expr.rsdiffbeforeafterboth
before · crates/jrsonnet-ir/src/expr.rs
1use std::{2	fmt::{self, Debug, Display},3	ops::{Deref, RangeInclusive},4};56use jrsonnet_gcmodule::Acyclic;7use jrsonnet_interner::IStr;89use crate::{10	NumValue,11	function::{FunctionSignature, ParamDefault, ParamName, ParamParse},12	source::Source,13};1415#[derive(Debug, PartialEq, Acyclic)]16pub enum FieldName {17	/// {fixed: 2}18	Fixed(IStr),19	/// {["dyn"+"amic"]: 3}20	Dyn(Expr),21}2223#[derive(Debug, Clone, Copy, PartialEq, Eq, Acyclic)]24#[repr(u8)]25pub enum Visibility {26	/// :27	Normal,28	/// ::29	Hidden,30	/// :::31	Unhide,32}3334impl Visibility {35	pub fn is_visible(&self) -> bool {36		matches!(self, Self::Normal | Self::Unhide)37	}38}3940#[derive(Debug, PartialEq, Acyclic)]41pub struct AssertStmt {42	pub assertion: Spanned<Expr>,43	pub message: Option<Expr>,44}4546#[derive(Debug, PartialEq, Acyclic)]47pub struct FieldMember {48	pub name: Spanned<FieldName>,49	pub plus: bool,50	pub params: Option<ExprParams>,51	pub visibility: Visibility,52	pub value: Expr,53}5455#[derive(Debug, PartialEq, Acyclic)]56pub enum Member {57	Field(FieldMember),58	BindStmt(BindSpec),59	AssertStmt(AssertStmt),60}6162#[derive(Debug, Clone, Copy, PartialEq, Eq, Acyclic)]63pub enum UnaryOpType {64	Plus,65	Minus,66	BitNot,67	Not,68}6970impl Display for UnaryOpType {71	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {72		use UnaryOpType::*;73		write!(74			f,75			"{}",76			match self {77				Plus => "+",78				Minus => "-",79				BitNot => "~",80				Not => "!",81			}82		)83	}84}8586#[derive(Debug, Clone, Copy, PartialEq, Eq, Acyclic)]87pub enum BinaryOpType {88	Mul,89	Div,9091	/// Implemented as intrinsic, put here for completeness92	Mod,9394	Add,95	Sub,9697	Lhs,98	Rhs,99100	Lt,101	Gt,102	Lte,103	Gte,104105	BitAnd,106	BitOr,107	BitXor,108109	Eq,110	Neq,111112	And,113	Or,114	#[cfg(feature = "exp-null-coaelse")]115	NullCoaelse,116117	// Equialent to std.objectHasEx(a, b, true)118	In,119}120121impl Display for BinaryOpType {122	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {123		use BinaryOpType::*;124		write!(125			f,126			"{}",127			match self {128				Mul => "*",129				Div => "/",130				Mod => "%",131				Add => "+",132				Sub => "-",133				Lhs => "<<",134				Rhs => ">>",135				Lt => "<",136				Gt => ">",137				Lte => "<=",138				Gte => ">=",139				BitAnd => "&",140				BitOr => "|",141				BitXor => "^",142				Eq => "==",143				Neq => "!=",144				And => "&&",145				Or => "||",146				In => "in",147				#[cfg(feature = "exp-null-coaelse")]148				NullCoaelse => "??",149			}150		)151	}152}153154/// name, default value155#[derive(Debug, PartialEq, Acyclic)]156pub struct ExprParam {157	pub destruct: Destruct,158	pub default: Option<Expr>,159}160161/// Defined function parameters162#[derive(Debug, PartialEq, Acyclic)]163pub struct ExprParams {164	pub exprs: Vec<ExprParam>,165	pub signature: FunctionSignature,166	pub(crate) binds_len: usize,167}168impl ExprParams {169	pub fn len(&self) -> usize {170		self.exprs.len()171	}172	pub fn is_empty(&self) -> bool {173		self.exprs.is_empty()174	}175176	pub fn binds_len(&self) -> usize {177		self.binds_len178	}179	pub fn new(exprs: Vec<ExprParam>) -> Self {180		Self {181			signature: FunctionSignature::new(182				exprs183					.iter()184					.map(|p| {185						ParamParse::new(186							p.destruct.name(),187							ParamDefault::exists(p.default.is_some()),188						)189					})190					.collect(),191			),192			binds_len: exprs.iter().map(|v| v.destruct.binds_len()).sum(),193			exprs,194		}195	}196}197198#[derive(Debug, PartialEq, Acyclic)]199pub struct ArgsDesc {200	pub unnamed: Vec<Expr>,201	pub names: Vec<IStr>,202	pub values: Vec<Expr>,203}204impl ArgsDesc {205	pub fn new(unnamed: Vec<Expr>, names: Vec<IStr>, values: Vec<Expr>) -> Self {206		Self {207			unnamed,208			names,209			values,210		}211	}212}213214#[derive(Debug, PartialEq, Eq, Acyclic)]215pub enum DestructRest {216	/// ...rest217	Keep(IStr),218	/// ...219	Drop,220}221222#[derive(Debug, PartialEq, Acyclic)]223pub enum Destruct {224	Full(Spanned<IStr>),225	#[cfg(feature = "exp-destruct")]226	Skip,227	#[cfg(feature = "exp-destruct")]228	Array {229		start: Vec<Destruct>,230		rest: Option<DestructRest>,231		end: Vec<Destruct>,232	},233	#[cfg(feature = "exp-destruct")]234	Object {235		#[allow(clippy::type_complexity)]236		fields: Vec<(IStr, Option<Destruct>, Option<Spanned<Expr>>)>,237		rest: Option<DestructRest>,238	},239}240impl Destruct {241	/// Name of destructure, used for function parameter names242	pub fn name(&self) -> ParamName {243		match self {244			Self::Full(name) => ParamName::Named(name.value.clone()),245			#[cfg(feature = "exp-destruct")]246			_ => ParamName::Unnamed,247		}248	}249	pub fn binds_len(&self) -> usize {250		#[cfg(feature = "exp-destruct")]251		fn cap_rest(rest: &Option<DestructRest>) -> usize {252			match rest {253				Some(DestructRest::Keep(_)) => 1,254				Some(DestructRest::Drop) => 0,255				None => 0,256			}257		}258		match self {259			Self::Full(_) => 1,260			#[cfg(feature = "exp-destruct")]261			Self::Skip => 0,262			#[cfg(feature = "exp-destruct")]263			Self::Array { start, rest, end } => {264				start.iter().map(Destruct::binds_len).sum::<usize>()265					+ end.iter().map(Destruct::binds_len).sum::<usize>()266					+ cap_rest(rest)267			}268			#[cfg(feature = "exp-destruct")]269			Self::Object { fields, rest } => {270				let mut out = 0;271				for (_, into, _) in fields {272					match into {273						Some(v) => out += v.binds_len(),274						// Field is destructured to default name275						None => out += 1,276					}277				}278				out + cap_rest(rest)279			}280		}281	}282}283284#[derive(Debug, PartialEq, Acyclic)]285pub enum BindSpec {286	Field {287		into: Destruct,288		value: Expr,289	},290	Function {291		name: IStr,292		params: ExprParams,293		value: Expr,294	},295}296impl BindSpec {297	pub fn binds_len(&self) -> usize {298		match self {299			BindSpec::Field { into, .. } => into.binds_len(),300			BindSpec::Function { .. } => 1,301		}302	}303}304305#[derive(Debug, PartialEq, Acyclic)]306pub struct IfSpecData {307	pub span: Span,308	pub cond: Expr,309}310311#[derive(Debug, PartialEq, Acyclic)]312pub struct ForSpecData {313	pub destruct: Destruct,314	pub over: Expr,315}316317#[derive(Debug, PartialEq, Acyclic)]318pub enum CompSpec {319	IfSpec(IfSpecData),320	ForSpec(ForSpecData),321}322323#[derive(Debug, PartialEq, Acyclic)]324pub struct ObjComp {325	pub locals: Vec<BindSpec>,326	pub field: Box<FieldMember>,327	pub compspecs: Vec<CompSpec>,328}329330#[derive(Debug, PartialEq, Acyclic)]331pub struct ObjMembers {332	pub locals: Vec<BindSpec>,333	pub asserts: Vec<AssertStmt>,334	pub fields: Vec<FieldMember>,335}336337#[derive(Debug, PartialEq, Acyclic)]338pub enum ObjBody {339	MemberList(ObjMembers),340	ObjComp(ObjComp),341}342343#[derive(Debug, PartialEq, Eq, Clone, Copy, Acyclic)]344pub enum LiteralType {345	This,346	Super,347	Dollar,348	Null,349	True,350	False,351}352353#[derive(Debug, PartialEq, Acyclic)]354pub struct SliceDesc {355	pub start: Option<Spanned<Expr>>,356	pub end: Option<Spanned<Expr>>,357	pub step: Option<Spanned<Expr>>,358}359360#[derive(Debug, PartialEq, Acyclic)]361pub struct AssertExpr {362	pub assert: AssertStmt,363	pub rest: Expr,364}365366#[derive(Debug, PartialEq, Acyclic)]367pub struct BinaryOp {368	pub lhs: Expr,369	pub op: BinaryOpType,370	pub rhs: Expr,371}372373#[derive(Debug, PartialEq, Acyclic, Clone, Copy)]374pub enum ImportKind {375	Normal,376	Str,377	Bin,378}379380#[derive(Debug, PartialEq, Acyclic)]381pub struct IfElse {382	pub cond: IfSpecData,383	pub cond_then: Expr,384	pub cond_else: Option<Expr>,385}386387#[derive(Debug, PartialEq, Acyclic)]388pub struct Slice {389	pub value: Expr,390	pub slice: SliceDesc,391}392393/// Syntax base394#[derive(Debug, PartialEq, Acyclic)]395pub enum Expr {396	Literal(LiteralType),397398	/// String value: "hello"399	Str(IStr),400	/// Number: 1, 2.0, 2e+20401	Num(NumValue),402	/// Variable name: test403	Var(Spanned<IStr>),404405	/// Array of expressions: [1, 2, "Hello"]406	Arr(Vec<Expr>),407	/// Array comprehension:408	/// ```jsonnet409	///  ingredients: [410	///    { kind: kind, qty: 4 / 3 }411	///    for kind in [412	///      'Honey Syrup',413	///      'Lemon Juice',414	///      'Farmers Gin',415	///    ]416	///  ],417	/// ```418	ArrComp(Box<Expr>, Vec<CompSpec>),419420	/// Object: {a: 2}421	Obj(ObjBody),422	/// Object extension: var1 {b: 2}423	ObjExtend(Box<Expr>, ObjBody),424425	/// -2426	UnaryOp(UnaryOpType, Box<Expr>),427	/// 2 - 2428	BinaryOp(Box<BinaryOp>),429	/// assert 2 == 2 : "Math is broken"430	AssertExpr(Box<AssertExpr>),431	/// local a = 2; { b: a }432	LocalExpr(Vec<BindSpec>, Box<Expr>),433434	/// import* "hello"435	Import(Spanned<ImportKind>, Box<Expr>),436	/// error "I'm broken"437	ErrorStmt(Span, Box<Expr>),438	/// a(b, c)439	Apply(Box<Expr>, Spanned<ArgsDesc>, bool),440	/// a[b], a.b, a?.b441	Index {442		indexable: Box<Expr>,443		parts: Vec<IndexPart>,444	},445	/// function(x) x446	Function(ExprParams, Box<Expr>),447	/// if true == false then 1 else 2448	IfElse(Box<IfElse>),449	Slice(Box<Slice>),450}451452#[derive(Debug, PartialEq, Acyclic)]453pub struct IndexPart {454	pub span: Span,455	pub value: Expr,456	#[cfg(feature = "exp-null-coaelse")]457	pub null_coaelse: bool,458}459460/// file, begin offset, end offset461#[derive(Clone, PartialEq, Eq, Acyclic)]462#[repr(C)]463pub struct Span(pub Source, pub u32, pub u32);464impl Span {465	pub fn belongs_to(&self, other: &Span) -> bool {466		other.0 == self.0 && other.1 <= self.1 && other.2 >= self.2467	}468	pub fn range(&self) -> RangeInclusive<usize> {469		let start = self.1;470		let mut end = self.2;471		if end > start {472			// Because it is originally exclusive473			end -= 1;474		}475		start as usize..=end as usize476	}477}478479#[cfg(target_pointer_width = "64")]480static_assertions::assert_eq_size!(Span, (usize, usize));481482impl Debug for Span {483	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {484		write!(f, "{:?}:{:?}-{:?}", self.0, self.1, self.2)485	}486}487488#[derive(Clone, PartialEq, Acyclic)]489pub struct Spanned<T: Acyclic> {490	pub value: T,491	pub span: Span,492}493impl<T: Acyclic> Deref for Spanned<T> {494	type Target = T;495	fn deref(&self) -> &Self::Target {496		&self.value497	}498}499impl<T: Acyclic> Spanned<T> {500	#[inline]501	pub fn new(value: T, span: Span) -> Self {502		Self { value, span }503	}504	pub fn map<U: Acyclic>(self, v: impl FnOnce(T) -> U) -> Spanned<U> {505		Spanned {506			span: self.span,507			value: v(self.value),508		}509	}510	pub fn as_ref<'a>(&'a self) -> Spanned<&'a T>511	where512		&'a T: Acyclic,513	{514		Spanned {515			span: self.span.clone(),516			value: &self.value,517		}518	}519}520521impl<T: Debug + Acyclic> Debug for Spanned<T> {522	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {523		let expr = &**self;524		if f.alternate() {525			write!(f, "{:#?}", expr)?;526		} else {527			write!(f, "{:?}", expr)?;528		}529		write!(f, " from {:?}", self.span)?;530		Ok(())531	}532}
modifiedcrates/jrsonnet-ir/src/visit.rsdiffbeforeafterboth
--- a/crates/jrsonnet-ir/src/visit.rs
+++ b/crates/jrsonnet-ir/src/visit.rs
@@ -1,5 +1,7 @@
 use jrsonnet_interner::IStr;
 
+#[cfg(feature = "exp-object-iteration")]
+use crate::ForObjSpecData;
 use crate::{
 	ArgsDesc, AssertExpr, AssertStmt, BinaryOp, BindSpec, CompSpec, Destruct, Expr, ExprParam,
 	ExprParams, FieldMember, FieldName, ForSpecData, IfElse, IfSpecData, ImportKind, IndexPart,
@@ -69,6 +71,17 @@
 			visit_destruct(v, destruct);
 			v.visit_expr(over);
 		}
+		#[cfg(feature = "exp-object-iteration")]
+		CompSpec::ForObjSpec(for_obj_spec_data) => {
+			let ForObjSpecData {
+				key: _,
+				visibility: _,
+				value,
+				over,
+			} = for_obj_spec_data;
+			visit_destruct(v, value);
+			v.visit_expr(over);
+		}
 	}
 }
 pub fn visit_params<V: Visitor>(v: &mut V, par: &ExprParams) {
modifiedcrates/jrsonnet-peg-parser/Cargo.tomldiffbeforeafterboth
--- a/crates/jrsonnet-peg-parser/Cargo.toml
+++ b/crates/jrsonnet-peg-parser/Cargo.toml
@@ -22,6 +22,7 @@
 
 [features]
 default = []
-experimental = ["exp-destruct", "exp-null-coaelse"]
+experimental = ["exp-destruct", "exp-null-coaelse", "exp-object-iteration"]
 exp-destruct = ["jrsonnet-ir/exp-destruct"]
 exp-null-coaelse = ["jrsonnet-ir/exp-null-coaelse"]
+exp-object-iteration = ["jrsonnet-ir/exp-object-iteration"]
modifiedcrates/jrsonnet-peg-parser/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-peg-parser/src/lib.rs
+++ b/crates/jrsonnet-peg-parser/src/lib.rs
@@ -241,11 +241,21 @@
 			= i:spanned(<keyword("if")>, s) _ cond:expr(s) {IfSpecData { span: i.span, cond }}
 		pub rule forspec(s: &ParserSettings) -> ForSpecData
 			= keyword("for") _ destruct:destruct(s) _ keyword("in") _ over:expr(s) { ForSpecData { destruct, over } }
+		rule ensure_object_iteration()
+			= "" {?
+				#[cfg(not(feature = "exp-object-iteration"))] return Err("!!!experimental object iteration was not enabled");
+				#[cfg(feature = "exp-object-iteration")] Ok(())
+			}
+		pub rule forobjspec(s: &ParserSettings) -> CompSpec
+			= ensure_object_iteration() keyword("for") _ "[" _ key:id() _ "]" _ vis:visibility() _ value:destruct(s) _ keyword("in") _ over:expr(s) {
+				#[cfg(feature = "exp-object-iteration")] return CompSpec::ForObjSpec(jrsonnet_ir::ForObjSpecData { key, visibility: vis, value, over });
+				#[cfg(not(feature = "exp-object-iteration"))] unreachable!("ensure_object_iteration will fail if feature is not enabled")
+			}
 		rule compspec(s: &ParserSettings) -> CompSpec
-			= i:ifspec(s) { CompSpec::IfSpec(i) } / f:forspec(s) {CompSpec::ForSpec(f)}
+			= i:ifspec(s) { CompSpec::IfSpec(i) } / f:forobjspec(s) { f } / f:forspec(s) {CompSpec::ForSpec(f)}
 		pub rule compspecs(s: &ParserSettings) -> Vec<CompSpec>
 			= specs:compspec(s) ++ _ {?
-				if !matches!(specs[0], CompSpec::ForSpec(_)) {
+				if matches!(specs[0], CompSpec::IfSpec(_)) {
 					return Err("<first compspec should be for>")
 				}
 				Ok(specs)