Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add primops to access the current diagnostic of a label #1156

Merged
merged 13 commits into from
Mar 8, 2023
6 changes: 3 additions & 3 deletions benches/mantis/lib.ncl
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@
if str_match value then
value
else
contract.blame_with "no match" label
else contract.blame_with "not a string" label,
contract.blame_with_message "no match" label
else contract.blame_with_message "not a string" label,
PseudoOr = fun alts label value =>
array.fold (fun ctr rest =>
if ctr.pred value then
ctr.contract value
else
rest)
(contract.blame_with "no alternative matched" label),
(contract.blame_with_message "no alternative matched" label),
OrableFromPred : (Dyn -> Bool)
-> {pred : Dyn -> Bool, contract: Dyn -> Dyn -> Dyn },
OrableFromPred = fun pred_ => {
Expand Down
14 changes: 7 additions & 7 deletions doc/manual/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,21 +61,21 @@ let IsFoo = fun label value =>
if value == "foo" then
value
else
contract.blame_with "not equal to \"foo\"" label
contract.blame_with_message "not equal to \"foo\"" label
else
contract.blame_with "not a string" label
contract.blame_with_message "not a string" label
```

A custom contract is a function of two arguments:

- A `label`. Provided by the interpreter, the label contains tracking information
for error reporting. Its main usage is to be passed to `contract.blame` or
`contract.blame_with` when the contract isn't satisfied.
`contract.blame_with_message` when the contract isn't satisfied.
- The value being checked.

Upon success, the contract must return the original value. We will see the
reason why in the [laziness](#laziness) section. To signal failure, we use
`contract.blame` or its variant `contract.blame_with` that takes an additional
`contract.blame` or its variant `contract.blame_with_message` that takes an additional
error message as a parameter. `blame` immediately aborts the execution and
reports a contract violation error.

Expand Down Expand Up @@ -586,17 +586,17 @@ let NumBoolDict = fun label value =>
if string.is_match "^\\d+$" field_name then
acc # unused and always null through iteration
else
contract.blame_with "field name `#{field_name}` is not a number" label
contract.blame_with_message "field name `#{field_name}` is not a number" label
) null in

value
|> record.map (fun name value =>
let label_with_msg =
contract.tag "field `#{name}` is not a boolean" label in
contract.label.with_message "field `#{name}` is not a boolean" label in
contract.apply Bool label_with_msg value)
|> builtin.seq check_fields
else
contract.blame_with "not a record" label
contract.blame_with_message "not a record" label
```

There is a lot to unwrap here. Please refer to the [syntax](./syntax.md) section
Expand Down
16 changes: 8 additions & 8 deletions examples/config-gcc/config-gcc.ncl
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,20 @@ let GccFlag =
array.any (fun x => x == string.substring 0 1 value) available then
value
else
contract.blame_with "unknown flag %{value}" label
contract.blame_with_message "unknown flag %{value}" label
else if builtin.is_record value then
if record.has_field "flag" value && record.has_field "arg" value then
if array.any (fun x => x == value.flag) available then
#Normalize the tag to a string
value.flag ++ value.arg
else
contract.blame_with "unknown flag %{value.flag}" label
contract.blame_with_message "unknown flag %{value.flag}" label
else
contract.blame_with
contract.blame_with_message
"bad record structure: missing field `flag` or `arg`"
label
else
contract.blame_with "expected record or string" label in
contract.blame_with_message "expected record or string" label in

let Path =
let pattern = m%"^(.+)/([^/]+)$"% in
Expand All @@ -33,18 +33,18 @@ let Path =
if string.is_match pattern value then
value
else
contract.blame_with "invalid path" label
contract.blame_with_message "invalid path" label
else
contract.blame_with "not a string" label in
contract.blame_with_message "not a string" label in

let SharedObjectFile = fun label value =>
if builtin.is_str value then
if string.is_match m%"\.so$"% value then
value
else
contract.blame_with "not an .so file" label
contract.blame_with_message "not an .so file" label
else
contract.blame_with "not a string" label in
contract.blame_with_message "not a string" label in

let OptLevel = fun label value =>
if value == 0 || value == 1 || value == 2 then
Expand Down
4 changes: 2 additions & 2 deletions lsp/nls/src/requests/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ impl IdentWithType {
if name.is_ascii() {
String::from(name)
} else {
format!("\"{}\"", name)
format!("\"{name}\"")
}
}
let doc = || {
Expand Down Expand Up @@ -810,7 +810,7 @@ mod tests {
let actual = get_identifier_path(input);
let expected: Option<Vec<_>> =
expected.map(|path| path.iter().map(|s| String::from(*s)).collect());
assert_eq!(actual, expected, "test failed: {}", case_name)
assert_eq!(actual, expected, "test failed: {case_name}")
}
}

Expand Down
17 changes: 9 additions & 8 deletions src/eval/merge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,22 +242,23 @@ pub fn merge<C: Cache>(
} = hashmap::split(r1.fields, r2.fields);

match mode {
MergeMode::Contract(mut lbl) if !r2.attrs.open && !left.is_empty() => {
MergeMode::Contract(label) if !r2.attrs.open && !left.is_empty() => {
let fields: Vec<String> =
left.keys().map(|field| format!("`{field}`")).collect();
let plural = if fields.len() == 1 { "" } else { "s" };
let fields_list = fields.join(",");

lbl.set_diagnostic_message(format!("extra field{plural} {fields_list}"));
lbl.set_diagnostic_notes(vec![
format!("Have you misspelled a field?"),
String::from("The record contract might also be too strict. By default, record contracts exclude any field which is not listed.
let label = label
.with_diagnostic_message(format!("extra field{plural} {fields_list}"))
.with_diagnostic_notes(vec![
String::from("Have you misspelled a field?"),
String::from("The record contract might also be too strict. By default, record contracts exclude any field which is not listed.
Append `, ..` at the end of the record contract, as in `{some_field | SomeContract, ..}`, to make it accept extra fields."),
]);
]);

return Err(EvalError::BlameError {
evaluated_arg: lbl.get_evaluated_arg(cache),
label: lbl,
evaluated_arg: label.get_evaluated_arg(cache),
label,
call_stack: CallStack::new(),
});
}
Expand Down
187 changes: 152 additions & 35 deletions src/eval/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1309,6 +1309,25 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
.map(|(next, ..)| next)
.ok_or_else(|| EvalError::NotEnoughArgs(2, String::from("trace"), pos_op))
}
UnaryOp::LabelPushDiag() => {
match_sharedterm! {t, with {
Term::Lbl(label) => {
let mut label = label;
label.push_diagnostic();
Ok(Closure {
body: RichTerm::new(Term::Lbl(label), pos),
env
})
}
} else {
Err(EvalError::TypeError(
String::from("Label"),
String::from("trace"),
arg_pos,
RichTerm { term: t, pos },
)) }
}
}
}
}

Expand Down Expand Up @@ -1690,41 +1709,6 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
))
}
}
BinaryOp::Tag() => match_sharedterm! {t1, with {
Term::Str(s) => match_sharedterm!{t2, with {
Term::Lbl(label) => {
let mut label = label;
label.set_diagnostic_message(s);

Ok(Closure::atomic_closure(RichTerm::new(
Term::Lbl(label),
pos_op_inh,
)))
}
} else {
Err(EvalError::TypeError(
String::from("Label"),
String::from("tag, 2nd argument"),
snd_pos,
RichTerm {
term: t2,
pos: pos2,
},
))
}
}
} else {
Err(EvalError::TypeError(
String::from("Str"),
String::from("tag, 1st argument"),
fst_pos,
RichTerm {
term: t1,
pos: pos1,
},
))
}
},
BinaryOp::Eq() => {
let mut env = Environment::new();

Expand Down Expand Up @@ -2629,6 +2613,139 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
))
}
}
BinaryOp::LabelWithMessage() => {
let t1 = t1.into_owned();
let t2 = t2.into_owned();

let Term::Str(message) = t1 else {
return Err(EvalError::TypeError(
String::from("Str"),
String::from("label_with_message, 1st argument"),
fst_pos,
RichTerm {
term: t1.into(),
pos: pos1,
},
))
};

let Term::Lbl(label) = t2 else {
return Err(EvalError::TypeError(
String::from("Lbl"),
String::from("label_with_message, 2nd argument"),
snd_pos,
RichTerm {
term: t2.into(),
pos: pos2,
},
))
};

Ok(Closure::atomic_closure(RichTerm::new(
Term::Lbl(label.with_diagnostic_message(message)),
pos_op_inh,
)))
}
BinaryOp::LabelWithNotes() => {
let t2 = t2.into_owned();

// We need to extract plain strings from a Nickel array, which most likely
// contains at least generated variables.
// As for serialization, we thus fully substitute all variables first.
let t1_subst = subst(
&self.cache,
RichTerm {
term: t1,
pos: pos1,
},
&Environment::new(),
&env1,
);
let t1 = t1_subst.term.into_owned();

let Term::Array(array, _) = t1 else {
return Err(EvalError::TypeError(
String::from("Array Str"),
String::from("label_with_notes, 1st argument"),
fst_pos,
RichTerm {
term: t1.into(),
pos: pos1,
},
));
};

let notes = array
.into_iter()
.map(|element| {
let term = element.term.into_owned();

if let Term::Str(s) = term {
Ok(s)
} else {
Err(EvalError::TypeError(
String::from("Str"),
String::from("label_with_notes, element of 1st argument"),
TermPos::None,
RichTerm {
term: term.into(),
pos: element.pos,
},
))
}
})
.collect::<Result<Vec<_>, _>>()?;

let Term::Lbl(label) = t2 else {
return Err(EvalError::TypeError(
String::from("Lbl"),
String::from("label_with_notes, 2nd argument"),
snd_pos,
RichTerm {
term: t2.into(),
pos: pos2,
},
))
};

Ok(Closure::atomic_closure(RichTerm::new(
Term::Lbl(label.with_diagnostic_notes(notes)),
pos_op_inh,
)))
}
BinaryOp::LabelAppendNote() => {
let t1 = t1.into_owned();
let t2 = t2.into_owned();

let Term::Str(note) = t1 else {
return Err(EvalError::TypeError(
String::from("Str"),
String::from("label_append_note, 1st argument"),
fst_pos,
RichTerm {
term: t1.into(),
pos: pos1,
},
));
};

let Term::Lbl(label) = t2 else {
return Err(EvalError::TypeError(
String::from("Lbl"),
String::from("label_append_note, 2nd argument"),
snd_pos,
RichTerm {
term: t2.into(),
pos: pos2,
},
))
};

Ok(Closure::atomic_closure(RichTerm::new(
Term::Lbl(label.append_diagnostic_note(note)),
pos2.into_inherited(),
)))
}
}
}

Expand Down
Loading