nickel_lang_core/label.rs
1//! Define the type of labels.
2//!
3//! A label is a value holding metadata relative to contract checking. It gives the user useful
4//! information about the context of a contract failure.
5use std::{iter, rc::Rc};
6
7use crate::{
8 ast::MergeKind,
9 eval::{
10 cache::{Cache as EvalCache, CacheIndex},
11 value::NickelValue,
12 },
13 identifier::LocIdent,
14 position::PosIdx,
15 term::{
16 SealingKey,
17 record::{Field, RecordData},
18 },
19 typ::{Type, TypeF},
20};
21
22use smallvec::SmallVec;
23
24pub mod ty_path {
25 //! Type paths.
26 //!
27 //! Checking higher-order contracts can involve a good share of intermediate contract checking.
28 //! Take the following example:
29 //! ```text
30 //! (fun ev => fun cst => ev (fun x => cst))
31 //! | ((Number -> Number) -> Number) -> Number -> Number,
32 //! ```
33 //! Once called, various checks will be performed on the arguments of functions and their return
34 //! values:
35 //!
36 //! 1. Check that `ev` provides a `Number` to `(fun x => cst)`
37 //! 2. Check that `(fun x => cst)` returns a `Number`
38 //! 3. Check that `ev (fun x => cst)` return a `Number`
39 //! 4. etc.
40 //!
41 //! Each check can be linked to a base type occurrence (here, a `Number`) in the original type:
42 //! ```text
43 //! ((Number -> Number) -> Number) -> Number -> Number
44 //! ^^^^^1 ^^^^^^2 ^^^^^^3 etc.
45 //! ```
46 //!
47 //! This is the information encoded by a type path: what part of the original type is currently
48 //! being checked by this label. It is then reported to the user in case of a blame.
49 //!
50 //! Paths are encoded as lists of elements, specifying if the next step is either to go to the
51 //! **domain** or to the **codomain**.
52 //!
53 //! When reporting a blame error on a record type, one faces the same situation as with
54 //! higher-order functions: the precise cause of an error can correspond to a small subtype of
55 //! the original record type. Type path elements can thus also consist of a record field,
56 //! indicating that the path leading to the subtype of interest goes through a record via a
57 //! particular field.
58
59 use crate::{
60 identifier::LocIdent,
61 position::RawSpan,
62 typ::{RecordRowF, RecordRowsIteratorItem, Type, TypeF},
63 };
64
65 use nickel_lang_vector::Vector;
66
67 /// An element of a path type.
68 #[derive(Debug, Clone, PartialEq, Eq, Copy)]
69 pub enum Elem {
70 Domain,
71 Codomain,
72 Field(LocIdent),
73 Array,
74 Dict,
75 }
76
77 /// We use an inline vector up to `n` elements, as paths as assumed to be reasonably small, and
78 /// labels (thus paths) are cloned a lot. For small but non-empty paths, which we expect to be
79 /// common, this avoids having a to copy a separate heap-allocation. We do pay a price that an
80 /// empty path is bigger than an empty `Vec`.
81 pub type Path = Vector<Elem, 4>;
82
83 /// Determine if the path has no `Domain` arrow component in it.
84 pub fn has_no_dom(p: &Path) -> bool {
85 !p.iter().any(|elt| matches!(*elt, Elem::Domain))
86 }
87
88 /// Determine if the path is not higher order, that is, if it doesn't contain any arrow.
89 pub fn has_no_arrow(p: &Path) -> bool {
90 !p.iter()
91 .any(|elt| matches!(*elt, Elem::Domain | Elem::Codomain))
92 }
93
94 #[derive(Clone, Debug)]
95 /// Represent the span of a type path in the string representation of the corresponding type,
96 /// together with additional data useful to error reporting.
97 ///
98 /// Produced by [`span`]. The last element of the path that could be matched with the type is
99 /// returned, as well as the last element of the part of the path that could be matched which
100 /// is an arrow element (`Domain` or `Codomain`).
101 ///
102 /// In the general case, this should be respectively the last element of the path and the last
103 /// element of the path filtered by keeping only `Domain` and `Codomain`. But sometimes the
104 /// `span` function couldn't make further progress:
105 ///
106 /// ```nickel
107 /// let Foo = Array Num in
108 /// let f : Num -> Foo = fun x => ["a"] in
109 /// f 0
110 /// ```
111 ///
112 /// This code will blame with path `[Codomain, Array]`. However, because the "alias" `Foo` is
113 /// used for `Array Num`, we can't descend further inside `Foo` which doesn't have the shape
114 /// `Array _`. `span` will stop here, and the last elements saved in `PathSpan` will both be
115 /// `Codomain`, instead of `Array` and `Codomain`. This helps specializing the error message
116 /// accordingly.
117 pub struct PathSpan {
118 /// The span of the subtype corresponding to the path.
119 pub span: RawSpan,
120 pub last: Option<Elem>,
121 pub last_arrow_elem: Option<Elem>,
122 }
123
124 /// Return the span encoded (as well as additional data: see [PathSpan]) by a type path in the
125 /// string representation of the corresponding type.
126 ///
127 /// Used in the error reporting of blame errors (see `crate::error::report_ty_path`).
128 ///
129 /// # Returns
130 ///
131 /// The function returns `None` if the position of the found subtype is not defined.
132 ///
133 /// # Example
134 ///
135 /// - Type path: `[Codomain, Domain]`
136 /// - Type : `Num -> Num -> Num`
137 /// - Return: `{start: 7, end: 10, last: Some(Domain), last_arrow_elem: Some(Domain)}`. The
138 /// span `(7, 10)` corresponds to the second `Num` occurrence.
139 ///
140 /// # Mismatch between `path` and `ty`
141 ///
142 /// If at some point the shape of the type `ty` doesn't correspond to the path (e.g. if the
143 /// next element is `Array`, but the type isn't of the form `Array _`), `span` just reports the
144 /// span of the whole current subtype.
145 ///
146 /// ```nickel
147 /// let Foo = Array Num in
148 /// ["a"] | Foo
149 /// ```
150 ///
151 /// Here, the type path will contain an `Array` (added by the builtin implementation of the
152 /// `Array` contract), but the original type will be `Foo`, which isn't of the form `Array _`.
153 /// Thus we can't underline the subtype `_`, and stops at the whole `Array T`.
154 pub fn span<'a, I>(mut path_it: std::iter::Peekable<I>, mut ty: &Type) -> Option<PathSpan>
155 where
156 I: Iterator<Item = &'a Elem>,
157 I: std::clone::Clone,
158 {
159 while let TypeF::Forall { body, .. } = &ty.typ {
160 ty = body.as_ref();
161 }
162
163 match (&ty.typ, path_it.next()) {
164 (TypeF::Arrow(dom, codom), Some(next)) => match next {
165 Elem::Domain => {
166 let path_span = span(path_it, dom.as_ref())?;
167 Some(PathSpan {
168 last: path_span.last.or(Some(*next)),
169 last_arrow_elem: path_span.last_arrow_elem.or(Some(*next)),
170 ..path_span
171 })
172 }
173 Elem::Codomain => {
174 let path_span = span(path_it, codom.as_ref())?;
175 Some(PathSpan {
176 last: path_span.last.or(Some(*next)),
177 last_arrow_elem: path_span.last_arrow_elem.or(Some(*next)),
178 ..path_span
179 })
180 }
181 _ => panic!(
182 "span(): \
183 seeing an arrow type, but the type path is neither domain nor codomain"
184 ),
185 },
186 (TypeF::Record(rows), next @ Some(Elem::Field(ident))) => {
187 for row_item in rows.iter() {
188 match row_item {
189 RecordRowsIteratorItem::Row(RecordRowF { id, typ: ty }) if id == *ident => {
190 let path_span = span(path_it, ty)?;
191
192 return Some(PathSpan {
193 last: path_span.last.or_else(|| next.copied()),
194 last_arrow_elem: path_span.last_arrow_elem,
195 ..path_span
196 });
197 }
198 RecordRowsIteratorItem::Row(RecordRowF { .. }) => (),
199 RecordRowsIteratorItem::TailDyn | RecordRowsIteratorItem::TailVar(_) => (),
200 }
201 }
202
203 panic!(
204 "span: current type path element indicates to go to field `{}`, \
205 but this field doesn't exist in {}",
206 ident,
207 Type::from(TypeF::Record(rows.clone())),
208 )
209 }
210 (TypeF::Array(ty), Some(Elem::Array)) if ty.as_ref().typ == TypeF::Dyn =>
211 // Dyn shouldn't be the target of any blame
212 {
213 panic!("span(): unexpected blame of a dyn contract inside an array")
214 }
215 (TypeF::Array(ty), next @ Some(Elem::Array)) => {
216 let path_span = span(path_it, ty)?;
217
218 Some(PathSpan {
219 last: path_span.last.or_else(|| next.copied()),
220 last_arrow_elem: path_span.last_arrow_elem,
221 ..path_span
222 })
223 }
224 (TypeF::Dict { type_fields, .. }, next @ Some(Elem::Dict)) => {
225 let path_span = span(path_it, type_fields)?;
226
227 Some(PathSpan {
228 last: path_span.last.or_else(|| next.copied()),
229 last_arrow_elem: path_span.last_arrow_elem,
230 ..path_span
231 })
232 }
233 // The type and the path don't match, or the path is empty. We stop here.
234 _ => ty.pos.into_opt().map(|span| PathSpan {
235 span,
236 last: None,
237 last_arrow_elem: None,
238 }),
239 }
240 }
241}
242
243/// A blame label.
244///
245/// A label is associated to a contract check (introduced by an explicit contract annotation, or
246/// implicitly by a merge expression) and contains information to report to the user when a
247/// contract fails and a blame occurs.
248/// It includes in particular a [type path][ty_path::Path] and a **polarity**.
249///
250/// # Polarity
251///
252/// One crucial aspect of first class contracts is to be able to check higher-order types, which
253/// are types with arrows in it. Consider the simplest example:
254///
255/// ```text
256/// f | Number -> Number
257/// ```
258///
259/// This does not entail that `f` returns a `Number` in *every* situation. The identity function
260/// `id = fun x => x` can certainly be given the type `Number -> Number`, but `id "a" = "a"` is not
261/// a `Number`.
262///
263/// To satisfy the contract `Number -> Number` for `f` is to satisfy the predicate "if you give me
264/// a `Number` as an argument, I give you a `Number` as a result". There is an additional contract
265/// to be checked, which is not the responsibility of `f`, but the caller's (or context) one.
266///
267/// `f | Number -> Number` should thus be evaluated as `fun arg => ((f (arg | Number)) | Number)`,
268/// but we want to report the failures of the two introduced subcontracts in a different way:
269///
270/// - The inner one (on the argument) says that `f` has been misused: it has been applied to
271/// something that is not a `Number`.
272/// - The outer one says that `f` failed to satisfy its contract, as it has been provided with a
273/// `Number` (otherwise the inner contracts would have failed before) but failed to deliver a
274/// `Number`.
275///
276/// This duality caller/callee or function/context is indicated by the polarity: the outer
277/// corresponds to a *positive* polarity (the contract is on the term), while the inner corresponds
278/// to a *negative* one (the contact is on the context). The polarity always starts as `true` in
279/// user-written contracts, but is toggled in the argument contract when the interpreter decomposes
280/// an higher order-contract. This also generalizes to higher types such as `((Number -> Number) ->
281/// Number) -> Number` where the polarity alternates each time.
282#[derive(Debug, Clone, PartialEq)]
283pub struct Label {
284 /// The type checked by the original contract.
285 pub typ: Rc<Type>,
286
287 /// Custom diagnostics set by user code. There might be several diagnostics stacked up, as some
288 /// contracts might in turn apply other subcontracts.
289 ///
290 /// The last diagnostic of the stack is usually the current working diagnostic (the one mutated
291 /// by corresponding primops), and the latest/most precise when a blame error is raised.
292 pub diagnostics: SmallVec<[ContractDiagnostic; 1]>,
293
294 /// The position of the original contract.
295 pub span: PosIdx,
296
297 /// The index corresponding to the value being checked. Set at run-time by the interpreter.
298 pub arg_idx: Option<CacheIndex>,
299
300 /// The original position of the value being checked. Set at run-time by the interpreter.
301 pub arg_pos: PosIdx,
302
303 /// The polarity, used for higher-order contracts, that specifies if the current contract is
304 /// on the environment (ex, the argument of a function) or on the term.
305 pub polarity: Polarity,
306
307 /// The path of the type being currently checked in the original type.
308 pub path: ty_path::Path,
309
310 /// An environment mapping type variables to [`TypeVarData`]. Used by polymorphic contracts to
311 /// decide which actions to take when encountering a `forall`.
312 ///
313 /// We use an associative map here instead of a proper hashmap because we never expect more
314 /// than a few type variables in an expression. The locality of an array makes it competitive,
315 /// and the empty case (which is overwhelmingly common) is smaller.
316 pub type_environment: Vec<(SealingKey, TypeVarData)>,
317
318 /// The name of the record field to report in blame errors. This is set
319 /// while first transforming a record as part of the pending contract generation.
320 /// Contract applications outside of records will have this field set to `None`.
321 pub field_name: Option<LocIdent>,
322}
323
324/// Data about type variables that is needed for polymorphic contracts to decide which actions to
325/// take.
326#[derive(Debug, Clone, PartialEq, Eq)]
327pub struct TypeVarData {
328 pub polarity: Polarity,
329}
330
331impl From<&TypeVarData> for NickelValue {
332 fn from(value: &TypeVarData) -> Self {
333 NickelValue::record_posless(RecordData {
334 fields: [(
335 LocIdent::new("polarity"),
336 Field::from(NickelValue::from(value.polarity)),
337 )]
338 .into(),
339 attrs: Default::default(),
340 sealed_tail: None,
341 })
342 }
343}
344
345/// A polarity. See [`Label`]
346#[derive(Debug, Clone, Copy, PartialEq, Eq)]
347pub enum Polarity {
348 Positive,
349 Negative,
350}
351
352impl Polarity {
353 pub fn flip(self) -> Self {
354 match self {
355 Polarity::Positive => Polarity::Negative,
356 Polarity::Negative => Polarity::Positive,
357 }
358 }
359}
360
361impl From<Polarity> for NickelValue {
362 fn from(value: Polarity) -> Self {
363 match value {
364 Polarity::Positive => NickelValue::enum_tag_posless(LocIdent::new("Positive")),
365 Polarity::Negative => NickelValue::enum_tag_posless(LocIdent::new("Negative")),
366 }
367 }
368}
369
370impl TryFrom<&NickelValue> for Polarity {
371 type Error = ();
372
373 fn try_from(value: &NickelValue) -> Result<Self, Self::Error> {
374 match value.as_enum_tag().ok_or(())? {
375 tag if tag.label() == "Positive" => Ok(Self::Positive),
376 tag if tag.label() == "Negative" => Ok(Self::Negative),
377 _ => Err(()),
378 }
379 }
380}
381
382/// Custom reporting diagnostic that can be set by user-code through the `label` API. Used to
383/// customize contract error messages, and provide more context than "a contract has failed".
384#[derive(Debug, Clone, PartialEq, Default)]
385pub struct ContractDiagnostic {
386 /// The main error message tag to be printed together with the error message.
387 pub message: Option<String>,
388 /// Additional notes printed at the end of the message.
389 pub notes: Vec<String>,
390}
391
392impl ContractDiagnostic {
393 pub fn new() -> Self {
394 Self::default()
395 }
396
397 /// Attach a message to this diagnostic, and return the updated value. Erase potential previous
398 /// message.
399 pub fn with_message(mut self, message: impl Into<String>) -> Self {
400 self.message = Some(message.into());
401 self
402 }
403
404 /// Attach notes to this diagnostic, and return the updated value. Erase potential previous
405 /// notes.
406 pub fn with_notes(mut self, notes: Vec<String>) -> Self {
407 self.notes = notes;
408 self
409 }
410
411 /// Append a note to this diagnostic.
412 pub fn append_note(&mut self, note: impl Into<String>) {
413 self.notes.push(note.into());
414 }
415
416 /// Return `true` if this diagnostic is empty, that is if `message` is either not set (`None`)
417 /// or is set but empty, AND notes are empty.
418 pub fn is_empty(&self) -> bool {
419 self.message.as_ref().map(String::is_empty).unwrap_or(true) && self.notes.is_empty()
420 }
421}
422
423impl Label {
424 /// Generate a dummy label for testing purpose.
425 pub fn dummy() -> Label {
426 Label {
427 typ: Rc::new(Type::from(TypeF::Number)),
428 diagnostics: iter::once(
429 ContractDiagnostic::new().with_message(String::from("testing")),
430 )
431 .collect(),
432 polarity: Polarity::Positive,
433 ..Default::default()
434 }
435 }
436
437 pub fn get_evaluated_arg<EC: EvalCache>(&self, cache: &EC) -> Option<NickelValue> {
438 self.arg_idx.clone().map(|idx| cache.get(idx).value)
439 }
440
441 /// Same as [`Self::with_diagnostic_message`], but mutate `self` in place.
442 pub fn set_diagnostic_message(&mut self, message: impl Into<String>) {
443 if let Some(current) = self.diagnostics.last_mut() {
444 current.message = Some(message.into());
445 } else {
446 self.diagnostics
447 .push(ContractDiagnostic::new().with_message(message));
448 };
449 }
450
451 /// Set the message of the current diagnostic (the last diagnostic of the stack). Potentially
452 /// erase the previous value.
453 ///
454 /// If the diagnostic stack is empty, this method pushes a new diagnostic with the given notes.
455 pub fn with_diagnostic_message(mut self, message: impl Into<String>) -> Self {
456 self.set_diagnostic_message(message);
457 self
458 }
459
460 /// Same as [`Self::with_diagnostic_notes`], but mutate `self` in place.
461 pub fn set_diagnostic_notes(&mut self, notes: Vec<String>) {
462 if let Some(current) = self.diagnostics.last_mut() {
463 current.notes = notes;
464 } else {
465 self.diagnostics
466 .push(ContractDiagnostic::new().with_notes(notes));
467 };
468 }
469
470 /// Set the notes of the current diagnostic (the last diagnostic of the stack). Potentially
471 /// erase the previous value.
472 ///
473 /// If the diagnostic stack is empty, this method pushes a new diagnostic with the given notes.
474 pub fn with_diagnostic_notes(mut self, notes: Vec<String>) -> Self {
475 self.set_diagnostic_notes(notes);
476 self
477 }
478
479 /// Appends a note to the current diagnostic (the last diagnostic of the stack). Potentially
480 /// erases the previous value.
481 ///
482 /// If the diagnostic stack is empty, this method pushes a new diagnostic with the given note.
483 pub fn with_append_diagnostic_note(mut self, note: impl Into<String>) -> Self {
484 self.append_diagnostic_note(note);
485 self
486 }
487
488 /// Same as [Self::with_append_diagnostic_note], but mutates `self` in place.
489 pub fn append_diagnostic_note(&mut self, note: impl Into<String>) {
490 if let Some(current) = self.diagnostics.last_mut() {
491 current.append_note(note);
492 } else {
493 self.diagnostics
494 .push(ContractDiagnostic::new().with_notes(vec![note.into()]));
495 }
496 }
497
498 /// Return a reference to the current contract diagnostic, which is the last element of the
499 /// stack, if any.
500 pub fn current_diagnostic(&self) -> Option<&ContractDiagnostic> {
501 self.diagnostics.last()
502 }
503
504 /// Push a new, fresh diagnostic on the stack if the current diagnostic isn't empty. This has
505 /// the effect of saving the current diagnostic, that can't then be mutated anymore by the
506 /// label's API.
507 pub fn push_diagnostic(&mut self) {
508 if matches!(self.current_diagnostic(), Some(diag) if !diag.is_empty()) {
509 self.diagnostics.push(ContractDiagnostic::new());
510 }
511 }
512
513 pub fn with_field_name(self, field_name: Option<LocIdent>) -> Self {
514 Label { field_name, ..self }
515 }
516
517 /// Tests if the contract associated to this label might have polymorphic subcontracts
518 /// (equivalently, if the contract is derived from a type which has free type variables). Such
519 /// contracts are special, in particular because they aren't idempotent and thus can't be
520 /// freely deduplicated.
521 ///
522 /// This check is an over approximation and might return `true` even if the contract is not
523 /// polymorphic, in exchange of being fast (constant time).
524 pub fn can_have_poly_ctrs(&self) -> bool {
525 // Checking that the type environment is not empty is a bit coarse: what it actually checks
526 // is that this contract is derived from the body of a `forall`. For example, in `forall a.
527 // a -> Number`, `Number` isn't polymorphic, but `has_polymorphic_ctrs` will return `true`.
528 !self.type_environment.is_empty()
529 }
530}
531
532impl Default for Label {
533 fn default() -> Label {
534 Label {
535 typ: Rc::new(Type::from(TypeF::Dyn)),
536 span: PosIdx::NONE,
537 polarity: Polarity::Positive,
538 diagnostics: Default::default(),
539 arg_idx: Default::default(),
540 arg_pos: Default::default(),
541 path: Default::default(),
542 type_environment: Default::default(),
543 field_name: None,
544 }
545 }
546}
547
548/// A merge label.
549///
550/// Like [`Label`], a merge label is used to carry and propagate error reporting data during the
551/// evaluation. While [`Label`] is used for contracts, `MergeLabel` is used for merging. The latter
552/// track less information than the former, but this information is still important. Indeed,
553/// recursive merging of records usually can't track original positions very well. The merge label
554/// allows to at least remember the original position of the merge, as written somewhere in a
555/// Nickel source.
556///
557/// Additionally, merging arrays currently generates a contract and its associated label for which
558/// we don't necessarily have a defined span at hand. The merge label makes it possible to fallback
559/// to the original position of the merge.
560#[derive(Copy, Clone, Debug, Eq, PartialEq)]
561pub struct MergeLabel {
562 /// The span of the original merge (which might then decompose into many others).
563 pub span: PosIdx,
564 pub kind: MergeKind,
565}
566
567impl From<Label> for MergeLabel {
568 fn from(label: Label) -> Self {
569 MergeLabel {
570 span: label.span,
571 kind: Default::default(),
572 }
573 }
574}