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

difftreelog

feat allow unnamed builtin arguments

Yaroslav Bolyukin2022-10-11parent: #1178a0b.patch.diff
in: master

5 files changed

modifiedbindings/jsonnet/src/native.rsdiffbeforeafterboth
--- a/bindings/jsonnet/src/native.rs
+++ b/bindings/jsonnet/src/native.rs
@@ -1,11 +1,12 @@
 use std::{
+	borrow::Cow,
 	ffi::{c_void, CStr},
 	os::raw::{c_char, c_int},
 };
 
 use jrsonnet_evaluator::{
 	error::{Error, LocError},
-	function::builtin::{BuiltinParam, NativeCallback, NativeCallbackHandler},
+	function::builtin::{NativeCallback, NativeCallbackHandler},
 	tb,
 	typed::Typed,
 	IStr, State, Val,
@@ -87,10 +88,7 @@
 		let param = CStr::from_ptr(*raw_params)
 			.to_str()
 			.expect("param name is not utf-8");
-		params.push(BuiltinParam {
-			name: param.into(),
-			has_default: false,
-		});
+		params.push(Cow::Owned(param.into()));
 		raw_params = raw_params.offset(1);
 	}
 
modifiedcrates/jrsonnet-evaluator/src/error.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/error.rs
+++ b/crates/jrsonnet-evaluator/src/error.rs
@@ -113,8 +113,8 @@
 	BindingParameterASecondTime(IStr),
 	#[error("too many args, function has {0}{}", format_signature(.1))]
 	TooManyArgsFunctionHas(usize, FunctionSignature),
-	#[error("function argument is not passed: {0}{}", format_signature(.1))]
-	FunctionParameterNotBoundInCall(IStr, FunctionSignature),
+	#[error("function argument is not passed: {}{}", .0.as_ref().map(|n| n.as_str()).unwrap_or("<unnamed>"), format_signature(.1))]
+	FunctionParameterNotBoundInCall(Option<IStr>, FunctionSignature),
 
 	#[error("external variable is not defined: {0}")]
 	UndefinedExternalVariable(IStr),
modifiedcrates/jrsonnet-evaluator/src/function/builtin.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function/builtin.rs
+++ b/crates/jrsonnet-evaluator/src/function/builtin.rs
@@ -9,7 +9,9 @@
 
 #[derive(Clone, Trace)]
 pub struct BuiltinParam {
-	pub name: BuiltinParamName,
+	/// Parameter name for named call parsing
+	pub name: Option<BuiltinParamName>,
+	/// Is implementation allowed to return empty value
 	pub has_default: bool,
 }
 
@@ -40,8 +42,20 @@
 }
 impl NativeCallback {
 	#[deprecated = "prefer using builtins directly, use this interface only for bindings"]
-	pub fn new(params: Vec<BuiltinParam>, handler: TraceBox<dyn NativeCallbackHandler>) -> Self {
-		Self { params, handler }
+	pub fn new(
+		params: Vec<Cow<'static, str>>,
+		handler: TraceBox<dyn NativeCallbackHandler>,
+	) -> Self {
+		Self {
+			params: params
+				.into_iter()
+				.map(|n| BuiltinParam {
+					name: Some(n),
+					has_default: false,
+				})
+				.collect(),
+			handler,
+		}
 	}
 }
 
@@ -58,11 +72,12 @@
 
 	fn call(&self, s: State, ctx: Context, _loc: CallLocation, args: &dyn ArgsLike) -> Result<Val> {
 		let args = parse_builtin_call(s.clone(), ctx, &self.params, args, true)?;
-		let mut out_args = Vec::with_capacity(self.params.len());
-		for p in &self.params {
-			out_args.push(args[&p.name].evaluate(s.clone())?);
-		}
-		self.handler.call(s, &out_args)
+		let args = args
+			.into_iter()
+			.map(|a| a.expect("legacy natives have no default params"))
+			.map(|a| a.evaluate(s.clone()))
+			.collect::<Result<Vec<Val>>>()?;
+		self.handler.call(s, &args)
 	}
 }
 
modifiedcrates/jrsonnet-evaluator/src/function/parse.rsdiffbeforeafterboth
before · crates/jrsonnet-evaluator/src/function/parse.rs
1use jrsonnet_gcmodule::Trace;2use jrsonnet_interner::IStr;3use jrsonnet_parser::{LocExpr, ParamsDesc};45use super::{6	arglike::ArgsLike,7	builtin::{BuiltinParam, BuiltinParamName},8};9use crate::{10	destructure::destruct,11	error::{Error::*, Result},12	evaluate_named,13	gc::GcHashMap,14	tb, throw,15	val::ThunkValue,16	Context, Pending, State, Thunk, Val,17};1819#[derive(Trace)]20struct EvaluateNamedThunk {21	ctx: Pending<Context>,22	name: IStr,23	value: LocExpr,24}2526impl ThunkValue for EvaluateNamedThunk {27	type Output = Val;28	fn get(self: Box<Self>, s: State) -> Result<Val> {29		evaluate_named(s, self.ctx.unwrap(), &self.value, self.name)30	}31}3233/// Creates correct [context](Context) for function body evaluation returning error on invalid call.34///35/// ## Parameters36/// * `ctx`: used for passed argument expressions' execution and for body execution (if `body_ctx` is not set)37/// * `body_ctx`: used for default parameter values' execution and for body execution (if set)38/// * `params`: function parameters' definition39/// * `args`: passed function arguments40/// * `tailstrict`: if set to `true` function arguments are eagerly executed, otherwise - lazily41pub fn parse_function_call(42	s: State,43	ctx: Context,44	body_ctx: Context,45	params: &ParamsDesc,46	args: &dyn ArgsLike,47	tailstrict: bool,48) -> Result<Context> {49	let mut passed_args = GcHashMap::with_capacity(params.len());50	if args.unnamed_len() > params.len() {51		throw!(TooManyArgsFunctionHas(52			params.len(),53			params.iter().map(|p| (p.0.name(), p.1.is_some())).collect()54		))55	}5657	let mut filled_named = 0;58	let mut filled_positionals = 0;5960	args.unnamed_iter(s.clone(), ctx.clone(), tailstrict, &mut |id, arg| {61		let name = params[id].0.clone();62		destruct(63			&name,64			arg,65			Pending::new_filled(ctx.clone()),66			&mut passed_args,67		)?;68		filled_positionals += 1;69		Ok(())70	})?;7172	args.named_iter(s, ctx, tailstrict, &mut |name, value| {73		// FIXME: O(n) for arg existence check74		if !params.iter().any(|p| p.0.name().as_ref() == Some(name)) {75			throw!(UnknownFunctionParameter((name as &str).to_owned()));76		}77		if passed_args.insert(name.clone(), value).is_some() {78			throw!(BindingParameterASecondTime(name.clone()));79		}80		filled_named += 1;81		Ok(())82	})?;8384	if filled_named + filled_positionals < params.len() {85		// Some args are unset, but maybe we have defaults for them86		// Default values should be created in newly created context87		let fctx = Context::new_future();88		let mut defaults =89			GcHashMap::with_capacity(params.len() - filled_named - filled_positionals);9091		for (idx, param) in params.iter().enumerate().filter(|p| p.1 .1.is_some()) {92			if let Some(name) = param.0.name() {93				if passed_args.contains_key(&name) {94					continue;95				}96			} else if idx < filled_positionals {97				continue;98			}99100			destruct(101				&param.0,102				Thunk::new(tb!(EvaluateNamedThunk {103					ctx: fctx.clone(),104					name: param.0.name().unwrap_or_else(|| "<destruct>".into()),105					value: param.1.clone().expect("default exists"),106				})),107				fctx.clone(),108				&mut defaults,109			)?;110			if param.0.name().is_some() {111				filled_named += 1;112			} else {113				filled_positionals += 1;114			}115		}116117		// Some args still weren't filled118		if filled_named + filled_positionals != params.len() {119			for param in params.iter().skip(args.unnamed_len()) {120				let mut found = false;121				args.named_names(&mut |name| {122					if Some(name) == param.0.name().as_ref() {123						found = true;124					}125				});126				if !found {127					throw!(FunctionParameterNotBoundInCall(128						param129							.0130							.clone()131							.name()132							.unwrap_or_else(|| "<destruct>".into()),133						params.iter().map(|p| (p.0.name(), p.1.is_some())).collect()134					));135				}136			}137			unreachable!();138		}139140		Ok(body_ctx141			.extend(passed_args, None, None, None)142			.extend(defaults, None, None, None)143			.into_future(fctx))144	} else {145		let body_ctx = body_ctx.extend(passed_args, None, None, None);146		Ok(body_ctx)147	}148}149150/// You shouldn't probally use this function, use `jrsonnet_macros::builtin` instead151///152/// ## Parameters153/// * `ctx`: used for passed argument expressions' execution and for body execution (if `body_ctx` is not set)154/// * `params`: function parameters' definition155/// * `args`: passed function arguments156/// * `tailstrict`: if set to `true` function arguments are eagerly executed, otherwise - lazily157pub fn parse_builtin_call(158	s: State,159	ctx: Context,160	params: &[BuiltinParam],161	args: &dyn ArgsLike,162	tailstrict: bool,163) -> Result<GcHashMap<BuiltinParamName, Thunk<Val>>> {164	let mut passed_args = GcHashMap::with_capacity(params.len());165	if args.unnamed_len() > params.len() {166		throw!(TooManyArgsFunctionHas(167			params.len(),168			params169				.iter()170				.map(|p| (Some(p.name.as_ref().into()), p.has_default))171				.collect()172		))173	}174175	let mut filled_args = 0;176177	args.unnamed_iter(s.clone(), ctx.clone(), tailstrict, &mut |id, arg| {178		let name = params[id].name.clone();179		passed_args.insert(name, arg);180		filled_args += 1;181		Ok(())182	})?;183184	args.named_iter(s, ctx, tailstrict, &mut |name, arg| {185		// FIXME: O(n) for arg existence check186		let p = params187			.iter()188			.find(|p| p.name == name as &str)189			.ok_or_else(|| UnknownFunctionParameter((name as &str).to_owned()))?;190		if passed_args.insert(p.name.clone(), arg).is_some() {191			throw!(BindingParameterASecondTime(name.clone()));192		}193		filled_args += 1;194		Ok(())195	})?;196197	if filled_args < params.len() {198		for param in params.iter().filter(|p| p.has_default) {199			if passed_args.contains_key(&param.name) {200				continue;201			}202			filled_args += 1;203		}204205		// Some args still wasn't filled206		if filled_args != params.len() {207			for param in params.iter().skip(args.unnamed_len()) {208				let mut found = false;209				args.named_names(&mut |name| {210					if name as &str == &param.name as &str {211						found = true;212					}213				});214				if !found {215					throw!(FunctionParameterNotBoundInCall(216						param.name.clone().into(),217						params218							.iter()219							.map(|p| (Some(p.name.as_ref().into()), p.has_default))220							.collect()221					));222				}223			}224			unreachable!();225		}226	}227	Ok(passed_args)228}229230/// Creates Context, which has all argument default values applied231/// and with unbound values causing error to be returned232pub fn parse_default_function_call(body_ctx: Context, params: &ParamsDesc) -> Result<Context> {233	#[derive(Trace)]234	struct DependsOnUnbound(IStr, ParamsDesc);235	impl ThunkValue for DependsOnUnbound {236		type Output = Val;237		fn get(self: Box<Self>, _: State) -> Result<Val> {238			Err(FunctionParameterNotBoundInCall(239				self.0.clone(),240				self.1.iter().map(|p| (p.0.name(), p.1.is_some())).collect(),241			)242			.into())243		}244	}245246	let fctx = Context::new_future();247248	let mut bindings = GcHashMap::new();249250	for param in params.iter() {251		if let Some(v) = &param.1 {252			destruct(253				&param.0.clone(),254				Thunk::new(tb!(EvaluateNamedThunk {255					ctx: fctx.clone(),256					name: param.0.name().unwrap_or_else(|| "<destruct>".into()),257					value: v.clone(),258				})),259				fctx.clone(),260				&mut bindings,261			)?;262		} else {263			destruct(264				&param.0,265				Thunk::new(tb!(DependsOnUnbound(266					param.0.name().unwrap_or_else(|| "<destruct>".into()),267					params.clone()268				))),269				fctx.clone(),270				&mut bindings,271			)?;272		}273	}274275	Ok(body_ctx276		.extend(bindings, None, None, None)277		.into_future(fctx))278}
after · crates/jrsonnet-evaluator/src/function/parse.rs
1use std::mem::replace;23use jrsonnet_gcmodule::Trace;4use jrsonnet_interner::IStr;5use jrsonnet_parser::{LocExpr, ParamsDesc};67use super::{arglike::ArgsLike, builtin::BuiltinParam};8use crate::{9	destructure::destruct,10	error::{Error::*, Result},11	evaluate_named,12	gc::GcHashMap,13	tb, throw,14	val::ThunkValue,15	Context, Pending, State, Thunk, Val,16};1718#[derive(Trace)]19struct EvaluateNamedThunk {20	ctx: Pending<Context>,21	name: IStr,22	value: LocExpr,23}2425impl ThunkValue for EvaluateNamedThunk {26	type Output = Val;27	fn get(self: Box<Self>, s: State) -> Result<Val> {28		evaluate_named(s, self.ctx.unwrap(), &self.value, self.name)29	}30}3132/// Creates correct [context](Context) for function body evaluation returning error on invalid call.33///34/// ## Parameters35/// * `ctx`: used for passed argument expressions' execution and for body execution (if `body_ctx` is not set)36/// * `body_ctx`: used for default parameter values' execution and for body execution (if set)37/// * `params`: function parameters' definition38/// * `args`: passed function arguments39/// * `tailstrict`: if set to `true` function arguments are eagerly executed, otherwise - lazily40pub fn parse_function_call(41	s: State,42	ctx: Context,43	body_ctx: Context,44	params: &ParamsDesc,45	args: &dyn ArgsLike,46	tailstrict: bool,47) -> Result<Context> {48	let mut passed_args = GcHashMap::with_capacity(params.len());49	if args.unnamed_len() > params.len() {50		throw!(TooManyArgsFunctionHas(51			params.len(),52			params.iter().map(|p| (p.0.name(), p.1.is_some())).collect()53		))54	}5556	let mut filled_named = 0;57	let mut filled_positionals = 0;5859	args.unnamed_iter(s.clone(), ctx.clone(), tailstrict, &mut |id, arg| {60		let name = params[id].0.clone();61		destruct(62			&name,63			arg,64			Pending::new_filled(ctx.clone()),65			&mut passed_args,66		)?;67		filled_positionals += 1;68		Ok(())69	})?;7071	args.named_iter(s, ctx, tailstrict, &mut |name, value| {72		// FIXME: O(n) for arg existence check73		if !params.iter().any(|p| p.0.name().as_ref() == Some(name)) {74			throw!(UnknownFunctionParameter((name as &str).to_owned()));75		}76		if passed_args.insert(name.clone(), value).is_some() {77			throw!(BindingParameterASecondTime(name.clone()));78		}79		filled_named += 1;80		Ok(())81	})?;8283	if filled_named + filled_positionals < params.len() {84		// Some args are unset, but maybe we have defaults for them85		// Default values should be created in newly created context86		let fctx = Context::new_future();87		let mut defaults =88			GcHashMap::with_capacity(params.len() - filled_named - filled_positionals);8990		for (idx, param) in params.iter().enumerate().filter(|p| p.1 .1.is_some()) {91			if let Some(name) = param.0.name() {92				if passed_args.contains_key(&name) {93					continue;94				}95			} else if idx < filled_positionals {96				continue;97			}9899			destruct(100				&param.0,101				Thunk::new(tb!(EvaluateNamedThunk {102					ctx: fctx.clone(),103					name: param.0.name().unwrap_or_else(|| "<destruct>".into()),104					value: param.1.clone().expect("default exists"),105				})),106				fctx.clone(),107				&mut defaults,108			)?;109			if param.0.name().is_some() {110				filled_named += 1;111			} else {112				filled_positionals += 1;113			}114		}115116		// Some args still weren't filled117		if filled_named + filled_positionals != params.len() {118			for param in params.iter().skip(args.unnamed_len()) {119				let mut found = false;120				args.named_names(&mut |name| {121					if Some(name) == param.0.name().as_ref() {122						found = true;123					}124				});125				if !found {126					throw!(FunctionParameterNotBoundInCall(127						param.0.clone().name(),128						params.iter().map(|p| (p.0.name(), p.1.is_some())).collect()129					));130				}131			}132			unreachable!();133		}134135		Ok(body_ctx136			.extend(passed_args, None, None, None)137			.extend(defaults, None, None, None)138			.into_future(fctx))139	} else {140		let body_ctx = body_ctx.extend(passed_args, None, None, None);141		Ok(body_ctx)142	}143}144145/// You shouldn't probally use this function, use `jrsonnet_macros::builtin` instead146///147/// ## Parameters148/// * `ctx`: used for passed argument expressions' execution and for body execution (if `body_ctx` is not set)149/// * `params`: function parameters' definition150/// * `args`: passed function arguments151/// * `tailstrict`: if set to `true` function arguments are eagerly executed, otherwise - lazily152pub fn parse_builtin_call(153	s: State,154	ctx: Context,155	params: &[BuiltinParam],156	args: &dyn ArgsLike,157	tailstrict: bool,158) -> Result<Vec<Option<Thunk<Val>>>> {159	let mut passed_args: Vec<Option<Thunk<Val>>> = vec![None; params.len()];160	if args.unnamed_len() > params.len() {161		throw!(TooManyArgsFunctionHas(162			params.len(),163			params164				.iter()165				.map(|p| (p.name.as_ref().map(|v| v.as_ref().into()), p.has_default))166				.collect()167		))168	}169170	let mut filled_args = 0;171172	args.unnamed_iter(s.clone(), ctx.clone(), tailstrict, &mut |id, arg| {173		passed_args[id] = Some(arg);174		filled_args += 1;175		Ok(())176	})?;177178	args.named_iter(s, ctx, tailstrict, &mut |name, arg| {179		// FIXME: O(n) for arg existence check180		let id = params181			.iter()182			.position(|p| {183				p.name184					.as_ref()185					.map(|v| &v as &str == name as &str)186					.unwrap_or(false)187			})188			.ok_or_else(|| UnknownFunctionParameter((name as &str).to_owned()))?;189		if replace(&mut passed_args[id], Some(arg)).is_some() {190			throw!(BindingParameterASecondTime(name.clone()));191		}192		filled_args += 1;193		Ok(())194	})?;195196	if filled_args < params.len() {197		for (id, _) in params.iter().enumerate().filter(|(_, p)| p.has_default) {198			if passed_args[id].is_some() {199				continue;200			}201			filled_args += 1;202		}203204		// Some args still wasn't filled205		if filled_args != params.len() {206			for param in params.iter().skip(args.unnamed_len()) {207				let mut found = false;208				args.named_names(&mut |name| {209					if param210						.name211						.as_ref()212						.map(|v| &v as &str == name as &str)213						.unwrap_or(false)214					{215						found = true;216					}217				});218				if !found {219					throw!(FunctionParameterNotBoundInCall(220						param.name.as_ref().map(|v| v.as_ref().into()),221						params222							.iter()223							.map(|p| (p.name.as_ref().map(|p| p.as_ref().into()), p.has_default))224							.collect()225					));226				}227			}228			unreachable!();229		}230	}231	Ok(passed_args)232}233234/// Creates Context, which has all argument default values applied235/// and with unbound values causing error to be returned236pub fn parse_default_function_call(body_ctx: Context, params: &ParamsDesc) -> Result<Context> {237	#[derive(Trace)]238	struct DependsOnUnbound(IStr, ParamsDesc);239	impl ThunkValue for DependsOnUnbound {240		type Output = Val;241		fn get(self: Box<Self>, _: State) -> Result<Val> {242			Err(FunctionParameterNotBoundInCall(243				Some(self.0.clone()),244				self.1.iter().map(|p| (p.0.name(), p.1.is_some())).collect(),245			)246			.into())247		}248	}249250	let fctx = Context::new_future();251252	let mut bindings = GcHashMap::new();253254	for param in params.iter() {255		if let Some(v) = &param.1 {256			destruct(257				&param.0.clone(),258				Thunk::new(tb!(EvaluateNamedThunk {259					ctx: fctx.clone(),260					name: param.0.name().unwrap_or_else(|| "<destruct>".into()),261					value: v.clone(),262				})),263				fctx.clone(),264				&mut bindings,265			)?;266		} else {267			destruct(268				&param.0,269				Thunk::new(tb!(DependsOnUnbound(270					param.0.name().unwrap_or_else(|| "<destruct>".into()),271					params.clone()272				))),273				fctx.clone(),274				&mut bindings,275			)?;276		}277	}278279	Ok(body_ctx280		.extend(bindings, None, None, None)281		.into_future(fctx))282}
modifiedcrates/jrsonnet-macros/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-macros/src/lib.rs
+++ b/crates/jrsonnet-macros/src/lib.rs
@@ -122,13 +122,13 @@
 	Normal {
 		ty: Box<Type>,
 		is_option: bool,
-		name: String,
+		name: Option<String>,
 		cfg_attrs: Vec<Attribute>,
 		// ident: Ident,
 	},
 	Lazy {
 		is_option: bool,
-		name: String,
+		name: Option<String>,
 	},
 	State,
 	Location,
@@ -142,8 +142,8 @@
 			FnArg::Typed(a) => a,
 		};
 		let ident = match &arg.pat as &Pat {
-			Pat::Ident(i) => i.ident.clone(),
-			_ => return Err(Error::new(arg.pat.span(), "arg should be plain identifier")),
+			Pat::Ident(i) => Some(i.ident.clone()),
+			_ => None,
 		};
 		let ty = &arg.ty;
 		if type_is_path(ty, "State").is_some() {
@@ -153,7 +153,7 @@
 		} else if type_is_path(ty, "Thunk").is_some() {
 			return Ok(Self::Lazy {
 				is_option: false,
-				name: ident.to_string(),
+				name: ident.map(|v| v.to_string()),
 			});
 		}
 
@@ -166,7 +166,7 @@
 			if type_is_path(ty, "Thunk").is_some() {
 				return Ok(Self::Lazy {
 					is_option: true,
-					name: ident.to_string(),
+					name: ident.map(|v| v.to_string()),
 				});
 			}
 
@@ -185,7 +185,7 @@
 		Ok(Self::Normal {
 			ty,
 			is_option,
-			name: ident.to_string(),
+			name: ident.map(|v| v.to_string()),
 			cfg_attrs,
 		})
 	}
@@ -248,69 +248,95 @@
 			name,
 			cfg_attrs,
 			..
-		} => Some(quote! {
-			#(#cfg_attrs)*
-			BuiltinParam {
-				name: std::borrow::Cow::Borrowed(#name),
-				has_default: #is_option,
-			},
-		}),
-		ArgInfo::Lazy { is_option, name } => Some(quote! {
-			BuiltinParam {
-				name: std::borrow::Cow::Borrowed(#name),
-				has_default: #is_option,
-			},
-		}),
+		} => {
+			let name = name
+				.as_ref()
+				.map(|n| quote! {Some(std::borrow::Cow::Borrowed(#n))})
+				.unwrap_or_else(|| quote! {None});
+			Some(quote! {
+				#(#cfg_attrs)*
+				BuiltinParam {
+					name: #name,
+					has_default: #is_option,
+				},
+			})
+		}
+		ArgInfo::Lazy { is_option, name } => {
+			let name = name
+				.as_ref()
+				.map(|n| quote! {Some(std::borrow::Cow::Borrowed(#n))})
+				.unwrap_or_else(|| quote! {None});
+			Some(quote! {
+				BuiltinParam {
+					name: #name,
+					has_default: #is_option,
+				},
+			})
+		}
 		ArgInfo::State => None,
 		ArgInfo::Location => None,
 		ArgInfo::This => None,
 	});
 
-	let pass = args.iter().map(|a| match a {
-		ArgInfo::Normal {
-			ty,
-			is_option,
-			name,
-			cfg_attrs,
-		} => {
-			let eval = quote! {s.push_description(
-				|| format!("argument <{}> evaluation", #name),
-				|| <#ty>::from_untyped(value.evaluate(s.clone())?, s.clone()),
-			)?};
-			let value = if *is_option {
-				quote! {if let Some(value) = parsed.get(#name) {
-					Some(#eval)
+	let mut id = 0usize;
+	let pass = args
+		.iter()
+		.map(|a| match a {
+			ArgInfo::Normal { .. } | ArgInfo::Lazy { .. } => {
+				let cid = id;
+				id += 1;
+				(quote! {#cid}, a)
+			}
+			ArgInfo::State | ArgInfo::Location | ArgInfo::This => {
+				(quote! {compile_error!("should not use id")}, a)
+			}
+		})
+		.map(|(id, a)| match a {
+			ArgInfo::Normal {
+				ty,
+				is_option,
+				name,
+				cfg_attrs,
+			} => {
+				let name = name.as_ref().map(|v| v.as_str()).unwrap_or("<unnamed>");
+				let eval = quote! {s.push_description(
+					|| format!("argument <{}> evaluation", #name),
+					|| <#ty>::from_untyped(value.evaluate(s.clone())?, s.clone()),
+				)?};
+				let value = if *is_option {
+					quote! {if let Some(value) = &parsed[#id] {
+						Some(#eval)
+					} else {
+						None
+					},}
 				} else {
-					None
-				},}
-			} else {
-				quote! {{
-					let value = parsed.get(#name).expect("args shape is checked");
-					#eval
-				},}
-			};
-			quote! {
-				#(#cfg_attrs)*
-				#value
+					quote! {{
+						let value = parsed[#id].as_ref().expect("args shape is checked");
+						#eval
+					},}
+				};
+				quote! {
+					#(#cfg_attrs)*
+					#value
+				}
 			}
-		}
-		ArgInfo::Lazy { is_option, name } => {
-			if *is_option {
-				quote! {if let Some(value) = parsed.get(#name) {
-					Some(value.clone())
+			ArgInfo::Lazy { is_option, .. } => {
+				if *is_option {
+					quote! {if let Some(value) = &parsed[#id] {
+						Some(value.clone())
+					} else {
+						None
+					}}
 				} else {
-					None
-				}}
-			} else {
-				quote! {
-					parsed.get(#name).expect("args shape is correct").clone(),
+					quote! {
+						parsed[#id].as_ref().expect("args shape is correct").clone(),
+					}
 				}
 			}
-		}
-		ArgInfo::State => quote! {s.clone(),},
-		ArgInfo::Location => quote! {location,},
-		ArgInfo::This => quote! {self,},
-	});
+			ArgInfo::State => quote! {s.clone(),},
+			ArgInfo::Location => quote! {location,},
+			ArgInfo::This => quote! {self,},
+		});
 
 	let fields = attr.fields.iter().map(|field| {
 		let name = &field.name;