Skip to main content

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}