Repository: tweag/nickel
Pull request #2074 (2024-10-21 07:00:56)

Switch to toml-edit for spanned deserialization
This little experiment shows that we can use toml-edit for spanned parsing instead of the toml crate. It doesn't actually add any dependencies, since `toml` uses `toml_edit` for parsing anyway.

github-actions[bot]: <h2><a href="https://bencher.dev/perf/nickel/reports/5df5f0fe-a6c2-4204-a4b0-e9c04395b39f?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/2074-merge?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">2074/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=2bb88eb0-78e8-430a-ab64-4f23d46c95e8&heads=c00eb79d-4d3b-4705-b49e-50523c035aba&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=3cfaf19e-c9c5-4341-95cd-703456d0fc9f&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1726903342000&end_time=1729495342000&report=5df5f0fe-a6c2-4204-a4b0-e9c04395b39f&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>494,650.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=2bb88eb0-78e8-430a-ab64-4f23d46c95e8&heads=c00eb79d-4d3b-4705-b49e-50523c035aba&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=95048806-9bf2-49e4-88f2-dbcd66cbf68a&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1726903342000&end_time=1729495342000&report=5df5f0fe-a6c2-4204-a4b0-e9c04395b39f&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,727,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=2bb88eb0-78e8-430a-ab64-4f23d46c95e8&heads=c00eb79d-4d3b-4705-b49e-50523c035aba&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=15373c31-496e-4f43-bbb4-466ef49fdd87&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1726903342000&end_time=1729495342000&report=5df5f0fe-a6c2-4204-a4b0-e9c04395b39f&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,618,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=2bb88eb0-78e8-430a-ab64-4f23d46c95e8&heads=c00eb79d-4d3b-4705-b49e-50523c035aba&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=87746452-c4df-48ea-b5e9-8d66f82ca556&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1726903342000&end_time=1729495342000&report=5df5f0fe-a6c2-4204-a4b0-e9c04395b39f&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,010,900.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=2bb88eb0-78e8-430a-ab64-4f23d46c95e8&heads=c00eb79d-4d3b-4705-b49e-50523c035aba&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=7f141ea0-9fbb-4305-bb63-78f163e546d6&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1726903342000&end_time=1729495342000&report=5df5f0fe-a6c2-4204-a4b0-e9c04395b39f&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>61,636,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=2bb88eb0-78e8-430a-ab64-4f23d46c95e8&heads=c00eb79d-4d3b-4705-b49e-50523c035aba&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=8c154d79-dc68-4d93-abb3-83183a62f6a2&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1726903342000&end_time=1729495342000&report=5df5f0fe-a6c2-4204-a4b0-e9c04395b39f&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>44,175,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=2bb88eb0-78e8-430a-ab64-4f23d46c95e8&heads=c00eb79d-4d3b-4705-b49e-50523c035aba&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=6d0222f6-0b85-4eaf-8c5c-e8957c522ef9&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1726903342000&end_time=1729495342000&report=5df5f0fe-a6c2-4204-a4b0-e9c04395b39f&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,053,800.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=2bb88eb0-78e8-430a-ab64-4f23d46c95e8&heads=c00eb79d-4d3b-4705-b49e-50523c035aba&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=be0d3f87-d089-4b81-af44-3bbc6e831697&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1726903342000&end_time=1729495342000&report=5df5f0fe-a6c2-4204-a4b0-e9c04395b39f&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,305,300.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=2bb88eb0-78e8-430a-ab64-4f23d46c95e8&heads=c00eb79d-4d3b-4705-b49e-50523c035aba&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=2946df33-45e4-4282-b749-040582021fe3&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1726903342000&end_time=1729495342000&report=5df5f0fe-a6c2-4204-a4b0-e9c04395b39f&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>767,310.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=2bb88eb0-78e8-430a-ab64-4f23d46c95e8&heads=c00eb79d-4d3b-4705-b49e-50523c035aba&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=5be25c57-d46f-4b48-b6f0-dee01b84220a&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1726903342000&end_time=1729495342000&report=5df5f0fe-a6c2-4204-a4b0-e9c04395b39f&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,219,100.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=2bb88eb0-78e8-430a-ab64-4f23d46c95e8&heads=c00eb79d-4d3b-4705-b49e-50523c035aba&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=7178a975-b548-4e78-8c84-4f3802668825&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1726903342000&end_time=1729495342000&report=5df5f0fe-a6c2-4204-a4b0-e9c04395b39f&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,494,500.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=2bb88eb0-78e8-430a-ab64-4f23d46c95e8&heads=c00eb79d-4d3b-4705-b49e-50523c035aba&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=6884f292-8796-44a8-a30c-d3cb2ed987f6&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1726903342000&end_time=1729495342000&report=5df5f0fe-a6c2-4204-a4b0-e9c04395b39f&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,162,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=2bb88eb0-78e8-430a-ab64-4f23d46c95e8&heads=c00eb79d-4d3b-4705-b49e-50523c035aba&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=2f5f7f6a-4e54-4542-aa09-da5671953fbb&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1726903342000&end_time=1729495342000&report=5df5f0fe-a6c2-4204-a4b0-e9c04395b39f&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>845,130.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=2bb88eb0-78e8-430a-ab64-4f23d46c95e8&heads=c00eb79d-4d3b-4705-b49e-50523c035aba&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=211bb66a-58f8-488b-97e4-7ea032870038&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1726903342000&end_time=1729495342000&report=5df5f0fe-a6c2-4204-a4b0-e9c04395b39f&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,543,500.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=2bb88eb0-78e8-430a-ab64-4f23d46c95e8&heads=c00eb79d-4d3b-4705-b49e-50523c035aba&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=3f1710c5-ce75-4dfb-aadb-1435fed8a093&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1726903342000&end_time=1729495342000&report=5df5f0fe-a6c2-4204-a4b0-e9c04395b39f&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>835,660.00</td></tr></tbody></table></details><a href="https://bencher.dev/perf/nickel/reports/5df5f0fe-a6c2-4204-a4b0-e9c04395b39f?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/2074-merge/ubuntu-latest/rust_criterion"></div>
jneem: > Purely stylistic nitpick: when I start to have 3 or more (xxx_to_yyy), I now don't hesitate to declare a local, private trait

Yes, that turned out nicely (and also removed the asymmetry between _to_term and _to_rich_term)

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/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