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
--- a/crates/jrsonnet-ir/src/expr.rs
+++ b/crates/jrsonnet-ir/src/expr.rs
@@ -314,10 +314,21 @@
 	pub over: Expr,
 }
 
+#[cfg(feature = "exp-object-iteration")]
 #[derive(Debug, PartialEq, Acyclic)]
+pub struct ForObjSpecData {
+	pub key: IStr,
+	pub visibility: Visibility,
+	pub value: Destruct,
+	pub over: Expr,
+}
+
+#[derive(Debug, PartialEq, Acyclic)]
 pub enum CompSpec {
 	IfSpec(IfSpecData),
 	ForSpec(ForSpecData),
+	#[cfg(feature = "exp-object-iteration")]
+	ForObjSpec(ForObjSpecData),
 }
 
 #[derive(Debug, PartialEq, Acyclic)]
modifiedcrates/jrsonnet-ir/src/visit.rsdiffbeforeafterboth
before · crates/jrsonnet-ir/src/visit.rs
1use jrsonnet_interner::IStr;23use crate::{4	ArgsDesc, AssertExpr, AssertStmt, BinaryOp, BindSpec, CompSpec, Destruct, Expr, ExprParam,5	ExprParams, FieldMember, FieldName, ForSpecData, IfElse, IfSpecData, ImportKind, IndexPart,6	ObjBody, ObjComp, ObjMembers, Slice, SliceDesc,7};89pub trait Visitor: Sized {10	fn visit_expr(&mut self, e: &Expr) {11		visit_expr(self, e)12	}13	fn visit_import(&mut self, _as_expression: bool, _value: IStr) {}14}1516#[cfg(feature = "exp-destruct")]17pub fn visit_destruct_rest<V: Visitor>(_v: &mut V, destruct: &crate::DestructRest) {18	match destruct {19		crate::DestructRest::Keep(_name) => {}20		crate::DestructRest::Drop => {}21	}22}2324#[allow(unused_variables, reason = "used with exp-destruct")]25pub fn visit_destruct<V: Visitor>(v: &mut V, destruct: &Destruct) {26	match destruct {27		Destruct::Full(_istr) => {}28		#[cfg(feature = "exp-destruct")]29		Destruct::Skip => {}30		#[cfg(feature = "exp-destruct")]31		Destruct::Array { start, rest, end } => {32			for s in start {33				visit_destruct(v, s);34			}35			if let Some(rest) = rest {36				visit_destruct_rest(v, rest);37			}38			for s in end {39				visit_destruct(v, s);40			}41		}42		#[cfg(feature = "exp-destruct")]43		Destruct::Object { fields, rest } => {44			for (_name, into, default) in fields {45				if let Some(into) = into {46					visit_destruct(v, into);47				}48				if let Some(default) = default {49					v.visit_expr(default);50				}51				if let Some(rest) = rest {52					visit_destruct_rest(v, rest);53				}54			}55		}56	}57}5859pub fn visit_if_spec<V: Visitor>(v: &mut V, cond: &IfSpecData) {60	let IfSpecData { span: _, cond } = cond;61	v.visit_expr(cond);62}6364pub fn visit_comp_spec<V: Visitor>(v: &mut V, c: &CompSpec) {65	match c {66		CompSpec::IfSpec(cond) => visit_if_spec(v, cond),67		CompSpec::ForSpec(for_spec_data) => {68			let ForSpecData { destruct, over } = for_spec_data;69			visit_destruct(v, destruct);70			v.visit_expr(over);71		}72	}73}74pub fn visit_params<V: Visitor>(v: &mut V, par: &ExprParams) {75	let ExprParams {76		exprs,77		signature: _,78		binds_len: _,79	} = par;80	for par in &**exprs {81		let ExprParam { destruct, default } = &par;82		visit_destruct(v, destruct);83		if let Some(default) = default {84			v.visit_expr(default);85		}86	}87}8889pub fn visit_bind_spec<V: Visitor>(v: &mut V, bind: &BindSpec) {90	match bind {91		BindSpec::Field { into, value } => {92			visit_destruct(v, into);93			v.visit_expr(value);94		}95		BindSpec::Function {96			name: _,97			params,98			value,99		} => {100			visit_params(v, params);101			v.visit_expr(value);102		}103	}104}105106pub fn visit_field_member<V: Visitor>(v: &mut V, mem: &FieldMember) {107	let FieldMember {108		name,109		plus: _,110		params,111		visibility: _,112		value,113	} = mem;114	match &**name {115		FieldName::Fixed(_istr) => {}116		FieldName::Dyn(expr) => v.visit_expr(expr),117	}118	if let Some(params) = params {119		visit_params(v, params);120	}121	v.visit_expr(value);122}123124pub fn visit_obj_body<V: Visitor>(v: &mut V, obj_body: &ObjBody) {125	match obj_body {126		ObjBody::MemberList(obj_members) => {127			let ObjMembers {128				locals,129				asserts,130				fields,131			} = obj_members;132			for local in &**locals {133				visit_bind_spec(v, local);134			}135			for assert in &**asserts {136				visit_assert_stmt(v, assert);137			}138			for field in fields {139				visit_field_member(v, field);140			}141		}142		ObjBody::ObjComp(obj_comp) => {143			let ObjComp {144				locals,145				field,146				compspecs,147			} = obj_comp;148			for local in &**locals {149				visit_bind_spec(v, local);150			}151			visit_field_member(v, field);152			for compspec in compspecs {153				visit_comp_spec(v, compspec);154			}155		}156	}157}158159pub fn visit_assert_stmt<V: Visitor>(v: &mut V, ass: &AssertStmt) {160	let AssertStmt { assertion, message } = ass;161	v.visit_expr(assertion);162	if let Some(message) = message {163		v.visit_expr(message);164	}165}166pub fn visit_expr<V: Visitor>(v: &mut V, e: &Expr) {167	match e {168		Expr::Literal(_literal_type) => {}169		Expr::Str(_istr) => {}170		Expr::Num(_num) => {}171		Expr::Var(_spanned) => {}172		Expr::Arr(exprs) => {173			for e in &**exprs {174				v.visit_expr(e);175			}176		}177		Expr::ArrComp(expr, comp_specs) => {178			v.visit_expr(expr);179			for ele in comp_specs {180				visit_comp_spec(v, ele);181			}182		}183		Expr::Obj(obj_body) => visit_obj_body(v, obj_body),184		Expr::ObjExtend(expr, obj_body) => {185			v.visit_expr(expr);186			visit_obj_body(v, obj_body);187		}188		Expr::UnaryOp(_unary_op_type, expr) => {189			v.visit_expr(expr);190		}191		Expr::BinaryOp(binary_op) => {192			let BinaryOp { lhs, op: _, rhs } = &**binary_op;193			v.visit_expr(lhs);194			v.visit_expr(rhs);195		}196		Expr::AssertExpr(assert_expr) => {197			let AssertExpr { assert, rest } = &**assert_expr;198			visit_assert_stmt(v, assert);199			v.visit_expr(rest);200		}201		Expr::LocalExpr(bind_specs, expr) => {202			for local in bind_specs {203				visit_bind_spec(v, local);204			}205			v.visit_expr(expr);206		}207		Expr::Import(kind, expr) => {208			v.visit_expr(expr);209210			if let Expr::Str(expr) = &**expr {211				v.visit_import(matches!(**kind, ImportKind::Normal), expr.clone());212			}213		}214		Expr::ErrorStmt(_span, expr) => {215			v.visit_expr(expr);216		}217		Expr::Apply(expr, spanned, _) => {218			v.visit_expr(expr);219			let ArgsDesc {220				unnamed,221				names: _,222				values,223			} = &**spanned;224			for unnamed in unnamed {225				v.visit_expr(unnamed);226			}227			for named in values {228				v.visit_expr(named);229			}230		}231		Expr::Index { indexable, parts } => {232			v.visit_expr(indexable);233234			for part in parts {235				let IndexPart {236					span: _,237					value,238					#[cfg(feature = "exp-null-coaelse")]239						null_coaelse: _,240				} = part;241				v.visit_expr(value);242			}243		}244		Expr::Function(expr_params, expr) => {245			visit_params(v, expr_params);246			v.visit_expr(expr);247		}248		Expr::IfElse(if_else) => {249			let IfElse {250				cond,251				cond_then,252				cond_else,253			} = &**if_else;254			visit_if_spec(v, cond);255			v.visit_expr(cond_then);256			if let Some(cond_else) = cond_else {257				v.visit_expr(cond_else);258			}259		}260		Expr::Slice(slice) => {261			let Slice { value, slice } = &**slice;262			v.visit_expr(value);263			let SliceDesc { start, end, step } = slice;264265			if let Some(start) = start {266				v.visit_expr(start);267			}268			if let Some(end) = end {269				v.visit_expr(end);270			}271			if let Some(step) = step {272				v.visit_expr(step);273			}274		}275	}276}
after · crates/jrsonnet-ir/src/visit.rs
1use jrsonnet_interner::IStr;23#[cfg(feature = "exp-object-iteration")]4use crate::ForObjSpecData;5use crate::{6	ArgsDesc, AssertExpr, AssertStmt, BinaryOp, BindSpec, CompSpec, Destruct, Expr, ExprParam,7	ExprParams, FieldMember, FieldName, ForSpecData, IfElse, IfSpecData, ImportKind, IndexPart,8	ObjBody, ObjComp, ObjMembers, Slice, SliceDesc,9};1011pub trait Visitor: Sized {12	fn visit_expr(&mut self, e: &Expr) {13		visit_expr(self, e)14	}15	fn visit_import(&mut self, _as_expression: bool, _value: IStr) {}16}1718#[cfg(feature = "exp-destruct")]19pub fn visit_destruct_rest<V: Visitor>(_v: &mut V, destruct: &crate::DestructRest) {20	match destruct {21		crate::DestructRest::Keep(_name) => {}22		crate::DestructRest::Drop => {}23	}24}2526#[allow(unused_variables, reason = "used with exp-destruct")]27pub fn visit_destruct<V: Visitor>(v: &mut V, destruct: &Destruct) {28	match destruct {29		Destruct::Full(_istr) => {}30		#[cfg(feature = "exp-destruct")]31		Destruct::Skip => {}32		#[cfg(feature = "exp-destruct")]33		Destruct::Array { start, rest, end } => {34			for s in start {35				visit_destruct(v, s);36			}37			if let Some(rest) = rest {38				visit_destruct_rest(v, rest);39			}40			for s in end {41				visit_destruct(v, s);42			}43		}44		#[cfg(feature = "exp-destruct")]45		Destruct::Object { fields, rest } => {46			for (_name, into, default) in fields {47				if let Some(into) = into {48					visit_destruct(v, into);49				}50				if let Some(default) = default {51					v.visit_expr(default);52				}53				if let Some(rest) = rest {54					visit_destruct_rest(v, rest);55				}56			}57		}58	}59}6061pub fn visit_if_spec<V: Visitor>(v: &mut V, cond: &IfSpecData) {62	let IfSpecData { span: _, cond } = cond;63	v.visit_expr(cond);64}6566pub fn visit_comp_spec<V: Visitor>(v: &mut V, c: &CompSpec) {67	match c {68		CompSpec::IfSpec(cond) => visit_if_spec(v, cond),69		CompSpec::ForSpec(for_spec_data) => {70			let ForSpecData { destruct, over } = for_spec_data;71			visit_destruct(v, destruct);72			v.visit_expr(over);73		}74		#[cfg(feature = "exp-object-iteration")]75		CompSpec::ForObjSpec(for_obj_spec_data) => {76			let ForObjSpecData {77				key: _,78				visibility: _,79				value,80				over,81			} = for_obj_spec_data;82			visit_destruct(v, value);83			v.visit_expr(over);84		}85	}86}87pub fn visit_params<V: Visitor>(v: &mut V, par: &ExprParams) {88	let ExprParams {89		exprs,90		signature: _,91		binds_len: _,92	} = par;93	for par in &**exprs {94		let ExprParam { destruct, default } = &par;95		visit_destruct(v, destruct);96		if let Some(default) = default {97			v.visit_expr(default);98		}99	}100}101102pub fn visit_bind_spec<V: Visitor>(v: &mut V, bind: &BindSpec) {103	match bind {104		BindSpec::Field { into, value } => {105			visit_destruct(v, into);106			v.visit_expr(value);107		}108		BindSpec::Function {109			name: _,110			params,111			value,112		} => {113			visit_params(v, params);114			v.visit_expr(value);115		}116	}117}118119pub fn visit_field_member<V: Visitor>(v: &mut V, mem: &FieldMember) {120	let FieldMember {121		name,122		plus: _,123		params,124		visibility: _,125		value,126	} = mem;127	match &**name {128		FieldName::Fixed(_istr) => {}129		FieldName::Dyn(expr) => v.visit_expr(expr),130	}131	if let Some(params) = params {132		visit_params(v, params);133	}134	v.visit_expr(value);135}136137pub fn visit_obj_body<V: Visitor>(v: &mut V, obj_body: &ObjBody) {138	match obj_body {139		ObjBody::MemberList(obj_members) => {140			let ObjMembers {141				locals,142				asserts,143				fields,144			} = obj_members;145			for local in &**locals {146				visit_bind_spec(v, local);147			}148			for assert in &**asserts {149				visit_assert_stmt(v, assert);150			}151			for field in fields {152				visit_field_member(v, field);153			}154		}155		ObjBody::ObjComp(obj_comp) => {156			let ObjComp {157				locals,158				field,159				compspecs,160			} = obj_comp;161			for local in &**locals {162				visit_bind_spec(v, local);163			}164			visit_field_member(v, field);165			for compspec in compspecs {166				visit_comp_spec(v, compspec);167			}168		}169	}170}171172pub fn visit_assert_stmt<V: Visitor>(v: &mut V, ass: &AssertStmt) {173	let AssertStmt { assertion, message } = ass;174	v.visit_expr(assertion);175	if let Some(message) = message {176		v.visit_expr(message);177	}178}179pub fn visit_expr<V: Visitor>(v: &mut V, e: &Expr) {180	match e {181		Expr::Literal(_literal_type) => {}182		Expr::Str(_istr) => {}183		Expr::Num(_num) => {}184		Expr::Var(_spanned) => {}185		Expr::Arr(exprs) => {186			for e in &**exprs {187				v.visit_expr(e);188			}189		}190		Expr::ArrComp(expr, comp_specs) => {191			v.visit_expr(expr);192			for ele in comp_specs {193				visit_comp_spec(v, ele);194			}195		}196		Expr::Obj(obj_body) => visit_obj_body(v, obj_body),197		Expr::ObjExtend(expr, obj_body) => {198			v.visit_expr(expr);199			visit_obj_body(v, obj_body);200		}201		Expr::UnaryOp(_unary_op_type, expr) => {202			v.visit_expr(expr);203		}204		Expr::BinaryOp(binary_op) => {205			let BinaryOp { lhs, op: _, rhs } = &**binary_op;206			v.visit_expr(lhs);207			v.visit_expr(rhs);208		}209		Expr::AssertExpr(assert_expr) => {210			let AssertExpr { assert, rest } = &**assert_expr;211			visit_assert_stmt(v, assert);212			v.visit_expr(rest);213		}214		Expr::LocalExpr(bind_specs, expr) => {215			for local in bind_specs {216				visit_bind_spec(v, local);217			}218			v.visit_expr(expr);219		}220		Expr::Import(kind, expr) => {221			v.visit_expr(expr);222223			if let Expr::Str(expr) = &**expr {224				v.visit_import(matches!(**kind, ImportKind::Normal), expr.clone());225			}226		}227		Expr::ErrorStmt(_span, expr) => {228			v.visit_expr(expr);229		}230		Expr::Apply(expr, spanned, _) => {231			v.visit_expr(expr);232			let ArgsDesc {233				unnamed,234				names: _,235				values,236			} = &**spanned;237			for unnamed in unnamed {238				v.visit_expr(unnamed);239			}240			for named in values {241				v.visit_expr(named);242			}243		}244		Expr::Index { indexable, parts } => {245			v.visit_expr(indexable);246247			for part in parts {248				let IndexPart {249					span: _,250					value,251					#[cfg(feature = "exp-null-coaelse")]252						null_coaelse: _,253				} = part;254				v.visit_expr(value);255			}256		}257		Expr::Function(expr_params, expr) => {258			visit_params(v, expr_params);259			v.visit_expr(expr);260		}261		Expr::IfElse(if_else) => {262			let IfElse {263				cond,264				cond_then,265				cond_else,266			} = &**if_else;267			visit_if_spec(v, cond);268			v.visit_expr(cond_then);269			if let Some(cond_else) = cond_else {270				v.visit_expr(cond_else);271			}272		}273		Expr::Slice(slice) => {274			let Slice { value, slice } = &**slice;275			v.visit_expr(value);276			let SliceDesc { start, end, step } = slice;277278			if let Some(start) = start {279				v.visit_expr(start);280			}281			if let Some(end) = end {282				v.visit_expr(end);283			}284			if let Some(step) = step {285				v.visit_expr(step);286			}287		}288	}289}
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)