difftreelog
feat(evaluator) function signature help
in: master
When calling functions with wrong arguments, evaluator will now suggest correct function signature
2 files changed
crates/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),
crates/jrsonnet-evaluator/src/function/parse.rsdiffbeforeafterboth1use 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 ¶m.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(¶m.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 == ¶m.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) = ¶m.1 {232 destruct(233 ¶m.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 ¶m.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}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 ¶m.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(¶m.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 == ¶m.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) = ¶m.1 {252 destruct(253 ¶m.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 ¶m.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}