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