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}