Repository: tweag/nickel
Pull request #2079 (2024-10-24 12:53:40)

[RFC007] New AST for types
Depends on #2072

The previous PR #2072 introducing the new AST used the "old" type representation from mainline Nickel. Now that `typ::Type` has been made more generic in #2078, we can use a better representation that can be arena allocated as well and can contain contracts that are themselves represented in the new AST.

This PR integrates this new type representation and adds corresponding translation functions from the mainline implementation.

github-actions[bot]: <h2><a href="https://bencher.dev/perf/nickel/reports/1b99ad50-dcce-476b-8a9c-765d811272ab?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel"><img src="https://bencher.dev/favicon.svg" width="24" height="24" alt="🐰" /> Bencher Report</a></h2><table><tr><td>Branch</td><td><a href="https://bencher.dev/perf/nickel/branches/2079-merge?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">2079/merge</a></td></tr><tr><td>Testbed</td><td><a href="https://bencher.dev/perf/nickel/testbeds/ubuntu-latest?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">ubuntu-latest</a></td></tr></table><blockquote><p><b>⚠️ WARNING:</b> The following Measure does not have a Threshold. Without a Threshold, no Alerts will ever be generated!</p><ul><li><a href="https://bencher.dev/perf/nickel/measures/latency?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">Latency</a></li></ul><p><a href="https://bencher.dev/console/projects/nickel/thresholds/add?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">Click here to create a new Threshold</a><br/>For more information, see <a href="https://bencher.dev/docs/explanation/thresholds/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">the Threshold documentation</a>.<br/>To only post results if a Threshold exists, set <a href="https://bencher.dev/docs/explanation/bencher-run/#--ci-only-thresholds?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">the <code lang="rust">--ci-only-thresholds</code> CLI flag</a>.</p></blockquote><details><summary>Click to view all benchmark results</summary><br/><table><thead><tr><th>Benchmark</th><th><a href="https://bencher.dev/perf/nickel/measures/latency?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">Latency</a></th><th>nanoseconds (ns)</th></tr></thead><tbody><tr><td><a href="https://bencher.dev/perf/nickel/benchmarks/fibonacci-10?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">fibonacci 10</a></td><td>📈 <a href="https://bencher.dev/perf/nickel?branches=b8f15526-aef7-445a-a7ea-3e6b5639e3cd&heads=e6bacebc-37c1-4068-ba94-9d799ac5ccb5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=3cfaf19e-c9c5-4341-95cd-703456d0fc9f&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727183151000&end_time=1729775151000&report=1b99ad50-dcce-476b-8a9c-765d811272ab&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>489,120.00</td></tr><tr><td><a href="https://bencher.dev/perf/nickel/benchmarks/foldl-arrays-50?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">foldl arrays 50</a></td><td>📈 <a href="https://bencher.dev/perf/nickel?branches=b8f15526-aef7-445a-a7ea-3e6b5639e3cd&heads=e6bacebc-37c1-4068-ba94-9d799ac5ccb5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=95048806-9bf2-49e4-88f2-dbcd66cbf68a&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727183151000&end_time=1729775151000&report=1b99ad50-dcce-476b-8a9c-765d811272ab&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>1,771,100.00</td></tr><tr><td><a href="https://bencher.dev/perf/nickel/benchmarks/foldl-arrays-500?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">foldl arrays 500</a></td><td>📈 <a href="https://bencher.dev/perf/nickel?branches=b8f15526-aef7-445a-a7ea-3e6b5639e3cd&heads=e6bacebc-37c1-4068-ba94-9d799ac5ccb5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=15373c31-496e-4f43-bbb4-466ef49fdd87&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727183151000&end_time=1729775151000&report=1b99ad50-dcce-476b-8a9c-765d811272ab&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>6,809,200.00</td></tr><tr><td><a href="https://bencher.dev/perf/nickel/benchmarks/foldr-strings-50?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">foldr strings 50</a></td><td>📈 <a href="https://bencher.dev/perf/nickel?branches=b8f15526-aef7-445a-a7ea-3e6b5639e3cd&heads=e6bacebc-37c1-4068-ba94-9d799ac5ccb5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=87746452-c4df-48ea-b5e9-8d66f82ca556&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727183151000&end_time=1729775151000&report=1b99ad50-dcce-476b-8a9c-765d811272ab&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>7,349,100.00</td></tr><tr><td><a href="https://bencher.dev/perf/nickel/benchmarks/foldr-strings-500?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">foldr strings 500</a></td><td>📈 <a href="https://bencher.dev/perf/nickel?branches=b8f15526-aef7-445a-a7ea-3e6b5639e3cd&heads=e6bacebc-37c1-4068-ba94-9d799ac5ccb5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=7f141ea0-9fbb-4305-bb63-78f163e546d6&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727183151000&end_time=1729775151000&report=1b99ad50-dcce-476b-8a9c-765d811272ab&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>63,918,000.00</td></tr><tr><td><a href="https://bencher.dev/perf/nickel/benchmarks/generate-normal-250?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">generate normal 250</a></td><td>📈 <a href="https://bencher.dev/perf/nickel?branches=b8f15526-aef7-445a-a7ea-3e6b5639e3cd&heads=e6bacebc-37c1-4068-ba94-9d799ac5ccb5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=8c154d79-dc68-4d93-abb3-83183a62f6a2&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727183151000&end_time=1729775151000&report=1b99ad50-dcce-476b-8a9c-765d811272ab&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>48,707,000.00</td></tr><tr><td><a href="https://bencher.dev/perf/nickel/benchmarks/generate-normal-50?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">generate normal 50</a></td><td>📈 <a href="https://bencher.dev/perf/nickel?branches=b8f15526-aef7-445a-a7ea-3e6b5639e3cd&heads=e6bacebc-37c1-4068-ba94-9d799ac5ccb5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=6d0222f6-0b85-4eaf-8c5c-e8957c522ef9&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727183151000&end_time=1729775151000&report=1b99ad50-dcce-476b-8a9c-765d811272ab&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>2,139,000.00</td></tr><tr><td><a href="https://bencher.dev/perf/nickel/benchmarks/generate-normal-unchecked-1000?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">generate normal unchecked 1000</a></td><td>📈 <a href="https://bencher.dev/perf/nickel?branches=b8f15526-aef7-445a-a7ea-3e6b5639e3cd&heads=e6bacebc-37c1-4068-ba94-9d799ac5ccb5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=be0d3f87-d089-4b81-af44-3bbc6e831697&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727183151000&end_time=1729775151000&report=1b99ad50-dcce-476b-8a9c-765d811272ab&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>3,351,600.00</td></tr><tr><td><a href="https://bencher.dev/perf/nickel/benchmarks/generate-normal-unchecked-200?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">generate normal unchecked 200</a></td><td>📈 <a href="https://bencher.dev/perf/nickel?branches=b8f15526-aef7-445a-a7ea-3e6b5639e3cd&heads=e6bacebc-37c1-4068-ba94-9d799ac5ccb5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=2946df33-45e4-4282-b749-040582021fe3&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727183151000&end_time=1729775151000&report=1b99ad50-dcce-476b-8a9c-765d811272ab&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>757,570.00</td></tr><tr><td><a href="https://bencher.dev/perf/nickel/benchmarks/pidigits-100?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">pidigits 100</a></td><td>📈 <a href="https://bencher.dev/perf/nickel?branches=b8f15526-aef7-445a-a7ea-3e6b5639e3cd&heads=e6bacebc-37c1-4068-ba94-9d799ac5ccb5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=5be25c57-d46f-4b48-b6f0-dee01b84220a&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727183151000&end_time=1729775151000&report=1b99ad50-dcce-476b-8a9c-765d811272ab&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>3,247,300.00</td></tr><tr><td><a href="https://bencher.dev/perf/nickel/benchmarks/pipe-normal-20?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">pipe normal 20</a></td><td>📈 <a href="https://bencher.dev/perf/nickel?branches=b8f15526-aef7-445a-a7ea-3e6b5639e3cd&heads=e6bacebc-37c1-4068-ba94-9d799ac5ccb5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=7178a975-b548-4e78-8c84-4f3802668825&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727183151000&end_time=1729775151000&report=1b99ad50-dcce-476b-8a9c-765d811272ab&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>1,507,700.00</td></tr><tr><td><a href="https://bencher.dev/perf/nickel/benchmarks/pipe-normal-200?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">pipe normal 200</a></td><td>📈 <a href="https://bencher.dev/perf/nickel?branches=b8f15526-aef7-445a-a7ea-3e6b5639e3cd&heads=e6bacebc-37c1-4068-ba94-9d799ac5ccb5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=6884f292-8796-44a8-a30c-d3cb2ed987f6&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727183151000&end_time=1729775151000&report=1b99ad50-dcce-476b-8a9c-765d811272ab&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>10,642,000.00</td></tr><tr><td><a href="https://bencher.dev/perf/nickel/benchmarks/product-30?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">product 30</a></td><td>📈 <a href="https://bencher.dev/perf/nickel?branches=b8f15526-aef7-445a-a7ea-3e6b5639e3cd&heads=e6bacebc-37c1-4068-ba94-9d799ac5ccb5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=2f5f7f6a-4e54-4542-aa09-da5671953fbb&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727183151000&end_time=1729775151000&report=1b99ad50-dcce-476b-8a9c-765d811272ab&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>820,180.00</td></tr><tr><td><a href="https://bencher.dev/perf/nickel/benchmarks/scalar-10?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">scalar 10</a></td><td>📈 <a href="https://bencher.dev/perf/nickel?branches=b8f15526-aef7-445a-a7ea-3e6b5639e3cd&heads=e6bacebc-37c1-4068-ba94-9d799ac5ccb5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=211bb66a-58f8-488b-97e4-7ea032870038&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727183151000&end_time=1729775151000&report=1b99ad50-dcce-476b-8a9c-765d811272ab&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>1,496,000.00</td></tr><tr><td><a href="https://bencher.dev/perf/nickel/benchmarks/sum-30?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">sum 30</a></td><td>📈 <a href="https://bencher.dev/perf/nickel?branches=b8f15526-aef7-445a-a7ea-3e6b5639e3cd&heads=e6bacebc-37c1-4068-ba94-9d799ac5ccb5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=3f1710c5-ce75-4dfb-aadb-1435fed8a093&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727183151000&end_time=1729775151000&report=1b99ad50-dcce-476b-8a9c-765d811272ab&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>830,100.00</td></tr></tbody></table></details><a href="https://bencher.dev/perf/nickel/reports/1b99ad50-dcce-476b-8a9c-765d811272ab?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">🐰 View full continuous benchmarking report in Bencher</a><div id="bencher.dev/projects/nickel/id/2079-merge/ubuntu-latest/rust_criterion"></div>
dpulls[bot]: :tada: All dependencies have been resolved !

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 98138b69..1efc85d3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1013,16 +1013,6 @@ version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
 
-[[package]]
-name = "erased-serde"
-version = "0.4.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d"
-dependencies = [
- "serde",
- "typeid",
-]
-
 [[package]]
 name = "errno"
 version = "0.3.8"
@@ -1868,7 +1858,6 @@ dependencies = [
  "rustyline",
  "rustyline-derive",
  "serde",
- "serde-untagged",
  "serde-wasm-bindgen",
  "serde_json",
  "serde_repr",
@@ -1883,6 +1872,7 @@ dependencies = [
  "termimad",
  "test-generator",
  "toml",
+ "toml_edit",
  "topiary-core",
  "topiary-queries",
  "tree-sitter-nickel",
@@ -2655,17 +2645,6 @@ dependencies = [
  "serde_derive",
 ]
 
-[[package]]
-name = "serde-untagged"
-version = "0.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2676ba99bd82f75cae5cbd2c8eda6fa0b8760f18978ea840e980dd5567b5c5b6"
-dependencies = [
- "erased-serde",
- "serde",
- "typeid",
-]
-
 [[package]]
 name = "serde-wasm-bindgen"
 version = "0.5.0"
@@ -3309,12 +3288,6 @@ version = "2.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
 
-[[package]]
-name = "typeid"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "059d83cc991e7a42fc37bd50941885db0888e34209f8cfd9aab07ddec03bc9cf"
-
 [[package]]
 name = "typenum"
 version = "1.17.0"
diff --git a/Cargo.toml b/Cargo.toml
index 8c13d089..73cb993b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -84,7 +84,6 @@ serde_json = "1.0.96"
 serde_repr = "0.1"
 serde-wasm-bindgen = "0.5.0"
 serde_yaml = "0.9.19"
-serde-untagged = "0.1"
 sha-1 = "0.10.0"
 sha2 = "0.10.6"
 similar = "2.2.1"
@@ -95,6 +94,7 @@ termimad = "0.23.1"
 test-generator = "0.3.1"
 thiserror = "1.0.44"
 toml = "0.8"
+toml_edit = "0.22"
 typed-arena = "2.0.2"
 unicode-segmentation = "1.10.1"
 void = "1"
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
index a3043d4b..d9a0b0fc 100644
--- a/cli/Cargo.toml
+++ b/cli/Cargo.toml
@@ -20,7 +20,6 @@ default = ["repl", "doc", "format"]
 repl = ["nickel-lang-core/repl"]
 doc = ["nickel-lang-core/doc", "comrak"]
 format = ["nickel-lang-core/format", "dep:tempfile"]
-spanned-deser = ["nickel-lang-core/spanned-deser"]
 metrics = ["dep:metrics", "dep:metrics-util", "nickel-lang-core/metrics"]
 
 [dependencies]
diff --git a/core/Cargo.toml b/core/Cargo.toml
index 783f3664..f2387d1e 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -28,7 +28,6 @@ format = ["dep:topiary-core", "dep:topiary-queries", "dep:tree-sitter-nickel"]
 metrics = ["dep:metrics"]
 nix-experimental = [ "dep:cxx", "dep:cxx-build", "dep:pkg-config" ]
 bytecode-experimental = ["dep:bumpalo"]
-spanned-deser = ["dep:serde-untagged"]
 benchmark-ci = []
 
 [build-dependencies]
@@ -50,8 +49,8 @@ smallvec.workspace = true
 serde = { workspace = true, features = ["derive"] }
 serde_json.workspace = true
 serde_yaml.workspace = true
-serde-untagged = { workspace = true, optional = true }
-toml = { workspace = true, features = ["parse"] }
+toml_edit = { workspace = true, features = ["parse"] }
+toml = { workspace = true }
 void.workspace = true
 sha-1.workspace = true
 sha2.workspace = true
diff --git a/core/src/bytecode/ast/compat.rs b/core/src/bytecode/ast/compat.rs
index 5ae1b6b9..50e496ab 100644
--- a/core/src/bytecode/ast/compat.rs
+++ b/core/src/bytecode/ast/compat.rs
@@ -5,7 +5,7 @@
 //! [crate::bytecode::ast].
 
 use super::{primop::PrimOp, *};
-use crate::term;
+use crate::{term, typ as mainline_type};
 
 /// Convert from the mainline Nickel representation to the new AST representation. This trait is
 /// mostly `From` with an additional argument for the allocator.
@@ -142,11 +142,14 @@ impl<'ast> FromMainline<'ast, term::pattern::OrPattern> for PatternData<'ast> {
 
 impl<'ast> FromMainline<'ast, term::TypeAnnotation> for Annotation<'ast> {
     fn from_mainline(alloc: &'ast AstAlloc, annot: &term::TypeAnnotation) -> Self {
-        let typ = annot.typ.as_ref().map(|typ| typ.typ.clone());
+        let typ = annot.typ.as_ref().map(|typ| typ.typ.to_ast(alloc));
 
-        let contracts = alloc
-            .type_arena
-            .alloc_extend(annot.contracts.iter().map(|contract| contract.typ.clone()));
+        let contracts = alloc.types(
+            annot
+                .contracts
+                .iter()
+                .map(|contract| contract.typ.to_ast(alloc)),
+        );
 
         Annotation { typ, contracts }
     }
@@ -175,6 +178,69 @@ impl<'ast> FromMainline<'ast, term::record::FieldMetadata> for record::FieldMeta
     }
 }
 
+impl<'ast> FromMainline<'ast, mainline_type::Type> for Type<'ast> {
+    fn from_mainline(alloc: &'ast AstAlloc, mainline: &mainline_type::Type) -> Self {
+        Type {
+            typ: mainline.typ.to_ast(alloc),
+            pos: mainline.pos,
+        }
+    }
+}
+
+type MainlineTypeUnr = mainline_type::TypeF<
+    Box<mainline_type::Type>,
+    mainline_type::RecordRows,
+    mainline_type::EnumRows,
+    term::RichTerm,
+>;
+
+impl<'ast> FromMainline<'ast, MainlineTypeUnr> for TypeUnr<'ast> {
+    fn from_mainline(alloc: &'ast AstAlloc, typ: &MainlineTypeUnr) -> Self {
+        typ.clone().map(
+            |typ| &*alloc.generic_arena.alloc((*typ).to_ast(alloc)),
+            |rrows| rrows.to_ast(alloc),
+            |erows| erows.to_ast(alloc),
+            |ctr| ctr.to_ast(alloc),
+        )
+    }
+}
+
+impl<'ast> FromMainline<'ast, mainline_type::RecordRows> for RecordRows<'ast> {
+    fn from_mainline(alloc: &'ast AstAlloc, rrows: &mainline_type::RecordRows) -> Self {
+        RecordRows(rrows.0.to_ast(alloc))
+    }
+}
+
+impl<'ast> FromMainline<'ast, mainline_type::EnumRows> for EnumRows<'ast> {
+    fn from_mainline(alloc: &'ast AstAlloc, erows: &mainline_type::EnumRows) -> Self {
+        EnumRows(erows.0.to_ast(alloc))
+    }
+}
+
+type MainlineEnumRowsUnr =
+    mainline_type::EnumRowsF<Box<mainline_type::Type>, Box<mainline_type::EnumRows>>;
+
+impl<'ast> FromMainline<'ast, MainlineEnumRowsUnr> for EnumRowsUnr<'ast> {
+    fn from_mainline(alloc: &'ast AstAlloc, erows: &MainlineEnumRowsUnr) -> Self {
+        erows.clone().map(
+            |typ| &*alloc.generic_arena.alloc((*typ).to_ast(alloc)),
+            |erows| &*alloc.generic_arena.alloc((*erows).to_ast(alloc)),
+        )
+    }
+}
+
+type MainlineRecordRowsUnr =
+    mainline_type::RecordRowsF<Box<mainline_type::Type>, Box<mainline_type::RecordRows>>;
+
+impl<'ast> FromMainline<'ast, MainlineRecordRowsUnr> for RecordRowsUnr<'ast> {
+    fn from_mainline(alloc: &'ast AstAlloc, rrows: &MainlineRecordRowsUnr) -> Self {
+        rrows.clone().map(
+            |typ| &*alloc.generic_arena.alloc((*typ).to_ast(alloc)),
+            |rrows| &*alloc.generic_arena.alloc((*rrows).to_ast(alloc)),
+        )
+    }
+}
+
 impl<'ast> FromMainline<'ast, term::Term> for &'ast Node<'ast> {
     fn from_mainline(alloc: &'ast AstAlloc, term: &term::Term) -> Self {
         use term::Term;
@@ -346,7 +412,7 @@ impl<'ast> FromMainline<'ast, term::Term> for &'ast Node<'ast> {
             }
             Term::Import { path, format } => alloc.import(path.clone(), *format),
             Term::ResolvedImport(_) => panic!("didn't expect a resolved import at parsing stage"),
-            Term::Type { typ, .. } => alloc.typ(typ.clone()),
+            Term::Type { typ, .. } => alloc.typ_move(typ.to_ast(alloc)),
             Term::CustomContract(_) => panic!("didn't expect a custom contract at parsing stage"),
             Term::ParseError(error) => alloc.parse_error(error.clone()),
             Term::RuntimeError(_) => panic!("didn't expect a runtime error at parsing stage"),
diff --git a/core/src/bytecode/ast/mod.rs b/core/src/bytecode/ast/mod.rs
index 8479ee31..69b54f12 100644
--- a/core/src/bytecode/ast/mod.rs
+++ b/core/src/bytecode/ast/mod.rs
@@ -14,9 +14,7 @@ use std::{ffi::OsString, rc};
 use pattern::Pattern;
 use record::Record;
 
-use crate::{
-    cache::InputFormat, error::ParseError, identifier::LocIdent, position::TermPos, typ::Type,
-};
+use crate::{cache::InputFormat, error::ParseError, identifier::LocIdent, position::TermPos};
 
 // For now, we reuse those types from the term module.
 pub use crate::term::{Number, StrChunk};
@@ -27,9 +25,11 @@ pub mod compat;
 pub mod pattern;
 pub mod primop;
 pub mod record;
+pub mod typ;
 
 use pattern::*;
 use primop::PrimOp;
+use typ::*;
 
 /// A Nickel AST. Contains a root node and a span.
 ///
@@ -146,7 +146,7 @@ pub enum Node<'ast> {
     /// A type in term position, such as in `let my_contract = Number -> Number in ...`.
     ///
     /// During evaluation, this will get turned into a contract.
-    Type(&'ast Type),
+    Type(&'ast Type<'ast>),
 
     /// A term that couldn't be parsed properly. Used by the LSP to handle partially valid
     /// programs.
@@ -190,10 +190,10 @@ pub struct Match<'ast> {
 #[derive(Debug, PartialEq, Clone, Default)]
 pub struct Annotation<'ast> {
     /// The type annotation (using `:`).
-    pub typ: Option<Type>,
+    pub typ: Option<Type<'ast>>,
 
     /// The contract annotations (using `|`).
-    pub contracts: &'ast [Type],
+    pub contracts: &'ast [Type<'ast>],
 }
 
 impl<'ast> Annotation<'ast> {
@@ -211,13 +211,14 @@ impl<'ast> Annotation<'ast> {
     /// Returns a string representation of the contracts (without the static type annotation) as a
     /// comma-separated list.
     pub fn contracts_to_string(&self) -> Option<String> {
-        (!self.contracts.is_empty()).then(|| {
-            self.contracts
-                .iter()
-                .map(|typ| format!("{typ}"))
-                .collect::<Vec<_>>()
-                .join(",")
-        })
+        todo!("requires pretty printing first")
+        //(!self.contracts.is_empty()).then(|| {
+        //    self.contracts
+        //        .iter()
+        //        .map(|typ| format!("{typ}"))
+        //        .collect::<Vec<_>>()
+        //        .join(",")
+        //})
     }
 
     /// Returns `true` if this annotation is empty, i.e. hold neither a type annotation nor
@@ -234,7 +235,7 @@ impl<'ast> Annotation<'ast> {
 /// The most popular choice for arena is the `bumpalo` crate, which is a fast bump allocator that
 /// can handle heterogeneous data. However, it doesn't support destructors, which is a problem
 /// because some of the nodes in the AST owns heap allocated data and needs to be de-allocated
-/// (`Number`, `Type` and parse errors currently).
+/// (numbers and parse errors currently).
 ///
 /// Another choice is `typed-arena` and derivatives, which do run destructors, but can only store
 /// one type of values. As the number of types that need to be dropped is relatively small, we use
@@ -242,7 +243,6 @@ impl<'ast> Annotation<'ast> {
 /// dropped.
 pub struct AstAlloc {
     generic_arena: Bump,
-    type_arena: typed_arena::Arena<Type>,
     number_arena: typed_arena::Arena<Number>,
     error_arena: typed_arena::Arena<ParseError>,
 }
@@ -252,7 +252,6 @@ impl AstAlloc {
     pub fn new() -> Self {
         Self {
             generic_arena: Bump::new(),
-            type_arena: typed_arena::Arena::new(),
             number_arena: typed_arena::Arena::new(),
             error_arena: typed_arena::Arena::new(),
         }
@@ -409,11 +408,39 @@ impl AstAlloc {
         self.generic_arena.alloc(Node::Import { path, format })
     }
 
-    pub fn typ(&self, typ: Type) -> &Node<'_> {
-        let typ = self.type_arena.alloc(typ);
+    pub fn typ<'ast>(&'ast self, typ: TypeUnr<'ast>, pos: TermPos) -> &'ast Node<'ast> {
+        let type_full = self.generic_arena.alloc(Type { typ, pos });
+        self.generic_arena.alloc(Node::Type(type_full))
+    }
+
+    /// As opposed to [Self::typ], this method takes an already constructed type and move it into
+    /// the arena, instead of taking each constituent separately.
+    pub fn typ_move<'ast>(&'ast self, typ: Type<'ast>) -> &'ast Node<'ast> {
+        let typ = self.generic_arena.alloc(typ);
         self.generic_arena.alloc(Node::Type(typ))
     }
 
+    pub fn typ_unr_move<'ast>(&'ast self, typ: TypeUnr<'ast>, pos: TermPos) -> &'ast Node<'ast> {
+        let type_full = self.generic_arena.alloc(Type { typ, pos });
+        self.generic_arena.alloc(Node::Type(type_full))
+    }
+
+    pub fn types<'ast, I>(&'ast self, types: I) -> &'ast [Type<'ast>]
+    where
+        I: IntoIterator<Item = Type<'ast>>,
+        I::IntoIter: ExactSizeIterator,
+    {
+        self.generic_arena.alloc_slice_fill_iter(types)
+    }
+
+    pub fn enum_rows<'ast>(&'ast self, erows: EnumRowsUnr<'ast>) -> &'ast EnumRows<'ast> {
+        self.generic_arena.alloc(EnumRows(erows))
+    }
+
+    pub fn record_rows<'ast>(&'ast self, rrows: RecordRowsUnr<'ast>) -> &'ast RecordRows<'ast> {
+        self.generic_arena.alloc(RecordRows(rrows))
+    }
+
     pub fn parse_error(&self, error: ParseError) -> &Node<'_> {
         let error = self.error_arena.alloc(error);
         self.generic_arena.alloc(Node::ParseError(error))
diff --git a/core/src/bytecode/ast/typ.rs b/core/src/bytecode/ast/typ.rs
new file mode 100644
index 00000000..bf6ee4d0
--- /dev/null
+++ b/core/src/bytecode/ast/typ.rs
@@ -0,0 +1,33 @@
+//! Representation of Nickel types in the AST.
+
+use super::{Ast, TermPos};
+use crate::typ as mainline_typ;
+pub use mainline_typ::{EnumRowF, EnumRowsF, RecordRowF, RecordRowsF, TypeF};
+
+/// The recursive unrolling of a type, that is when we "peel off" the top-level layer to find the actual
+/// structure represented by an instantiation of `TypeF`.
+pub type TypeUnr<'ast> = TypeF<&'ast Type<'ast>, RecordRows<'ast>, EnumRows<'ast>, Ast<'ast>>;
+
+/// The recursive unrolling of a enum rows.
+pub type EnumRowsUnr<'ast> = EnumRowsF<&'ast Type<'ast>, &'ast EnumRows<'ast>>;
+
+/// The recursive unrolling of record rows.
+pub type RecordRowsUnr<'ast> = RecordRowsF<&'ast Type<'ast>, &'ast RecordRows<'ast>>;
+
+/// Concrete, recursive definition for an enum row.
+pub type EnumRow<'ast> = EnumRowF<&'ast Type<'ast>>;
+/// Concrete, recursive definition for enum rows.
+#[derive(Clone, PartialEq, Debug)]
+pub struct EnumRows<'ast>(pub EnumRowsUnr<'ast>);
+/// Concrete, recursive definition for a record row.
+pub type RecordRow<'ast> = RecordRowF<&'ast Type<'ast>>;
+#[derive(Clone, PartialEq, Debug)]
+/// Concrete, recursive definition for record rows.
+pub struct RecordRows<'ast>(pub RecordRowsUnr<'ast>);
+
+/// Concrete, recursive type for a Nickel type.
+#[derive(Clone, PartialEq, Debug)]
+pub struct Type<'ast> {
+    pub typ: TypeUnr<'ast>,
+    pub pos: TermPos,
+}
diff --git a/core/src/error/mod.rs b/core/src/error/mod.rs
index 12d9af8a..6376af46 100644
--- a/core/src/error/mod.rs
+++ b/core/src/error/mod.rs
@@ -862,7 +862,7 @@ impl ParseError {
         )
     }
 
-    pub fn from_toml(error: toml::de::Error, file_id: FileId) -> Self {
+    pub fn from_toml(error: toml_edit::TomlError, file_id: FileId) -> Self {
         use codespan::{ByteIndex, ByteOffset};
 
         let span = error.span();
diff --git a/core/src/serialize.rs b/core/src/serialize.rs
index 0bbc92f6..663812ff 100644
--- a/core/src/serialize.rs
+++ b/core/src/serialize.rs
@@ -458,184 +458,117 @@ pub fn to_string(format: ExportFormat, rt: &RichTerm) -> Result<String, ExportEr
     Ok(String::from_utf8_lossy(&buffer).into_owned())
 }
 
-/// TOML deserialization wrappers. Depending on the `spanned-deser` feature being
-/// enabled, [::toml::from_str] will either simply call [toml::from_str] or a custom
-/// deserializer that preserves span information.
+/// We don't use serde to deserialize toml, because
+/// - this bug: <https://github.com/toml-rs/toml/issues/798>
+/// - the machinery for getting spans for toml+serde is more code than
+///   what we have below
+///
+/// Instead, we parse the toml using `toml-edit` and then convert from their
+/// representation to a `RichTerm`. Using `toml-edit` here doesn't introduce
+/// any new dependencies, because it's used by `toml` internally anyway.
 pub mod toml_deser {
-    use crate::term::RichTerm;
-    use codespan::FileId;
-
-    /// Utility module for spanned deserialization which preserves position information for some
-    /// parsers that support it. Although we haven't measured the effect, it's likely that
-    /// including span information affects performance and binary size (by pulling in alternate
-    /// parsers supporting spanned deserialization or using YAML parser for JSON deserialization),
-    /// so we gate this functionality behind the `spanned-deser` feature flag. It's still
-    /// enabled by default, being useful for error reporting, but can be disabled when building a
-    /// lightweight version of the interpreter e.g. to be embedded in a production environment.
-    #[cfg(feature = "spanned-deser")]
-    pub mod spanned {
-        use super::*;
-
-        use crate::{
-            identifier::LocIdent,
-            position::RawSpan,
-            term::{record::RecordData, string::NickelString, Number, Term},
-        };
-
-        use serde::{
-            de::{Deserializer, MapAccess},
-            Deserialize,
-        };
-        use serde_untagged::UntaggedEnumVisitor;
-
-        use toml::Spanned;
-
-        /// The inner data of a TOML value with span information. Similar to
-        /// `toml::Spanned<toml::Value>`, but where atoms use Nickel types instead (`NickelString`,
-        /// `Number`, etc)
-        #[derive(Debug, Clone)]
-        pub enum SpannedValueData {
-            String(NickelString),
-            Number(Number),
-            Boolean(bool),
-            Array(Vec<SpannedValue>),
-            /// Since we are going to iterate through the map and collect it in a new data
-            /// structure to fit Nickel's record representation anyway, we store maps as vectors of
-            /// entries instead of building an actual map.
-            Map(Vec<(Spanned<String>, SpannedValue)>),
-        }
-
-        /// A TOML value with span information.
-        ///
-        /// After some experimentation, it seems that in order to properly trigger spanned
-        /// deserialization in the `toml` crate, we need to write the deserializers for both
-        /// [SpannedValueData] and [SpannedValue] manually, although they are pretty mechanical and
-        /// should in principle be similar to what `derive(Deserialize)` would generate. However,
-        /// the latter doesn't work in practice (giving "data did not match any variant of untagged
-        /// enum" errors when importing TOML).
-        #[derive(Debug, Clone)]
-        pub struct SpannedValue(Spanned<SpannedValueData>);
-
-        impl<'de> Deserialize<'de> for SpannedValue {
-            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-            where
-                D: Deserializer<'de>,
-            {
-                let spanned: Spanned<SpannedValueData> = Deserialize::deserialize(deserializer)?;
-                Ok(SpannedValue(spanned))
+    use crate::{
+        identifier::LocIdent,
+        position::{RawSpan, TermPos},
+        term::{
+            array::ArrayAttrs,
+            record::{RecordAttrs, RecordData},
+            RichTerm, Term,
+        },
+    };
+    use codespan::{ByteIndex, FileId};
+    use malachite::{num::conversion::traits::ExactFrom as _, Rational};
+    use std::ops::Range;
+    use toml_edit::Value;
+
+    fn range_pos(range: Option<Range<usize>>, src_id: FileId) -> TermPos {
+        range.map_or(TermPos::None, |span| {
+            RawSpan {
+                src_id,
+                start: ByteIndex(span.start as u32),
+                end: ByteIndex(span.end as u32),
             }
-        }
-
-        impl<'de> Deserialize<'de> for SpannedValueData {
-            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-            where
-                D: Deserializer<'de>,
-            {
-                use malachite::num::basic::floats::PrimitiveFloat;
+            .into()
+        })
+    }
 
-                fn deser_int<I: Into<Number>, E>(n: I) -> Result<SpannedValueData, E> {
-                    Ok(SpannedValueData::Number(n.into()))
-                }
+    trait ToTerm {
+        fn to_term(&self, src_id: FileId) -> Term;
 
-                fn deser_float<F : PrimitiveFloat>(f: F) -> Result<SpannedValueData, serde_untagged::de::Error>
-                where Number: TryFrom<F, Error = malachite_q::conversion::from_primitive_float::RationalFromPrimitiveFloatError>{
-                    let as_number =
-                        crate::serialize::number_from_float::<F, serde_untagged::de::Error>(f)?;
-                    Ok(SpannedValueData::Number(as_number))
-                }
+        fn to_rich_term(&self, range: Option<Range<usize>>, src_id: FileId) -> RichTerm {
+            let pos = range_pos(range, src_id);
+            RichTerm::new(self.to_term(src_id), pos)
+        }
+    }
 
-                let data = UntaggedEnumVisitor::new()
-                    .string(|str| Ok(SpannedValueData::String(str.into())))
-                    .i8(deser_int)
-                    .i16(deser_int)
-                    .i32(deser_int)
-                    .i64(deser_int)
-                    .i128(deser_int)
-                    .u8(deser_int)
-                    .u16(deser_int)
-                    .u32(deser_int)
-                    .u64(deser_int)
-                    .u128(deser_int)
-                    .f32(deser_float)
-                    .f64(deser_float)
-                    .bool(|b| Ok(SpannedValueData::Boolean(b)))
-                    .seq(|seq| {
-                        let v: Result<Vec<SpannedValue>, serde_untagged::de::Error> =
-                            seq.deserialize();
-                        Ok(SpannedValueData::Array(v?))
-                    })
-                    .map(|mut map| {
-                        let mut result = if let Some(hint) = map.size_hint() {
-                            Vec::with_capacity(hint)
-                        } else {
-                            Vec::new()
-                        };
-
-                        while let Some((k, v)) =
-                            map.next_entry::<Spanned<String>, SpannedValue>()?
-                        {
-                            result.push((k, v));
-                        }
-
-                        Ok(SpannedValueData::Map(result))
+    impl ToTerm for toml_edit::Table {
+        fn to_term(&self, src_id: FileId) -> Term {
+            Term::Record(RecordData::new(
+                self.iter()
+                    .map(|(key, val)| {
+                        (
+                            LocIdent::new(key),
+                            val.to_rich_term(val.span(), src_id).into(),
+                        )
                     })
-                    .deserialize(deserializer)?;
-
-                Ok(data)
-            }
+                    .collect(),
+                RecordAttrs::default(),
+                None,
+            ))
         }
+    }
 
-        impl SpannedValue {
-            /// Take the id of the current file and convert a spanned value to a Nickel term with
-            /// position information properly set.
-            pub fn into_term(self, file_id: FileId) -> RichTerm {
-                let span = self.0.span();
-
-                let term = match self.0.into_inner() {
-                    SpannedValueData::String(s) => Term::Str(s),
-                    SpannedValueData::Number(n) => Term::Num(n),
-                    SpannedValueData::Boolean(b) => Term::Bool(b),
-                    SpannedValueData::Array(v) => Term::Array(
-                        v.into_iter().map(|v| v.into_term(file_id)).collect(),
-                        Default::default(),
-                    ),
-                    SpannedValueData::Map(m) => {
-                        let data = m
-                            .into_iter()
-                            .map(|(k, v)| {
-                                let key_span = k.span();
-                                let ident = LocIdent::new_with_pos(
-                                    k.into_inner(),
-                                    RawSpan::from_range(file_id, key_span).into(),
-                                );
-
-                                (ident, v.into_term(file_id).into())
-                            })
-                            .collect();
-
-                        Term::Record(RecordData {
-                            fields: data,
-                            ..Default::default()
+    impl ToTerm for toml_edit::Value {
+        fn to_term(&self, src_id: FileId) -> Term {
+            match self {
+                Value::String(s) => Term::Str(s.value().into()),
+                Value::Integer(i) => Term::Num((*i.value()).into()),
+                Value::Float(f) => Term::Num(Rational::exact_from(*f.value())),
+                Value::Boolean(b) => Term::Bool(*b.value()),
+                Value::Array(vs) => Term::Array(
+                    vs.iter()
+                        .map(|t| t.to_rich_term(t.span(), src_id))
+                        .collect(),
+                    ArrayAttrs::default(),
+                ),
+                Value::InlineTable(t) => Term::Record(RecordData::new(
+                    t.iter()
+                        .map(|(key, val)| {
+                            (
+                                LocIdent::new(key),
+                                val.to_rich_term(val.span(), src_id).into(),
+                            )
                         })
-                    }
-                };
+                        .collect(),
+                    RecordAttrs::default(),
+                    None,
+                )),
+                Value::Datetime(_) => todo!(),
+            }
+        }
+    }
 
-                RichTerm::new(term, RawSpan::from_range(file_id, span).into())
+    impl ToTerm for toml_edit::Item {
+        fn to_term(&self, src_id: FileId) -> Term {
+            match self {
+                toml_edit::Item::None => Term::Null,
+                toml_edit::Item::Table(t) => t.to_term(src_id),
+                toml_edit::Item::ArrayOfTables(ts) => Term::Array(
+                    ts.iter()
+                        .map(|t| t.to_rich_term(t.span(), src_id))
+                        .collect(),
+                    ArrayAttrs::default(),
+                ),
+                toml_edit::Item::Value(v) => v.to_term(src_id),
             }
         }
     }
 
     /// Deserialize a Nickel term with position information from a TOML source provided as a
     /// string and the file id of this source.
-    #[cfg(feature = "spanned-deser")]
-    pub fn from_str(s: &str, file_id: FileId) -> Result<RichTerm, toml::de::Error> {
-        let spanned: spanned::SpannedValue = toml::from_str(s)?;
-        Ok(spanned.into_term(file_id))
-    }
-
-    #[cfg(not(feature = "spanned-deser"))]
-    pub fn from_str(s: &str, _file_id: FileId) -> Result<RichTerm, toml::de::Error> {
-        toml::from_str(s)
+    pub fn from_str(s: &str, file_id: FileId) -> Result<RichTerm, toml_edit::TomlError> {
+        let doc: toml_edit::ImDocument<_> = s.parse()?;
+        Ok(doc.as_item().to_rich_term(doc.span(), file_id))
     }
 }
 
diff --git a/lsp/nls/Cargo.toml b/lsp/nls/Cargo.toml
index bde2b44b..1a556372 100644
--- a/lsp/nls/Cargo.toml
+++ b/lsp/nls/Cargo.toml
@@ -36,14 +36,6 @@ lazy_static.workspace = true
 log.workspace = true
 lsp-server.workspace = true
 lsp-types.workspace = true
-# We enable the `spanned-deser` feature, anticipating the fact that NLS can be
-# used to open standard JSON or YAML files with a convention to know which
-# contract they should follow, e.g. with an environment variable or just the
-# filesystem layout, in which case we will be able to show Nickel diagnostics
-# directly inside a TOML or a YAML file. It's not used as of yet, though.
-#
-# Actually, never mind. We temporarily disable spanned-deser because of
-# https://github.com/tweag/nickel/issues/2059
 nickel-lang-core = {workspace = true, default-features = false, features = ["format"]}
 pretty.workspace = true
 regex.workspace = true


Test patch:
diff --git a/cli/tests/snapshot/snapshots/snapshot__eval_stderr_spanned_toml.ncl.snap b/cli/tests/snapshot/snapshots/snapshot__eval_stderr_spanned_toml.ncl.snap
index d0628873..df0e816d 100644
--- a/cli/tests/snapshot/snapshots/snapshot__eval_stderr_spanned_toml.ncl.snap
+++ b/cli/tests/snapshot/snapshots/snapshot__eval_stderr_spanned_toml.ncl.snap
@@ -8,7 +8,7 @@ error: contract broken by the value of `name`
 5 │   package.name | String,
   │                  ------ expected type
   │
-  ┌─ <unknown> (generated by evaluation):1:1
+  ┌─ [IMPORTS_PATH]/package.toml:2:8
   │
-1 │ 5
-  │ - evaluated to this value
+2 │ name = 5
+  │        ^ applied to this expression
diff --git a/core/tests/integration/inputs/imports/imported/nested_table.toml b/core/tests/integration/inputs/imports/imported/nested_table.toml
new file mode 100644
index 00000000..934bfd11
--- /dev/null
+++ b/core/tests/integration/inputs/imports/imported/nested_table.toml
@@ -0,0 +1,2 @@
+[foo.bar]
+qux = 42
diff --git a/core/tests/integration/inputs/imports/nested_toml_table.ncl b/core/tests/integration/inputs/imports/nested_toml_table.ncl
new file mode 100644
index 00000000..10b4a98f
--- /dev/null
+++ b/core/tests/integration/inputs/imports/nested_toml_table.ncl
@@ -0,0 +1,4 @@
+# test.type = 'pass'
+#
+# # Regression test for #2059, where importing toml would fail on nested tables
+(import "imported/nested_table.toml").foo.bar.qux == 42