Repository: tweag/nickel
Pull request #1149 (2023-02-28 15:06:06)

[Refactor] Add a position field to types in the AST
In the Nickel AST, `Types` do not keep track of their position, and this can make error reporting for blame errors, in some cases, quite involved. This pull request adds a position field to `Types`

ebresafegaga: @vkleen  Thanks!

Patch:
diff --git a/flake.lock b/flake.lock
index 38d1eacb..d9b5e444 100644
--- a/flake.lock
+++ b/flake.lock
@@ -10,11 +10,11 @@
         "rust-overlay": "rust-overlay"
       },
       "locked": {
-        "lastModified": 1677370089,
-        "narHash": "sha256-sdZ3ull2bldQYXK7tsX097C8JEuhBGIFPSfmA5nVoXE=",
+        "lastModified": 1677892403,
+        "narHash": "sha256-/Wi0L1spSWLFj+UQxN3j0mPYMoc7ZoAujpUF/juFVII=",
         "owner": "ipetkov",
         "repo": "crane",
-        "rev": "685e7494b02c6ac505482b1a471de4c285e87543",
+        "rev": "105e27adb70a9890986b6d543a67761cbc1964a2",
         "type": "github"
       },
       "original": {
@@ -108,11 +108,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1677342105,
-        "narHash": "sha256-kv1fpkfCJGb0M+LZaCHFUuIS9kRIwyVgupHu86Y28nc=",
+        "lastModified": 1677932085,
+        "narHash": "sha256-+AB4dYllWig8iO6vAiGGYl0NEgmMgGHpy9gzWJ3322g=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "b1f87ca164a9684404c8829b851c3586c4d9f089",
+        "rev": "3c5319ad3aa51551182ac82ea17ab1c6b0f0df89",
         "type": "github"
       },
       "original": {
@@ -150,11 +150,11 @@
         "nixpkgs-stable": "nixpkgs-stable"
       },
       "locked": {
-        "lastModified": 1677160285,
-        "narHash": "sha256-tBzpCjMP+P3Y3nKLYvdBkXBg3KvTMo3gvi8tLQaqXVY=",
+        "lastModified": 1677832802,
+        "narHash": "sha256-XQf+k6mBYTiQUjWRf/0fozy5InAs03O1b30adCpWeXs=",
         "owner": "cachix",
         "repo": "pre-commit-hooks.nix",
-        "rev": "2bd861ab81469428d9c823ef72c4bb08372dd2c4",
+        "rev": "382bee738397ca005206eefa36922cc10df8a21c",
         "type": "github"
       },
       "original": {
@@ -184,11 +184,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1676437770,
-        "narHash": "sha256-mhJye91Bn0jJIE7NnEywGty/U5qdELfsT8S+FBjTdG4=",
+        "lastModified": 1677812689,
+        "narHash": "sha256-EakqhgRnjVeYJv5+BJx/NZ7/eFTMBxc4AhICUNquhUg=",
         "owner": "oxalica",
         "repo": "rust-overlay",
-        "rev": "a619538647bd03e3ee1d7b947f7c11ff289b376e",
+        "rev": "e53e8853aa7b0688bc270e9e6a681d22e01cf299",
         "type": "github"
       },
       "original": {
@@ -207,11 +207,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1677292251,
-        "narHash": "sha256-D+6q5Z2MQn3UFJtqsM5/AvVHi3NXKZTIMZt1JGq/spA=",
+        "lastModified": 1677896940,
+        "narHash": "sha256-F4YW6/+35dwctY4Ogb/vvMfvA6O0UwhDbXE7urDhk4A=",
         "owner": "oxalica",
         "repo": "rust-overlay",
-        "rev": "34cdbf6ad480ce13a6a526f57d8b9e609f3d65dc",
+        "rev": "c25d3e1951863ac0061d47a3fabf9aa7c91db5e5",
         "type": "github"
       },
       "original": {
diff --git a/lsp/nls/src/linearization/mod.rs b/lsp/nls/src/linearization/mod.rs
index 7330c106..4a921eed 100644
--- a/lsp/nls/src/linearization/mod.rs
+++ b/lsp/nls/src/linearization/mod.rs
@@ -562,9 +562,9 @@ impl<'a> Linearizer for AnalysisHost<'a> {
                 id_mapping.insert(*id, index);
             });
 
-        fn transform_wildcard(wildcars: &[Types], t: Types) -> Types {
-            match t {
-                Types(TypeF::Wildcard(i)) => wildcars.get(i).unwrap_or(&t).clone(),
+        fn transform_wildcard(wildcards: &[Types], t: Types) -> Types {
+            match t.types {
+                TypeF::Wildcard(i) => wildcards.get(i).unwrap_or(&t).clone(),
                 _ => t,
             }
         }
diff --git a/lsp/nls/src/requests/completion.rs b/lsp/nls/src/requests/completion.rs
index 4aab7d6d..bcbe5e33 100644
--- a/lsp/nls/src/requests/completion.rs
+++ b/lsp/nls/src/requests/completion.rs
@@ -47,7 +47,7 @@ impl From<Ident> for IdentWithType {
     fn from(ident: Ident) -> Self {
         IdentWithType {
             ident,
-            ty: Types(TypeF::Dyn),
+            ty: Types::from(TypeF::Dyn),
             meta: None,
         }
     }
@@ -57,7 +57,7 @@ impl From<&str> for IdentWithType {
     fn from(ident: &str) -> Self {
         IdentWithType {
             ident: Ident::from(ident),
-            ty: Types(TypeF::Dyn),
+            ty: Types::from(TypeF::Dyn),
             meta: None,
         }
     }
@@ -246,13 +246,13 @@ fn find_fields_from_type(
     path: &mut Vec<Ident>,
     info @ ComplCtx { .. }: &'_ ComplCtx<'_>,
 ) -> Vec<IdentWithType> {
-    match ty {
-        Types(TypeF::Record(row)) => find_fields_from_rrows(row, path, info),
-        Types(TypeF::Dict(ty)) => match path.pop() {
+    match &ty.types {
+        TypeF::Record(row) => find_fields_from_rrows(row, path, info),
+        TypeF::Dict(ty) => match path.pop() {
             Some(..) => find_fields_from_type(ty, path, info),
             _ => Vec::new(),
         },
-        Types(TypeF::Flat(term)) => find_fields_from_term(term, path, info),
+        TypeF::Flat(term) => find_fields_from_term(term, path, info),
         _ => Vec::new(),
     }
 }
@@ -270,10 +270,14 @@ fn find_fields_from_rrows(
         });
 
         match type_of_current {
-            Some(Types(TypeF::Record(rrows_current))) => {
-                find_fields_from_rrows(&rrows_current, path, info)
-            }
-            Some(Types(TypeF::Flat(term))) => find_fields_from_term(&term, path, info),
+            Some(Types {
+                types: TypeF::Record(rrows_current),
+                ..
+            }) => find_fields_from_rrows(&rrows_current, path, info),
+            Some(Types {
+                types: TypeF::Flat(term),
+                ..
+            }) => find_fields_from_term(&term, path, info),
             _ => Vec::new(),
         }
     } else {
@@ -331,7 +335,7 @@ fn find_fields_from_term(
                     ident: *ident,
                     // This Dyn type is only displayed if the metadata's
                     // contract or type annotation is not present.
-                    ty: Types(TypeF::Dyn),
+                    ty: Types::from(TypeF::Dyn),
                     meta: Some(field.metadata.clone()),
                 })
                 .collect(),
@@ -449,9 +453,9 @@ fn collect_record_info(
         .get_item(id, lin_cache)
         .map(|item| {
             let (ty, _) = linearization.resolve_item_type_meta(item, lin_cache);
-            match (&item.kind, ty) {
+            match (&item.kind, &ty.types) {
                 // Get record fields from static type info
-                (_, Types(TypeF::Record(rrows))) => find_fields_from_rrows(&rrows, path, &info),
+                (_, TypeF::Record(rrows)) => find_fields_from_rrows(rrows, path, &info),
                 (
                     TermKind::Declaration {
                         value: ValueState::Known(body_id),
@@ -726,7 +730,7 @@ mod tests {
             env: Environment::new(),
             id,
             pos: TermPos::None,
-            ty: Types(TypeF::Dyn),
+            ty: Types::from(TypeF::Dyn),
             kind,
             metadata,
         }
diff --git a/src/destructuring.rs b/src/destructuring.rs
index b5dc8735..a5cf280a 100644
--- a/src/destructuring.rs
+++ b/src/destructuring.rs
@@ -4,7 +4,7 @@
 use crate::{
     identifier::Ident,
     label::Label,
-    position::RawSpan,
+    position::{RawSpan, TermPos},
     term::{
         record::{Field, RecordAttrs, RecordData},
         LabeledType, Term,
@@ -66,16 +66,19 @@ impl RecordPattern {
 
     fn into_contract_with_lbl(self, label: Label) -> LabeledType {
         let is_open = self.is_open();
-
+        let pos = TermPos::Original(self.span);
         LabeledType {
-            types: Types(TypeF::Flat(
-                Term::Record(RecordData::new(
-                    self.inner().into_iter().map(Match::as_binding).collect(),
-                    RecordAttrs { open: is_open },
-                    None,
-                ))
-                .into(),
-            )),
+            types: Types {
+                types: TypeF::Flat(
+                    Term::Record(RecordData::new(
+                        self.inner().into_iter().map(Match::as_binding).collect(),
+                        RecordAttrs { open: is_open },
+                        None,
+                    ))
+                    .into(),
+                ),
+                pos,
+            },
             label,
         }
     }
diff --git a/src/error.rs b/src/error.rs
index e5d3006c..3dc4b8f1 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -1550,13 +1550,13 @@ impl IntoDiagnostics<FileId> for TypecheckError {
             ,
             TypecheckError::TypeMismatch(expd, actual, span_opt) => {
                 fn addendum(ty: &Types) -> &str {
-                    if ty.0.is_flat() {
+                    if ty.types.is_flat() {
                         " (a contract)"
                     } else {
                         ""
                     }
                 }
-                let last_note = if expd.0.is_flat() ^ actual.0.is_flat() {
+                let last_note = if expd.types.is_flat() ^ actual.types.is_flat() {
                     "Static types and contracts are not compatible"
                 } else {
                     "These types are not compatible"
@@ -1609,7 +1609,7 @@ impl IntoDiagnostics<FileId> for TypecheckError {
                 let row_msg = |word, field, ty| format!("The type of the expression was {word} to have the row `{field}: {ty}`");
                 let default_msg = |word, ty| format!("The type of the expression was {word} to be `{ty}`");
 
-                let note1 = if let TypeF::Record(rrows) = &expd.0 {
+                let note1 = if let TypeF::Record(rrows) = &expd.types {
                     match rrows.row_find_path(path.as_slice()) {
                         Some(ty) => row_msg("expected", &field, ty),
                         None => default_msg("expected", &expd),
@@ -1618,7 +1618,7 @@ impl IntoDiagnostics<FileId> for TypecheckError {
                     default_msg("expected", &expd)
                 };
 
-                let note2 = if let TypeF::Record(rrows) = &actual.0 {
+                let note2 = if let TypeF::Record(rrows) = &actual.types {
                     match rrows.row_find_path(path.as_slice()) {
                         Some(ty) => row_msg("inferred", &field, ty),
                         None => default_msg("inferred", &expd),
diff --git a/src/label.rs b/src/label.rs
index f3f42ac2..5c1d501e 100644
--- a/src/label.rs
+++ b/src/label.rs
@@ -138,7 +138,7 @@ pub mod ty_path {
         // peek() returns a reference, and hence keeps a mutable borrow of `path_it` which forbids
         // to call to next() in the same region. This is why we need to split the match in two
         // different blocks.
-        let forall_offset = match (&ty.0, path_it.peek()) {
+        let forall_offset = match (&ty.types, path_it.peek()) {
             (_, None) => {
                 let repr = format!("{ty}");
                 return PathSpan {
@@ -151,7 +151,7 @@ pub mod ty_path {
             (TypeF::Forall { .. }, Some(_)) => {
                 // The length of "forall" plus the final separating dot and whitespace ". "
                 let mut result = 8;
-                while let TypeF::Forall { var, body, .. } = &ty.0 {
+                while let TypeF::Forall { var, body, .. } = &ty.types {
                     // The length of the identifier plus the preceding whitespace
                     result += var.to_string().len() + 1;
                     ty = body.as_ref();
@@ -162,7 +162,7 @@ pub mod ty_path {
             _ => 0,
         };
 
-        match (&ty.0, path_it.next()) {
+        match (&ty.types, path_it.next()) {
             (TypeF::Arrow(dom, codom), Some(next)) => {
                 // The potential shift of the start position of the domain introduced by the couple
                 // of parentheses around the domain. Parentheses are added when printing a function
@@ -170,7 +170,7 @@ pub mod ty_path {
                 // For example, `Arrow(Arrow(Num, Num), Num)` is rendered as "(Num -> Num) -> Num".
                 // In this case, the position of the sub-type "Num -> Num" starts at 1 instead of
                 // 0.
-                let paren_offset = match dom.0 {
+                let paren_offset = match dom.types {
                     TypeF::Arrow(_, _) => 1,
                     _ => 0,
                 };
@@ -248,10 +248,10 @@ pub mod ty_path {
                     "span: current type path element indicates to go to field `{}`,\
 but this field doesn't exist in {}",
                     ident,
-                    Types(TypeF::Record(rows.clone()))
+                    Types::from(TypeF::Record(rows.clone())),
                 )
             }
-            (TypeF::Array(ty), Some(Elem::Array)) if *ty.as_ref() == Types(TypeF::Dyn) =>
+            (TypeF::Array(ty), Some(Elem::Array)) if ty.as_ref().types == TypeF::Dyn =>
             // Dyn shouldn't be the target of any blame
             {
                 panic!("span(): unexpected blame of a dyn contract inside an array")
@@ -411,7 +411,7 @@ impl Label {
     /// Generate a dummy label for testing purpose.
     pub fn dummy() -> Label {
         Label {
-            types: Rc::new(Types(TypeF::Num)),
+            types: Rc::new(Types::from(TypeF::Num)),
             diagnostics: vec![ContractDiagnostic::new().with_message(String::from("testing"))],
             span: RawSpan {
                 src_id: Files::new().add("<test>", String::from("empty")),
@@ -476,7 +476,7 @@ impl Label {
 impl Default for Label {
     fn default() -> Label {
         Label {
-            types: Rc::new(Types(TypeF::Dyn)),
+            types: Rc::new(Types::from(TypeF::Dyn)),
             span: RawSpan {
                 src_id: Files::new().add("<null>", String::from("")),
                 start: 0.into(),
diff --git a/src/parser/grammar.lalrpop b/src/parser/grammar.lalrpop
index 87c9d207..6f1dbed0 100644
--- a/src/parser/grammar.lalrpop
+++ b/src/parser/grammar.lalrpop
@@ -259,7 +259,7 @@ Forall: Types =
             // The variable kind will be determined during the `fix_type_vars`
             // phase. For now, we put a random one (which is also the default
             // one, for unused type variables)
-            |acc, var| Types(TypeF::Forall { var, var_kind: VarKind::Type, body: Box::new(acc) })
+            |acc, var| Types::from(TypeF::Forall { var, var_kind: VarKind::Type, body: Box::new(acc) })
         )
     };
 
@@ -298,8 +298,11 @@ Applicative: UniTerm = {
 };
 
 // The parametrized array type.
-TypeArray: Types = "Array" <AsType<RecordOperand>> =>
-    Types(TypeF::Array(Box::new(<>)));
+TypeArray: Types = "Array" <t: AsType<RecordOperand>> =>
+    // For some reason, we have to bind the type into a `t`
+    // rather than using the usual `<>` placeholder, otherwise,
+    // it doens't compile.
+    Types::from(TypeF::Array(Box::new(t)));
 
 RecordOperand: UniTerm = {
     Atom,
@@ -367,7 +370,7 @@ Atom: UniTerm = {
 
         UniTerm::from(Term::Array(terms, Default::default()))
     },
-    AsUniTerm<TypeAtom>,
+    AsUniTerm<WithPos<TypeAtom>>,
 };
 
 // A record field definition. The is the only place where we don't fix the type
@@ -822,7 +825,7 @@ InfixExpr: UniTerm = {
 
     #[precedence(level="11")] #[assoc(side="right")]
     <s: AsType<InfixExpr>> "->" <t: AsType<InfixExpr>> =>
-        UniTerm::from(Types(TypeF::Arrow(Box::new(s), Box::new(t)))),
+        UniTerm::from(Types::from(TypeF::Arrow(Box::new(s), Box::new(t)))),
 }
 
 BOpPre: BinaryOp = {
@@ -863,10 +866,10 @@ NOpPre<ArgRule>: UniTerm = {
 }
 
 TypeBuiltin: Types = {
-    "Dyn" => Types(TypeF::Dyn),
-    "Num" => Types(TypeF::Num),
-    "Bool" => Types(TypeF::Bool),
-    "Str" => Types(TypeF::Str),
+     "Dyn" => Types::from(TypeF::Dyn),
+     "Num" => Types::from(TypeF::Num),
+     "Bool" => Types::from(TypeF::Bool),
+     "Str" => Types::from(TypeF::Str),
 }
 
 TypeAtom: Types = {
@@ -888,13 +891,15 @@ TypeAtom: Types = {
                 ),
                 |erows, row| EnumRows(EnumRowsF::Extend { row, tail: Box::new(erows) })
             );
-        Types(TypeF::Enum(ty))
+ 	    Types::from(TypeF::Enum(ty))
+    },
+    "{" "_" ":" <t: WithPos<Types>> "}" => {
+        Types::from(TypeF::Dict(Box::new(t)))
     },
-    "{" "_" ":" <Types> "}" => Types(TypeF::Dict(Box::new(<>))),
     "_" => {
         let id = *next_wildcard_id;
-        *next_wildcard_id += 1;
-        Types(TypeF::Wildcard(id))
+        *next_wildcard_id += 1;        
+        Types::from(TypeF::Wildcard(id))
     },
 }
 
diff --git a/src/parser/uniterm.rs b/src/parser/uniterm.rs
index 1ced757c..1a4f0739 100644
--- a/src/parser/uniterm.rs
+++ b/src/parser/uniterm.rs
@@ -79,10 +79,16 @@ impl TryFrom<UniTerm> for Types {
 
     fn try_from(ut: UniTerm) -> Result<Self, ParseError> {
         match ut.node {
-            UniTermNode::Var(id) => Ok(Types(TypeF::Var(id))),
+            UniTermNode::Var(id) => Ok(Types {
+                types: TypeF::Var(id),
+                pos: ut.pos,
+            }),
             UniTermNode::Record(r) => Types::try_from(r),
             UniTermNode::Types(ty) => Ok(ty),
-            UniTermNode::Term(rt) => Ok(Types(TypeF::Flat(rt))),
+            UniTermNode::Term(rt) => Ok(Types {
+                types: TypeF::Flat(rt),
+                pos: ut.pos,
+            }),
         }
     }
 }
@@ -129,9 +135,10 @@ impl From<Term> for UniTerm {
 
 impl From<Types> for UniTerm {
     fn from(ty: Types) -> Self {
+        let pos = ty.pos;
         UniTerm {
             node: UniTermNode::Types(ty),
-            pos: TermPos::None,
+            pos,
         }
     }
 }
@@ -267,7 +274,10 @@ impl UniRecord {
                     }
                 },
             )?;
-        Ok(Types(TypeF::Record(rrows)))
+        Ok(Types {
+            types: TypeF::Record(rrows),
+            pos: self.pos,
+        })
     }
 
     pub fn with_pos(mut self, pos: TermPos) -> Self {
@@ -338,9 +348,13 @@ impl TryFrom<UniRecord> for Types {
                     )
                 })
         } else {
-            ur.clone()
-                .into_type_strict()
-                .or_else(|_| RichTerm::try_from(ur).map(|rt| Types(TypeF::Flat(rt))))
+            let pos = ur.pos;
+            ur.clone().into_type_strict().or_else(|_| {
+                RichTerm::try_from(ur).map(|rt| Types {
+                    types: TypeF::Flat(rt),
+                    pos,
+                })
+            })
         }
     }
 }
@@ -499,7 +513,7 @@ impl FixTypeVars for Types {
         mut bound_vars: BoundVarEnv,
         span: RawSpan,
     ) -> Result<(), ParseError> {
-        match self.0 {
+        match self.types {
             TypeF::Dyn
             | TypeF::Num
             | TypeF::Bool
@@ -519,7 +533,7 @@ impl FixTypeVars for Types {
                 } else {
                     let id = *id;
                     let pos = id.pos;
-                    self.0 = TypeF::Flat(RichTerm::new(Term::Var(id), pos));
+                    self.types = TypeF::Flat(RichTerm::new(Term::Var(id), pos));
                 }
                 Ok(())
             }
diff --git a/src/parser/utils.rs b/src/parser/utils.rs
index 663891ef..ccaaa2e5 100644
--- a/src/parser/utils.rs
+++ b/src/parser/utils.rs
@@ -316,7 +316,10 @@ pub fn combine_match_annots(
         (None, None) => {
             let dummy_annot = TypeAnnotation {
                 contracts: vec![LabeledType {
-                    types: Types(TypeF::Dyn),
+                    types: Types {
+                        types: TypeF::Dyn,
+                        pos: TermPos::Original(span),
+                    },
                     label: Label {
                         span,
                         ..Default::default()
diff --git a/src/pretty.rs b/src/pretty.rs
index 81b9f80f..9955f4b7 100644
--- a/src/pretty.rs
+++ b/src/pretty.rs
@@ -796,7 +796,7 @@ where
 {
     fn pretty(self, allocator: &'a D) -> DocBuilder<'a, D, A> {
         use TypeF::*;
-        match self.0 {
+        match self.types {
             Dyn => allocator.text("Dyn"),
             Num => allocator.text("Num"),
             Bool => allocator.text("Bool"),
@@ -816,7 +816,11 @@ where
             Forall { var, ref body, .. } => {
                 let mut curr = body.as_ref();
                 let mut foralls = vec![&var];
-                while let Types(Forall { var, ref body, .. }) = curr {
+                while let Types {
+                    types: Forall { var, ref body, .. },
+                    ..
+                } = curr
+                {
                     foralls.push(var);
                     curr = body;
                 }
@@ -843,7 +847,7 @@ where
                 .append(ty.pretty(allocator))
                 .append(allocator.line())
                 .braces(),
-            Arrow(dom, codom) => match dom.0 {
+            Arrow(dom, codom) => match dom.types {
                 Arrow(..) | Forall { .. } => dom
                     .pretty(allocator)
                     .parens()
diff --git a/src/term/mod.rs b/src/term/mod.rs
index b349883f..b804bd84 100644
--- a/src/term/mod.rs
+++ b/src/term/mod.rs
@@ -615,7 +615,7 @@ impl Term {
             }
             Annotated(ref mut annot, ref mut t) => {
                 annot.iter_mut().for_each(|LabeledType { types, .. }| {
-                    if let TypeF::Flat(ref mut rt) = types.0 {
+                    if let TypeF::Flat(ref mut rt) = types.types {
                         func(rt)
                     }
                 });
@@ -1960,7 +1960,7 @@ mod tests {
 
         let inner = TypeAnnotation {
             types: Some(LabeledType {
-                types: Types(TypeF::Num),
+                types: Types::from(TypeF::Num),
                 label: Label::dummy(),
             }),
             ..Default::default()
diff --git a/src/transform/free_vars.rs b/src/transform/free_vars.rs
index 0d866d4d..5e1868d6 100644
--- a/src/transform/free_vars.rs
+++ b/src/transform/free_vars.rs
@@ -178,7 +178,7 @@ impl CollectFreeVars for RichTerm {
 
 impl CollectFreeVars for Types {
     fn collect_free_vars(&mut self, set: &mut HashSet<Ident>) {
-        match &mut self.0 {
+        match &mut self.types {
             TypeF::Dyn
             | TypeF::Num
             | TypeF::Bool
diff --git a/src/transform/substitute_wildcards.rs b/src/transform/substitute_wildcards.rs
index 39387fff..da2c827c 100644
--- a/src/transform/substitute_wildcards.rs
+++ b/src/transform/substitute_wildcards.rs
@@ -53,7 +53,10 @@ pub fn transform_one(rt: RichTerm, wildcards: &Wildcards) -> RichTerm {
 
 /// Get the inferred type for a wildcard, or `Dyn` if no type was inferred.
 fn get_wildcard_type(wildcards: &Wildcards, id: usize) -> Types {
-    wildcards.get(id).cloned().unwrap_or(Types(TypeF::Dyn))
+    wildcards
+        .get(id)
+        .cloned()
+        .unwrap_or(Types::from(TypeF::Dyn))
 }
 
 trait SubstWildcard {
@@ -64,8 +67,8 @@ trait SubstWildcard {
 impl SubstWildcard for Types {
     fn subst_wildcards(self, wildcards: &Wildcards) -> Types {
         self.traverse::<_, _, Infallible>(
-            &|ty, _| {
-                if let Types(TypeF::Wildcard(id)) = ty {
+            &|ty: Types, _| {
+                if let TypeF::Wildcard(id) = ty.types {
                     Ok(get_wildcard_type(wildcards, id))
                 } else {
                     Ok(ty)
diff --git a/src/typecheck/mod.rs b/src/typecheck/mod.rs
index 4adcbb8a..69629fba 100644
--- a/src/typecheck/mod.rs
+++ b/src/typecheck/mod.rs
@@ -184,9 +184,15 @@ impl<E: TermEnvironment + Clone> std::convert::TryInto<Types> for GenericUnifTyp
                     GenericUnifRecordRows::try_into,
                     UnifEnumRows::try_into,
                 )?;
-                Ok(Types(converted))
+                Ok(Types::from(converted))
+            }
+            GenericUnifType::Contract(t, _) => {
+                let pos = t.pos;
+                Ok(Types {
+                    types: TypeF::Flat(t),
+                    pos,
+                })
             }
-            GenericUnifType::Contract(t, _) => Ok(Types(TypeF::Flat(t))),
             _ => Err(()),
         }
     }
@@ -340,7 +346,7 @@ impl<E: TermEnvironment + Clone> GenericUnifType<E> {
     /// [`GenericUnifType::Contract`] which also stores a term environment, required for checking type
     /// equality involving contracts.
     pub fn from_type(ty: Types, env: &E) -> Self {
-        match ty.0 {
+        match ty.types {
             TypeF::Flat(t) => GenericUnifType::Contract(t, env.clone()),
             ty => GenericUnifType::Concrete(ty.map(
                 |ty_| Box::new(GenericUnifType::from_type(*ty_, env)),
@@ -433,18 +439,18 @@ impl UnifType {
         match self {
             UnifType::UnifVar(p) => match table.root_type(p) {
                 t @ UnifType::Concrete(_) => t.into_type(table),
-                _ => Types(TypeF::Dyn),
+                _ => Types::from(TypeF::Dyn),
             },
-            UnifType::Constant(_) => Types(TypeF::Dyn),
+            UnifType::Constant(_) => Types::from(TypeF::Dyn),
             UnifType::Concrete(t) => {
                 let mapped = t.map(
                     |btyp| Box::new(btyp.into_type(table)),
                     |urrows| urrows.into_rrows(table),
                     |uerows| uerows.into_erows(table),
                 );
-                Types(mapped)
+                Types::from(mapped)
             }
-            UnifType::Contract(t, _) => Types(TypeF::Flat(t)),
+            UnifType::Contract(t, _) => Types::from(TypeF::Flat(t)),
         }
     }
 
@@ -962,7 +968,7 @@ fn walk_type<L: Linearizer>(
     mut linearizer: L,
     ty: &Types,
 ) -> Result<(), TypecheckError> {
-    match &ty.0 {
+    match &ty.types {
        TypeF::Dyn
        | TypeF::Num
        | TypeF::Bool
@@ -1652,10 +1658,10 @@ fn replace_wildcards_with_var(
         ))
     }
 
-    match ty.0 {
+    match ty.types {
         TypeF::Wildcard(i) => get_wildcard_var(table, wildcard_vars, i),
         TypeF::Flat(t) => UnifType::Contract(t, env.clone()),
-        _ => UnifType::Concrete(ty.0.map_state(
+        _ => UnifType::Concrete(ty.types.map_state(
             |ty, (table, wildcard_vars)| {
                 Box::new(replace_wildcards_with_var(table, wildcard_vars, *ty, env))
             },
@@ -1690,11 +1696,11 @@ pub enum ApparentType {
 impl From<ApparentType> for Types {
     fn from(at: ApparentType) -> Self {
         match at {
-            ApparentType::Annotated(ty) if has_wildcards(&ty) => Types(TypeF::Dyn),
+            ApparentType::Annotated(ty) if has_wildcards(&ty) => Types::from(TypeF::Dyn),
             ApparentType::Annotated(ty)
             | ApparentType::Inferred(ty)
             | ApparentType::Approximated(ty) => ty,
-            ApparentType::FromEnv(uty) => uty.try_into().ok().unwrap_or(Types(TypeF::Dyn)),
+            ApparentType::FromEnv(uty) => uty.try_into().ok().unwrap_or(Types::from(TypeF::Dyn)),
         }
     }
 }
@@ -1719,7 +1725,7 @@ fn field_apparent_type(
                 .as_ref()
                 .map(|v| apparent_type(v.as_ref(), env, resolver))
         })
-        .unwrap_or(ApparentType::Approximated(Types(TypeF::Dyn)))
+        .unwrap_or(ApparentType::Approximated(Types::from(TypeF::Dyn)))
 }
 
 /// Determine the apparent type of a let-bound expression.
@@ -1767,17 +1773,17 @@ pub fn apparent_type(
                 .first()
                 .map(|labeled_ty| ApparentType::Annotated(labeled_ty.types.clone()))
                 .unwrap_or_else(|| apparent_type(value.as_ref(), env, resolver)),
-            Term::Num(_) => ApparentType::Inferred(Types(TypeF::Num)),
-            Term::Bool(_) => ApparentType::Inferred(Types(TypeF::Bool)),
-            Term::SealingKey(_) => ApparentType::Inferred(Types(TypeF::Sym)),
-            Term::Str(_) | Term::StrChunks(_) => ApparentType::Inferred(Types(TypeF::Str)),
-            Term::Array(..) => {
-                ApparentType::Approximated(Types(TypeF::Array(Box::new(Types(TypeF::Dyn)))))
-            }
+            Term::Num(_) => ApparentType::Inferred(Types::from(TypeF::Num)),
+            Term::Bool(_) => ApparentType::Inferred(Types::from(TypeF::Bool)),
+            Term::SealingKey(_) => ApparentType::Inferred(Types::from(TypeF::Sym)),
+            Term::Str(_) | Term::StrChunks(_) => ApparentType::Inferred(Types::from(TypeF::Str)),
+            Term::Array(..) => ApparentType::Approximated(Types::from(TypeF::Array(Box::new(
+                Types::from(TypeF::Dyn),
+            )))),
             Term::Var(id) => env
                 .and_then(|envs| envs.get(id).cloned())
                 .map(ApparentType::FromEnv)
-                .unwrap_or(ApparentType::Approximated(Types(TypeF::Dyn))),
+                .unwrap_or(ApparentType::Approximated(Types::from(TypeF::Dyn))),
             Term::ResolvedImport(file_id) => match resolver {
                 Some(r) if !imports_seen.contains(file_id) => {
                     imports_seen.insert(*file_id);
@@ -1787,9 +1793,9 @@ pub fn apparent_type(
                         .expect("Internal error: resolved import not found during typechecking.");
                     apparent_type_check_cycle(&t.term, env, Some(r), imports_seen)
                 }
-                _ => ApparentType::Approximated(Types(TypeF::Dyn)),
+                _ => ApparentType::Approximated(Types::from(TypeF::Dyn)),
             },
-            _ => ApparentType::Approximated(Types(TypeF::Dyn)),
+            _ => ApparentType::Approximated(Types::from(TypeF::Dyn)),
         }
     }
 
@@ -1831,7 +1837,7 @@ fn has_wildcards(ty: &Types) -> bool {
     ty.clone()
         .traverse::<_, _, std::convert::Infallible>(
             &|ty: Types, has_wildcard| {
-                if ty.0.is_wildcard() {
+                if ty.types.is_wildcard() {
                     *has_wildcard = true;
                 }
                 Ok(ty)
diff --git a/src/typecheck/reporting.rs b/src/typecheck/reporting.rs
index 079d5a2a..169f73db 100644
--- a/src/typecheck/reporting.rs
+++ b/src/typecheck/reporting.rs
@@ -163,8 +163,8 @@ pub fn to_type(
     let ty = ty.into_root(table);
 
     match ty {
-        UnifType::UnifVar(p) => Types(TypeF::Var(var_name(reported_names, names, p))),
-        UnifType::Constant(c) => Types(TypeF::Var(cst_name(reported_names, names, c))),
+        UnifType::UnifVar(p) => Types::from(TypeF::Var(var_name(reported_names, names, p))),
+        UnifType::Constant(c) => Types::from(TypeF::Var(cst_name(reported_names, names, c))),
         UnifType::Concrete(t) => {
             let mapped = t.map_state(
                 |btyp, names| Box::new(to_type(table, reported_names, names, *btyp)),
@@ -172,8 +172,8 @@ pub fn to_type(
                 |erows, names| erows_to_type(table, reported_names, names, erows),
                 names,
             );
-            Types(mapped)
+            Types::from(mapped)
         }
-        UnifType::Contract(t, _) => Types(TypeF::Flat(t)),
+        UnifType::Contract(t, _) => Types::from(TypeF::Flat(t)),
     }
 }
diff --git a/src/types.rs b/src/types.rs
index 7ce4e503..47ba4c0c 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -53,6 +53,7 @@ use crate::{
     error::{EvalError, ParseError, ParseErrors, TypecheckError},
     identifier::Ident,
     mk_app, mk_fun,
+    position::TermPos,
     term::make as mk_term,
     term::{record::RecordData, RichTerm, Term, Traverse, TraverseOrder},
 };
@@ -262,9 +263,13 @@ pub type RecordRow = RecordRowF<Box<Types>>;
 #[derive(Clone, PartialEq, Debug)]
 /// Concrete, recursive definition for record rows.
 pub struct RecordRows(pub RecordRowsF<Box<Types>, Box<RecordRows>>);
+
 /// Concrete, recursive type for a Nickel type.
 #[derive(Clone, PartialEq, Debug)]
-pub struct Types(pub TypeF<Box<Types>, RecordRows, EnumRows>);
+pub struct Types {
+    pub types: TypeF<Box<Types>, RecordRows, EnumRows>,
+    pub pos: TermPos,
+}
 
 impl<Ty, RRows> RecordRowsF<Ty, RRows> {
     /// Map functions over the children nodes of record rows, when seen as a tree. The mutable
@@ -805,7 +810,7 @@ impl RecordRows {
         if path.len() == 1 {
             next
         } else {
-            match next.map(|ty| ty.0) {
+            match next.map(|ty| ty.types) {
                 Some(TypeF::Record(rrows)) => rrows.row_find_path(&path[1..]),
                 _ => None,
             }
@@ -820,7 +825,21 @@ impl RecordRows {
     }
 }
 
+impl From<TypeF<Box<Types>, RecordRows, EnumRows>> for Types {
+    fn from(types: TypeF<Box<Types>, RecordRows, EnumRows>) -> Self {
+        Types {
+            types,
+            pos: TermPos::None,
+        }
+    }
+}
+
 impl Types {
+    /// Creates a `Type` with the specified position
+    pub fn with_pos(self, pos: TermPos) -> Types {
+        Types { pos, ..self }
+    }
+
     /// Return the contract corresponding to a type, either as a function or a record. Said
     /// contract must then be applied using the `Assume` primitive operation.
     pub fn contract(&self) -> Result<RichTerm, UnboundTypeVariableError> {
@@ -830,10 +849,10 @@ impl Types {
 
     /// Returns true if this type is a function type, false otherwise.
     pub fn is_function_type(&self) -> bool {
-        match self {
-            Types(TypeF::Forall { body, .. }) => body.is_function_type(),
-            Types(TypeF::Arrow(..)) => true,
-            Types(_) => false,
+        match &self.types {
+            TypeF::Forall { body, .. } => body.is_function_type(),
+            TypeF::Arrow(..) => true,
+            _ => false,
         }
     }
 
@@ -854,7 +873,7 @@ impl Types {
     ) -> Result<RichTerm, UnboundTypeVariableError> {
         use crate::stdlib::contract;
 
-        let ctr = match self.0 {
+        let ctr = match self.types {
             TypeF::Dyn => contract::dynamic(),
             TypeF::Num => contract::num(),
             TypeF::Bool => contract::bool(),
@@ -906,7 +925,7 @@ impl Types {
     pub fn fmt_is_atom(&self) -> bool {
         use TypeF::*;
 
-        match &self.0 {
+        match &self.types {
             Dyn | Num | Bool | Str | Var(_) | Record(_) | Enum(_) => true,
             Flat(rt) if matches!(*rt.term, Term::Var(_)) => true,
             _ => false,
@@ -921,24 +940,24 @@ impl Traverse<Types> for Types {
     {
         match order {
             TraverseOrder::TopDown => {
-                let inner = f(self, state)?.0.try_map_state(
+                let inner = f(self, state)?.types.try_map_state(
                     |ty, state| Ok(Box::new(ty.traverse(f, state, order)?)),
                     |rrows, state| rrows.traverse(f, state, order),
                     |erows, _| Ok(erows),
                     state,
                 )?;
 
-                Ok(Types(inner))
+                Ok(Types::from(inner))
             }
             TraverseOrder::BottomUp => {
-                let traversed_depth_first = self.0.try_map_state(
+                let traversed_depth_first = self.types.try_map_state(
                     |ty, state| Ok(Box::new(ty.traverse(f, state, order)?)),
                     |rrows, state| rrows.traverse(f, state, order),
                     |erows, _| Ok(erows),
                     state,
                 )?;
 
-                f(Types(traversed_depth_first), state)
+                f(Types::from(traversed_depth_first), state)
             }
         }
     }
@@ -949,8 +968,8 @@ impl Traverse<RichTerm> for Types {
     where
         F: Fn(RichTerm, &mut S) -> Result<RichTerm, E>,
     {
-        let f_on_type = |ty: Types, s: &mut S| match ty.0 {
-            TypeF::Flat(t) => t.traverse(f, s, order).map(|t| Types(TypeF::Flat(t))),
+        let f_on_type = |ty: Types, s: &mut S| match ty.types {
+            TypeF::Flat(t) => t.traverse(f, s, order).map(|t| Types::from(TypeF::Flat(t))),
             _ => Ok(ty),
         };
 
@@ -995,7 +1014,7 @@ impl Display for EnumRows {
 
 impl Display for Types {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match &self.0 {
+        match &self.types {
             TypeF::Dyn => write!(f, "Dyn"),
             TypeF::Num => write!(f, "Num"),
             TypeF::Bool => write!(f, "Bool"),
@@ -1015,7 +1034,11 @@ impl Display for Types {
             TypeF::Forall { var, ref body, .. } => {
                 let mut curr: &Types = body.as_ref();
                 write!(f, "forall {var}")?;
-                while let Types(TypeF::Forall { var, ref body, .. }) = curr {
+                while let Types {
+                    types: TypeF::Forall { var, ref body, .. },
+                    ..
+                } = curr
+                {
                     write!(f, " {var}")?;
                     curr = body;
                 }
@@ -1024,7 +1047,7 @@ impl Display for Types {
             TypeF::Enum(row) => write!(f, "[|{row}|]"),
             TypeF::Record(row) => write!(f, "{{{row}}}"),
             TypeF::Dict(ty) => write!(f, "{{_: {ty}}}"),
-            TypeF::Arrow(dom, codom) => match dom.0 {
+            TypeF::Arrow(dom, codom) => match dom.types {
                 TypeF::Arrow(_, _) | TypeF::Forall { .. } => write!(f, "({dom}) -> {codom}"),
                 _ => write!(f, "{dom} -> {codom}"),
             },


Test patch:
diff --git a/tests/integration/typecheck_fail.rs b/tests/integration/typecheck_fail.rs
index 7799256e..6bb6a15d 100644
--- a/tests/integration/typecheck_fail.rs
+++ b/tests/integration/typecheck_fail.rs
@@ -287,8 +287,14 @@ let g : Num = f 0 in
 g"#
         ),
         Err(TypecheckError::TypeMismatch(
-            Types(TypeF::Arrow(_, _)),
-            Types(TypeF::Dyn),
+            Types {
+                types: TypeF::Arrow(_, _),
+                ..
+            },
+            Types {
+                types: TypeF::Dyn,
+                ..
+            },
             _
         ))
     );
@@ -304,8 +310,14 @@ fn locally_different_flat_types() {
              foo : lib.Contract"
         ),
         Err(TypecheckError::TypeMismatch(
-            Types(TypeF::Flat(..)),
-            Types(TypeF::Flat(..)),
+            Types {
+                types: TypeF::Flat(..),
+                ..
+            },
+            Types {
+                types: TypeF::Flat(..),
+                ..
+            },
             _
         ))
     );