git.delta.rocks / jrsonnet / refs/commits / 50ca1d2d134a

difftreelog

feat(evaluator) function signature help

Yaroslav Bolyukin2022-07-23parent: #907d6da.patch.diff
in: master
When calling functions with wrong arguments, evaluator will now suggest
correct function signature

2 files changed

modifiedcrates/jrsonnet-evaluator/src/error.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/error.rs
+++ b/crates/jrsonnet-evaluator/src/error.rs
@@ -32,6 +32,31 @@
 	out
 }
 
+fn format_signature(sig: &FunctionSignature) -> String {
+	let mut out = String::new();
+	out.push_str("\nFunction has the following signature: ");
+	out.push('(');
+	if sig.is_empty() {
+		out.push_str("/*no arguments*/");
+	} else {
+		for (i, (name, has_default)) in sig.iter().enumerate() {
+			if i != 0 {
+				out.push_str(", ");
+			}
+			if let Some(name) = name {
+				out.push_str(name);
+			} else {
+				out.push_str("<unnamed>");
+			}
+			if *has_default {
+				out.push_str(" = <default>");
+			}
+		}
+	}
+	out.push(')');
+	out
+}
+
 const fn format_empty_str(str: &str) -> &str {
 	if str.is_empty() {
 		"\"\" (empty string)"
@@ -40,6 +65,8 @@
 	}
 }
 
+type FunctionSignature = Vec<(Option<IStr>, bool)>;
+
 #[derive(Error, Debug, Clone, Trace)]
 pub enum Error {
 	#[error("intrinsic not found: {0}")]
@@ -84,10 +111,10 @@
 	UnknownFunctionParameter(String),
 	#[error("argument {0} is already bound")]
 	BindingParameterASecondTime(IStr),
-	#[error("too many args, function has {0}")]
-	TooManyArgsFunctionHas(usize),
-	#[error("function argument is not passed: {0}")]
-	FunctionParameterNotBoundInCall(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("external variable is not defined: {0}")]
 	UndefinedExternalVariable(IStr),
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(params.len()))52	}5354	let mut filled_named = 0;55	let mut filled_positionals = 0;5657	args.unnamed_iter(s.clone(), ctx.clone(), tailstrict, &mut |id, arg| {58		let name = params[id].0.clone();59		destruct(60			&name,61			arg,62			Pending::new_filled(ctx.clone()),63			&mut passed_args,64		)?;65		filled_positionals += 1;66		Ok(())67	})?;6869	args.named_iter(s, ctx, tailstrict, &mut |name, value| {70		// FIXME: O(n) for arg existence check71		if !params.iter().any(|p| p.0.name().as_ref() == Some(name)) {72			throw!(UnknownFunctionParameter((name as &str).to_owned()));73		}74		if passed_args.insert(name.clone(), value).is_some() {75			throw!(BindingParameterASecondTime(name.clone()));76		}77		filled_named += 1;78		Ok(())79	})?;8081	if filled_named + filled_positionals < params.len() {82		// Some args are unset, but maybe we have defaults for them83		// Default values should be created in newly created context84		let fctx = Context::new_future();85		let mut defaults =86			GcHashMap::with_capacity(params.len() - filled_named - filled_positionals);8788		for (idx, param) in params.iter().enumerate().filter(|p| p.1 .1.is_some()) {89			if let Some(name) = param.0.name() {90				if passed_args.contains_key(&name) {91					continue;92				}93			} else if idx < filled_positionals {94				continue;95			}9697			destruct(98				&param.0,99				Thunk::new(tb!(EvaluateNamedThunk {100					ctx: fctx.clone(),101					name: param.0.name().unwrap_or_else(|| "<destruct>".into()),102					value: param.1.clone().expect("default exists"),103				})),104				fctx.clone(),105				&mut defaults,106			)?;107			if param.0.name().is_some() {108				filled_named += 1;109			} else {110				filled_positionals += 1;111			}112		}113114		// Some args still weren't filled115		if filled_named + filled_positionals != params.len() {116			for param in params.iter().skip(args.unnamed_len()) {117				let mut found = false;118				args.named_names(&mut |name| {119					if Some(name) == param.0.name().as_ref() {120						found = true;121					}122				});123				if !found {124					throw!(FunctionParameterNotBoundInCall(125						param126							.0127							.clone()128							.name()129							.unwrap_or_else(|| "<destruct>".into())130					));131				}132			}133			unreachable!();134		}135136		Ok(body_ctx137			.extend(passed_args, None, None, None)138			.extend(defaults, None, None, None)139			.into_future(fctx))140	} else {141		let body_ctx = body_ctx.extend(passed_args, None, None, None);142		Ok(body_ctx)143	}144}145146/// You shouldn't probally use this function, use `jrsonnet_macros::builtin` instead147///148/// ## Parameters149/// * `ctx`: used for passed argument expressions' execution and for body execution (if `body_ctx` is not set)150/// * `params`: function parameters' definition151/// * `args`: passed function arguments152/// * `tailstrict`: if set to `true` function arguments are eagerly executed, otherwise - lazily153pub fn parse_builtin_call(154	s: State,155	ctx: Context,156	params: &[BuiltinParam],157	args: &dyn ArgsLike,158	tailstrict: bool,159) -> Result<GcHashMap<BuiltinParamName, Thunk<Val>>> {160	let mut passed_args = GcHashMap::with_capacity(params.len());161	if args.unnamed_len() > params.len() {162		throw!(TooManyArgsFunctionHas(params.len()))163	}164165	let mut filled_args = 0;166167	args.unnamed_iter(s.clone(), ctx.clone(), tailstrict, &mut |id, arg| {168		let name = params[id].name.clone();169		passed_args.insert(name, arg);170		filled_args += 1;171		Ok(())172	})?;173174	args.named_iter(s, ctx, tailstrict, &mut |name, arg| {175		// FIXME: O(n) for arg existence check176		let p = params177			.iter()178			.find(|p| p.name == name as &str)179			.ok_or_else(|| UnknownFunctionParameter((name as &str).to_owned()))?;180		if passed_args.insert(p.name.clone(), arg).is_some() {181			throw!(BindingParameterASecondTime(name.clone()));182		}183		filled_args += 1;184		Ok(())185	})?;186187	if filled_args < params.len() {188		for param in params.iter().filter(|p| p.has_default) {189			if passed_args.contains_key(&param.name) {190				continue;191			}192			filled_args += 1;193		}194195		// Some args still wasn't filled196		if filled_args != params.len() {197			for param in params.iter().skip(args.unnamed_len()) {198				let mut found = false;199				args.named_names(&mut |name| {200					if name as &str == &param.name as &str {201						found = true;202					}203				});204				if !found {205					throw!(FunctionParameterNotBoundInCall(param.name.clone().into()));206				}207			}208			unreachable!();209		}210	}211	Ok(passed_args)212}213214/// Creates Context, which has all argument default values applied215/// and with unbound values causing error to be returned216pub fn parse_default_function_call(body_ctx: Context, params: &ParamsDesc) -> Result<Context> {217	#[derive(Trace)]218	struct DependsOnUnbound(IStr);219	impl ThunkValue for DependsOnUnbound {220		type Output = Val;221		fn get(self: Box<Self>, _: State) -> Result<Val> {222			Err(FunctionParameterNotBoundInCall(self.0.clone()).into())223		}224	}225226	let fctx = Context::new_future();227228	let mut bindings = GcHashMap::new();229230	for param in params.iter() {231		if let Some(v) = &param.1 {232			destruct(233				&param.0.clone(),234				Thunk::new(tb!(EvaluateNamedThunk {235					ctx: fctx.clone(),236					name: param.0.name().unwrap_or_else(|| "<destruct>".into()),237					value: v.clone(),238				})),239				fctx.clone(),240				&mut bindings,241			)?;242		} else {243			destruct(244				&param.0,245				Thunk::new(tb!(DependsOnUnbound(246					param.0.name().unwrap_or_else(|| "<destruct>".into())247				))),248				fctx.clone(),249				&mut bindings,250			)?;251		}252	}253254	Ok(body_ctx255		.extend(bindings, None, None, None)256		.into_future(fctx))257}
after · 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}