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

difftreelog

fix destructure

mzvklmvoYaroslav Bolyukin2026-03-23parent: #a34df9d.patch.diff
in: master

4 files changed

modifiedcrates/jrsonnet-evaluator/src/evaluate/destructure.rsdiffbeforeafterboth
before · crates/jrsonnet-evaluator/src/evaluate/destructure.rs
1use std::{collections::HashMap, hash::BuildHasher};23use jrsonnet_interner::IStr;4use jrsonnet_ir::{BindSpec, Destruct};56use crate::{7	bail,8	error::{ErrorKind::*, Result},9	evaluate_method, evaluate_named_param, Context, Pending, Thunk, Val,10};1112#[cfg(feature = "exp-preserve-order")]13use crate::evaluate;1415#[allow(clippy::too_many_lines)]16#[allow(unused_variables)]17pub fn destruct<H: BuildHasher>(18	d: &Destruct,19	parent: Thunk<Val>,20	fctx: Pending<Context>,21	new_bindings: &mut HashMap<IStr, Thunk<Val>, H>,22) -> Result<()> {23	match d {24		Destruct::Full(v) => {25			let old = new_bindings.insert(v.clone(), parent);26			if old.is_some() {27				bail!(DuplicateLocalVar(v.clone()))28			}29		}30		#[cfg(feature = "exp-destruct")]31		Destruct::Skip => {}32		#[cfg(feature = "exp-destruct")]33		Destruct::Array { start, rest, end } => {34			use jrsonnet_ir::DestructRest;3536			let min_len = start.len() + end.len();37			let has_rest = rest.is_some();38			let full = Thunk!(move || {39				let v = parent.evaluate()?;40				let Val::Arr(arr) = v else {41					bail!("expected array");42				};43				if !has_rest {44					if arr.len() != min_len {45						bail!("expected {} elements, got {}", min_len, arr.len())46					}47				} else if arr.len() < min_len {48					bail!(49						"expected at least {} elements, but array was only {}",50						min_len,51						arr.len()52					)53				}54				Ok(arr)55			});5657			{58				for (i, d) in start.iter().enumerate() {59					let full = full.clone();60					destruct(61						d,62						Thunk!(move || Ok(full.evaluate()?.get(i)?.expect("length is checked"))),63						fctx.clone(),64						new_bindings,65					)?;66				}67			}6869			match rest {70				Some(DestructRest::Keep(v)) => {71					let start = start.len();72					let end = end.len();73					let full = full.clone();74					destruct(75						&Destruct::Full(v.clone()),76						Thunk!(move || {77							let full = full.evaluate()?;78							let to = full.len() - end;79							Ok(Val::Arr(full.slice(80								Some(start as i32),81								Some(to as i32),82								None,83							)))84						}),85						fctx.clone(),86						new_bindings,87					)?;88				}89				Some(DestructRest::Drop) | None => {}90			}9192			{93				for (i, d) in end.iter().enumerate() {94					let full = full.clone();95					let end = end.len();96					destruct(97						d,98						Thunk!(move || {99							let full = full.evaluate()?;100							Ok(full.get(full.len() - end + i)?.expect("length is checked"))101						}),102						fctx.clone(),103						new_bindings,104					)?;105				}106			}107		}108		#[cfg(feature = "exp-destruct")]109		Destruct::Object { fields, rest } => {110			let field_names: Vec<_> = fields111				.iter()112				.map(|f| (f.0.clone(), f.2.is_some()))113				.collect();114			let has_rest = rest.is_some();115			let full = Thunk!(move || {116				let v = parent.evaluate()?;117				let Val::Obj(obj) = v else {118					bail!("expected object");119				};120				for (field, has_default) in &field_names {121					if !has_default && !obj.has_field_ex(field.clone(), true) {122						bail!("missing field: {field}");123					}124				}125				if !has_rest {126					let len = obj.len();127					if len > field_names.len() {128						bail!("too many fields, and rest not found");129					}130				}131				Ok(obj)132			});133134			for (field, d, default) in fields {135				let default = default.clone().map(|e| (fctx.clone(), e));136				let value = {137					let field = field.clone();138					let full = full.clone();139					Thunk!(move || {140						let full = full.evaluate()?;141						if let Some(field) = full.get(field)? {142							Ok(field)143						} else {144							let (fctx, expr) = default.as_ref().expect("shape is checked");145							Ok(evaluate(fctx.clone().unwrap(), expr)?)146						}147					})148				};149150				if let Some(d) = d {151					destruct(d, value, fctx.clone(), new_bindings)?;152				} else {153					destruct(154						&Destruct::Full(field.clone()),155						value,156						fctx.clone(),157						new_bindings,158					)?;159				}160			}161		}162	}163	Ok(())164}165166pub fn evaluate_dest<H: BuildHasher>(167	d: &BindSpec,168	fctx: Pending<Context>,169	new_bindings: &mut HashMap<IStr, Thunk<Val>, H>,170) -> Result<()> {171	match d {172		BindSpec::Field { into, value } => {173			let name = into.name();174			let value = value.clone();175			let data = {176				let fctx = fctx.clone();177				Thunk!(move || evaluate_named_param(fctx.unwrap(), &value, name))178			};179			destruct(into, data, fctx, new_bindings)?;180		}181		BindSpec::Function {182			name,183			params,184			value,185		} => {186			let params = params.clone();187			let name = name.clone();188			let value = value.clone();189			let old = new_bindings.insert(name.clone(), {190				let name = name.clone();191				Thunk!(move || Ok(evaluate_method(fctx.unwrap(), name, params, value)))192			});193			if old.is_some() {194				bail!(DuplicateLocalVar(name))195			}196		}197	}198	Ok(())199}
after · crates/jrsonnet-evaluator/src/evaluate/destructure.rs
1use std::{collections::HashMap, hash::BuildHasher};23use jrsonnet_interner::IStr;4use jrsonnet_ir::{BindSpec, Destruct};56use crate::{7	bail,8	error::{ErrorKind::*, Result},9	evaluate_method, evaluate_named_param, Context, Pending, Thunk, Val,10};1112#[cfg(feature = "exp-preserve-order")]13use crate::evaluate;1415#[allow(clippy::too_many_lines)]16#[allow(unused_variables)]17pub fn destruct<H: BuildHasher>(18	d: &Destruct,19	parent: Thunk<Val>,20	fctx: Pending<Context>,21	new_bindings: &mut HashMap<IStr, Thunk<Val>, H>,22) -> Result<()> {23	match d {24		Destruct::Full(v) => {25			let old = new_bindings.insert(v.clone(), parent);26			if old.is_some() {27				bail!(DuplicateLocalVar(v.clone()))28			}29		}30		#[cfg(feature = "exp-destruct")]31		Destruct::Skip => {}32		#[cfg(feature = "exp-destruct")]33		Destruct::Array { start, rest, end } => {34			use jrsonnet_ir::DestructRest;3536			let min_len = start.len() + end.len();37			let has_rest = rest.is_some();38			let full = Thunk!(move || {39				let v = parent.evaluate()?;40				let Val::Arr(arr) = v else {41					bail!("expected array");42				};43				if !has_rest {44					if arr.len() != min_len {45						bail!("expected {} elements, got {}", min_len, arr.len())46					}47				} else if arr.len() < min_len {48					bail!(49						"expected at least {} elements, but array was only {}",50						min_len,51						arr.len()52					)53				}54				Ok(arr)55			});5657			{58				for (i, d) in start.iter().enumerate() {59					let full = full.clone();60					destruct(61						d,62						Thunk!(move || Ok(full.evaluate()?.get(i)?.expect("length is checked"))),63						fctx.clone(),64						new_bindings,65					)?;66				}67			}6869			match rest {70				Some(DestructRest::Keep(v)) => {71					let start = start.len();72					let end = end.len();73					let full = full.clone();74					destruct(75						&Destruct::Full(v.clone()),76						Thunk!(move || {77							let full = full.evaluate()?;78							let to = full.len() - end;79							Ok(Val::Arr(full.slice(80								Some(start as i32),81								Some(to as i32),82								None,83							)))84						}),85						fctx.clone(),86						new_bindings,87					)?;88				}89				Some(DestructRest::Drop) | None => {}90			}9192			{93				for (i, d) in end.iter().enumerate() {94					let full = full.clone();95					let end = end.len();96					destruct(97						d,98						Thunk!(move || {99							let full = full.evaluate()?;100							Ok(full.get(full.len() - end + i)?.expect("length is checked"))101						}),102						fctx.clone(),103						new_bindings,104					)?;105				}106			}107		}108		#[cfg(feature = "exp-destruct")]109		Destruct::Object { fields, rest } => {110			let field_names: Vec<_> = fields111				.iter()112				.map(|f| (f.0.clone(), f.2.is_some()))113				.collect();114			let has_rest = rest.is_some();115			let full = Thunk!(move || {116				let v = parent.evaluate()?;117				let Val::Obj(obj) = v else {118					bail!("expected object");119				};120				for (field, has_default) in &field_names {121					if !has_default && !obj.has_field_ex(field.clone(), true) {122						bail!("missing field: {field}");123					}124				}125				if !has_rest {126					let len = obj.len();127					if len > field_names.len() {128						bail!("too many fields, and rest not found");129					}130				}131				Ok(obj)132			});133134			for (field, d, default) in fields {135				let default = default.clone().map(|e| (fctx.clone(), e));136				let value = {137					let field = field.clone();138					let full = full.clone();139					Thunk!(move || {140						let full = full.evaluate()?;141						if let Some(field) = full.get(field)? {142							Ok(field)143						} else {144							let (fctx, expr) = default.as_ref().expect("shape is checked");145							Ok(crate::evaluate(fctx.clone().unwrap(), expr)?)146						}147					})148				};149150				if let Some(d) = d {151					destruct(d, value, fctx.clone(), new_bindings)?;152				} else {153					destruct(154						&Destruct::Full(field.clone()),155						value,156						fctx.clone(),157						new_bindings,158					)?;159				}160			}161		}162	}163	Ok(())164}165166pub fn evaluate_dest<H: BuildHasher>(167	d: &BindSpec,168	fctx: Pending<Context>,169	new_bindings: &mut HashMap<IStr, Thunk<Val>, H>,170) -> Result<()> {171	match d {172		BindSpec::Field { into, value } => {173			let name = into.name();174			let value = value.clone();175			let data = {176				let fctx = fctx.clone();177				Thunk!(move || evaluate_named_param(fctx.unwrap(), &value, name))178			};179			destruct(into, data, fctx, new_bindings)?;180		}181		BindSpec::Function {182			name,183			params,184			value,185		} => {186			let params = params.clone();187			let name = name.clone();188			let value = value.clone();189			let old = new_bindings.insert(name.clone(), {190				let name = name.clone();191				Thunk!(move || Ok(evaluate_method(fctx.unwrap(), name, params, value)))192			});193			if old.is_some() {194				bail!(DuplicateLocalVar(name))195			}196		}197	}198	Ok(())199}
modifiedcrates/jrsonnet-evaluator/src/evaluate/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/evaluate/mod.rs
+++ b/crates/jrsonnet-evaluator/src/evaluate/mod.rs
@@ -142,7 +142,7 @@
 						false,
 					) {
 						let fctx = Pending::new();
-						let mut new_bindings = FxHashMap::with_capacity(var.binds_len());
+						let mut new_bindings = FxHashMap::with_capacity(into.binds_len());
 						let obj = obj.clone();
 						let value = Thunk::evaluated(Val::Arr(ArrValue::lazy(vec![
 							Thunk::evaluated(Val::string(field.clone())),
@@ -150,7 +150,7 @@
 								"field exists, as field name was obtained from object.fields()",
 							)),
 						])));
-						destruct(var, value, fctx.clone(), &mut new_bindings)?;
+						destruct(into, value, fctx.clone(), &mut new_bindings)?;
 						let ctx = ctx.clone().extend_bindings(new_bindings).into_future(fctx);
 
 						evaluate_comp(ctx, &specs[1..], callback)?;
modifiedcrates/jrsonnet-ir-parser/Cargo.tomldiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/Cargo.toml
+++ b/crates/jrsonnet-ir-parser/Cargo.toml
@@ -8,6 +8,7 @@
 
 [features]
 exp-null-coaelse = ["jrsonnet-ir/exp-null-coaelse"]
+exp-destruct = ["jrsonnet-ir/exp-destruct"]
 
 [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
@@ -3,9 +3,9 @@
 use jrsonnet_gcmodule::Acyclic;
 use jrsonnet_ir::{
 	unescape, ArgsDesc, AssertExpr, AssertStmt, BinaryOp, BinaryOpType, BindSpec, CompSpec,
-	Destruct, Expr, ExprParam, ExprParams, FieldMember, FieldName, ForSpecData, IStr, IfElse,
-	IfSpecData, ImportKind, IndexPart, LiteralType, Member, ObjBody, ObjComp, ObjMembers, Slice,
-	SliceDesc, Source, Span, Spanned, UnaryOpType, Visibility,
+	Destruct, DestructRest, Expr, ExprParam, ExprParams, FieldMember, FieldName, ForSpecData, IStr,
+	IfElse, IfSpecData, ImportKind, IndexPart, LiteralType, Member, ObjBody, ObjComp, ObjMembers,
+	Slice, SliceDesc, Source, Span, Spanned, UnaryOpType, Visibility,
 };
 use jrsonnet_lexer::{collect_lexed_str_block, Lexeme, Lexer, SyntaxKind, T};
 
@@ -316,7 +316,114 @@
 }
 
 fn destruct(p: &mut Parser<'_>) -> R<Destruct> {
-	Ok(Destruct::Full(p.expect_ident()?))
+	if p.at_ident() {
+		return Ok(Destruct::Full(p.expect_ident()?));
+	}
+	#[cfg(not(feature = "exp-destruct"))]
+	return Err(p.error(format!(
+		"expected identifier, got {}",
+		p.current_desc()
+	)));
+	#[cfg(feature = "exp-destruct")]
+	{
+		if p.try_eat(T![?]) {
+			return Ok(Destruct::Skip);
+		}
+		if p.at(T!['[']) {
+			return destruct_array(p);
+		}
+		if p.at(T!['{']) {
+			return destruct_object(p);
+		}
+		Err(p.error(format!(
+			"expected destructure pattern, got {}",
+			p.current_desc()
+		)))
+	}
+}
+
+#[cfg(feature = "exp-destruct")]
+fn destruct_rest(p: &mut Parser<'_>) -> R<DestructRest> {
+	p.eat(T![...])?;
+	if p.at_ident() {
+		Ok(DestructRest::Keep(p.expect_ident()?))
+	} else {
+		Ok(DestructRest::Drop)
+	}
+}
+
+#[cfg(feature = "exp-destruct")]
+fn destruct_array(p: &mut Parser<'_>) -> R<Destruct> {
+	p.eat(T!['['])?;
+	let mut start = Vec::new();
+	let mut rest = None;
+	let mut end = Vec::new();
+	if !p.at(T![']']) {
+		loop {
+			if p.at(T![...]) {
+				rest = Some(destruct_rest(p)?);
+				if p.try_eat(T![,]) {
+					if !p.at(T![']']) {
+						loop {
+							end.push(destruct(p)?);
+							if !p.try_eat(T![,]) {
+								break;
+							}
+							if p.at(T![']']) {
+								break;
+							}
+						}
+					}
+				}
+				break;
+			}
+			start.push(destruct(p)?);
+			if !p.try_eat(T![,]) {
+				break;
+			}
+			if p.at(T![']']) {
+				break;
+			}
+		}
+	}
+	p.eat(T![']'])?;
+	Ok(Destruct::Array { start, rest, end })
+}
+
+#[cfg(feature = "exp-destruct")]
+fn destruct_object(p: &mut Parser<'_>) -> R<Destruct> {
+	p.eat(T!['{'])?;
+	let mut fields = Vec::new();
+	let mut rest = None;
+	if !p.at(T!['}']) {
+		loop {
+			if p.at(T![...]) {
+				rest = Some(destruct_rest(p)?);
+				p.try_eat(T![,]);
+				break;
+			}
+			let name = p.expect_ident()?;
+			let into = if p.try_eat(T![:]) {
+				Some(destruct(p)?)
+			} else {
+				None
+			};
+			let default = if p.try_eat(T![=]) {
+				Some(Rc::new(spanned(p, expr)?))
+			} else {
+				None
+			};
+			fields.push((name, into, default));
+			if !p.try_eat(T![,]) {
+				break;
+			}
+			if p.at(T!['}']) {
+				break;
+			}
+		}
+	}
+	p.eat(T!['}'])?;
+	Ok(Destruct::Object { fields, rest })
 }
 
 fn params(p: &mut Parser<'_>) -> R<ExprParams> {
@@ -383,6 +490,15 @@
 }
 
 fn bind(p: &mut Parser<'_>) -> R<BindSpec> {
+	#[cfg(feature = "exp-destruct")]
+	{
+		if !p.at_ident() {
+			let d = destruct(p)?;
+			p.eat(T![=])?;
+			let value = Rc::new(expr(p)?);
+			return Ok(BindSpec::Field { into: d, value });
+		}
+	}
 	let name = p.expect_ident()?;
 	if p.try_eat(T!['(']) {
 		let ps = params(p)?;