Repository: tweag/nickel
Pull request #2086 (2024-11-01 12:45:56)

Support warnings
This adds a bunch of infrastructure for warnings (and for reporting multiple errors, potentially), and also a warning for applying a naked function as a contract.

Most of the PR involves the threading of some extra global state through the program and the vm. I could see a few options:
1. pass the extra state as a parameter to lots of functions
2. pass in the extra state as something to be owned by the vm, and then allow it to be taken back out
3. add a lifetime parameter to the program and vm, so that a `&mut <global state>` can be passed in.

I went with the third option. The first option would require a lot of churn (and then potentially more churn if we want to change the type of the global state). The second option would require the global state to either be a concrete type (but I wanted different types in the cli and nls) or a type parameter. So then given that it looked like we'd want an additional generic parameter anyway, I went for the lifetime parameter of the third option.

github-actions[bot]: <h2><a href="https://bencher.dev/perf/nickel/reports/e155fcbb-dda0-4548-9a99-440903420bfa?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/2086-merge?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">2086/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=9428b261-bb9b-4a54-8468-decdd9c074bc&heads=9f4ea53f-7cfd-45dc-ae83-e58c2170ca37&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=3cfaf19e-c9c5-4341-95cd-703456d0fc9f&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727873906000&end_time=1730465906000&report=e155fcbb-dda0-4548-9a99-440903420bfa&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>487,540.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=9428b261-bb9b-4a54-8468-decdd9c074bc&heads=9f4ea53f-7cfd-45dc-ae83-e58c2170ca37&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=95048806-9bf2-49e4-88f2-dbcd66cbf68a&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727873906000&end_time=1730465906000&report=e155fcbb-dda0-4548-9a99-440903420bfa&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,700,700.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=9428b261-bb9b-4a54-8468-decdd9c074bc&heads=9f4ea53f-7cfd-45dc-ae83-e58c2170ca37&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=15373c31-496e-4f43-bbb4-466ef49fdd87&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727873906000&end_time=1730465906000&report=e155fcbb-dda0-4548-9a99-440903420bfa&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,771,300.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=9428b261-bb9b-4a54-8468-decdd9c074bc&heads=9f4ea53f-7cfd-45dc-ae83-e58c2170ca37&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=87746452-c4df-48ea-b5e9-8d66f82ca556&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727873906000&end_time=1730465906000&report=e155fcbb-dda0-4548-9a99-440903420bfa&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,083,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=9428b261-bb9b-4a54-8468-decdd9c074bc&heads=9f4ea53f-7cfd-45dc-ae83-e58c2170ca37&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=7f141ea0-9fbb-4305-bb63-78f163e546d6&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727873906000&end_time=1730465906000&report=e155fcbb-dda0-4548-9a99-440903420bfa&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>62,745,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=9428b261-bb9b-4a54-8468-decdd9c074bc&heads=9f4ea53f-7cfd-45dc-ae83-e58c2170ca37&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=8c154d79-dc68-4d93-abb3-83183a62f6a2&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727873906000&end_time=1730465906000&report=e155fcbb-dda0-4548-9a99-440903420bfa&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,942,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=9428b261-bb9b-4a54-8468-decdd9c074bc&heads=9f4ea53f-7cfd-45dc-ae83-e58c2170ca37&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=6d0222f6-0b85-4eaf-8c5c-e8957c522ef9&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727873906000&end_time=1730465906000&report=e155fcbb-dda0-4548-9a99-440903420bfa&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,120,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=9428b261-bb9b-4a54-8468-decdd9c074bc&heads=9f4ea53f-7cfd-45dc-ae83-e58c2170ca37&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=be0d3f87-d089-4b81-af44-3bbc6e831697&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727873906000&end_time=1730465906000&report=e155fcbb-dda0-4548-9a99-440903420bfa&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,390,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=9428b261-bb9b-4a54-8468-decdd9c074bc&heads=9f4ea53f-7cfd-45dc-ae83-e58c2170ca37&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=2946df33-45e4-4282-b749-040582021fe3&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727873906000&end_time=1730465906000&report=e155fcbb-dda0-4548-9a99-440903420bfa&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>768,710.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=9428b261-bb9b-4a54-8468-decdd9c074bc&heads=9f4ea53f-7cfd-45dc-ae83-e58c2170ca37&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=5be25c57-d46f-4b48-b6f0-dee01b84220a&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727873906000&end_time=1730465906000&report=e155fcbb-dda0-4548-9a99-440903420bfa&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,213,600.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=9428b261-bb9b-4a54-8468-decdd9c074bc&heads=9f4ea53f-7cfd-45dc-ae83-e58c2170ca37&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=7178a975-b548-4e78-8c84-4f3802668825&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727873906000&end_time=1730465906000&report=e155fcbb-dda0-4548-9a99-440903420bfa&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,200.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=9428b261-bb9b-4a54-8468-decdd9c074bc&heads=9f4ea53f-7cfd-45dc-ae83-e58c2170ca37&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=6884f292-8796-44a8-a30c-d3cb2ed987f6&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727873906000&end_time=1730465906000&report=e155fcbb-dda0-4548-9a99-440903420bfa&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,187,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=9428b261-bb9b-4a54-8468-decdd9c074bc&heads=9f4ea53f-7cfd-45dc-ae83-e58c2170ca37&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=2f5f7f6a-4e54-4542-aa09-da5671953fbb&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727873906000&end_time=1730465906000&report=e155fcbb-dda0-4548-9a99-440903420bfa&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>829,500.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=9428b261-bb9b-4a54-8468-decdd9c074bc&heads=9f4ea53f-7cfd-45dc-ae83-e58c2170ca37&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=211bb66a-58f8-488b-97e4-7ea032870038&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727873906000&end_time=1730465906000&report=e155fcbb-dda0-4548-9a99-440903420bfa&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,525,200.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=9428b261-bb9b-4a54-8468-decdd9c074bc&heads=9f4ea53f-7cfd-45dc-ae83-e58c2170ca37&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=3f1710c5-ce75-4dfb-aadb-1435fed8a093&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727873906000&end_time=1730465906000&report=e155fcbb-dda0-4548-9a99-440903420bfa&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>826,000.00</td></tr></tbody></table></details><a href="https://bencher.dev/perf/nickel/reports/e155fcbb-dda0-4548-9a99-440903420bfa?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/2086-merge/ubuntu-latest/rust_criterion"></div>
yannham: I haven't looked at it in details yet, but why won't something like `Vec<Box<dyn IntoDiagnostic>>` work for 2. ? Do you need to access the actual structure of the data? As a first impression, I'm not too keen on adding yet another lifetime  parameter to `Vm` and `Program` just for warnings if it can be avoided (and as long as it doesn't impact performance on the happy path, but this shouldn't).
jneem: Fair enough, I'll explore option 2 a little. I didn't want to tie the implementation to a vec of diagnostics, because I want to allow for the possibility that some diagnostics handlers will want to issue diagnostics immediately while evaluation continues.

I do have another secret motivation for wanting to try out a lifetime parameter on `Vm`, which is to allow a `Cache` that isn't owned by the vm, so that it can be re-used.
yannham: > Fair enough, I'll explore option 2 a little. I didn't want to tie the implementation to a vec of diagnostics, because I want to allow for the possibility that some diagnostics handlers will want to issue diagnostics immediately while evaluation continues.

By issuing you mean that you might want to print a diagnostic immediately, instead of accumulating and only at the end printing them? In that case I think you can still keep the `Renderer` idea. But as this isn't expected to be called a lot or to matter that much for performance, I would put it under a `Box<dyn _>` instead of carrying a lifetime parameter everywhere. In fact, it's a separate subject but that's probably what we should havedone as well for the `ImportResolver` parameter, because the number of times `resolve` is called doesn't justify the pollution of the `VirtualMachine` type with `R: ImportResolver` everywhere,  IMHO.

> I do have another secret motivation for wanting to try out a lifetime parameter on Vm, which is to allow a Cache that isn't owned by the vm, so that it can be re-used.

That's a fair motivation, but probably slightly orthogonal (at least in the `Box<dyn _>` vs lifetime parameter debate). Also, would those lifetime be necessarily  the same ? (I guess the lifetime is in fact a way of speaking about `'self` in some sense?)
github-actions[bot]: <h2><a href="https://bencher.dev/perf/nickel/reports/5fc155eb-ac37-4114-a142-e621c4e71c8a?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/error-files?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">error-files</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><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=2da4d14d-e458-40ef-a04a-9bd7676a90d6&heads=b0117bb2-776e-4b3a-b047-30c06935a1b5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=3cfaf19e-c9c5-4341-95cd-703456d0fc9f&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1728883540000&end_time=1731475540000&report=5fc155eb-ac37-4114-a142-e621c4e71c8a&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>🚷 <a href="https://bencher.dev/perf/nickel/thresholds/2d2f42a7-20e9-4cc7-8339-0c1b4b982a59?model=097f8f23-3a40-4e82-b95e-07a29cda8bdf&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view threshold</a></td><td>484,190.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=2da4d14d-e458-40ef-a04a-9bd7676a90d6&heads=b0117bb2-776e-4b3a-b047-30c06935a1b5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=95048806-9bf2-49e4-88f2-dbcd66cbf68a&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1728883540000&end_time=1731475540000&report=5fc155eb-ac37-4114-a142-e621c4e71c8a&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>🚷 <a href="https://bencher.dev/perf/nickel/thresholds/2d2f42a7-20e9-4cc7-8339-0c1b4b982a59?model=097f8f23-3a40-4e82-b95e-07a29cda8bdf&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view threshold</a></td><td>1,653,400.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=2da4d14d-e458-40ef-a04a-9bd7676a90d6&heads=b0117bb2-776e-4b3a-b047-30c06935a1b5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=15373c31-496e-4f43-bbb4-466ef49fdd87&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1728883540000&end_time=1731475540000&report=5fc155eb-ac37-4114-a142-e621c4e71c8a&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>🚷 <a href="https://bencher.dev/perf/nickel/thresholds/2d2f42a7-20e9-4cc7-8339-0c1b4b982a59?model=097f8f23-3a40-4e82-b95e-07a29cda8bdf&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view threshold</a></td><td>6,634,000.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=2da4d14d-e458-40ef-a04a-9bd7676a90d6&heads=b0117bb2-776e-4b3a-b047-30c06935a1b5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=87746452-c4df-48ea-b5e9-8d66f82ca556&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1728883540000&end_time=1731475540000&report=5fc155eb-ac37-4114-a142-e621c4e71c8a&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>🚷 <a href="https://bencher.dev/perf/nickel/thresholds/2d2f42a7-20e9-4cc7-8339-0c1b4b982a59?model=097f8f23-3a40-4e82-b95e-07a29cda8bdf&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view threshold</a></td><td>7,083,000.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=2da4d14d-e458-40ef-a04a-9bd7676a90d6&heads=b0117bb2-776e-4b3a-b047-30c06935a1b5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=7f141ea0-9fbb-4305-bb63-78f163e546d6&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1728883540000&end_time=1731475540000&report=5fc155eb-ac37-4114-a142-e621c4e71c8a&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>🚷 <a href="https://bencher.dev/perf/nickel/thresholds/2d2f42a7-20e9-4cc7-8339-0c1b4b982a59?model=097f8f23-3a40-4e82-b95e-07a29cda8bdf&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view threshold</a></td><td>61,699,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=2da4d14d-e458-40ef-a04a-9bd7676a90d6&heads=b0117bb2-776e-4b3a-b047-30c06935a1b5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=8c154d79-dc68-4d93-abb3-83183a62f6a2&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1728883540000&end_time=1731475540000&report=5fc155eb-ac37-4114-a142-e621c4e71c8a&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>🚷 <a href="https://bencher.dev/perf/nickel/thresholds/2d2f42a7-20e9-4cc7-8339-0c1b4b982a59?model=097f8f23-3a40-4e82-b95e-07a29cda8bdf&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view threshold</a></td><td>41,272,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=2da4d14d-e458-40ef-a04a-9bd7676a90d6&heads=b0117bb2-776e-4b3a-b047-30c06935a1b5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=6d0222f6-0b85-4eaf-8c5c-e8957c522ef9&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1728883540000&end_time=1731475540000&report=5fc155eb-ac37-4114-a142-e621c4e71c8a&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>🚷 <a href="https://bencher.dev/perf/nickel/thresholds/2d2f42a7-20e9-4cc7-8339-0c1b4b982a59?model=097f8f23-3a40-4e82-b95e-07a29cda8bdf&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view threshold</a></td><td>1,908,900.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=2da4d14d-e458-40ef-a04a-9bd7676a90d6&heads=b0117bb2-776e-4b3a-b047-30c06935a1b5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=be0d3f87-d089-4b81-af44-3bbc6e831697&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1728883540000&end_time=1731475540000&report=5fc155eb-ac37-4114-a142-e621c4e71c8a&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>🚷 <a href="https://bencher.dev/perf/nickel/thresholds/2d2f42a7-20e9-4cc7-8339-0c1b4b982a59?model=097f8f23-3a40-4e82-b95e-07a29cda8bdf&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view threshold</a></td><td>3,304,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=2da4d14d-e458-40ef-a04a-9bd7676a90d6&heads=b0117bb2-776e-4b3a-b047-30c06935a1b5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=2946df33-45e4-4282-b749-040582021fe3&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1728883540000&end_time=1731475540000&report=5fc155eb-ac37-4114-a142-e621c4e71c8a&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>🚷 <a href="https://bencher.dev/perf/nickel/thresholds/2d2f42a7-20e9-4cc7-8339-0c1b4b982a59?model=097f8f23-3a40-4e82-b95e-07a29cda8bdf&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view threshold</a></td><td>758,390.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=2da4d14d-e458-40ef-a04a-9bd7676a90d6&heads=b0117bb2-776e-4b3a-b047-30c06935a1b5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=5be25c57-d46f-4b48-b6f0-dee01b84220a&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1728883540000&end_time=1731475540000&report=5fc155eb-ac37-4114-a142-e621c4e71c8a&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>🚷 <a href="https://bencher.dev/perf/nickel/thresholds/2d2f42a7-20e9-4cc7-8339-0c1b4b982a59?model=097f8f23-3a40-4e82-b95e-07a29cda8bdf&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view threshold</a></td><td>3,158,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=2da4d14d-e458-40ef-a04a-9bd7676a90d6&heads=b0117bb2-776e-4b3a-b047-30c06935a1b5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=7178a975-b548-4e78-8c84-4f3802668825&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1728883540000&end_time=1731475540000&report=5fc155eb-ac37-4114-a142-e621c4e71c8a&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>🚷 <a href="https://bencher.dev/perf/nickel/thresholds/2d2f42a7-20e9-4cc7-8339-0c1b4b982a59?model=097f8f23-3a40-4e82-b95e-07a29cda8bdf&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view threshold</a></td><td>1,503,400.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=2da4d14d-e458-40ef-a04a-9bd7676a90d6&heads=b0117bb2-776e-4b3a-b047-30c06935a1b5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=6884f292-8796-44a8-a30c-d3cb2ed987f6&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1728883540000&end_time=1731475540000&report=5fc155eb-ac37-4114-a142-e621c4e71c8a&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>🚷 <a href="https://bencher.dev/perf/nickel/thresholds/2d2f42a7-20e9-4cc7-8339-0c1b4b982a59?model=097f8f23-3a40-4e82-b95e-07a29cda8bdf&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view threshold</a></td><td>10,017,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=2da4d14d-e458-40ef-a04a-9bd7676a90d6&heads=b0117bb2-776e-4b3a-b047-30c06935a1b5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=2f5f7f6a-4e54-4542-aa09-da5671953fbb&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1728883540000&end_time=1731475540000&report=5fc155eb-ac37-4114-a142-e621c4e71c8a&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>🚷 <a href="https://bencher.dev/perf/nickel/thresholds/2d2f42a7-20e9-4cc7-8339-0c1b4b982a59?model=097f8f23-3a40-4e82-b95e-07a29cda8bdf&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view threshold</a></td><td>822,570.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=2da4d14d-e458-40ef-a04a-9bd7676a90d6&heads=b0117bb2-776e-4b3a-b047-30c06935a1b5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=211bb66a-58f8-488b-97e4-7ea032870038&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1728883540000&end_time=1731475540000&report=5fc155eb-ac37-4114-a142-e621c4e71c8a&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>🚷 <a href="https://bencher.dev/perf/nickel/thresholds/2d2f42a7-20e9-4cc7-8339-0c1b4b982a59?model=097f8f23-3a40-4e82-b95e-07a29cda8bdf&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view threshold</a></td><td>1,519,600.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=2da4d14d-e458-40ef-a04a-9bd7676a90d6&heads=b0117bb2-776e-4b3a-b047-30c06935a1b5&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=3f1710c5-ce75-4dfb-aadb-1435fed8a093&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1728883540000&end_time=1731475540000&report=5fc155eb-ac37-4114-a142-e621c4e71c8a&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>🚷 <a href="https://bencher.dev/perf/nickel/thresholds/2d2f42a7-20e9-4cc7-8339-0c1b4b982a59?model=097f8f23-3a40-4e82-b95e-07a29cda8bdf&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view threshold</a></td><td>805,790.00</td></tr></tbody></table></details><a href="https://bencher.dev/perf/nickel/reports/5fc155eb-ac37-4114-a142-e621c4e71c8a?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/error-files/ubuntu-latest/rust_criterion"></div>
jneem: > That's a fair motivation, but probably slightly orthogonal (at least in the Box<dyn _> vs lifetime parameter debate). Also, would those lifetime be necessarily the same ? (I guess the lifetime is in fact a way of speaking about 'self in some sense?)

I think we could get by with a single lifetime parameter, but I guess that's something to look into later anyway.

> But as this isn't expected to be called a lot or to matter that much for performance, I would put it under a Box<dyn _> instead of carrying a lifetime parameter everywhere.

The previous iteration did in fact use a `Box<dyn Reporter>`, but it had a lifetime (i.e. `Box<dyn Reporter + 'ctx>`) because it was internally using a mutable reference to push errors into something owned by something longer-lived than the `Program`. The latest version removes the lifetime, and instead uses an mpsc channel to deliver the errors. It feels a little strange because I usually think of channels as involving threads, but it actually worked pretty well I think. (And it's encapsulated, so no one else has to think about the channels)

jneem: Thanks! I'm glad the refactors made sense, because it was starting to feel like I lost track of the original goal...

> Various flags to control warnings (I suppose if you run this version of Nickel on a large existing code base that use naked contracts you'll get a full load of warnings). Like -Werror and to disable them at least?

Maybe it makes sense to bikeshed some sort of global nickel config? There's a long discussion [here](https://github.com/clap-rs/clap/discussions/2763) about how to integrate layered config into clap, but it appears there's no resolution yet. (I didn't read the whole thing, just scrolled to the end)

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 9fac09ed..0693d5ad 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -17,6 +17,12 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
 
+[[package]]
+name = "adler2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+
 [[package]]
 name = "ahash"
 version = "0.8.11"
@@ -39,6 +45,27 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "allocator-api2"
+version = "0.2.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9"
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "anes"
 version = "0.1.6"
@@ -107,6 +134,9 @@ name = "anyhow"
 version = "1.0.80"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
+dependencies = [
+ "backtrace",
+]
 
 [[package]]
 name = "arbitrary"
@@ -126,6 +156,12 @@ dependencies = [
  "arbitrary",
 ]
 
+[[package]]
+name = "arc-swap"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
+
 [[package]]
 name = "archery"
 version = "1.2.1"
@@ -177,6 +213,12 @@ version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9"
 
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
 [[package]]
 name = "atty"
 version = "0.2.14"
@@ -204,7 +246,7 @@ dependencies = [
  "cc",
  "cfg-if",
  "libc",
- "miniz_oxide",
+ "miniz_oxide 0.7.2",
  "object",
  "rustc-demangle",
 ]
@@ -215,6 +257,12 @@ version = "0.21.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
 
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
 [[package]]
 name = "beef"
 version = "0.5.2"
@@ -256,6 +304,9 @@ name = "bitflags"
 version = "2.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "bitmaps"
@@ -295,6 +346,24 @@ version = "1.14.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f"
 
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
+
+[[package]]
+name = "bytesize"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc"
+
 [[package]]
 name = "cast"
 version = "0.3.0"
@@ -316,6 +385,25 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "chrono"
+version = "0.4.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "num-traits",
+ "serde",
+ "windows-targets 0.52.4",
+]
+
 [[package]]
 name = "ciborium"
 version = "0.2.2"
@@ -394,9 +482,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47"
 dependencies = [
  "heck",
- "proc-macro2 1.0.78",
+ "proc-macro2 1.0.89",
  "quote 1.0.35",
- "syn 2.0.52",
+ "syn 2.0.87",
 ]
 
 [[package]]
@@ -425,6 +513,12 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "clru"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbd0f76e066e64fdc5631e3bb46381254deab9ef1158292f27c8c57e3bf3fe59"
+
 [[package]]
 name = "codespan"
 version = "0.11.1"
@@ -499,6 +593,22 @@ dependencies = [
  "crossterm",
 ]
 
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
 [[package]]
 name = "cpp_demangle"
 version = "0.4.3"
@@ -727,10 +837,10 @@ dependencies = [
  "cc",
  "codespan-reporting",
  "once_cell",
- "proc-macro2 1.0.78",
+ "proc-macro2 1.0.89",
  "quote 1.0.35",
  "scratch",
- "syn 2.0.52",
+ "syn 2.0.87",
 ]
 
 [[package]]
@@ -745,9 +855,9 @@ version = "1.0.119"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "70b5b86cf65fa0626d85720619d80b288013477a91a0389fa8bc716bf4903ad1"
 dependencies = [
- "proc-macro2 1.0.78",
+ "proc-macro2 1.0.89",
  "quote 1.0.35",
- "syn 2.0.52",
+ "syn 2.0.87",
 ]
 
 [[package]]
@@ -768,10 +878,10 @@ checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120"
 dependencies = [
  "fnv",
  "ident_case",
- "proc-macro2 1.0.78",
+ "proc-macro2 1.0.89",
  "quote 1.0.35",
  "strsim 0.11.1",
- "syn 2.0.52",
+ "syn 2.0.87",
 ]
 
 [[package]]
@@ -782,7 +892,21 @@ checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178"
 dependencies = [
  "darling_core",
  "quote 1.0.35",
- "syn 2.0.52",
+ "syn 2.0.87",
+]
+
+[[package]]
+name = "dashmap"
+version = "6.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+ "hashbrown 0.14.3",
+ "lock_api",
+ "once_cell",
+ "parking_lot_core",
 ]
 
 [[package]]
@@ -801,6 +925,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
 dependencies = [
  "powerfmt",
+ "serde",
 ]
 
 [[package]]
@@ -809,9 +934,9 @@ version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
 dependencies = [
- "proc-macro2 1.0.78",
+ "proc-macro2 1.0.89",
  "quote 1.0.35",
- "syn 2.0.52",
+ "syn 2.0.87",
 ]
 
 [[package]]
@@ -830,9 +955,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d"
 dependencies = [
  "darling",
- "proc-macro2 1.0.78",
+ "proc-macro2 1.0.89",
  "quote 1.0.35",
- "syn 2.0.52",
+ "syn 2.0.87",
 ]
 
 [[package]]
@@ -842,7 +967,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b"
 dependencies = [
  "derive_builder_core",
- "syn 2.0.52",
+ "syn 2.0.87",
 ]
 
 [[package]]
@@ -852,7 +977,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
 dependencies = [
  "convert_case",
- "proc-macro2 1.0.78",
+ "proc-macro2 1.0.89",
  "quote 1.0.35",
  "rustc_version",
  "syn 1.0.109",
@@ -927,12 +1052,29 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2 1.0.89",
+ "quote 1.0.35",
+ "syn 2.0.87",
+]
+
 [[package]]
 name = "doc-comment"
 version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
 
+[[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
 [[package]]
 name = "either"
 version = "1.10.0"
@@ -954,6 +1096,15 @@ version = "0.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
 
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
 [[package]]
 name = "endian-type"
 version = "0.1.2"
@@ -1015,11 +1166,20 @@ dependencies = [
  "regex",
 ]
 
+[[package]]
+name = "faster-hex"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "fastrand"
-version = "2.0.1"
+version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
+checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4"
 
 [[package]]
 name = "fd-lock"
@@ -1032,6 +1192,18 @@ dependencies = [
  "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "filetime"
+version = "0.2.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "findshlibs"
 version = "0.10.2"
@@ -1052,12 +1224,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
 
 [[package]]
 name = "flate2"
-version = "1.0.28"
+version = "1.0.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"
+checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
 dependencies = [
  "crc32fast",
- "miniz_oxide",
+ "miniz_oxide 0.8.0",
 ]
 
 [[package]]
@@ -1129,9 +1301,9 @@ version = "0.3.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
 dependencies = [
- "proc-macro2 1.0.78",
+ "proc-macro2 1.0.89",
  "quote 1.0.35",
- "syn 2.0.52",
+ "syn 2.0.87",
 ]
 
 [[package]]
@@ -1181,8 +1353,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
 dependencies = [
  "cfg-if",
+ "js-sys",
  "libc",
  "wasi",
+ "wasm-bindgen",
 ]
 
 [[package]]
@@ -1206,156 +1380,1360 @@ version = "0.3.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0"
 dependencies = [
- "proc-macro2 1.0.78",
+ "proc-macro2 1.0.89",
  "quote 1.0.35",
- "syn 2.0.52",
+ "syn 2.0.87",
+]
+
+[[package]]
+name = "gix"
+version = "0.67.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7d3e78ddac368d3e3bfbc2862bc2aafa3d89f1b15fed898d9761e1ec6f3f17f"
+dependencies = [
+ "gix-actor",
+ "gix-archive",
+ "gix-attributes",
+ "gix-command",
+ "gix-commitgraph",
+ "gix-config",
+ "gix-credentials",
+ "gix-date",
+ "gix-diff",
+ "gix-dir",
+ "gix-discover",
+ "gix-features",
+ "gix-filter",
+ "gix-fs",
+ "gix-glob",
+ "gix-hash",
+ "gix-hashtable",
+ "gix-ignore",
+ "gix-index",
+ "gix-lock",
+ "gix-mailmap",
+ "gix-merge",
+ "gix-negotiate",
+ "gix-object",
+ "gix-odb",
+ "gix-pack",
+ "gix-path",
+ "gix-pathspec",
+ "gix-prompt",
+ "gix-protocol",
+ "gix-ref",
+ "gix-refspec",
+ "gix-revision",
+ "gix-revwalk",
+ "gix-sec",
+ "gix-status",
+ "gix-submodule",
+ "gix-tempfile",
+ "gix-trace",
+ "gix-transport",
+ "gix-traverse",
+ "gix-url",
+ "gix-utils",
+ "gix-validate",
+ "gix-worktree",
+ "gix-worktree-state",
+ "gix-worktree-stream",
+ "once_cell",
+ "parking_lot",
+ "regex",
+ "serde",
+ "signal-hook",
+ "smallvec",
+ "thiserror 1.0.69",
 ]
 
 [[package]]
-name = "glob"
-version = "0.3.1"
+name = "gix-actor"
+version = "0.33.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
+checksum = "59226ef06661c756e664b46b1d3b2c198f6adc5407a484c086d0171108a70027"
+dependencies = [
+ "bstr",
+ "gix-date",
+ "gix-utils",
+ "itoa",
+ "serde",
+ "thiserror 1.0.69",
+ "winnow",
+]
 
 [[package]]
-name = "half"
-version = "2.4.0"
+name = "gix-archive"
+version = "0.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e"
+checksum = "c4771188ac63c8f597042bd86561ad66cb1b5f795f963f6e5f71fc4f04853126"
 dependencies = [
- "cfg-if",
- "crunchy",
+ "bstr",
+ "gix-date",
+ "gix-object",
+ "gix-worktree-stream",
+ "jiff",
+ "thiserror 1.0.69",
 ]
 
 [[package]]
-name = "hashbrown"
-version = "0.12.3"
+name = "gix-attributes"
+version = "0.23.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+checksum = "31a102d201ef0e5a848458a82292581e7641e52f0f52e693b6cbdd05a652c029"
+dependencies = [
+ "bstr",
+ "gix-glob",
+ "gix-path",
+ "gix-quote",
+ "gix-trace",
+ "kstring",
+ "serde",
+ "smallvec",
+ "thiserror 1.0.69",
+ "unicode-bom",
+]
 
 [[package]]
-name = "hashbrown"
-version = "0.13.1"
+name = "gix-bitmap"
+version = "0.2.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33ff8ae62cd3a9102e5637afc8452c55acf3844001bd5374e0b0bd7b6616c038"
+checksum = "10f78312288bd02052be5dbc2ecbc342c9f4eb791986d86c0a5c06b92dc72efa"
 dependencies = [
- "ahash",
+ "thiserror 1.0.69",
 ]
 
 [[package]]
-name = "hashbrown"
-version = "0.14.3"
+name = "gix-chunk"
+version = "0.4.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
+checksum = "6c28b58ba04f0c004722344390af9dbc85888fbb84be1981afb934da4114d4cf"
 dependencies = [
- "ahash",
+ "thiserror 1.0.69",
 ]
 
 [[package]]
-name = "heck"
-version = "0.4.1"
+name = "gix-command"
+version = "0.3.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+checksum = "c201d2b9e9cce2365a6638fd0a966f751ed92d74be5c0727ac331e6a29ef5846"
+dependencies = [
+ "bstr",
+ "gix-path",
+ "gix-trace",
+ "shell-words",
+]
 
 [[package]]
-name = "hermit-abi"
-version = "0.1.19"
+name = "gix-commitgraph"
+version = "0.25.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+checksum = "41db900b189e62dc61575f06fdf1a3b6901d264a99be9d32b286af6b2e3984e1"
 dependencies = [
- "libc",
+ "bstr",
+ "gix-chunk",
+ "gix-features",
+ "gix-hash",
+ "memmap2 0.9.5",
+ "serde",
+ "thiserror 1.0.69",
 ]
 
 [[package]]
-name = "hermit-abi"
-version = "0.3.9"
+name = "gix-config"
+version = "0.41.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
+checksum = "0bedd1bf1c7b994be9d57207e8e0de79016c05e2e8701d3015da906e65ac445e"
+dependencies = [
+ "bstr",
+ "gix-config-value",
+ "gix-features",
+ "gix-glob",
+ "gix-path",
+ "gix-ref",
+ "gix-sec",
+ "memchr",
+ "once_cell",
+ "smallvec",
+ "thiserror 1.0.69",
+ "unicode-bom",
+ "winnow",
+]
 
 [[package]]
-name = "humantime"
-version = "2.1.0"
+name = "gix-config-value"
+version = "0.14.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+checksum = "f3de3fdca9c75fa4b83a76583d265fa49b1de6b088ebcd210749c24ceeb74660"
+dependencies = [
+ "bitflags 2.4.2",
+ "bstr",
+ "gix-path",
+ "libc",
+ "thiserror 1.0.69",
+]
 
 [[package]]
-name = "ident_case"
-version = "1.0.1"
+name = "gix-credentials"
+version = "0.25.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+checksum = "d713bac4bf7df5801012285366dae6625d675baec4ba6e443d64e83559bec068"
+dependencies = [
+ "bstr",
+ "gix-command",
+ "gix-config-value",
+ "gix-path",
+ "gix-prompt",
+ "gix-sec",
+ "gix-trace",
+ "gix-url",
+ "serde",
+ "thiserror 1.0.69",
+]
 
 [[package]]
-name = "idna"
-version = "0.5.0"
+name = "gix-date"
+version = "0.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+checksum = "d10d543ac13c97292a15e8e8b7889cd006faf739777437ed95362504b8fe81a0"
 dependencies = [
- "unicode-bidi",
- "unicode-normalization",
+ "bstr",
+ "itoa",
+ "jiff",
+ "serde",
+ "thiserror 1.0.69",
 ]
 
 [[package]]
-name = "imbl-sized-chunks"
-version = "0.1.2"
+name = "gix-diff"
+version = "0.47.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "144006fb58ed787dcae3f54575ff4349755b00ccc99f4b4873860b654be1ed63"
+checksum = "c9850fd0c15af113db6f9e130d13091ba0d3754e570a2afdff9e2f3043da260e"
 dependencies = [
- "bitmaps",
+ "bstr",
+ "gix-command",
+ "gix-filter",
+ "gix-fs",
+ "gix-hash",
+ "gix-object",
+ "gix-path",
+ "gix-tempfile",
+ "gix-trace",
+ "gix-traverse",
+ "gix-worktree",
+ "imara-diff",
+ "thiserror 1.0.69",
 ]
 
 [[package]]
-name = "indexmap"
-version = "1.9.3"
+name = "gix-dir"
+version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+checksum = "bbf6c29bf17baf3996d4925fad5e10c1a12eac9b3a0d8475d89292e0e5ba34a3"
 dependencies = [
- "autocfg",
- "hashbrown 0.12.3",
- "serde",
+ "bstr",
+ "gix-discover",
+ "gix-fs",
+ "gix-ignore",
+ "gix-index",
+ "gix-object",
+ "gix-path",
+ "gix-pathspec",
+ "gix-trace",
+ "gix-utils",
+ "gix-worktree",
+ "thiserror 1.0.69",
 ]
 
 [[package]]
-name = "indexmap"
-version = "2.2.5"
+name = "gix-discover"
+version = "0.36.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4"
+checksum = "c522e31f458f50af09dfb014e10873c5378f702f8049c96f508989aad59671f6"
 dependencies = [
- "equivalent",
- "hashbrown 0.14.3",
+ "bstr",
+ "dunce",
+ "gix-fs",
+ "gix-hash",
+ "gix-path",
+ "gix-ref",
+ "gix-sec",
+ "thiserror 1.0.69",
 ]
 
 [[package]]
-name = "indoc"
-version = "1.0.9"
+name = "gix-features"
+version = "0.39.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306"
+checksum = "8e0eb9efdf96c35c0bed7596d1bef2d4ce6360a1d09738001f9d3e402aa7ba3e"
+dependencies = [
+ "bytes",
+ "bytesize",
+ "crc32fast",
+ "crossbeam-channel",
+ "flate2",
+ "gix-hash",
+ "gix-trace",
+ "gix-utils",
+ "libc",
+ "once_cell",
+ "parking_lot",
+ "prodash",
+ "sha1_smol",
+ "thiserror 1.0.69",
+ "walkdir",
+]
 
 [[package]]
-name = "indoc"
-version = "2.0.4"
+name = "gix-filter"
+version = "0.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8"
+checksum = "6b37f82359a4485770ed8993ae715ced1bf674f2a63e45f5a0786d38310665ea"
+dependencies = [
+ "bstr",
+ "encoding_rs",
+ "gix-attributes",
+ "gix-command",
+ "gix-hash",
+ "gix-object",
+ "gix-packetline-blocking",
+ "gix-path",
+ "gix-quote",
+ "gix-trace",
+ "gix-utils",
+ "smallvec",
+ "thiserror 1.0.69",
+]
 
 [[package]]
-name = "inferno"
-version = "0.11.19"
+name = "gix-fs"
+version = "0.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "321f0f839cd44a4686e9504b0a62b4d69a50b62072144c71c68f5873c167b8d9"
+checksum = "34740384d8d763975858fa2c176b68652a6fcc09f616e24e3ce967b0d370e4d8"
 dependencies = [
- "ahash",
- "indexmap 2.2.5",
- "is-terminal",
- "itoa",
- "log",
- "num-format",
- "once_cell",
- "quick-xml 0.26.0",
- "rgb",
- "str_stack",
+ "fastrand",
+ "gix-features",
+ "gix-utils",
 ]
 
 [[package]]
-name = "insta"
+name = "gix-glob"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "254b5101cf7facc00d9b5ff564cf46302ca76695cca23d33bc958a707b6fc857"
+dependencies = [
+ "bitflags 2.4.2",
+ "bstr",
+ "gix-features",
+ "gix-path",
+ "serde",
+]
+
+[[package]]
+name = "gix-hash"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "952c3a29f1bc1007cc901abce7479943abfa42016db089de33d0a4fa3c85bfe8"
+dependencies = [
+ "faster-hex",
+ "serde",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gix-hashtable"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ef65b256631078ef733bc5530c4e6b1c2e7d5c2830b75d4e9034ab3997d18fe"
+dependencies = [
+ "gix-hash",
+ "hashbrown 0.14.3",
+ "parking_lot",
+]
+
+[[package]]
+name = "gix-ignore"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba55a9b582dc26a639875497615959a8127ac5c37b2426dc50f037fada33a4b7"
+dependencies = [
+ "bstr",
+ "gix-glob",
+ "gix-path",
+ "gix-trace",
+ "serde",
+ "unicode-bom",
+]
+
+[[package]]
+name = "gix-index"
+version = "0.36.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27619009ca1ea33fd885041273f5fa5a09163a5c1d22a913b28d7b985e66fe29"
+dependencies = [
+ "bitflags 2.4.2",
+ "bstr",
+ "filetime",
+ "fnv",
+ "gix-bitmap",
+ "gix-features",
+ "gix-fs",
+ "gix-hash",
+ "gix-lock",
+ "gix-object",
+ "gix-traverse",
+ "gix-utils",
+ "gix-validate",
+ "hashbrown 0.14.3",
+ "itoa",
+ "libc",
+ "memmap2 0.9.5",
+ "rustix",
+ "serde",
+ "smallvec",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gix-lock"
+version = "15.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5102acdf4acae2644e38dbbd18cdfba9597a218f7d85f810fe5430207e03c2de"
+dependencies = [
+ "gix-tempfile",
+ "gix-utils",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gix-mailmap"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27d6ac205276a817b6efec0fdb6c62861d01d71f344dc7e5ebe81ab2907ebd0a"
+dependencies = [
+ "bstr",
+ "gix-actor",
+ "gix-date",
+ "serde",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gix-merge"
+version = "0.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fef00c86d0a5a12d95fd73dfa30608882c1d2366c7ad8a27322283a2e6fa0048"
+dependencies = [
+ "bstr",
+ "gix-command",
+ "gix-filter",
+ "gix-fs",
+ "gix-hash",
+ "gix-object",
+ "gix-path",
+ "gix-quote",
+ "gix-tempfile",
+ "gix-trace",
+ "gix-worktree",
+ "imara-diff",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gix-negotiate"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "414806291838c3349ea939c6d840ff854f84cd29bd3dde8f904f60b0e5b7d0bd"
+dependencies = [
+ "bitflags 2.4.2",
+ "gix-commitgraph",
+ "gix-date",
+ "gix-hash",
+ "gix-object",
+ "gix-revwalk",
+ "smallvec",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gix-object"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a77b6e7753d298553d9ae8b1744924481e7a49170983938bb578dccfbc6fc1a"
+dependencies = [
+ "bstr",
+ "gix-actor",
+ "gix-date",
+ "gix-features",
+ "gix-hash",
+ "gix-hashtable",
+ "gix-utils",
+ "gix-validate",
+ "itoa",
+ "serde",
+ "smallvec",
+ "thiserror 1.0.69",
+ "winnow",
+]
+
+[[package]]
+name = "gix-odb"
+version = "0.64.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bb86aadf7f1b2f980601b4fc94309706f9700f8008f935dc512d556c9e60f61"
+dependencies = [
+ "arc-swap",
+ "gix-date",
+ "gix-features",
+ "gix-fs",
+ "gix-hash",
+ "gix-hashtable",
+ "gix-object",
+ "gix-pack",
+ "gix-path",
+ "gix-quote",
+ "parking_lot",
+ "serde",
+ "tempfile",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gix-pack"
+version = "0.54.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "363e6e59a855ba243672408139db68e2478126cdcfeabb420777df4a1f20026b"
+dependencies = [
+ "clru",
+ "gix-chunk",
+ "gix-features",
+ "gix-hash",
+ "gix-hashtable",
+ "gix-object",
+ "gix-path",
+ "gix-tempfile",
+ "memmap2 0.9.5",
+ "parking_lot",
+ "serde",
+ "smallvec",
+ "thiserror 1.0.69",
+ "uluru",
+]
+
+[[package]]
+name = "gix-packetline"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f14a110eb16e27b4ebdae4ca8b389df3ad637d3020077e6b606b1d078745b65"
+dependencies = [
+ "bstr",
+ "faster-hex",
+ "gix-trace",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gix-packetline-blocking"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "decace940e8ba8e29d29b73b843a6cbae67503887f3e5fb7e688d0f4f6ee0757"
+dependencies = [
+ "bstr",
+ "faster-hex",
+ "gix-trace",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gix-path"
+version = "0.10.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c04e5a94fdb56b1e91eb7df2658ad16832428b8eeda24ff1a0f0288de2bce554"
+dependencies = [
+ "bstr",
+ "gix-trace",
+ "home",
+ "once_cell",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gix-pathspec"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70f02bf7625dbf15bf9fedbeace2ac1ce1c5177806bdbc24c441d664c75c00e4"
+dependencies = [
+ "bitflags 2.4.2",
+ "bstr",
+ "gix-attributes",
+ "gix-config-value",
+ "gix-glob",
+ "gix-path",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gix-prompt"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57944bbdb87f7a9893907032276e99ff4eba3640d8db1bdfb1eba8c07edfd006"
+dependencies = [
+ "gix-command",
+ "gix-config-value",
+ "parking_lot",
+ "rustix",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gix-protocol"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac4ebf25f20ac6055728eaa80951acf2cf83948a64af6565b98e7d42b1ab6691"
+dependencies = [
+ "bstr",
+ "gix-credentials",
+ "gix-date",
+ "gix-features",
+ "gix-hash",
+ "gix-transport",
+ "gix-utils",
+ "maybe-async",
+ "serde",
+ "thiserror 1.0.69",
+ "winnow",
+]
+
+[[package]]
+name = "gix-quote"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f89f9a1525dcfd9639e282ea939f5ab0d09d93cf2b90c1fc6104f1b9582a8e49"
+dependencies = [
+ "bstr",
+ "gix-utils",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gix-ref"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a47385e71fa2d9da8c35e642ef4648808ddf0a52bc93425879088c706dfeaea2"
+dependencies = [
+ "gix-actor",
+ "gix-features",
+ "gix-fs",
+ "gix-hash",
+ "gix-lock",
+ "gix-object",
+ "gix-path",
+ "gix-tempfile",
+ "gix-utils",
+ "gix-validate",
+ "memmap2 0.9.5",
+ "serde",
+ "thiserror 1.0.69",
+ "winnow",
+]
+
+[[package]]
+name = "gix-refspec"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0022038a09d80d9abf773be8efcbb502868d97f6972b8633bfb52ab6edaac442"
+dependencies = [
+ "bstr",
+ "gix-hash",
+ "gix-revision",
+ "gix-validate",
+ "smallvec",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gix-revision"
+version = "0.30.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ee8eb4088fece3562af4a5d751e069f90e93345524ad730512185234c4b55f1"
+dependencies = [
+ "bitflags 2.4.2",
+ "bstr",
+ "gix-commitgraph",
+ "gix-date",
+ "gix-hash",
+ "gix-hashtable",
+ "gix-object",
+ "gix-revwalk",
+ "gix-trace",
+ "serde",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gix-revwalk"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c9a9496da98d36ff19063a8576bf09a87425583b709a56dc5594fffa9d39b2"
+dependencies = [
+ "gix-commitgraph",
+ "gix-date",
+ "gix-hash",
+ "gix-hashtable",
+ "gix-object",
+ "smallvec",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gix-sec"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2007538eda296445c07949cf04f4a767307d887184d6b3e83e2d636533ddc6e"
+dependencies = [
+ "bitflags 2.4.2",
+ "gix-path",
+ "libc",
+ "serde",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "gix-status"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57414886e750161b4c86d8bca6b2d15bcc87f37ddc46684bb05cebbd29390543"
+dependencies = [
+ "bstr",
+ "filetime",
+ "gix-diff",
+ "gix-dir",
+ "gix-features",
+ "gix-filter",
+ "gix-fs",
+ "gix-hash",
+ "gix-index",
+ "gix-object",
+ "gix-path",
+ "gix-pathspec",
+ "gix-worktree",
+ "portable-atomic",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gix-submodule"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ed099621873cd36c580fc822176a32a7e50fef15a5c2ed81aaa087296f0497a"
+dependencies = [
+ "bstr",
+ "gix-config",
+ "gix-path",
+ "gix-pathspec",
+ "gix-refspec",
+ "gix-url",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gix-tempfile"
+version = "15.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2feb86ef094cc77a4a9a5afbfe5de626897351bbbd0de3cb9314baf3049adb82"
+dependencies = [
+ "dashmap",
+ "gix-fs",
+ "libc",
+ "once_cell",
+ "parking_lot",
+ "signal-hook",
+ "signal-hook-registry",
+ "tempfile",
+]
+
+[[package]]
+name = "gix-trace"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04bdde120c29f1fc23a24d3e115aeeea3d60d8e65bab92cc5f9d90d9302eb952"
+
+[[package]]
+name = "gix-transport"
+version = "0.43.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c485a345f41b8c0256cb86e95ed93e0692d203fd6c769b0433f7352c13608ad"
+dependencies = [
+ "base64 0.22.1",
+ "bstr",
+ "gix-command",
+ "gix-credentials",
+ "gix-features",
+ "gix-packetline",
+ "gix-quote",
+ "gix-sec",
+ "gix-url",
+ "reqwest",
+ "serde",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gix-traverse"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f20f1b13cc4fa6ba92b24e6aa0c2fb6a34beb4458ef88c6300212db504e818df"
+dependencies = [
+ "bitflags 2.4.2",
+ "gix-commitgraph",
+ "gix-date",
+ "gix-hash",
+ "gix-hashtable",
+ "gix-object",
+ "gix-revwalk",
+ "smallvec",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gix-url"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33e7c297c3265015c133a2c02199610b6e1373a09dc4be057d0c1b5285737f06"
+dependencies = [
+ "bstr",
+ "gix-features",
+ "gix-path",
+ "serde",
+ "thiserror 1.0.69",
+ "url",
+]
+
+[[package]]
+name = "gix-utils"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba427e3e9599508ed98a6ddf8ed05493db114564e338e41f6a996d2e4790335f"
+dependencies = [
+ "bstr",
+ "fastrand",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "gix-validate"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e187b263461bc36cea17650141567753bc6207d036cedd1de6e81a52f277ff68"
+dependencies = [
+ "bstr",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gix-worktree"
+version = "0.37.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d345e5b523550fe4fa0e912bf957de752011ccfc87451968fda1b624318f29c"
+dependencies = [
+ "bstr",
+ "gix-attributes",
+ "gix-features",
+ "gix-fs",
+ "gix-glob",
+ "gix-hash",
+ "gix-ignore",
+ "gix-index",
+ "gix-object",
+ "gix-path",
+ "gix-validate",
+ "serde",
+]
+
+[[package]]
+name = "gix-worktree-state"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e72b00e02f3bd737caae9c20a98e70749f42ae18c8f0b68aac3210b42a0b8da"
+dependencies = [
+ "bstr",
+ "gix-features",
+ "gix-filter",
+ "gix-fs",
+ "gix-glob",
+ "gix-hash",
+ "gix-index",
+ "gix-object",
+ "gix-path",
+ "gix-worktree",
+ "io-close",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gix-worktree-stream"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81d351819e81b97e4d5657db097fad8f91a5b2ec6d7151b2dae9c3e6dc642a66"
+dependencies = [
+ "gix-attributes",
+ "gix-features",
+ "gix-filter",
+ "gix-fs",
+ "gix-hash",
+ "gix-object",
+ "gix-path",
+ "gix-traverse",
+ "parking_lot",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
+
+[[package]]
+name = "h2"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap 2.2.5",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "half"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e"
+dependencies = [
+ "cfg-if",
+ "crunchy",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hashbrown"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ff8ae62cd3a9102e5637afc8452c55acf3844001bd5374e0b0bd7b6616c038"
+dependencies = [
+ "ahash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
+dependencies = [
+ "ahash",
+ "allocator-api2",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "home"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "http"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
+dependencies = [
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946"
+
+[[package]]
+name = "human_format"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c3b1f728c459d27b12448862017b96ad4767b1ec2ec5e6434e99f1577f085b8"
+
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
+name = "hyper"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
+dependencies = [
+ "futures-util",
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+ "webpki-roots",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "icu_collections"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locid"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locid_transform"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
+dependencies = [
+ "displaydoc",
+ "icu_locid",
+ "icu_locid_transform_data",
+ "icu_provider",
+ "tinystr",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locid_transform_data"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
+
+[[package]]
+name = "icu_normalizer"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "utf16_iter",
+ "utf8_iter",
+ "write16",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
+
+[[package]]
+name = "icu_properties"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locid_transform",
+ "icu_properties_data",
+ "icu_provider",
+ "tinystr",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
+
+[[package]]
+name = "icu_provider"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
+dependencies = [
+ "displaydoc",
+ "icu_locid",
+ "icu_provider_macros",
+ "stable_deref_trait",
+ "tinystr",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_provider_macros"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
+dependencies = [
+ "proc-macro2 1.0.89",
+ "quote 1.0.35",
+ "syn 2.0.87",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "imara-diff"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc9da1a252bd44cd341657203722352efc9bc0c847d06ea6d2dc1cd1135e0a01"
+dependencies = [
+ "ahash",
+ "hashbrown 0.14.3",
+]
+
+[[package]]
+name = "imbl-sized-chunks"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "144006fb58ed787dcae3f54575ff4349755b00ccc99f4b4873860b654be1ed63"
+dependencies = [
+ "bitmaps",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+ "serde",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.14.3",
+ "serde",
+]
+
+[[package]]
+name = "indoc"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306"
+
+[[package]]
+name = "indoc"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8"
+
+[[package]]
+name = "inferno"
+version = "0.11.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "321f0f839cd44a4686e9504b0a62b4d69a50b62072144c71c68f5873c167b8d9"
+dependencies = [
+ "ahash",
+ "indexmap 2.2.5",
+ "is-terminal",
+ "itoa",
+ "log",
+ "num-format",
+ "once_cell",
+ "quick-xml 0.26.0",
+ "rgb",
+ "str_stack",
+]
+
+[[package]]
+name = "insta"
 version = "1.36.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0a7c22c4d34ef4788c351e971c52bfdfe7ea2766f8c5466bc175dd46e52ac22e"
@@ -1368,6 +2746,22 @@ dependencies = [
  "yaml-rust",
 ]
 
+[[package]]
+name = "io-close"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cadcf447f06744f8ce713d2d6239bb5bde2c357a452397a9ed90c625da390bc"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708"
+
 [[package]]
 name = "is-terminal"
 version = "0.4.12"
@@ -1403,6 +2797,31 @@ version = "1.0.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
 
+[[package]]
+name = "jiff"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d9d414fc817d3e3d62b2598616733f76c4cc74fbac96069674739b881295c8"
+dependencies = [
+ "jiff-tzdb-platform",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "jiff-tzdb"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91335e575850c5c4c673b9bd467b0e025f164ca59d0564f69d0c2ee0ffad4653"
+
+[[package]]
+name = "jiff-tzdb-platform"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9835f0060a626fe59f160437bc725491a6af23133ea906500027d1bd2f8f4329"
+dependencies = [
+ "jiff-tzdb",
+]
+
 [[package]]
 name = "js-sys"
 version = "0.3.68"
@@ -1412,6 +2831,16 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "kstring"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1"
+dependencies = [
+ "serde",
+ "static_assertions",
+]
+
 [[package]]
 name = "lalrpop"
 version = "0.20.2"
@@ -1451,9 +2880,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
 [[package]]
 name = "libc"
-version = "0.2.153"
+version = "0.2.162"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
+checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
 
 [[package]]
 name = "libm"
@@ -1502,6 +2931,12 @@ version = "0.4.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
 
+[[package]]
+name = "litemap"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704"
+
 [[package]]
 name = "lock_api"
 version = "0.4.11"
@@ -1535,7 +2970,7 @@ checksum = "a1d849148dbaf9661a6151d1ca82b13bb4c4c128146a88d05253b38d4e2f496c"
 dependencies = [
  "beef",
  "fnv",
- "proc-macro2 1.0.78",
+ "proc-macro2 1.0.89",
  "quote 1.0.35",
  "regex-syntax 0.6.29",
  "syn 1.0.109",
@@ -1654,6 +3089,17 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "maybe-async"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11"
+dependencies = [
+ "proc-macro2 1.0.89",
+ "quote 1.0.35",
+ "syn 2.0.87",
+]
+
 [[package]]
 name = "md-5"
 version = "0.10.6"
@@ -1679,6 +3125,15 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "memmap2"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "memoffset"
 version = "0.6.5"
@@ -1705,9 +3160,9 @@ version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "38b4faf00617defe497754acde3024865bc143d44a86799b24e191ecff91354f"
 dependencies = [
- "proc-macro2 1.0.78",
+ "proc-macro2 1.0.89",
  "quote 1.0.35",
- "syn 2.0.52",
+ "syn 2.0.87",
 ]
 
 [[package]]
@@ -1729,6 +3184,12 @@ dependencies = [
  "sketches-ddsketch",
 ]
 
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
 [[package]]
 name = "minimad"
 version = "0.12.0"
@@ -1747,6 +3208,15 @@ dependencies = [
  "adler",
 ]
 
+[[package]]
+name = "miniz_oxide"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
+dependencies = [
+ "adler2",
+]
+
 [[package]]
 name = "mio"
 version = "0.8.11"
@@ -1855,6 +3325,19 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "nickel-lang-git"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "clap 4.5.2",
+ "gix",
+ "serde",
+ "serde_with",
+ "tempfile",
+ "thiserror 1.0.69",
+]
+
 [[package]]
 name = "nickel-lang-lsp"
 version = "1.9.0"
@@ -1890,7 +3373,7 @@ dependencies = [
  "serde",
  "serde_json",
  "test-generator",
- "thiserror",
+ "thiserror 1.0.69",
 ]
 
 [[package]]
@@ -2115,7 +3598,7 @@ version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e5699cc8a63d1aa2b1ee8e12b9ad70ac790d65788cd36101fa37f87ea46c4cef"
 dependencies = [
- "base64",
+ "base64 0.21.7",
  "indexmap 2.2.5",
  "line-wrap",
  "quick-xml 0.31.0",
@@ -2182,7 +3665,16 @@ dependencies = [
  "smallvec",
  "symbolic-demangle",
  "tempfile",
- "thiserror",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
+dependencies = [
+ "zerocopy",
 ]
 
 [[package]]
@@ -2261,13 +3753,25 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.78"
+version = "1.0.89"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
+checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
 dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "prodash"
+version = "29.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a266d8d6020c61a437be704c5e618037588e1985c7dbb7bf8d265db84cffe325"
+dependencies = [
+ "bytesize",
+ "human_format",
+ "log",
+ "parking_lot",
+]
+
 [[package]]
 name = "pyckel"
 version = "1.9.0"
@@ -2321,7 +3825,7 @@ version = "0.17.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "94144a1266e236b1c932682136dc35a9dee8d3589728f68130c7c3861ef96b28"
 dependencies = [
- "proc-macro2 1.0.78",
+ "proc-macro2 1.0.89",
  "pyo3-macros-backend",
  "quote 1.0.35",
  "syn 1.0.109",
@@ -2333,7 +3837,7 @@ version = "0.17.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c8df9be978a2d2f0cdebabb03206ed73b11314701a5bfe71b0d753b81997777f"
 dependencies = [
- "proc-macro2 1.0.78",
+ "proc-macro2 1.0.89",
  "quote 1.0.35",
  "syn 1.0.109",
 ]
@@ -2372,6 +3876,58 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "quinn"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef"
+dependencies = [
+ "bytes",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls",
+ "socket2",
+ "thiserror 2.0.3",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d"
+dependencies = [
+ "bytes",
+ "getrandom",
+ "rand",
+ "ring",
+ "rustc-hash",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror 2.0.3",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d5a626c6807713b15cac82a6acaccd6043c9a5408c24baae07611fec3f243da"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2",
+ "tracing",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "quote"
 version = "0.6.13"
@@ -2387,7 +3943,7 @@ version = "1.0.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
 dependencies = [
- "proc-macro2 1.0.78",
+ "proc-macro2 1.0.89",
 ]
 
 [[package]]
@@ -2400,6 +3956,36 @@ dependencies = [
  "nibble_vec",
 ]
 
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
 [[package]]
 name = "raw-cpuid"
 version = "10.7.0"
@@ -2446,7 +4032,7 @@ checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4"
 dependencies = [
  "getrandom",
  "libredox",
- "thiserror",
+ "thiserror 1.0.69",
 ]
 
 [[package]]
@@ -2484,6 +4070,52 @@ version = "0.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
 
+[[package]]
+name = "reqwest"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "encoding_rs",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-pemfile",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "system-configuration",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "webpki-roots",
+ "winreg",
+]
+
 [[package]]
 name = "rgb"
 version = "0.8.37"
@@ -2493,41 +4125,105 @@ dependencies = [
  "bytemuck",
 ]
 
+[[package]]
+name = "ring"
+version = "0.17.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom",
+ "libc",
+ "spin",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "rpds"
 version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a0e15515d3ce3313324d842629ea4905c25a13f81953eadb88f85516f59290a4"
 dependencies = [
- "archery",
+ "archery",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
+
+[[package]]
+name = "rustc-hash"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "0.38.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
+dependencies = [
+ "bitflags 2.4.2",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e"
+dependencies = [
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
 ]
 
 [[package]]
-name = "rustc-demangle"
-version = "0.1.23"
+name = "rustls-pemfile"
+version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
+checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
+dependencies = [
+ "rustls-pki-types",
+]
 
 [[package]]
-name = "rustc_version"
-version = "0.4.0"
+name = "rustls-pki-types"
+version = "1.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
+checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b"
 dependencies = [
- "semver",
+ "web-time",
 ]
 
 [[package]]
-name = "rustix"
-version = "0.38.31"
+name = "rustls-webpki"
+version = "0.102.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
+checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
 dependencies = [
- "bitflags 2.4.2",
- "errno",
- "libc",
- "linux-raw-sys",
- "windows-sys 0.52.0",
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
 ]
 
 [[package]]
@@ -2565,7 +4261,7 @@ version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8218eaf5d960e3c478a1b0f129fa888dd3d8d22eb3de097e9af14c1ab4438024"
 dependencies = [
- "proc-macro2 1.0.78",
+ "proc-macro2 1.0.89",
  "quote 1.0.35",
  "syn 1.0.109",
 ]
@@ -2635,9 +4331,9 @@ version = "1.0.197"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
 dependencies = [
- "proc-macro2 1.0.78",
+ "proc-macro2 1.0.89",
  "quote 1.0.35",
- "syn 2.0.52",
+ "syn 2.0.87",
 ]
 
 [[package]]
@@ -2657,9 +4353,9 @@ version = "0.1.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb"
 dependencies = [
- "proc-macro2 1.0.78",
+ "proc-macro2 1.0.89",
  "quote 1.0.35",
- "syn 2.0.52",
+ "syn 2.0.87",
 ]
 
 [[package]]
@@ -2671,6 +4367,48 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_with"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817"
+dependencies = [
+ "base64 0.22.1",
+ "chrono",
+ "hex",
+ "indexmap 1.9.3",
+ "indexmap 2.2.5",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "serde_with_macros",
+ "time",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d"
+dependencies = [
+ "darling",
+ "proc-macro2 1.0.89",
+ "quote 1.0.35",
+ "syn 2.0.87",
+]
+
 [[package]]
 name = "serde_yaml"
 version = "0.9.32"
@@ -2695,6 +4433,12 @@ dependencies = [
  "digest",
 ]
 
+[[package]]
+name = "sha1_smol"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
+
 [[package]]
 name = "sha2"
 version = "0.10.8"
@@ -2796,6 +4540,25 @@ name = "smallvec"
 version = "1.13.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "socket2"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
 
 [[package]]
 name = "stable_deref_trait"
@@ -2803,6 +4566,12 @@ version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
 
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
 [[package]]
 name = "str-buf"
 version = "1.0.6"
@@ -2849,6 +4618,12 @@ version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
 
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
 [[package]]
 name = "symbolic-common"
 version = "10.2.1"
@@ -2856,7 +4631,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1b55cdc318ede251d0957f07afe5fed912119b8c1bc5a7804151826db999e737"
 dependencies = [
  "debugid",
- "memmap2",
+ "memmap2 0.5.10",
  "stable_deref_trait",
  "uuid",
 ]
@@ -2889,22 +4664,39 @@ version = "1.0.109"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
 dependencies = [
- "proc-macro2 1.0.78",
+ "proc-macro2 1.0.89",
  "quote 1.0.35",
  "unicode-ident",
 ]
 
 [[package]]
 name = "syn"
-version = "2.0.52"
+version = "2.0.87"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07"
+checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
 dependencies = [
- "proc-macro2 1.0.78",
+ "proc-macro2 1.0.89",
  "quote 1.0.35",
  "unicode-ident",
 ]
 
+[[package]]
+name = "sync_wrapper"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
+
+[[package]]
+name = "synstructure"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
+dependencies = [
+ "proc-macro2 1.0.89",
+ "quote 1.0.35",
+ "syn 2.0.87",
+]
+
 [[package]]
 name = "syntect"
 version = "5.2.0"
@@ -2923,11 +4715,32 @@ dependencies = [
  "serde",
  "serde_derive",
  "serde_json",
- "thiserror",
+ "thiserror 1.0.69",
  "walkdir",
  "yaml-rust",
 ]
 
+[[package]]
+name = "system-configuration"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
 [[package]]
 name = "target-lexicon"
 version = "0.12.14"
@@ -2976,7 +4789,7 @@ dependencies = [
  "crossbeam",
  "crossterm",
  "minimad",
- "thiserror",
+ "thiserror 1.0.69",
  "unicode-width",
 ]
 
@@ -3016,29 +4829,49 @@ checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"
 
 [[package]]
 name = "thiserror"
-version = "1.0.57"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b"
+checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa"
 dependencies = [
- "thiserror-impl",
+ "thiserror-impl 2.0.3",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.57"
+version = "1.0.69"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
 dependencies = [
- "proc-macro2 1.0.78",
+ "proc-macro2 1.0.89",
  "quote 1.0.35",
- "syn 2.0.52",
+ "syn 2.0.87",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568"
+dependencies = [
+ "proc-macro2 1.0.89",
+ "quote 1.0.35",
+ "syn 2.0.87",
 ]
 
 [[package]]
 name = "time"
-version = "0.3.34"
+version = "0.3.36"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749"
+checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
 dependencies = [
  "deranged",
  "itoa",
@@ -3057,9 +4890,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
 
 [[package]]
 name = "time-macros"
-version = "0.2.17"
+version = "0.2.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774"
+checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
 dependencies = [
  "num-conv",
  "time-core",
@@ -3074,6 +4907,16 @@ dependencies = [
  "crunchy",
 ]
 
+[[package]]
+name = "tinystr"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
 [[package]]
 name = "tinytemplate"
 version = "1.2.1"
@@ -3106,8 +4949,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
 dependencies = [
  "backtrace",
+ "bytes",
+ "libc",
+ "mio",
  "pin-project-lite",
+ "socket2",
  "tokio-macros",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -3116,9 +4964,33 @@ version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
 dependencies = [
- "proc-macro2 1.0.78",
+ "proc-macro2 1.0.89",
  "quote 1.0.35",
- "syn 2.0.52",
+ "syn 2.0.87",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
+dependencies = [
+ "rustls",
+ "rustls-pki-types",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
 ]
 
 [[package]]
@@ -3204,6 +5076,31 @@ dependencies = [
  "web-sys",
 ]
 
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
+dependencies = [
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
+dependencies = [
+ "once_cell",
+]
+
 [[package]]
 name = "tree-sitter"
 version = "0.22.6"
@@ -3230,6 +5127,12 @@ version = "0.1.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e6631e42e10b40c0690bf92f404ebcfe6e1fdb480391d15f17cc8e96eeed5369"
 
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
 [[package]]
 name = "typed-arena"
 version = "2.0.2"
@@ -3243,10 +5146,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
 
 [[package]]
-name = "unicode-bidi"
-version = "0.3.15"
+name = "uluru"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c8a2469e56e6e5095c82ccd3afb98dad95f7af7929aab6d8ba8d6e0f73657da"
+dependencies = [
+ "arrayvec 0.7.4",
+]
+
+[[package]]
+name = "unicode-bom"
+version = "2.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
+checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217"
 
 [[package]]
 name = "unicode-ident"
@@ -3305,11 +5217,17 @@ version = "0.2.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b"
 
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
 [[package]]
 name = "url"
-version = "2.5.0"
+version = "2.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
+checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada"
 dependencies = [
  "form_urlencoded",
  "idna",
@@ -3317,6 +5235,18 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "utf16_iter"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
 [[package]]
 name = "utf8parse"
 version = "0.2.1"
@@ -3357,7 +5287,7 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff"
 dependencies = [
- "proc-macro2 1.0.78",
+ "proc-macro2 1.0.89",
  "quote 1.0.35",
 ]
 
@@ -3380,6 +5310,15 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
 [[package]]
 name = "wasi"
 version = "0.11.0+wasi-snapshot-preview1"
@@ -3407,9 +5346,9 @@ dependencies = [
  "bumpalo",
  "log",
  "once_cell",
- "proc-macro2 1.0.78",
+ "proc-macro2 1.0.89",
  "quote 1.0.35",
- "syn 2.0.52",
+ "syn 2.0.87",
  "wasm-bindgen-shared",
 ]
 
@@ -3441,9 +5380,9 @@ version = "0.2.91"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66"
 dependencies = [
- "proc-macro2 1.0.78",
+ "proc-macro2 1.0.89",
  "quote 1.0.35",
- "syn 2.0.52",
+ "syn 2.0.87",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
@@ -3464,6 +5403,25 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "0.26.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958"
+dependencies = [
+ "rustls-pki-types",
+]
+
 [[package]]
 name = "winapi"
 version = "0.3.9"
@@ -3495,6 +5453,15 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
+[[package]]
+name = "windows-core"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+dependencies = [
+ "windows-targets 0.52.4",
+]
+
 [[package]]
 name = "windows-sys"
 version = "0.48.0"
@@ -3629,13 +5596,35 @@ checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
 
 [[package]]
 name = "winnow"
-version = "0.6.11"
+version = "0.6.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56c52728401e1dc672a56e81e593e912aa54c78f40246869f78359a2bf24d29d"
+checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
 dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "winreg"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "write16"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
+
+[[package]]
+name = "writeable"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
+
 [[package]]
 name = "xdg"
 version = "2.5.2"
@@ -3657,12 +5646,37 @@ version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
 
+[[package]]
+name = "yoke"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5"
+dependencies = [
+ "serde",
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
+dependencies = [
+ "proc-macro2 1.0.89",
+ "quote 1.0.35",
+ "syn 2.0.87",
+ "synstructure",
+]
+
 [[package]]
 name = "zerocopy"
 version = "0.7.32"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
 dependencies = [
+ "byteorder",
  "zerocopy-derive",
 ]
 
@@ -3672,7 +5686,56 @@ version = "0.7.32"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
 dependencies = [
- "proc-macro2 1.0.78",
+ "proc-macro2 1.0.89",
+ "quote 1.0.35",
+ "syn 2.0.87",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
+dependencies = [
+ "proc-macro2 1.0.89",
+ "quote 1.0.35",
+ "syn 2.0.87",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
+
+[[package]]
+name = "zerovec"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
+dependencies = [
+ "proc-macro2 1.0.89",
  "quote 1.0.35",
- "syn 2.0.52",
+ "syn 2.0.87",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index 86259433..433cecb1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,6 +2,7 @@
 members = [
     "core",
     "cli",
+    "git",
     "vector",
     "lsp/nls",
     "lsp/lsp-harness",
@@ -23,6 +24,7 @@ readme = "README.md"
 
 [workspace.dependencies]
 nickel-lang-core = { version = "0.10.0", path = "./core", default-features = false }
+nickel-lang-git = { version = "0.1.0", path = "./git" }
 nickel-lang-vector = { version = "0.1.0", path = "./vector" }
 nickel-lang-utils = { version = "0.1.0", path = "./utils" }
 lsp-harness = { version = "0.1.0", path = "./lsp/lsp-harness" }
@@ -55,6 +57,7 @@ derive_more = "0.99"
 directories = "4.0.1"
 env_logger = "0.10"
 git-version = "0.3.5"
+gix = "0.67.0"
 indexmap = "1.9.3"
 indoc = "2"
 insta = "1.29.0"
@@ -84,6 +87,7 @@ serde = "1.0.164"
 serde_json = "1.0.96"
 serde_repr = "0.1"
 serde-wasm-bindgen = "0.5.0"
+serde_with = "3.11.0"
 serde_yaml = "0.9.19"
 sha-1 = "0.10.0"
 sha2 = "0.10.6"
diff --git a/cli/src/completions.rs b/cli/src/completions.rs
index f741bae8..7008dda0 100644
--- a/cli/src/completions.rs
+++ b/cli/src/completions.rs
@@ -1,7 +1,4 @@
-use crate::{
-    cli::{GlobalOptions, Options},
-    error::CliResult,
-};
+use crate::{cli::Options, global::GlobalContext};
 
 #[derive(clap::Parser, Debug)]
 pub struct GenCompletionsCommand {
@@ -10,13 +7,12 @@ pub struct GenCompletionsCommand {
 }
 
 impl GenCompletionsCommand {
-    pub fn run(self, _: GlobalOptions) -> CliResult<()> {
+    pub fn run(self, _: &mut GlobalContext) {
         clap_complete::generate(
             self.shell,
             &mut <Options as clap::CommandFactory>::command(),
             env!("CARGO_BIN_NAME"),
             &mut std::io::stdout(),
         );
-        Ok(())
     }
 }
diff --git a/cli/src/customize/mod.rs b/cli/src/customize/mod.rs
index 0c06a47d..7b20cf72 100644
--- a/cli/src/customize/mod.rs
+++ b/cli/src/customize/mod.rs
@@ -18,7 +18,10 @@ use nickel_lang_core::{
     typ::{RecordRowF, RecordRowsIteratorItem, Type, TypeF},
 };
 
-use crate::error::{CliResult, CliUsageError, Error, UnknownFieldData};
+use crate::{
+    error::{CliResult, CliUsageError, Error, UnknownFieldData},
+    input::{PrepareError, PrepareResult},
+};
 
 pub mod interface;
 
@@ -233,7 +236,7 @@ impl CustomizableFields {
 
 pub trait Customize {
     /// Customize the program with the given options.
-    fn customize(&self, program: Program<CBNCache>) -> CliResult<Program<CBNCache>>;
+    fn customize(&self, program: Program<CBNCache>) -> PrepareResult<Program<CBNCache>>;
     /// Return the value of the `field` argument, if specified and if supported by the current
     /// customize variant.
     fn field(&self) -> Option<&String> {
@@ -274,7 +277,12 @@ impl CustomizeOptions {
 
         let assignment_overrides = match assignment_overrides {
             Ok(assignment_overrides) => assignment_overrides,
-            Err(error) => return CliResult::Err(Error::CliUsage { error, program }),
+            Err(error) => {
+                return CliResult::Err(Error::CliUsage {
+                    error,
+                    files: program.files(),
+                })
+            }
         };
 
         let force_overrides: Result<Vec<_>, CliUsageError> = self
@@ -300,7 +308,12 @@ impl CustomizeOptions {
 
         let force_overrides = match force_overrides {
             Ok(force_overrides) => force_overrides,
-            Err(error) => return CliResult::Err(Error::CliUsage { error, program }),
+            Err(error) => {
+                return CliResult::Err(Error::CliUsage {
+                    error,
+                    files: program.files(),
+                })
+            }
         };
 
         program.add_overrides(assignment_overrides.into_iter().chain(force_overrides));
@@ -311,25 +324,26 @@ impl CustomizeOptions {
 impl CustomizeMode {
     // Contains most of the actual implementation of the customizing logic for overriding, but
     // doesn't set the extracted field.
-    fn customize_impl(&self, mut program: Program<CBNCache>) -> CliResult<Program<CBNCache>> {
+    fn customize_impl(&self, mut program: Program<CBNCache>) -> PrepareResult<Program<CBNCache>> {
         if self.customize_mode.is_empty() {
             return Ok(program);
         }
 
-        let evaled = match program.eval_record_spine() {
-            Ok(evaled) => evaled,
-            // We need a `return` control-flow to be able to take `program` out
-            Err(error) => return CliResult::Err(Error::Program { error, program }),
-        };
+        let evaled = program
+            .eval_record_spine()
+            .map_err(|error| Error::Program {
+                error,
+                files: program.files(),
+            })?;
 
         let customizable_fields = CustomizableFields::new(TermInterface::from(evaled.as_ref()));
         let opts = CustomizeOptions::parse_from(self.customize_mode.iter());
 
         match opts.command {
-            None => opts.do_customize(customizable_fields, program),
+            None => Ok(opts.do_customize(customizable_fields, program)?),
             Some(CustomizeCommand::List(list_command)) => {
                 list_command.run(&customizable_fields)?;
-                Err(Error::CustomizeInfoPrinted)
+                Err(PrepareError::EarlyReturn)
             }
         }
     }
@@ -339,7 +353,7 @@ impl Customize for CustomizeMode {
     // XXX: we should give a nice error message when someone tries to evaluate some
     //      expression that has unset values, telling them they can set them using
     //      this method
-    fn customize(&self, program: Program<CBNCache>) -> CliResult<Program<CBNCache>> {
+    fn customize(&self, program: Program<CBNCache>) -> PrepareResult<Program<CBNCache>> {
         program_with_field(
             self.customize_impl(program)?,
             self.extract_field.field.clone(),
@@ -363,7 +377,7 @@ pub struct ExtractFieldOnly {
 }
 
 impl Customize for ExtractFieldOnly {
-    fn customize(&self, program: Program<CBNCache>) -> CliResult<Program<CBNCache>> {
+    fn customize(&self, program: Program<CBNCache>) -> PrepareResult<Program<CBNCache>> {
         program_with_field(program, self.field.clone())
     }
 
@@ -377,7 +391,7 @@ impl Customize for ExtractFieldOnly {
 pub struct NoCustomizeMode;
 
 impl Customize for NoCustomizeMode {
-    fn customize(&self, program: Program<CBNCache>) -> CliResult<Program<CBNCache>> {
+    fn customize(&self, program: Program<CBNCache>) -> PrepareResult<Program<CBNCache>> {
         Ok(program)
     }
 }
@@ -385,18 +399,15 @@ impl Customize for NoCustomizeMode {
 fn program_with_field(
     mut program: Program<CBNCache>,
     field: Option<String>,
-) -> CliResult<Program<CBNCache>> {
+) -> PrepareResult<Program<CBNCache>> {
     if let Some(field) = field {
-        match program.parse_field_path(field) {
-            Ok(field_path) => program.field = field_path,
-            Err(error) => {
-                return Err(Error::CliUsage {
-                    error: CliUsageError::FieldPathParseError { error },
-                    program,
-                });
-            }
-        }
-    };
+        program.field = program
+            .parse_field_path(field)
+            .map_err(|error| Error::CliUsage {
+                error: CliUsageError::FieldPathParseError { error },
+                files: program.files(),
+            })?;
+    }
 
     Ok(program)
 }
diff --git a/cli/src/doc.rs b/cli/src/doc.rs
index 60f4a988..5204ba04 100644
--- a/cli/src/doc.rs
+++ b/cli/src/doc.rs
@@ -10,12 +10,7 @@ use nickel_lang_core::{
     program::Program,
 };
 
-use crate::{
-    cli::GlobalOptions,
-    customize::ExtractFieldOnly,
-    error::{CliResult, ResultErrorExt},
-    input::{InputOptions, Prepare},
-};
+use crate::{customize::ExtractFieldOnly, global::GlobalContext, input::InputOptions};
 
 #[derive(Copy, Clone, Eq, PartialEq, Debug, Default, clap::ValueEnum)]
 pub enum DocFormat {
@@ -63,12 +58,11 @@ pub struct DocCommand {
 const DEFAULT_OUT_DIR: &str = ".nickel/doc/";
 
 impl DocCommand {
-    pub fn run(self, global: GlobalOptions) -> CliResult<()> {
-        let mut program = self.input.prepare(&global)?;
-        self.export_doc(&mut program).report_with_program(program)
+    pub fn run(self, ctxt: &mut GlobalContext) {
+        ctxt.with_program(&self.input, |prog| self.export_doc(prog));
     }
 
-    fn export_doc(self, program: &mut Program<CacheImpl>) -> Result<(), Error> {
+    fn export_doc(&self, program: &mut Program<CacheImpl>) -> Result<(), Error> {
         let doc = program.extract_doc()?;
 
         let (mut out, out_path): (Box<dyn std::io::Write>, Option<Cow<'_, str>>) = if self.stdout {
diff --git a/cli/src/error.rs b/cli/src/error.rs
index f2ec8e57..7642d639 100644
--- a/cli/src/error.rs
+++ b/cli/src/error.rs
@@ -2,12 +2,11 @@
 
 use nickel_lang_core::{
     error::{
-        report::{ColorOpt, ErrorFormat},
+        report::{report, ColorOpt, ErrorFormat},
         Diagnostic, IntoDiagnostics, ParseError,
     },
-    eval::cache::lazy::CBNCache,
     files::{FileId, Files},
-    program::{FieldOverride, FieldPath, Program},
+    program::{FieldOverride, FieldPath},
 };
 
 /// Data about an unknown field error.
@@ -34,7 +33,7 @@ pub enum CliUsageError {
 
 pub enum Error {
     Program {
-        program: Program<CBNCache>,
+        files: Files,
         error: nickel_lang_core::error::Error,
     },
     Io {
@@ -51,15 +50,9 @@ pub enum Error {
     /// An invalid invocation of the CLI that couldn't be caught by the simple parsing provided by
     /// clap.
     CliUsage {
-        program: Program<CBNCache>,
+        files: Files,
         error: CliUsageError,
     },
-    /// Not an actual failure but a special early return to indicate that information was printed
-    /// during the usage of the customize mode, because a subcommand such as `list`, `show`, etc.
-    /// was used, and thus no customized program can be returned.
-    ///
-    /// Upon receiving this error, the caller should simply exit without proceeding with evaluation.
-    CustomizeInfoPrinted,
     FailedTests,
 }
 
@@ -153,29 +146,75 @@ impl IntoDiagnostics for CliUsageError {
     }
 }
 
+#[derive(Clone, Debug)]
 /// Warning emitted by the CLI.
 pub enum Warning {
     /// The user queried a program without providing any path. In this case,
     /// querying won't show most information, and it's most probably not
     /// what the user wanted.
     EmptyQueryPath,
+
+    /// A warning caused by doing something with a `Program` (e.g. parsing,
+    /// evaluating, ...). The inner warning can reference source file locations,
+    /// so it needs to be packaged with a files collection.
+    Program {
+        files: Files,
+        warning: nickel_lang_core::error::Warning,
+    },
+}
+
+impl Eq for Warning {}
+
+// This impl only compares the content of the warnings and ignores the `Files`
+// database for obvious performance reasons. Since this is only used for
+// deduplicating warnings, the impact of a false answer is limited.
+impl PartialEq for Warning {
+    fn eq(&self, other: &Self) -> bool {
+        match (self, other) {
+            (Warning::EmptyQueryPath, Warning::EmptyQueryPath) => true,
+            (Warning::Program { warning: a, .. }, Warning::Program { warning: b, .. }) => a == b,
+            _ => false,
+        }
+    }
+}
+
+impl std::hash::Hash for Warning {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        std::mem::discriminant(self).hash(state);
+        match self {
+            Warning::Program { warning, .. } => warning.hash(state),
+            Warning::EmptyQueryPath => {}
+        }
+    }
 }
 
-impl IntoDiagnostics for Warning {
-    fn into_diagnostics(self, _files: &mut Files) -> Vec<Diagnostic<FileId>> {
-        vec![Diagnostic::warning()
-            .with_message("empty query path")
-            .with_notes(vec![
-                "You queried a value without requesting a specific field path. \
+impl Warning {
+    /// Report this warning on the standard error stream.
+    pub fn report(self, format: ErrorFormat, color: ColorOpt) {
+        use nickel_lang_core::error::report::report as core_report;
+        match self {
+            Warning::EmptyQueryPath => {
+                let mut files = Files::new();
+                let diag = Diagnostic::warning()
+                    .with_message("empty query path")
+                    .with_notes(vec![
+                        "You queried a value without requesting a specific field path. \
             This operation can't find any metadata, beside listing the fields of a record."
-                    .into(),
-                "Try to query the root configuration and provide a query path instead.".into(),
-                "For example, instead of querying the expression \
+                            .into(),
+                        "Try to query the root configuration and provide a query path instead."
+                            .into(),
+                        "For example, instead of querying the expression \
             `(import \"config.ncl\").module.input` with an empty path, query \
             `config.ncl` with the `module.input` path: \
             \n`nickel query config.ncl --field module.input"
-                    .into(),
-            ])]
+                            .into(),
+                    ]);
+                core_report(&mut files, diag, format, color);
+            }
+            Warning::Program { mut files, warning } => {
+                core_report(&mut files, warning, format, color)
+            }
+        }
     }
 }
 
@@ -201,61 +240,46 @@ impl From<nickel_lang_core::repl::InitError> for Error {
     }
 }
 
-pub trait ResultErrorExt<T> {
-    fn report_with_program(self, program: Program<CBNCache>) -> CliResult<T>;
-}
+// Report a standalone error which doesn't actually refer to any source code.
+//
+// Wrapping all errors in a diagnostic makes sure all errors are rendered using
+// the same format set (potentially by default) by the `--error-format` flag.
+// This also makes error styling more consistent.
+fn report_standalone(main_label: &str, msg: Option<String>, format: ErrorFormat, color: ColorOpt) {
+    let mut dummy_files = Files::new();
+    let diagnostic = Diagnostic::error()
+        .with_message(main_label)
+        .with_notes(msg.into_iter().collect());
 
-impl<T> ResultErrorExt<T> for Result<T, nickel_lang_core::error::Error> {
-    fn report_with_program(self, program: Program<CBNCache>) -> CliResult<T> {
-        self.map_err(|error| Error::Program { program, error })
-    }
+    report(&mut dummy_files, diagnostic, format, color);
 }
 
 impl Error {
     /// Report this error on the standard error stream.
     pub fn report(self, format: ErrorFormat, color: ColorOpt) {
-        // Report a standalone error which doesn't actually refer to any source code.
-        let report_standalone = |main_label: &str, msg: Option<String>| {
-            use nickel_lang_core::{
-                cache::{Cache, ErrorTolerance},
-                error::report::report as core_report,
-            };
-
-            let mut dummy_cache = Cache::new(ErrorTolerance::Tolerant);
-            let diagnostic = Diagnostic::error()
-                .with_message(main_label)
-                .with_notes(msg.into_iter().collect());
+        use nickel_lang_core::error::report::report as core_report;
 
-            core_report(&mut dummy_cache, diagnostic, format, color);
-        };
+        let report_str = |main_label: &str| report_standalone(main_label, None, format, color);
+        let report_with_msg =
+            |main_label: &str, msg: String| report_standalone(main_label, Some(msg), format, color);
 
-        // We try to fit every error in a diagnostic. This makes sure all errors are rendered using
-        // the same format set (potentitally by default) by the `--error-format` flag. This also
-        // makes error styling more consistent.
         match self {
-            Error::Program { mut program, error } => program.report(error, format),
-            Error::Io { error } => {
-                report_standalone("IO error", Some(error.to_string()));
-            }
+            Error::Program { mut files, error } => core_report(&mut files, error, format, color),
+            Error::Io { error } => report_with_msg("IO error", error.to_string()),
             #[cfg(feature = "repl")]
             Error::Repl { error } => {
                 use nickel_lang_core::repl::InitError;
                 match error {
-                    InitError::Stdlib => {
-                        report_standalone("failed to initialize the standard library", None)
-                    }
+                    InitError::Stdlib => report_str("failed to initialize the standard library"),
                     InitError::ReadlineError(msg) => {
-                        report_standalone("failed to initialize the terminal interface", Some(msg))
+                        report_with_msg("failed to initialize the terminal interface", msg)
                     }
                 }
             }
             #[cfg(feature = "format")]
-            Error::Format { error } => report_standalone("format error", Some(error.to_string())),
-            Error::CliUsage { error, mut program } => program.report(error, format),
-            Error::FailedTests => report_standalone("tests failed", None),
-            Error::CustomizeInfoPrinted => {
-                // Nothing to do, the caller should simply exit.
-            }
+            Error::Format { error } => report_with_msg("format error", error.to_string()),
+            Error::CliUsage { error, mut files } => core_report(&mut files, error, format, color),
+            Error::FailedTests => report_str("tests failed"),
         }
     }
 }
diff --git a/cli/src/eval.rs b/cli/src/eval.rs
index e0064cf7..6dd4a804 100644
--- a/cli/src/eval.rs
+++ b/cli/src/eval.rs
@@ -1,9 +1,4 @@
-use crate::{
-    cli::GlobalOptions,
-    customize::CustomizeMode,
-    error::{CliResult, ResultErrorExt},
-    input::{InputOptions, Prepare},
-};
+use crate::{customize::CustomizeMode, global::GlobalContext, input::InputOptions};
 
 #[derive(clap::Parser, Debug)]
 pub struct EvalCommand {
@@ -12,12 +7,9 @@ pub struct EvalCommand {
 }
 
 impl EvalCommand {
-    pub fn run(self, global: GlobalOptions) -> CliResult<()> {
-        let mut program = self.input.prepare(&global)?;
-
-        program
-            .eval_full()
-            .map(|t| println!("{t}"))
-            .report_with_program(program)
+    pub fn run(self, ctxt: &mut GlobalContext) {
+        ctxt.with_program(&self.input, |program| {
+            program.eval_full().map(|t| println!("{t}"))
+        });
     }
 }
diff --git a/cli/src/export.rs b/cli/src/export.rs
index 7d22e4b2..690dc011 100644
--- a/cli/src/export.rs
+++ b/cli/src/export.rs
@@ -7,12 +7,7 @@ use nickel_lang_core::{
     serialize::{self, ExportFormat},
 };
 
-use crate::{
-    cli::GlobalOptions,
-    customize::CustomizeMode,
-    error::{CliResult, ResultErrorExt},
-    input::{InputOptions, Prepare},
-};
+use crate::{customize::CustomizeMode, global::GlobalContext, input::InputOptions};
 
 #[derive(clap::Parser, Debug)]
 pub struct ExportCommand {
@@ -28,13 +23,11 @@ pub struct ExportCommand {
 }
 
 impl ExportCommand {
-    pub fn run(self, global: GlobalOptions) -> CliResult<()> {
-        let mut program = self.input.prepare(&global)?;
-
-        self.export(&mut program).report_with_program(program)
+    pub fn run(self, ctxt: &mut GlobalContext) {
+        ctxt.with_program(&self.input, |program| self.export(program));
     }
 
-    fn export(self, program: &mut Program<CBNCache>) -> Result<(), Error> {
+    fn export(&self, program: &mut Program<CBNCache>) -> Result<(), Error> {
         let rt = program.eval_full_for_export()?;
 
         // We only add a trailing newline for JSON exports. Both YAML and TOML
@@ -43,7 +36,7 @@ impl ExportCommand {
 
         serialize::validate(self.format, &rt)?;
 
-        if let Some(file) = self.output {
+        if let Some(file) = &self.output {
             let mut file = fs::File::create(file).map_err(IOError::from)?;
             serialize::to_writer(&mut file, self.format, &rt)?;
 
diff --git a/cli/src/format.rs b/cli/src/format.rs
index 84fff195..770c4075 100644
--- a/cli/src/format.rs
+++ b/cli/src/format.rs
@@ -8,7 +8,7 @@ use std::{
 use tempfile::NamedTempFile;
 
 use crate::{
-    cli::GlobalOptions, customize::NoCustomizeMode, error::CliResult, input::InputOptions,
+    customize::NoCustomizeMode, error::CliResult, global::GlobalContext, input::InputOptions,
 };
 
 #[derive(Debug)]
@@ -82,20 +82,24 @@ pub struct FormatCommand {
 }
 
 impl FormatCommand {
-    pub fn run(self, _global: GlobalOptions) -> CliResult<()> {
+    pub fn run(self, ctxt: &mut GlobalContext) {
         fn format(input: impl Read, mut output: Output) -> CliResult<()> {
             nickel_lang_core::format::format(input, &mut output)
                 .map_err(FormatError::FormatError)?;
             output.persist();
             Ok(())
         }
-        if self.input.files.is_empty() {
-            return format(stdin(), Output::Stdout);
+
+        fn format_path(path: &Path) -> CliResult<()> {
+            format(BufReader::new(File::open(path)?), Output::from_path(path)?)
         }
 
-        for file in self.input.files.iter() {
-            format(BufReader::new(File::open(file)?), Output::from_path(file)?)?;
+        if self.input.files.is_empty() {
+            ctxt.reporter.report_result(format(stdin(), Output::Stdout));
+        } else {
+            for file in self.input.files.iter() {
+                ctxt.reporter.report_result(format_path(file));
+            }
         }
-        Ok(())
     }
 }
diff --git a/cli/src/global.rs b/cli/src/global.rs
new file mode 100644
index 00000000..53b9827e
--- /dev/null
+++ b/cli/src/global.rs
@@ -0,0 +1,141 @@
+//! Global state for the nickel CLI.
+
+use std::{
+    collections::HashSet,
+    sync::mpsc::{channel, Receiver, Sender},
+};
+
+use nickel_lang_core::{
+    error::Error as CoreError, error::Reporter, eval::cache::lazy::CBNCache, files::Files,
+    program::Program,
+};
+
+use crate::{
+    cli::GlobalOptions,
+    error::{Error, Warning},
+    input::{Prepare, PrepareError},
+};
+
+/// Contains global state for the nickel CLI.
+pub struct GlobalContext {
+    /// The CLI arguments that are applicable to all commands.
+    pub opts: GlobalOptions,
+    /// Collects all errors that occur. Currently, there is at most one error but that
+    /// might change.
+    pub errors: Receiver<Error>,
+    /// Collects all emitted warnings.
+    pub warnings: Receiver<Warning>,
+    /// Contains the sending ends of the `errors` and `warnings` channels. This can be
+    /// cloned and passed to anything that needs to send errors.
+    pub reporter: CliReporter,
+}
+
+/// An implementation of [`nickel_lang_core::error::Reporter`] that makes its errors
+/// available on the [`GlobalContext`].
+///
+/// We use `mpsc` channels, not because we want to send errors across threads but so that
+/// we can decouple the lifetimes between the sending and receiving ends.
+#[derive(Clone)]
+pub struct CliReporter {
+    errors: Sender<Error>,
+    warnings: Sender<Warning>,
+}
+
+impl GlobalContext {
+    pub fn new(opts: GlobalOptions) -> Self {
+        let (errors_tx, errors) = channel();
+        let (warnings_tx, warnings) = channel();
+        GlobalContext {
+            opts,
+            errors,
+            warnings,
+            reporter: CliReporter {
+                errors: errors_tx,
+                warnings: warnings_tx,
+            },
+        }
+    }
+
+    /// Creates a program and applies a callback to it.
+    ///
+    /// The program is run with this global context as its warning collector, so
+    /// that all of the program's warnings will be available on the global context
+    /// after the program has finished.
+    ///
+    /// If the callback returns an error, that error will also be available on the
+    /// global context.
+    pub fn with_program<
+        T,
+        P: Prepare,
+        F: FnOnce(&mut Program<CBNCache>) -> Result<T, CoreError>,
+    >(
+        &mut self,
+        preparer: &P,
+        f: F,
+    ) -> Option<T> {
+        let result = match preparer.prepare(self) {
+            Ok(mut prog) => f(&mut prog).map_err(|error| {
+                Error::Program {
+                    error,
+                    files: prog.files(),
+                }
+                .into()
+            }),
+            Err(e) => Err(e),
+        };
+
+        match result {
+            Err(PrepareError::Error(error)) => {
+                self.reporter.report(error);
+                None
+            }
+            Err(PrepareError::EarlyReturn) => None,
+            Ok(x) => Some(x),
+        }
+    }
+
+    /// Drains all the warnings that have been received so far and deduplicates them.
+    pub fn deduplicated_warnings(&mut self) -> Vec<Warning> {
+        let mut seen: HashSet<Warning> = HashSet::new();
+        let mut ret = Vec::new();
+        for w in self.warnings.try_iter() {
+            if seen.insert(w.clone()) {
+                ret.push(w);
+            }
+        }
+        ret
+    }
+}
+
+impl CliReporter {
+    // This is just an alias for the trait method, but having an inherent impl
+    // makes type inference better because rust defaults to the inherit impl and
+    // doesn't get confused between `Warning` and `Error`.
+    pub fn report_result<T, E: Into<Error>>(&mut self, result: Result<T, E>) {
+        <Self as Reporter<Error>>::report_result(self, result)
+    }
+}
+
+impl Reporter<Error> for CliReporter {
+    fn report(&mut self, e: Error) {
+        // Ignore any errors, because if no one is listening for these diagnostics
+        // then they aren't needed.
+        let _ = self.errors.send(e);
+    }
+}
+
+impl Reporter<Warning> for CliReporter {
+    fn report(&mut self, w: Warning) {
+        // Ignore any errors, because if no one is listening for these diagnostics
+        // then they aren't needed.
+        let _ = self.warnings.send(w);
+    }
+}
+
+impl Reporter<(nickel_lang_core::error::Warning, Files)> for CliReporter {
+    fn report(&mut self, (warning, files): (nickel_lang_core::error::Warning, Files)) {
+        // Ignore any errors, because if no one is listening for these diagnostics
+        // then they aren't needed.
+        let _ = self.warnings.send(Warning::Program { warning, files });
+    }
+}
diff --git a/cli/src/input.rs b/cli/src/input.rs
index f02a9080..6033a833 100644
--- a/cli/src/input.rs
+++ b/cli/src/input.rs
@@ -2,7 +2,7 @@ use std::path::PathBuf;
 
 use nickel_lang_core::{eval::cache::lazy::CBNCache, program::Program};
 
-use crate::{cli::GlobalOptions, color_opt_from_clap, customize::Customize, error::CliResult};
+use crate::{customize::Customize, global::GlobalContext};
 
 #[derive(clap::Parser, Debug)]
 pub struct InputOptions<Customize: clap::Args> {
@@ -28,20 +28,38 @@ pub struct InputOptions<Customize: clap::Args> {
     pub customize_mode: Customize,
 }
 
+pub enum PrepareError {
+    /// Not a real error.
+    ///
+    /// Input preparation sometimes wants to print some info and then exit,
+    /// without proceeding to program evaluation. This error variant is used to
+    /// trigger the early return, but we don't print an error message or exit
+    /// with an error status.
+    EarlyReturn,
+    /// A real error.
+    Error(crate::error::Error),
+}
+
+impl<E: Into<crate::error::Error>> From<E> for PrepareError {
+    fn from(e: E) -> Self {
+        PrepareError::Error(e.into())
+    }
+}
+
+pub type PrepareResult<T> = Result<T, PrepareError>;
+
 pub trait Prepare {
-    fn prepare(&self, global: &GlobalOptions) -> CliResult<Program<CBNCache>>;
+    fn prepare(&self, ctx: &mut GlobalContext) -> PrepareResult<Program<CBNCache>>;
 }
 
 impl<C: clap::Args + Customize> Prepare for InputOptions<C> {
-    fn prepare(&self, global: &GlobalOptions) -> CliResult<Program<CBNCache>> {
+    fn prepare(&self, ctx: &mut GlobalContext) -> PrepareResult<Program<CBNCache>> {
         let mut program = match self.files.as_slice() {
-            [] => Program::new_from_stdin(std::io::stderr()),
-            [p] => Program::new_from_file(p, std::io::stderr()),
-            files => Program::new_from_files(files, std::io::stderr()),
+            [] => Program::new_from_stdin(std::io::stderr(), ctx.reporter.clone()),
+            [p] => Program::new_from_file(p, std::io::stderr(), ctx.reporter.clone()),
+            files => Program::new_from_files(files, std::io::stderr(), ctx.reporter.clone()),
         }?;
 
-        program.color_opt = color_opt_from_clap(global.color);
-
         program.add_import_paths(self.import_path.iter());
 
         if let Ok(nickel_path) = std::env::var("NICKEL_IMPORT_PATH") {
diff --git a/cli/src/main.rs b/cli/src/main.rs
index f2ddd9bd..319d9aba 100644
--- a/cli/src/main.rs
+++ b/cli/src/main.rs
@@ -17,6 +17,7 @@ mod customize;
 mod error;
 mod eval;
 mod export;
+mod global;
 mod input;
 mod pprint_ast;
 mod query;
@@ -24,6 +25,8 @@ mod typecheck;
 
 use std::process::ExitCode;
 
+use global::GlobalContext;
+
 use crate::cli::{Command, Options};
 
 fn main() -> ExitCode {
@@ -37,24 +40,26 @@ fn main() -> ExitCode {
     #[cfg(feature = "metrics")]
     let report_metrics = opts.global.metrics;
 
-    let result = match opts.command {
-        Command::Eval(eval) => eval.run(opts.global),
-        Command::PprintAst(pprint_ast) => pprint_ast.run(opts.global),
-        Command::Export(export) => export.run(opts.global),
-        Command::Query(query) => query.run(opts.global),
-        Command::Typecheck(typecheck) => typecheck.run(opts.global),
-        Command::GenCompletions(completions) => completions.run(opts.global),
+    let mut ctxt = GlobalContext::new(opts.global);
+
+    match opts.command {
+        Command::Eval(eval) => eval.run(&mut ctxt),
+        Command::PprintAst(pprint_ast) => pprint_ast.run(&mut ctxt),
+        Command::Export(export) => export.run(&mut ctxt),
+        Command::Query(query) => query.run(&mut ctxt),
+        Command::Typecheck(typecheck) => typecheck.run(&mut ctxt),
+        Command::GenCompletions(completions) => completions.run(&mut ctxt),
 
         #[cfg(feature = "repl")]
-        Command::Repl(repl) => repl.run(opts.global),
+        Command::Repl(repl) => repl.run(&mut ctxt),
 
         #[cfg(feature = "doc")]
-        Command::Doc(doc) => doc.run(opts.global),
+        Command::Doc(doc) => doc.run(&mut ctxt),
         #[cfg(feature = "doc")]
-        Command::Test(test) => test.run(opts.global),
+        Command::Test(test) => test.run(&mut ctxt),
 
         #[cfg(feature = "format")]
-        Command::Format(format) => format.run(opts.global),
+        Command::Format(format) => format.run(&mut ctxt),
     };
 
     #[cfg(feature = "metrics")]
@@ -62,15 +67,17 @@ fn main() -> ExitCode {
         metrics.report();
     }
 
-    match result {
-        // CustomizeInfoPrinted is used for early return, but it's not actually an error from the
-        // user's point of view.
-        Ok(()) | Err(error::Error::CustomizeInfoPrinted) => ExitCode::SUCCESS,
-        Err(error) => {
-            error.report(error_format, color_opt_from_clap(color));
-            ExitCode::FAILURE
-        }
+    for w in ctxt.deduplicated_warnings() {
+        w.report(error_format, color_opt_from_clap(color));
     }
+
+    let mut code = ExitCode::SUCCESS;
+    for e in ctxt.errors.try_iter() {
+        e.report(error_format, color_opt_from_clap(color));
+        code = ExitCode::FAILURE;
+    }
+
+    code
 }
 
 fn color_opt_from_clap(c: clap::ColorChoice) -> nickel_lang_core::error::report::ColorOpt {
diff --git a/cli/src/pprint_ast.rs b/cli/src/pprint_ast.rs
index cd940d6b..6517d0f9 100644
--- a/cli/src/pprint_ast.rs
+++ b/cli/src/pprint_ast.rs
@@ -1,9 +1,4 @@
-use crate::{
-    cli::GlobalOptions,
-    customize::NoCustomizeMode,
-    error::{CliResult, ResultErrorExt},
-    input::{InputOptions, Prepare},
-};
+use crate::{customize::NoCustomizeMode, global::GlobalContext, input::InputOptions};
 
 #[derive(clap::Parser, Debug)]
 pub struct PprintAstCommand {
@@ -16,10 +11,9 @@ pub struct PprintAstCommand {
 }
 
 impl PprintAstCommand {
-    pub fn run(self, global: GlobalOptions) -> CliResult<()> {
-        let mut program = self.inputs.prepare(&global)?;
-        program
-            .pprint_ast(&mut std::io::stdout(), self.transform)
-            .report_with_program(program)
+    pub fn run(self, ctxt: &mut GlobalContext) {
+        ctxt.with_program(&self.inputs, |program| {
+            program.pprint_ast(&mut std::io::stdout(), self.transform)
+        });
     }
 }
diff --git a/cli/src/query.rs b/cli/src/query.rs
index fdf28c75..1d95b3e5 100644
--- a/cli/src/query.rs
+++ b/cli/src/query.rs
@@ -1,9 +1,11 @@
 use std::{fs, io::Write, path::PathBuf};
 
 use nickel_lang_core::{
-    error::{Error, IOError},
+    error::{Error, IOError, Reporter as _},
+    eval::cache::lazy::CBNCache,
     identifier::{Ident, LocIdent},
     pretty::PrettyPrintCap,
+    program::Program,
     repl::query_print,
     serialize::{self, MetadataExportFormat},
     term::{record::Field, LabeledType, MergePriority, Term},
@@ -11,10 +13,10 @@ use nickel_lang_core::{
 use serde::Serialize;
 
 use crate::{
-    cli::GlobalOptions,
     customize::{Customize, ExtractFieldOnly},
-    error::{CliResult, ResultErrorExt, Warning},
-    input::{InputOptions, Prepare},
+    error::Warning,
+    global::GlobalContext,
+    input::InputOptions,
 };
 
 const VALUE_EXPORT_MAX_WIDTH: usize = 80;
@@ -127,7 +129,7 @@ impl QueryCommand {
         }
     }
 
-    fn export<T>(self, res: T, format: MetadataExportFormat) -> Result<(), Error>
+    fn export<T>(&self, res: T, format: MetadataExportFormat) -> Result<(), Error>
     where
         T: Serialize,
     {
@@ -137,7 +139,7 @@ impl QueryCommand {
         // exporters already append a trailing newline by default.
         let trailing_newline = format == MetadataExportFormat::Json;
 
-        if let Some(file) = self.output {
+        if let Some(file) = &self.output {
             let mut file = fs::File::create(file).map_err(IOError::from)?;
             serialize::to_writer_metadata(&mut file, format, &res)?;
 
@@ -155,30 +157,32 @@ impl QueryCommand {
         Ok(())
     }
 
-    pub fn run(self, global: GlobalOptions) -> CliResult<()> {
-        let mut program = self.inputs.prepare(&global)?;
-
+    pub fn run(self, ctxt: &mut GlobalContext) {
         if self.inputs.customize_mode.field().is_none() {
-            program.report(Warning::EmptyQueryPath, global.error_format);
+            ctxt.reporter.report(Warning::EmptyQueryPath);
         }
 
+        ctxt.with_program(&self.inputs, |program| self.output(program));
+    }
+
+    fn output(
+        &self,
+        program: &mut Program<CBNCache>,
+    ) -> Result<(), nickel_lang_core::error::Error> {
         use MetadataExportFormat::*;
         match self.format {
             Markdown => {
                 if self.output.is_some() {
                     eprintln!("Output query result in markdown format to a file is currently not supported.")
                 } else {
-                    let found = program
-                        .query()
-                        .map(|field| {
-                            query_print::write_query_result(
-                                &mut std::io::stdout(),
-                                &field,
-                                self.query_attributes(),
-                            )
-                            .unwrap()
-                        })
-                        .report_with_program(program)?;
+                    let found = program.query().map(|field| {
+                        query_print::write_query_result(
+                            &mut std::io::stdout(),
+                            &field,
+                            self.query_attributes(),
+                        )
+                        .unwrap()
+                    })?;
 
                     if !found {
                         eprintln!("No metadata found for this field.")
@@ -189,8 +193,7 @@ impl QueryCommand {
                 let _ = &program
                     .query()
                     .map(QueryResult::from)
-                    .map(|res| self.export(res, format))
-                    .report_with_program(program)?;
+                    .map(|res| self.export(res, format))?;
             }
         }
         Ok(())
diff --git a/cli/src/repl.rs b/cli/src/repl.rs
index 9f71ab62..d37a702c 100644
--- a/cli/src/repl.rs
+++ b/cli/src/repl.rs
@@ -3,7 +3,7 @@ use std::path::PathBuf;
 use directories::BaseDirs;
 use nickel_lang_core::repl::rustyline_frontend;
 
-use crate::{cli::GlobalOptions, color_opt_from_clap, error::CliResult};
+use crate::{color_opt_from_clap, global::GlobalContext};
 
 #[derive(clap::Parser, Debug)]
 pub struct ReplCommand {
@@ -12,7 +12,7 @@ pub struct ReplCommand {
 }
 
 impl ReplCommand {
-    pub fn run(self, global: GlobalOptions) -> CliResult<()> {
+    pub fn run(self, ctxt: &mut GlobalContext) {
         let histfile = if let Some(h) = self.history_file {
             h
         } else {
@@ -21,9 +21,9 @@ impl ReplCommand {
                 .home_dir()
                 .join(".nickel_history")
         };
-        Ok(rustyline_frontend::repl(
+        ctxt.reporter.report_result(rustyline_frontend::repl(
             histfile,
-            color_opt_from_clap(global.color),
-        )?)
+            color_opt_from_clap(ctxt.opts.color),
+        ));
     }
 }
diff --git a/cli/src/typecheck.rs b/cli/src/typecheck.rs
index eed375da..17520966 100644
--- a/cli/src/typecheck.rs
+++ b/cli/src/typecheck.rs
@@ -1,11 +1,6 @@
 use nickel_lang_core::typecheck::TypecheckMode;
 
-use crate::{
-    cli::GlobalOptions,
-    customize::NoCustomizeMode,
-    error::{CliResult, ResultErrorExt},
-    input::{InputOptions, Prepare},
-};
+use crate::{customize::NoCustomizeMode, global::GlobalContext, input::InputOptions};
 
 #[derive(clap::Parser, Debug)]
 pub struct TypecheckCommand {
@@ -18,7 +13,7 @@ pub struct TypecheckCommand {
 }
 
 impl TypecheckCommand {
-    pub fn run(self, global: GlobalOptions) -> CliResult<()> {
+    pub fn run(self, ctxt: &mut GlobalContext) {
         if self.strict_typechecking {
             // In strict mode we run *both* forms of typechecking, because in
             // fact neither one is more strict than the other. For example,
@@ -29,14 +24,8 @@ impl TypecheckCommand {
             // typechecking in "enforce" mode will succeed because it will infer
             // `x: Number`, while typechecking in walk mode will fail because it
             // will treat `x` as `Dyn` and then try to typecheck `x + 1`.
-            let mut program = self.inputs.prepare(&global)?;
-            program
-                .typecheck(TypecheckMode::Enforce)
-                .report_with_program(program)?;
+            ctxt.with_program(&self.inputs, |prog| prog.typecheck(TypecheckMode::Enforce));
         }
-        let mut program = self.inputs.prepare(&global)?;
-        program
-            .typecheck(TypecheckMode::Walk)
-            .report_with_program(program)
+        ctxt.with_program(&self.inputs, |prog| prog.typecheck(TypecheckMode::Walk));
     }
 }
diff --git a/core/src/cache.rs b/core/src/cache.rs
index 6c9818f2..1d179b0e 100644
--- a/core/src/cache.rs
+++ b/core/src/cache.rs
@@ -1430,6 +1430,9 @@ pub trait ImportResolver {
         pos: &TermPos,
     ) -> Result<(ResolvedTerm, FileId), ImportError>;
 
+    /// Return a reference to the file database.
+    fn files(&self) -> &Files;
+
     /// Get a resolved import from the term cache.
     fn get(&self, file_id: FileId) -> Option<RichTerm>;
     /// Return the (potentially normalized) file path corresponding to the ID of a resolved import.
@@ -1523,6 +1526,10 @@ impl ImportResolver for Cache {
         Ok((result, file_id))
     }
 
+    fn files(&self) -> &Files {
+        &self.files
+    }
+
     fn get(&self, file_id: FileId) -> Option<RichTerm> {
         self.terms
             .get(&file_id)
@@ -1649,6 +1656,10 @@ pub mod resolvers {
             panic!("cache::resolvers: dummy resolver should not have been invoked");
         }
 
+        fn files(&self) -> &Files {
+            panic!("cache::resolvers: dummy resolver should not have been invoked");
+        }
+
         fn get(&self, _file_id: FileId) -> Option<RichTerm> {
             panic!("cache::resolvers: dummy resolver should not have been invoked");
         }
@@ -1720,6 +1731,10 @@ pub mod resolvers {
             }
         }
 
+        fn files(&self) -> &Files {
+            &self.files
+        }
+
         fn get(&self, file_id: FileId) -> Option<RichTerm> {
             self.term_cache.get(&file_id).cloned()
         }
diff --git a/core/src/deserialize.rs b/core/src/deserialize.rs
index 05336ec1..75f74d5a 100644
--- a/core/src/deserialize.rs
+++ b/core/src/deserialize.rs
@@ -589,16 +589,23 @@ mod tests {
     use std::io::Cursor;
 
     use nickel_lang_utils::{
-        nickel_lang_core::{deserialize::RustDeserializationError, term::RichTerm},
+        nickel_lang_core::{
+            deserialize::RustDeserializationError, error::NullReporter, term::RichTerm,
+        },
         test_program::TestProgram,
     };
     use serde::Deserialize;
 
     fn eval(source: &str) -> RichTerm {
-        TestProgram::new_from_source(Cursor::new(source), "source", std::io::stderr())
-            .expect("program shouldn't fail")
-            .eval_full()
-            .expect("evaluation shouldn't fail")
+        TestProgram::new_from_source(
+            Cursor::new(source),
+            "source",
+            std::io::stderr(),
+            NullReporter {},
+        )
+        .expect("program shouldn't fail")
+        .eval_full()
+        .expect("evaluation shouldn't fail")
     }
 
     #[test]
diff --git a/core/src/error/mod.rs b/core/src/error/mod.rs
index 8fff0ea6..bca92750 100644
--- a/core/src/error/mod.rs
+++ b/core/src/error/mod.rs
@@ -11,7 +11,6 @@ use lalrpop_util::ErrorRecovery;
 use malachite::num::conversion::traits::ToSci;
 
 use crate::{
-    cache::Cache,
     eval::callstack::CallStack,
     files::{FileId, Files},
     identifier::{Ident, LocIdent},
@@ -35,6 +34,62 @@ use crate::{
 
 pub mod report;
 pub mod suggest;
+pub mod warning;
+
+pub use warning::Warning;
+
+/// A `Reporter` is basically a callback function for reporting errors and/or warnings.
+///
+/// The error type `E` is a generic parameter, so the same object can be a `Reporter`
+/// of various different things.
+pub trait Reporter<E> {
+    /// Called when there is something (`e`) for the reporter to report.
+    fn report(&mut self, e: E);
+
+    /// A utility function for reporting error variants.
+    ///
+    /// When this is called with an `Ok(_)` it does nothing; when called with an `Err(e)`
+    /// it reports `e`.
+    fn report_result<T, E2>(&mut self, result: Result<T, E2>)
+    where
+        Self: Sized,
+        E2: Into<E>,
+    {
+        if let Err(e) = result {
+            self.report(e.into());
+        }
+    }
+}
+
+impl<E, R: Reporter<E>> Reporter<E> for &mut R {
+    fn report(&mut self, e: E) {
+        R::report(*self, e)
+    }
+}
+
+/// A [`Reporter`] that just collects errors.
+pub struct Sink<E> {
+    pub errors: Vec<E>,
+}
+
+impl<E> Default for Sink<E> {
+    fn default() -> Self {
+        Sink { errors: Vec::new() }
+    }
+}
+
+impl<E> Reporter<E> for Sink<E> {
+    fn report(&mut self, e: E) {
+        self.errors.push(e);
+    }
+}
+
+/// A [`Reporter`] that throws away all its errors.
+pub struct NullReporter {}
+
+impl<E> Reporter<E> for NullReporter {
+    fn report(&mut self, _e: E) {}
+}
 
 /// A general error occurring during either parsing or evaluation.
 #[derive(Debug, Clone, PartialEq)]
diff --git a/core/src/error/report.rs b/core/src/error/report.rs
index c2ea6445..d7817eae 100644
--- a/core/src/error/report.rs
+++ b/core/src/error/report.rs
@@ -1,4 +1,6 @@
 //! Error diagnostics reporting and serialization.
+use codespan_reporting::term::termcolor::{Ansi, NoColor};
+
 use super::*;
 
 /// Serializable wrapper type to export diagnostics with a top-level attribute.
@@ -48,7 +50,7 @@ fn colors_for_terminal(color_opt: ColorOpt, is_terminal: bool) -> ColorChoice {
 /// - `cache` is the file cache used during the evaluation, which is required by the reporting
 ///   infrastructure to point at specific locations and print snippets when needed.
 pub fn report<E: IntoDiagnostics>(
-    cache: &mut Cache,
+    files: &mut Files,
     error: E,
     format: ErrorFormat,
     color_opt: ColorOpt,
@@ -57,7 +59,7 @@ pub fn report<E: IntoDiagnostics>(
 
     report_with(
         &mut StandardStream::stderr(colors_for_terminal(color_opt, stderr().is_terminal())).lock(),
-        &mut cache.files().clone(),
+        files,
         error,
         format,
     )
@@ -70,7 +72,7 @@ pub fn report<E: IntoDiagnostics>(
 /// - `cache` is the file cache used during the evaluation, which is required by the reporting
 ///   infrastructure to point at specific locations and print snippets when needed.
 pub fn report_to_stdout<E: IntoDiagnostics>(
-    cache: &mut Cache,
+    files: &mut Files,
     error: E,
     format: ErrorFormat,
     color_opt: ColorOpt,
@@ -79,12 +81,34 @@ pub fn report_to_stdout<E: IntoDiagnostics>(
 
     report_with(
         &mut StandardStream::stdout(colors_for_terminal(color_opt, stdout().is_terminal())).lock(),
-        &mut cache.files().clone(),
+        files,
         error,
         format,
     )
 }
 
+/// Build an error report as a string and return it.
+pub fn report_as_str<E: IntoDiagnostics>(
+    files: &mut Files,
+    error: E,
+    color_opt: ColorOpt,
+) -> String {
+    let mut buffer = Vec::new();
+    let mut with_color;
+    let mut no_color;
+    let writer: &mut dyn WriteColor = if color_opt == colorchoice::ColorChoice::Never {
+        no_color = NoColor::new(&mut buffer);
+        &mut no_color
+    } else {
+        with_color = Ansi::new(&mut buffer);
+        &mut with_color
+    };
+
+    report_with(writer, files, error, ErrorFormat::Text);
+    // unwrap(): report_with() should only print valid utf8 to the the buffer
+    String::from_utf8(buffer).unwrap()
+}
+
 /// Report an error on `stderr`, provided a file database and a list of stdlib file ids.
 pub fn report_with<E: IntoDiagnostics>(
     writer: &mut dyn WriteColor,
diff --git a/core/src/error/warning.rs b/core/src/error/warning.rs
new file mode 100644
index 00000000..cb683230
--- /dev/null
+++ b/core/src/error/warning.rs
@@ -0,0 +1,40 @@
+use codespan_reporting::diagnostic::Diagnostic;
+
+use crate::error::{primary, secondary, IntoDiagnostics};
+use crate::files::{FileId, Files};
+use crate::position::TermPos;
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+pub enum Warning {
+    /// Applied a `fun label value => ...` (or match) as a contract directly,
+    /// instead of using the constract constructors in the standard library.
+    NakedFunctionContract {
+        /// The position of the function that was used as a contract.
+        func_pos: TermPos,
+        /// The position of the thing that the contract was applied to.
+        app_pos: TermPos,
+    },
+}
+
+impl IntoDiagnostics for Warning {
+    fn into_diagnostics(self, _files: &mut Files) -> Vec<Diagnostic<FileId>> {
+        match self {
+            Warning::NakedFunctionContract { func_pos, app_pos } => {
+                let mut labels = vec![];
+                if let Some(span) = app_pos.into_opt() {
+                    labels.push(primary(&span).with_message("applied to this term"));
+                }
+
+                if let Some(func_span) = func_pos.into_opt() {
+                    labels.push(secondary(&func_span).with_message("this function"));
+                }
+
+                vec![Diagnostic::warning()
+                    .with_message("plain functions as contracts are deprecated")
+                    .with_labels(labels)
+                    .with_notes(vec!["wrap this function using one of the constructors in `std.contract` instead, like `std.contract.from_validator` or `std.contract.custom`".to_owned()])
+                ]
+            }
+        }
+    }
+}
diff --git a/core/src/eval/mod.rs b/core/src/eval/mod.rs
index a4f329cd..eb5e2d04 100644
--- a/core/src/eval/mod.rs
+++ b/core/src/eval/mod.rs
@@ -77,8 +77,8 @@ use crate::{
     cache::{Cache as ImportCache, Envs, ImportResolver},
     closurize::{closurize_rec_record, Closurize},
     environment::Environment as GenericEnvironment,
-    error::{Error, EvalError},
-    files::FileId,
+    error::{warning::Warning, Error, EvalError, Reporter},
+    files::{FileId, Files},
     identifier::{Ident, LocIdent},
     match_sharedterm,
     metrics::{increment, measure_runtime},
@@ -130,10 +130,18 @@ pub struct VirtualMachine<R: ImportResolver, C: Cache> {
     initial_env: Environment,
     // The stream for writing trace output.
     trace: Box<dyn Write>,
+    /// A collector for warnings. Currently we only collect warnings and not errors; errors
+    /// terminate evaluation (or typechecking, or whatever) immediately, and so they just
+    /// get early-returned in a `Result`.
+    pub reporter: Box<dyn Reporter<(Warning, Files)>>,
 }
 
 impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
-    pub fn new(import_resolver: R, trace: impl Write + 'static) -> Self {
+    pub fn new(
+        import_resolver: R,
+        trace: impl Write + 'static,
+        reporter: impl Reporter<(Warning, Files)> + 'static,
+    ) -> Self {
         VirtualMachine {
             import_resolver,
             call_stack: Default::default(),
@@ -141,10 +149,16 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
             cache: Cache::new(),
             initial_env: Environment::new(),
             trace: Box::new(trace),
+            reporter: Box::new(reporter),
         }
     }
 
-    pub fn new_with_cache(import_resolver: R, cache: C, trace: impl Write + 'static) -> Self {
+    pub fn new_with_cache(
+        import_resolver: R,
+        cache: C,
+        trace: impl Write + 'static,
+        reporter: impl Reporter<(Warning, Files)> + 'static,
+    ) -> Self {
         VirtualMachine {
             import_resolver,
             call_stack: Default::default(),
@@ -152,9 +166,25 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
             cache,
             trace: Box::new(trace),
             initial_env: Environment::new(),
+            reporter: Box::new(reporter),
         }
     }
 
+    pub fn with_reporter(
+        self,
+        reporter: impl Reporter<(Warning, Files)> + 'static,
+    ) -> VirtualMachine<R, C> {
+        VirtualMachine {
+            reporter: Box::new(reporter),
+            ..self
+        }
+    }
+
+    pub fn warn(&mut self, warning: Warning) {
+        self.reporter
+            .report((warning, self.import_resolver.files().clone()));
+    }
+
     /// Reset the state of the machine (stacks, eval mode and state of cached elements) to prepare
     /// for another evaluation round.
     pub fn reset(&mut self) {
@@ -959,7 +989,7 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
 
     /// Evaluate a term, but attempt to continue on errors.
     ///
-    /// This differs from `VirtualMachine::eval_full` in 2 ways:
+    /// This differs from `VirtualMachine::eval_full` in three ways:
     /// - We try to accumulate errors instead of bailing out. When recursing into record
     ///   fields and array elements, we keep evaluating subsequent elements even if one
     ///   fails.
diff --git a/core/src/eval/operation.rs b/core/src/eval/operation.rs
index c5a7b5f5..ce8c409e 100644
--- a/core/src/eval/operation.rs
+++ b/core/src/eval/operation.rs
@@ -18,7 +18,7 @@ use crate::nix_ffi;
 
 use crate::{
     closurize::Closurize,
-    error::{EvalError, IllegalPolymorphicTailAction},
+    error::{EvalError, IllegalPolymorphicTailAction, Warning},
     identifier::LocIdent,
     label::{ty_path, Polarity, TypeVarData},
     match_sharedterm,
@@ -26,8 +26,7 @@ use crate::{
     mk_app, mk_fun, mk_record,
     parser::utils::parse_number_sci,
     position::TermPos,
-    serialize,
-    serialize::ExportFormat,
+    serialize::{self, ExportFormat},
     stdlib::internals,
     term::{
         array::{Array, ArrayAttrs},
@@ -1859,6 +1858,17 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
                                 pos: pos1,
                             };
 
+                            // Warn on naked function contracts, but not if they came from the
+                            // stdlib. Some stdlib functions return naked function contracts.
+                            if let Some(pos) = pos1.as_opt_ref() {
+                                if !self.import_resolver.files().is_stdlib(pos.src_id) {
+                                    self.warn(Warning::NakedFunctionContract {
+                                        func_pos: pos1,
+                                        app_pos: pos_op,
+                                    });
+                                }
+                            }
+
                             if let BinaryOp::ContractApply = b_op {
                                 Closure {
                                     body: as_naked,
@@ -3893,6 +3903,7 @@ where
 mod tests {
     use super::*;
     use crate::cache::resolvers::DummyResolver;
+    use crate::error::NullReporter;
     use crate::eval::cache::CacheImpl;
     use crate::eval::Environment;
 
@@ -3900,7 +3911,7 @@ mod tests {
     fn ite_operation() {
         let cont: OperationCont = OperationCont::Op1(UnaryOp::IfThenElse, TermPos::None);
         let mut vm: VirtualMachine<DummyResolver, CacheImpl> =
-            VirtualMachine::new(DummyResolver {}, std::io::sink());
+            VirtualMachine::new(DummyResolver {}, std::io::sink(), NullReporter {});
 
         vm.stack
             .push_arg(Closure::atomic_closure(mk_term::integer(5)), TermPos::None);
@@ -3941,7 +3952,7 @@ mod tests {
             body: mk_term::integer(7),
             env: Environment::new(),
         };
-        let mut vm = VirtualMachine::new(DummyResolver {}, std::io::sink());
+        let mut vm = VirtualMachine::new(DummyResolver {}, std::io::sink(), NullReporter {});
         vm.stack.push_op_cont(cont, 0, TermPos::None);
 
         clos = vm.continuate_operation(clos).unwrap();
@@ -3986,7 +3997,7 @@ mod tests {
         );
 
         let mut vm: VirtualMachine<DummyResolver, CacheImpl> =
-            VirtualMachine::new(DummyResolver {}, std::io::sink());
+            VirtualMachine::new(DummyResolver {}, std::io::sink(), NullReporter {});
         let mut clos = Closure {
             body: mk_term::integer(6),
             env: Environment::new(),
diff --git a/core/src/files.rs b/core/src/files.rs
index be132327..a752d67a 100644
--- a/core/src/files.rs
+++ b/core/src/files.rs
@@ -117,7 +117,7 @@ impl Files {
         // This implementation would be a little nicer if `Vector` supported mutable access.
         // unwrap: we're allowed to panic if file_id is invalid
         let mut old = self.get(file_id).unwrap().clone();
-        old.source = source.into();
+        old = File::new(old.name, source);
         self.files.set(file_id.0 as usize, old);
     }
 
diff --git a/core/src/package.rs b/core/src/package.rs
index 53ab8043..06e2470a 100644
--- a/core/src/package.rs
+++ b/core/src/package.rs
@@ -60,7 +60,7 @@ mod tests {
     use std::io::Cursor;
 
     use super::*;
-    use crate::{eval::cache::CacheImpl, program::Program, term::Term};
+    use crate::{error::NullReporter, eval::cache::CacheImpl, program::Program, term::Term};
     use nickel_lang_utils::project_root::project_root;
 
     // Test basic package map functionality by building one manually out of
@@ -76,8 +76,13 @@ mod tests {
             packages: std::iter::once(((pkg1, Ident::new("dep")), pkg2)).collect(),
         };
 
-        let mut p: Program<CacheImpl> =
-            Program::new_from_source(Cursor::new("import pkg"), "<test>", std::io::sink()).unwrap();
+        let mut p: Program<CacheImpl> = Program::new_from_source(
+            Cursor::new("import pkg"),
+            "<test>",
+            std::io::sink(),
+            NullReporter {},
+        )
+        .unwrap();
         p.set_package_map(map);
 
         assert_eq!(p.eval_full().unwrap().as_ref(), &Term::Num(44.into()));
diff --git a/core/src/program.rs b/core/src/program.rs
index da006db5..d59ecf32 100644
--- a/core/src/program.rs
+++ b/core/src/program.rs
@@ -23,12 +23,9 @@
 use crate::{
     cache::*,
     closurize::Closurize as _,
-    error::{
-        report::{report, report_to_stdout, report_with, ColorOpt, ErrorFormat},
-        Error, EvalError, IOError, IntoDiagnostics, ParseError,
-    },
+    error::{warning::Warning, Error, EvalError, IOError, ParseError, Reporter},
     eval::{cache::Cache as EvalCache, Closure, VirtualMachine},
-    files::FileId,
+    files::{FileId, Files},
     identifier::LocIdent,
     label::Label,
     metrics::increment,
@@ -42,9 +39,6 @@ use crate::{
     typecheck::TypecheckMode,
 };
 
-use codespan_reporting::term::termcolor::{Ansi, NoColor, WriteColor};
-use colorchoice::ColorChoice;
-
 use std::{
     ffi::OsString,
     fmt,
@@ -181,8 +175,6 @@ pub struct Program<EC: EvalCache> {
     main_id: FileId,
     /// The state of the Nickel virtual machine.
     vm: VirtualMachine<Cache, EC>,
-    /// The color option to use when reporting errors.
-    pub color_opt: ColorOpt,
     /// A list of [`FieldOverride`]s. During [`prepare_eval`], each
     /// override is imported in a separate in-memory source, for complete isolation (this way,
     /// overrides can't accidentally or intentionally capture other fields of the configuration).
@@ -209,8 +201,11 @@ pub enum Input<T, S> {
 
 impl<EC: EvalCache> Program<EC> {
     /// Create a program by reading it from the standard input.
-    pub fn new_from_stdin(trace: impl Write + 'static) -> std::io::Result<Self> {
-        Program::new_from_source(io::stdin(), "<stdin>", trace)
+    pub fn new_from_stdin(
+        trace: impl Write + 'static,
+        reporter: impl Reporter<(Warning, Files)> + 'static,
+    ) -> std::io::Result<Self> {
+        Program::new_from_source(io::stdin(), "<stdin>", trace, reporter)
     }
 
     /// Contructor that abstracts over the Input type (file, string, etc.). Used by
@@ -218,6 +213,7 @@ impl<EC: EvalCache> Program<EC> {
     pub fn new_from_input<T, S>(
         input: Input<T, S>,
         trace: impl Write + 'static,
+        reporter: impl Reporter<(Warning, Files)> + 'static,
     ) -> std::io::Result<Self>
     where
         T: Read,
@@ -234,11 +230,10 @@ impl<EC: EvalCache> Program<EC> {
             }
         };
 
-        let vm = VirtualMachine::new(cache, trace);
+        let vm = VirtualMachine::new(cache, trace, reporter);
         Ok(Self {
             main_id,
             vm,
-            color_opt: colorchoice::ColorChoice::Auto,
             overrides: Vec::new(),
             field: FieldPath::new(),
             contracts: Vec::new(),
@@ -248,7 +243,11 @@ impl<EC: EvalCache> Program<EC> {
     /// Constructor that abstracts over an iterator of Inputs (file, strings,
     /// etc). Published for those that need abstraction over the kind of Input
     /// or want to mix multiple different kinds of Input.
-    pub fn new_from_inputs<I, T, S>(inputs: I, trace: impl Write + 'static) -> std::io::Result<Self>
+    pub fn new_from_inputs<I, T, S>(
+        inputs: I,
+        trace: impl Write + 'static,
+        reporter: impl Reporter<(Warning, Files)> + 'static,
+    ) -> std::io::Result<Self>
     where
         I: IntoIterator<Item = Input<T, S>>,
         T: Read,
@@ -283,12 +282,11 @@ impl<EC: EvalCache> Program<EC> {
             format!("{merge_term}"),
         );
 
-        let vm = VirtualMachine::new(cache, trace);
+        let vm = VirtualMachine::new(cache, trace, reporter);
 
         Ok(Self {
             main_id,
             vm,
-            color_opt: colorchoice::ColorChoice::Auto,
             overrides: Vec::new(),
             field: FieldPath::new(),
             contracts: Vec::new(),
@@ -298,7 +296,11 @@ impl<EC: EvalCache> Program<EC> {
     /// Create program from possibly multiple files. Each input `path` is
     /// turned into a [`Term::Import`] and the main program will be the
     /// [`BinaryOp::Merge`] of all the inputs.
-    pub fn new_from_files<I, P>(paths: I, trace: impl Write + 'static) -> std::io::Result<Self>
+    pub fn new_from_files<I, P>(
+        paths: I,
+        trace: impl Write + 'static,
+        reporter: impl Reporter<(Warning, Files)> + 'static,
+    ) -> std::io::Result<Self>
     where
         I: IntoIterator<Item = P>,
         P: Into<OsString>,
@@ -308,16 +310,18 @@ impl<EC: EvalCache> Program<EC> {
         Self::new_from_inputs(
             paths.into_iter().map(Input::<std::fs::File, _>::Path),
             trace,
+            reporter,
         )
     }
 
     pub fn new_from_file(
         path: impl Into<OsString>,
         trace: impl Write + 'static,
+        reporter: impl Reporter<(Warning, Files)> + 'static,
     ) -> std::io::Result<Self> {
         // The File type parameter is a dummy type and not used.
         // It just needed to be something that implements Read, and File seemed fitting.
-        Self::new_from_input(Input::<std::fs::File, _>::Path(path), trace)
+        Self::new_from_input(Input::<std::fs::File, _>::Path(path), trace, reporter)
     }
 
     /// Create a program by reading it from a generic source.
@@ -325,12 +329,13 @@ impl<EC: EvalCache> Program<EC> {
         source: T,
         source_name: S,
         trace: impl Write + 'static,
+        reporter: impl Reporter<(Warning, Files)> + 'static,
     ) -> std::io::Result<Self>
     where
         T: Read,
         S: Into<OsString>,
     {
-        Self::new_from_input(Input::Source(source, source_name), trace)
+        Self::new_from_input(Input::Source(source, source_name), trace, reporter)
     }
 
     /// Create program from possibly multiple sources. The main program will be
@@ -338,6 +343,7 @@ impl<EC: EvalCache> Program<EC> {
     pub fn new_from_sources<I, T, S>(
         sources: I,
         trace: impl Write + 'static,
+        reporter: impl Reporter<(Warning, Files)> + 'static,
     ) -> std::io::Result<Self>
     where
         I: IntoIterator<Item = (T, S)>,
@@ -345,7 +351,7 @@ impl<EC: EvalCache> Program<EC> {
         S: Into<OsString>,
     {
         let inputs = sources.into_iter().map(|(s, n)| Input::Source(s, n));
-        Self::new_from_inputs(inputs, trace)
+        Self::new_from_inputs(inputs, trace, reporter)
     }
 
     /// Parse an assignment of the form `path.to_field=value` as an override, with the provided
@@ -576,45 +582,6 @@ impl<EC: EvalCache> Program<EC> {
         Ok(())
     }
 
-    /// Wrapper for [`report`].
-    pub fn report<E>(&mut self, error: E, format: ErrorFormat)
-    where
-        E: IntoDiagnostics,
-    {
-        report(self.vm.import_resolver_mut(), error, format, self.color_opt)
-    }
-
-    /// Wrapper for [`report_to_stdout`].
-    pub fn report_to_stdout<E>(&mut self, error: E, format: ErrorFormat)
-    where
-        E: IntoDiagnostics,
-    {
-        report_to_stdout(self.vm.import_resolver_mut(), error, format, self.color_opt)
-    }
-
-    /// Build an error report as a string and return it.
-    pub fn report_as_str<E>(&mut self, error: E) -> String
-    where
-        E: IntoDiagnostics,
-    {
-        let cache = self.vm.import_resolver_mut();
-
-        let mut buffer = Vec::new();
-        let mut with_color;
-        let mut no_color;
-        let writer: &mut dyn WriteColor = if self.color_opt == ColorChoice::Never {
-            no_color = NoColor::new(&mut buffer);
-            &mut no_color
-        } else {
-            with_color = Ansi::new(&mut buffer);
-            &mut with_color
-        };
-
-        report_with(writer, &mut cache.files().clone(), error, ErrorFormat::Text);
-        // unwrap(): report_with() should only print valid utf8 to the the buffer
-        String::from_utf8(buffer).unwrap()
-    }
-
     /// Evaluate a program into a record spine, a form suitable for extracting the general
     /// structure of a configuration, and in particular its interface (fields that might need to be
     /// filled).
@@ -877,6 +844,11 @@ impl<EC: EvalCache> Program<EC> {
 
         Ok(())
     }
+
+    /// Returns a copy of the program's current `Files` database. This doesn't actually clone the content of the source files, see [crate::files::Files].
+    pub fn files(&self) -> Files {
+        self.vm.import_resolver().files().clone()
+    }
 }
 
 #[cfg(feature = "doc")]
@@ -1169,7 +1141,7 @@ mod doc {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::error::EvalError;
+    use crate::error::{EvalError, NullReporter};
     use crate::eval::cache::CacheImpl;
     use crate::identifier::LocIdent;
     use crate::position::TermPos;
@@ -1180,26 +1152,30 @@ mod tests {
     fn eval_full(s: &str) -> Result<RichTerm, Error> {
         let src = Cursor::new(s);
 
-        let mut p: Program<CacheImpl> = Program::new_from_source(src, "<test>", std::io::sink())
-            .map_err(|io_err| {
-                Error::EvalError(EvalError::Other(
-                    format!("IO error: {io_err}"),
-                    TermPos::None,
-                ))
-            })?;
+        let mut p: Program<CacheImpl> =
+            Program::new_from_source(src, "<test>", std::io::sink(), NullReporter {}).map_err(
+                |io_err| {
+                    Error::EvalError(EvalError::Other(
+                        format!("IO error: {io_err}"),
+                        TermPos::None,
+                    ))
+                },
+            )?;
         p.eval_full()
     }
 
     fn typecheck(s: &str) -> Result<(), Error> {
         let src = Cursor::new(s);
 
-        let mut p: Program<CacheImpl> = Program::new_from_source(src, "<test>", std::io::sink())
-            .map_err(|io_err| {
-                Error::EvalError(EvalError::Other(
-                    format!("IO error: {io_err}"),
-                    TermPos::None,
-                ))
-            })?;
+        let mut p: Program<CacheImpl> =
+            Program::new_from_source(src, "<test>", std::io::sink(), NullReporter {}).map_err(
+                |io_err| {
+                    Error::EvalError(EvalError::Other(
+                        format!("IO error: {io_err}"),
+                        TermPos::None,
+                    ))
+                },
+            )?;
         p.typecheck(TypecheckMode::Walk)
     }
 
diff --git a/core/src/repl/mod.rs b/core/src/repl/mod.rs
index 6b3812a7..3789792d 100644
--- a/core/src/repl/mod.rs
+++ b/core/src/repl/mod.rs
@@ -7,6 +7,7 @@
 //! jupyter-kernel (which is not exactly user-facing, but still manages input/output and
 //! formatting), etc.
 use crate::cache::{Cache, Envs, ErrorTolerance, InputFormat, SourcePath};
+use crate::error::NullReporter;
 use crate::error::{
     report::{self, ColorOpt, ErrorFormat},
     Error, EvalError, IOError, IntoDiagnostics, ParseError, ParseErrors, ReplError,
@@ -98,7 +99,7 @@ impl<EC: EvalCache> ReplImpl<EC> {
             parser: grammar::ExtendedTermParser::new(),
             env: Envs::new(),
             initial_type_ctxt: typecheck::Context::new(),
-            vm: VirtualMachine::new(Cache::new(ErrorTolerance::Strict), trace),
+            vm: VirtualMachine::new(Cache::new(ErrorTolerance::Strict), trace, NullReporter {}),
         }
     }
 
@@ -217,8 +218,10 @@ impl<EC: EvalCache> ReplImpl<EC> {
         }
     }
 
+    #[cfg(feature = "repl")]
     fn report(&mut self, err: impl IntoDiagnostics, color_opt: ColorOpt) {
-        report::report(self.cache_mut(), err, ErrorFormat::Text, color_opt);
+        let mut files = self.cache_mut().files().clone();
+        report::report(&mut files, err, ErrorFormat::Text, color_opt);
     }
 }
 
diff --git a/core/src/serialize.rs b/core/src/serialize.rs
index d7553676..993100ff 100644
--- a/core/src/serialize.rs
+++ b/core/src/serialize.rs
@@ -585,6 +585,7 @@ pub mod toml_deser {
 mod tests {
     use super::*;
     use crate::cache::resolvers::DummyResolver;
+    use crate::error::NullReporter;
     use crate::eval::cache::CacheImpl;
     use crate::eval::VirtualMachine;
     use crate::program::Program;
@@ -594,8 +595,13 @@ mod tests {
 
     fn eval(s: &str) -> RichTerm {
         let src = Cursor::new(s);
-        let mut prog =
-            Program::<CacheImpl>::new_from_source(src, "<test>", std::io::stderr()).unwrap();
+        let mut prog = Program::<CacheImpl>::new_from_source(
+            src,
+            "<test>",
+            std::io::stderr(),
+            NullReporter {},
+        )
+        .unwrap();
         prog.eval_full().expect("program eval should succeed")
     }
 
@@ -610,9 +616,13 @@ mod tests {
     #[track_caller]
     fn assert_nickel_eq(term: RichTerm, expected: RichTerm) {
         assert_eq!(
-            VirtualMachine::<_, CacheImpl>::new(DummyResolver {}, std::io::stderr())
-                .eval(mk_term::op2(BinaryOp::Eq, term, expected))
-                .map(Term::from),
+            VirtualMachine::<_, CacheImpl>::new(
+                DummyResolver {},
+                std::io::stderr(),
+                NullReporter {}
+            )
+            .eval(mk_term::op2(BinaryOp::Eq, term, expected))
+            .map(Term::from),
             Ok(Term::Bool(true))
         )
     }
diff --git a/git/Cargo.toml b/git/Cargo.toml
new file mode 100644
index 00000000..d991a9e5
--- /dev/null
+++ b/git/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "nickel-lang-git"
+description = "Git utility functions for internal use in nickel"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+anyhow.workspace = true
+gix = { workspace = true, features = ["blocking-network-client", "serde"] }
+serde = { workspace = true, features = ["derive"] }
+serde_with.workspace = true
+tempfile.workspace = true
+thiserror.workspace = true
+
+[dev-dependencies]
+anyhow = { workspace = true, features = ["backtrace"] }
+clap = { workspace = true, features = ["derive"] }
+gix = { workspace = true, features = ["blocking-http-transport-reqwest-rust-tls"] }
diff --git a/git/examples/fetch.rs b/git/examples/fetch.rs
new file mode 100644
index 00000000..8af21141
--- /dev/null
+++ b/git/examples/fetch.rs
@@ -0,0 +1,44 @@
+use std::path::PathBuf;
+
+use clap::{ArgGroup, Parser};
+use gix::ObjectId;
+use nickel_lang_git::{Spec, Target};
+
+#[derive(Parser)]
+#[command(group = ArgGroup::new("target").args(["branch", "tag", "commit"]))]
+struct Args {
+    repository: String,
+
+    directory: PathBuf,
+
+    #[arg(long)]
+    branch: Option<String>,
+
+    #[arg(long)]
+    tag: Option<String>,
+
+    #[arg(long)]
+    commit: Option<ObjectId>,
+}
+
+pub fn main() -> anyhow::Result<()> {
+    let args = Args::parse();
+
+    let target = if let Some(branch) = args.branch {
+        Target::Branch(branch)
+    } else if let Some(tag) = args.tag {
+        Target::Tag(tag)
+    } else if let Some(commit) = args.commit {
+        Target::Commit(commit)
+    } else {
+        Target::Head
+    };
+
+    let spec = Spec {
+        url: args.repository.try_into()?,
+        target,
+    };
+
+    nickel_lang_git::fetch(&spec, args.directory)?;
+    Ok(())
+}
diff --git a/git/src/lib.rs b/git/src/lib.rs
new file mode 100644
index 00000000..cbffdabe
--- /dev/null
+++ b/git/src/lib.rs
@@ -0,0 +1,200 @@
+//! A small crate providing a convienient interface to the git operations
+//! that nickel uses.
+
+use anyhow::anyhow;
+use gix::{
+    interrupt::IS_INTERRUPTED,
+    objs::Kind,
+    progress::Discard,
+    remote::{self, fetch, Direction},
+    worktree::state::checkout,
+    ObjectId,
+};
+use std::{num::NonZero, path::Path};
+
+/// An error that occurred during a git operation.
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+    #[error("io error {error} at {path}")]
+    Io {
+        error: std::io::Error,
+        path: std::path::PathBuf,
+    },
+
+    #[error("target `{0}` not found")]
+    TargetNotFound(Target),
+
+    #[error(transparent)]
+    Internal(#[from] anyhow::Error),
+}
+
+pub type Result<T, E = Error> = std::result::Result<T, E>;
+
+trait IoResultExt<T> {
+    fn with_path<P: AsRef<Path>>(self, path: P) -> Result<T>;
+}
+
+impl<T> IoResultExt<T> for Result<T, std::io::Error> {
+    fn with_path<P: AsRef<Path>>(self, path: P) -> Result<T> {
+        self.map_err(|error| Error::Io {
+            error,
+            path: path.as_ref().to_owned(),
+        })
+    }
+}
+
+trait InternalResultExt<T> {
+    fn wrap_err(self) -> Result<T>;
+}
+
+impl<T, E: Into<anyhow::Error>> InternalResultExt<T> for Result<T, E> {
+    fn wrap_err(self) -> Result<T> {
+        self.map_err(|e| Error::Internal(e.into()))
+    }
+}
+
+/// Specifies a git location that we can fetch.
+#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+pub struct Spec {
+    /// The url of a git repository.
+    pub url: gix::Url,
+    /// Specifies the target commit within the repository.
+    pub target: Target,
+}
+
+impl Spec {
+    /// The HEAD commit at the given url.
+    pub fn head(url: gix::Url) -> Self {
+        Self {
+            url,
+            target: Target::Head,
+        }
+    }
+
+    pub fn commit(url: gix::Url, commit: ObjectId) -> Self {
+        Self {
+            url,
+            target: Target::Commit(commit),
+        }
+    }
+}
+
+/// The different kinds of git "thing" that we can target.
+#[serde_with::serde_as]
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]
+pub enum Target {
+    /// By default, we target the remote HEAD.
+    #[default]
+    Head,
+    /// Target the tip of a specific branch.
+    Branch(String),
+    /// Target a specific tag.
+    Tag(String),
+    /// Target a specific commit.
+    ///
+    /// Currently, we only support a full commit: this needs to be a full hex-encoded
+    /// sha hash. We could try to support prefixes also, but it requires some work
+    /// and it *appears* to be inherently less efficient because there doesn't seem
+    /// to be a way to fetch it in one shot. At least, when `cargo` needs to fetch an
+    /// abbreviated hash it fetches everything and then looks for the hash among the
+    /// things that it finds.
+    Commit(#[serde_as(as = "serde_with::DisplayFromStr")] ObjectId),
+}
+
+impl std::fmt::Display for Target {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Target::Head => write!(f, "HEAD"),
+            Target::Branch(branch) => write!(f, "refs/heads/{branch}"),
+            Target::Tag(tag) => write!(f, "refs/tags/{tag}"),
+            Target::Commit(c) => write!(f, "{}", c),
+        }
+    }
+}
+
+fn source_object_id(source: &fetch::Source) -> Result<ObjectId> {
+    match source {
+        fetch::Source::ObjectId(id) => Ok(*id),
+        fetch::Source::Ref(r) => {
+            let (_name, id, peeled) = r.unpack();
+
+            Ok(peeled
+                .or(id)
+                .ok_or_else(|| anyhow!("unborn reference"))?
+                .to_owned())
+        }
+    }
+}
+
+/// Fetches the contents of a git repository into directory.
+///
+/// The directory will be created if it doesn't exist yet. The data will be
+/// fetched from scratch, even if it has already been fetched before. However,
+/// we will try to minimize the amount of data to fetch (for example, by doing a
+/// shallow fetch).
+///
+/// Only the contents of the git repository will be written to the given
+/// directory; the git directory itself will be discarded.
+pub fn fetch(spec: &Spec, dir: impl AsRef<Path>) -> Result<ObjectId> {
+    let dir = dir.as_ref();
+    std::fs::create_dir_all(dir).with_path(dir)?;
+
+    // Fetch the git directory somewhere temporary.
+    let git_tempdir = tempfile::tempdir().wrap_err()?;
+    let repo = gix::init(git_tempdir.path()).wrap_err()?;
+    let refspec = spec.target.to_string();
+
+    let remote = repo
+        .remote_at(spec.url.clone())
+        .wrap_err()?
+        .with_fetch_tags(fetch::Tags::None)
+        .with_refspecs(Some(refspec.as_str()), Direction::Fetch)
+        .wrap_err()?;
+
+    // This does similar credentials stuff to the git CLI (e.g. it looks for ssh
+    // keys if it's a fetch over ssh, or it tries to run `askpass` if it needs
+    // credentials for https). Maybe we want to have explicit credentials
+    // configuration instead of or in addition to the default?
+    let connection = remote.connect(Direction::Fetch).wrap_err()?;
+    let outcome = connection
+        .prepare_fetch(&mut Discard, remote::ref_map::Options::default())
+        .wrap_err()?
+        // For now, we always fetch shallow. Maybe for the index it's more efficient to
+        // keep a single repo around and update it? But that might be in another method.
+        .with_shallow(fetch::Shallow::DepthAtRemote(NonZero::new(1).unwrap()))
+        .receive(&mut Discard, &IS_INTERRUPTED)
+        .map_err(|e| match e {
+            fetch::Error::NoMapping { .. } => Error::TargetNotFound(spec.target.clone()),
+            _ => Error::Internal(e.into()),
+        })?;
+
+    if outcome.ref_map.mappings.len() > 1 {
+        return Err(anyhow!("we only asked for 1 ref; why did we get more?")).wrap_err();
+    }
+    if outcome.ref_map.mappings.is_empty() {
+        return Err(Error::TargetNotFound(spec.target.clone()));
+    }
+    let object_id = source_object_id(&outcome.ref_map.mappings[0].remote)?;
+
+    let object = repo.find_object(object_id).wrap_err()?;
+    let commit = object.clone().peel_to_kind(Kind::Commit).wrap_err()?;
+    let tree_id = object.peel_to_tree().wrap_err()?.id();
+    let mut index = repo.index_from_tree(&tree_id).wrap_err()?;
+
+    checkout(
+        &mut index,
+        dir,
+        repo.objects.clone(),
+        &Discard,
+        &Discard,
+        &IS_INTERRUPTED,
+        checkout::Options {
+            overwrite_existing: true,
+            ..Default::default()
+        },
+    )
+    .wrap_err()?;
+    index.write(Default::default()).wrap_err()?;
+
+    Ok(commit.id)
+}
diff --git a/lsp/nls/src/background.rs b/lsp/nls/src/background.rs
index 389cdba4..59369eac 100644
--- a/lsp/nls/src/background.rs
+++ b/lsp/nls/src/background.rs
@@ -16,8 +16,8 @@ use nickel_lang_core::{
 use serde::{Deserialize, Serialize};
 
 use crate::{
-    cache::CacheExt as _, config, diagnostic::SerializableDiagnostic, files::uri_to_path,
-    world::World,
+    cache::CacheExt as _, config, diagnostic::SerializableDiagnostic, error::WarningReporter,
+    files::uri_to_path, world::World,
 };
 
 // Environment variable used to pass the recursion limit value to the child worker
@@ -101,14 +101,20 @@ pub fn worker_main() -> anyhow::Result<()> {
 
         // Evaluation diagnostics (but only if there were no parse/type errors).
         if diagnostics.is_empty() {
+            let (reporter, warnings) = WarningReporter::new();
             // TODO: avoid cloning the cache.
-            let mut vm =
-                VirtualMachine::<_, CacheImpl>::new(world.cache.clone(), std::io::stderr());
+            let mut vm = VirtualMachine::<_, CacheImpl>::new(
+                world.cache.clone(),
+                std::io::stderr(),
+                reporter,
+            );
             // We've already checked that parsing and typechecking are successful, so we
             // don't expect further errors.
             let rt = vm.prepare_eval(file_id).unwrap();
             let recursion_limit = std::env::var(RECURSION_LIMIT_ENV_VAR_NAME)?.parse::<usize>()?;
             let errors = vm.eval_permissive(rt, recursion_limit);
+            let mut files = vm.import_resolver().files().clone();
+
             diagnostics.extend(
                 errors
                     .into_iter()
@@ -118,8 +124,11 @@ pub fn worker_main() -> anyhow::Result<()> {
                             nickel_lang_core::error::EvalError::MissingFieldDef { .. }
                         )
                     })
-                    .flat_map(|e| world.lsp_diagnostics(file_id, e)),
+                    .flat_map(|e| SerializableDiagnostic::from(e, &mut files, file_id)),
             );
+            diagnostics.extend(warnings.try_iter().flat_map(|(warning, mut files)| {
+                SerializableDiagnostic::from(warning, &mut files, file_id)
+            }));
         }
 
         diagnostics.sort();
diff --git a/lsp/nls/src/command.rs b/lsp/nls/src/command.rs
index 7a0a9d19..56c0c291 100644
--- a/lsp/nls/src/command.rs
+++ b/lsp/nls/src/command.rs
@@ -2,7 +2,12 @@ use lsp_server::{RequestId, Response, ResponseError};
 use lsp_types::{ExecuteCommandParams, TextDocumentIdentifier, Url};
 use nickel_lang_core::eval::{cache::CacheImpl, VirtualMachine};
 
-use crate::{cache::CacheExt, error::Error, server::Server};
+use crate::{
+    cache::CacheExt,
+    diagnostic::SerializableDiagnostic,
+    error::{Error, WarningReporter},
+    server::Server,
+};
 
 pub fn handle_command(
     params: ExecuteCommandParams,
@@ -24,14 +29,33 @@ pub fn handle_command(
 
 fn eval(server: &mut Server, uri: &Url) -> Result<(), Error> {
     if let Some(file_id) = server.world.cache.file_id(uri)? {
+        let (reporter, warnings) = WarningReporter::new();
         // TODO: avoid cloning the cache. Maybe we can have a VM with a &mut Cache?
-        let mut vm =
-            VirtualMachine::<_, CacheImpl>::new(server.world.cache.clone(), std::io::stderr());
+        let mut vm = VirtualMachine::<_, CacheImpl>::new(
+            server.world.cache.clone(),
+            std::io::stderr(),
+            reporter,
+        );
         let rt = vm.prepare_eval(file_id)?;
-        if let Err(e) = vm.eval_full(rt) {
-            let diags = server.world.lsp_diagnostics(file_id, e);
-            server.issue_diagnostics(file_id, diags);
+
+        let result = vm.eval_full(rt);
+        // Get a possibly-updated files from the vm instead of relying on the one
+        // in `world`.
+        let mut files = vm.import_resolver().files().clone();
+
+        let mut diags: Vec<_> = warnings
+            .try_iter()
+            .flat_map(|(warning, mut files)| {
+                SerializableDiagnostic::from(warning, &mut files, file_id)
+            })
+            .collect();
+
+        if let Err(e) = result {
+            let mut error_diags = SerializableDiagnostic::from(e, &mut files, file_id);
+            diags.append(&mut error_diags);
         }
+
+        server.issue_diagnostics(file_id, diags);
     }
     Ok(())
 }
diff --git a/lsp/nls/src/diagnostic.rs b/lsp/nls/src/diagnostic.rs
index af0ccc84..e3d811b8 100644
--- a/lsp/nls/src/diagnostic.rs
+++ b/lsp/nls/src/diagnostic.rs
@@ -2,6 +2,7 @@ use std::ops::Range;
 
 use codespan_reporting::diagnostic::{self, Diagnostic, LabelStyle};
 use lsp_types::{DiagnosticRelatedInformation, NumberOrString};
+use nickel_lang_core::error::IntoDiagnostics;
 use nickel_lang_core::files::{FileId, Files};
 use nickel_lang_core::{error::UNKNOWN_SOURCE_NAME, position::RawSpan};
 use serde::{Deserialize, Serialize};
@@ -26,6 +27,23 @@ pub struct SerializableDiagnostic {
     pub related_information: Option<Vec<OrdDiagnosticRelatedInformation>>,
 }
 
+impl SerializableDiagnostic {
+    /// Converts a nickel error into a collection of diagnostic messages.
+    ///
+    /// `files` is the collection of all files that might be referred to by the diagnostics.
+    /// It's mutable because diagnostics may need to add temporary generated files; see
+    /// [`nickel_lang_core::error`] for more details.
+    ///
+    /// `current_file` is the file that we're currently reporting diagnostics for. (LSP reports
+    /// diagnostics per-file.)
+    pub fn from<E: IntoDiagnostics>(e: E, files: &mut Files, current_file: FileId) -> Vec<Self> {
+        e.into_diagnostics(files)
+            .into_iter()
+            .flat_map(|d| SerializableDiagnostic::from_codespan(current_file, d, files))
+            .collect()
+    }
+}
+
 #[derive(Debug, Eq, PartialEq, Copy, Clone, Default, Deserialize, Serialize)]
 pub struct OrdRange(pub lsp_types::Range);
 
diff --git a/lsp/nls/src/error.rs b/lsp/nls/src/error.rs
index d114310d..6dbeb067 100644
--- a/lsp/nls/src/error.rs
+++ b/lsp/nls/src/error.rs
@@ -1,5 +1,11 @@
+use std::sync::mpsc;
+
 use lsp_server::{ErrorCode, ResponseError};
 use lsp_types::{Position, Url};
+use nickel_lang_core::{
+    error::{Reporter, Warning},
+    files::Files,
+};
 
 /// Non-fatal errors generated by nls.
 #[derive(Debug, thiserror::Error)]
@@ -56,3 +62,25 @@ impl From<Error> for ResponseError {
         }
     }
 }
+
+/// An implementation of [`nickel_lang_core::error::Reporter`] that sends warnings
+/// across a channel.
+#[derive(Clone)]
+pub struct WarningReporter {
+    warnings: mpsc::Sender<(Warning, Files)>,
+}
+
+impl WarningReporter {
+    /// Create both a `WarningReporter` and a `mpsc::Receiver` that receives the
+    /// reported warnings.
+    pub fn new() -> (Self, mpsc::Receiver<(Warning, Files)>) {
+        let (warnings, warnings_rx) = mpsc::channel();
+        (WarningReporter { warnings }, warnings_rx)
+    }
+}
+
+impl Reporter<(Warning, Files)> for WarningReporter {
+    fn report(&mut self, e: (Warning, Files)) {
+        let _ = self.warnings.send(e);
+    }
+}
diff --git a/lsp/nls/src/world.rs b/lsp/nls/src/world.rs
index 30646547..f90c98cb 100644
--- a/lsp/nls/src/world.rs
+++ b/lsp/nls/src/world.rs
@@ -19,7 +19,7 @@ use nickel_lang_core::{
 use crate::{
     analysis::{Analysis, AnalysisRegistry},
     cache::CacheExt as _,
-    diagnostic::{DiagnosticCompat, SerializableDiagnostic},
+    diagnostic::SerializableDiagnostic,
     field_walker::{Def, FieldResolver},
     files::uri_to_path,
     identifier::LocIdent,
@@ -138,11 +138,7 @@ impl World {
         file_id: FileId,
         err: impl IntoDiagnostics,
     ) -> Vec<SerializableDiagnostic> {
-        let mut files = self.cache.files().clone();
-        err.into_diagnostics(&mut files)
-            .into_iter()
-            .flat_map(|d| SerializableDiagnostic::from_codespan(file_id, d, &files))
-            .collect()
+        SerializableDiagnostic::from(err, &mut self.cache.files().clone(), file_id)
     }
 
     // Make a record of I/O errors in imports so that we can retry them when appropriate.
diff --git a/pyckel/src/lib.rs b/pyckel/src/lib.rs
index de85ea17..949417b6 100644
--- a/pyckel/src/lib.rs
+++ b/pyckel/src/lib.rs
@@ -1,7 +1,10 @@
 use std::io::Cursor;
 
 use nickel_lang_core::{
-    error::Error,
+    error::{
+        report::{report_as_str, ColorOpt},
+        Error, NullReporter,
+    },
     eval::cache::{Cache, CacheImpl},
     program::Program,
     serialize,
@@ -13,14 +16,18 @@ create_exception!(pyckel, NickelException, PyException);
 
 /// Turn an internal Nickel error into a PyErr with a fancy diagnostic message
 fn error_to_exception<E: Into<Error>, EC: Cache>(error: E, program: &mut Program<EC>) -> PyErr {
-    NickelException::new_err(program.report_as_str(error.into()))
+    NickelException::new_err(report_as_str(
+        &mut program.files(),
+        error.into(),
+        ColorOpt::default(),
+    ))
 }
 
 /// Evaluate from a Python str of a Nickel expression to a Python str of the resulting JSON.
 #[pyfunction]
 pub fn run(s: String) -> PyResult<String> {
     let mut program: Program<CacheImpl> =
-        Program::new_from_source(Cursor::new(s), "python", std::io::sink())?;
+        Program::new_from_source(Cursor::new(s), "python", std::io::sink(), NullReporter {})?;
 
     let term = program
         .eval_full()
diff --git a/utils/src/bench.rs b/utils/src/bench.rs
index 71fec8a6..f53c85c4 100644
--- a/utils/src/bench.rs
+++ b/utils/src/bench.rs
@@ -1,6 +1,7 @@
 use criterion::Criterion;
 use nickel_lang_core::{
     cache::{Cache, Envs, ErrorTolerance, InputFormat},
+    error::NullReporter,
     eval::{
         cache::{Cache as EvalCache, CacheImpl},
         VirtualMachine,
@@ -131,6 +132,7 @@ pub fn bench_terms<'r>(rts: Vec<Bench<'r>>) -> Box<dyn Fn(&mut Criterion) + 'r>
                                 c_local,
                                 eval_cache.clone(),
                                 std::io::sink(),
+                                NullReporter {},
                             )
                             .with_initial_env(eval_env.clone())
                             .eval(t)
@@ -225,13 +227,14 @@ macro_rules! ncl_bench_group {
                                 let mut vm = VirtualMachine::new_with_cache(
                                     c_local,
                                     eval_cache.clone(),
-                                    std::io::sink()
+                                    std::io::sink(),
+                                    nickel_lang_core::error::NullReporter {},
                                 )
                                 .with_initial_env(eval_env.clone());
 
                                 if let Err(e) = vm.eval(t) {
                                     report(
-                                        vm.import_resolver_mut(),
+                                        &mut vm.import_resolver_mut().files().clone(),
                                         e,
                                         ErrorFormat::Text,
                                         ColorOpt::default(),


Test patch:
diff --git a/cli/src/doctest.rs b/cli/src/doctest.rs
index 7d86cb4f..d6c2be37 100644
--- a/cli/src/doctest.rs
+++ b/cli/src/doctest.rs
@@ -7,8 +7,14 @@ use std::{collections::HashMap, io::Write as _, path::PathBuf, rc::Rc};
 use comrak::{arena_tree::NodeEdge, nodes::AstNode, Arena, ComrakOptions};
 use nickel_lang_core::{
     cache::{Cache, ImportResolver, InputFormat, SourcePath},
-    error::{Error as CoreError, EvalError},
-    eval::{cache::CacheImpl, Closure, Environment},
+    error::{
+        report::{report_as_str, report_to_stdout, ColorOpt},
+        Error as CoreError, EvalError, Reporter as _,
+    },
+    eval::{
+        cache::{lazy::CBNCache, CacheImpl},
+        Closure, Environment,
+    },
     identifier::{Ident, LocIdent},
     label::Label,
     match_sharedterm, mk_app, mk_fun,
@@ -24,10 +30,7 @@ use once_cell::sync::Lazy;
 use regex::Regex;
 
 use crate::{
-    cli::GlobalOptions,
-    customize::ExtractFieldOnly,
-    error::CliResult,
-    input::{InputOptions, Prepare},
+    color_opt_from_clap, customize::ExtractFieldOnly, global::GlobalContext, input::InputOptions,
 };
 
 #[derive(clap::Parser, Debug)]
@@ -163,6 +166,7 @@ fn run_tests(
     errors: &mut Vec<Error>,
     registry: &TestRegistry,
     spine: &RichTerm,
+    color: ColorOpt,
 ) {
     match spine.as_ref() {
         Term::Record(data) | Term::RecRecord(data, ..) => {
@@ -194,7 +198,7 @@ fn run_tests(
                         }
                         Err(e) => {
                             if let Some(expected) = &entry.expected_error {
-                                let message = prog.report_as_str(e);
+                                let message = report_as_str(&mut prog.files(), e, color);
                                 if !message.contains(expected) {
                                     Some(ErrorKind::WrongTestFailure {
                                         message,
@@ -221,7 +225,7 @@ fn run_tests(
                     path.pop();
                 } else if let Some(val) = field.value.as_ref() {
                     path.push(*id);
-                    run_tests(path, prog, errors, registry, val);
+                    run_tests(path, prog, errors, registry, val, color);
                     path.pop();
                 }
             }
@@ -231,17 +235,29 @@ fn run_tests(
 }
 
 impl TestCommand {
-    pub fn run(self, global: GlobalOptions) -> CliResult<()> {
-        let mut program = self.input.prepare(&global)?;
+    pub fn run(self, ctxt: &mut GlobalContext) {
+        let color: ColorOpt = color_opt_from_clap(ctxt.opts.color);
+        let num_errors = ctxt
+            .with_program(&self.input, |prog| self.run_tests(prog, color))
+            .unwrap_or(0);
 
-        let (spine, registry) = match self.prepare_tests(&mut program) {
-            Ok(x) => x,
-            Err(error) => return Err(crate::error::Error::Program { program, error }),
-        };
+        if num_errors > 0 {
+            eprintln!("{num_errors} failures");
+            ctxt.reporter.report(crate::error::Error::FailedTests);
+        }
+    }
+
+    /// Returns the number of test failures.
+    fn run_tests(
+        &self,
+        program: &mut Program<CBNCache>,
+        color: ColorOpt,
+    ) -> Result<usize, CoreError> {
+        let (spine, registry) = self.prepare_tests(program)?;
 
         let mut path = Vec::new();
         let mut errors = Vec::new();
-        run_tests(&mut path, &mut program, &mut errors, &registry, &spine);
+        run_tests(&mut path, program, &mut errors, &registry, &spine, color);
 
         let num_errors = errors.len();
         for e in errors {
@@ -262,24 +278,21 @@ impl TestCommand {
                 }
                 ErrorKind::UnexpectedFailure { error } => {
                     println!("test {}/{} failed", path_display, e.idx);
-                    program.report_to_stdout(
+                    report_to_stdout(
+                        &mut program.files(),
                         error,
                         nickel_lang_core::error::report::ErrorFormat::Text,
+                        color,
                     );
                 }
             }
         }
 
-        if num_errors > 0 {
-            eprintln!("{num_errors} failures");
-            Err(crate::error::Error::FailedTests)
-        } else {
-            Ok(())
-        }
+        Ok(num_errors)
     }
 
     fn prepare_tests(
-        self,
+        &self,
         program: &mut Program<CacheImpl>,
     ) -> Result<(RichTerm, TestRegistry), CoreError> {
         let mut registry = TestRegistry::default();
diff --git a/cli/tests/snapshot/inputs/warnings/naked_contract.ncl b/cli/tests/snapshot/inputs/warnings/naked_contract.ncl
new file mode 100644
index 00000000..5c130bd0
--- /dev/null
+++ b/cli/tests/snapshot/inputs/warnings/naked_contract.ncl
@@ -0,0 +1,7 @@
+# capture = 'stderr'
+# command = ['eval']
+let C = fun label value => value in
+[
+  1 | C,
+  [1, 2] | Array C
+]
diff --git a/cli/tests/snapshot/inputs/warnings/naked_contract_parametrized.ncl b/cli/tests/snapshot/inputs/warnings/naked_contract_parametrized.ncl
new file mode 100644
index 00000000..55638f8a
--- /dev/null
+++ b/cli/tests/snapshot/inputs/warnings/naked_contract_parametrized.ncl
@@ -0,0 +1,7 @@
+# capture = 'stderr'
+# command = ['eval']
+let C = fun arg label value => value in
+[
+  1 | C "hi",
+  [1, 2] | Array (C "hi")
+]
diff --git a/cli/tests/snapshot/snapshots/snapshot__eval_stderr_contract_with_custom_diagnostic.ncl.snap b/cli/tests/snapshot/snapshots/snapshot__eval_stderr_contract_with_custom_diagnostic.ncl.snap
index 4bfe2a6d..1a82dda1 100644
--- a/cli/tests/snapshot/snapshots/snapshot__eval_stderr_contract_with_custom_diagnostic.ncl.snap
+++ b/cli/tests/snapshot/snapshots/snapshot__eval_stderr_contract_with_custom_diagnostic.ncl.snap
@@ -2,6 +2,24 @@
 source: cli/tests/snapshot/main.rs
 expression: err
 ---
+warning: plain functions as contracts are deprecated
+   ┌─ [INPUTS_PATH]/errors/contract_with_custom_diagnostic.ncl:13:1
+   │  
+ 3 │   let Contract = fun label _value =>
+   │ ╭────────────────'
+ 4 │ │   label
+ 5 │ │   |> std.contract.label.with_message "main error message"
+ 6 │ │   |> std.contract.label.with_notes [
+   · │
+ 9 │ │     ]
+10 │ │   |> std.contract.blame
+   │ ╰───────────────────────' this function
+   · │
+13 │   null | Contract
+   │   ^^^^ applied to this term
+   │  
+   = wrap this function using one of the constructors in `std.contract` instead, like `std.contract.from_validator` or `std.contract.custom`
+
 error: contract broken by a value
        main error message
    ┌─ [INPUTS_PATH]/errors/contract_with_custom_diagnostic.ncl:13:1
@@ -13,5 +31,3 @@ error: contract broken by a value
    │
    = This is the first note
    = This is the second note
-
-
diff --git a/cli/tests/snapshot/snapshots/snapshot__eval_stderr_naked_contract.ncl.snap b/cli/tests/snapshot/snapshots/snapshot__eval_stderr_naked_contract.ncl.snap
new file mode 100644
index 00000000..31543fbb
--- /dev/null
+++ b/cli/tests/snapshot/snapshots/snapshot__eval_stderr_naked_contract.ncl.snap
@@ -0,0 +1,25 @@
+---
+source: cli/tests/snapshot/main.rs
+expression: err
+---
+warning: plain functions as contracts are deprecated
+  ┌─ [INPUTS_PATH]/warnings/naked_contract.ncl:6:3
+  │
+3 │ let C = fun label value => value in
+  │         ------------------------ this function
+  ·
+6 │   [1, 2] | Array C
+  │   ^^^^^^ applied to this term
+  │
+  = wrap this function using one of the constructors in `std.contract` instead, like `std.contract.from_validator` or `std.contract.custom`
+
+warning: plain functions as contracts are deprecated
+  ┌─ [INPUTS_PATH]/warnings/naked_contract.ncl:5:3
+  │
+3 │ let C = fun label value => value in
+  │         ------------------------ this function
+4 │ [
+5 │   1 | C,
+  │   ^ applied to this term
+  │
+  = wrap this function using one of the constructors in `std.contract` instead, like `std.contract.from_validator` or `std.contract.custom`
diff --git a/cli/tests/snapshot/snapshots/snapshot__eval_stderr_naked_contract_parametrized.ncl.snap b/cli/tests/snapshot/snapshots/snapshot__eval_stderr_naked_contract_parametrized.ncl.snap
new file mode 100644
index 00000000..d212c9e1
--- /dev/null
+++ b/cli/tests/snapshot/snapshots/snapshot__eval_stderr_naked_contract_parametrized.ncl.snap
@@ -0,0 +1,25 @@
+---
+source: cli/tests/snapshot/main.rs
+expression: err
+---
+warning: plain functions as contracts are deprecated
+  ┌─ [INPUTS_PATH]/warnings/naked_contract_parametrized.ncl:6:3
+  │
+3 │ let C = fun arg label value => value in
+  │         ---------------------------- this function
+  ·
+6 │   [1, 2] | Array (C "hi")
+  │   ^^^^^^ applied to this term
+  │
+  = wrap this function using one of the constructors in `std.contract` instead, like `std.contract.from_validator` or `std.contract.custom`
+
+warning: plain functions as contracts are deprecated
+  ┌─ [INPUTS_PATH]/warnings/naked_contract_parametrized.ncl:5:3
+  │
+3 │ let C = fun arg label value => value in
+  │         ---------------------------- this function
+4 │ [
+5 │   1 | C "hi",
+  │   ^ applied to this term
+  │
+  = wrap this function using one of the constructors in `std.contract` instead, like `std.contract.from_validator` or `std.contract.custom`
diff --git a/cli/tests/snapshot/snapshots/snapshot__eval_stderr_subcontract_nested_custom_diagnostics.ncl.snap b/cli/tests/snapshot/snapshots/snapshot__eval_stderr_subcontract_nested_custom_diagnostics.ncl.snap
index 95fdf8d3..7b26d4ab 100644
--- a/cli/tests/snapshot/snapshots/snapshot__eval_stderr_subcontract_nested_custom_diagnostics.ncl.snap
+++ b/cli/tests/snapshot/snapshots/snapshot__eval_stderr_subcontract_nested_custom_diagnostics.ncl.snap
@@ -2,6 +2,42 @@
 source: cli/tests/snapshot/main.rs
 expression: err
 ---
+warning: plain functions as contracts are deprecated
+   ┌─ [INPUTS_PATH]/errors/subcontract_nested_custom_diagnostics.ncl:19:1
+   │  
+10 │   let ParentContract = fun label value =>
+   │ ╭──────────────────────'
+11 │ │   let label =
+12 │ │     label
+13 │ │     |> std.contract.label.with_message "parent's message"
+14 │ │     |> std.contract.label.append_note "parent's note"
+15 │ │   in
+16 │ │   std.contract.apply ChildContract label value
+   │ ╰──────────────────────────────────────────────' this function
+   · │
+19 │   null | ParentContract
+   │   ^^^^ applied to this term
+   │  
+   = wrap this function using one of the constructors in `std.contract` instead, like `std.contract.from_validator` or `std.contract.custom`
+
+warning: plain functions as contracts are deprecated
+     ┌─ <stdlib/std.ncl>:1549:9
+     │
+1549 │         %contract/apply% contract (%label/push_diag% label) value,
+     │         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ applied to this term
+     │
+     ┌─ [INPUTS_PATH]/errors/subcontract_nested_custom_diagnostics.ncl:3:21
+     │  
+   3 │   let ChildContract = fun label value =>
+     │ ╭─────────────────────'
+   4 │ │   label
+   5 │ │   |> std.contract.label.with_message "child's message"
+   6 │ │   |> std.contract.label.append_note "child's note"
+   7 │ │   |> std.contract.blame
+     │ ╰───────────────────────' this function
+     │  
+     = wrap this function using one of the constructors in `std.contract` instead, like `std.contract.from_validator` or `std.contract.custom`
+
 error: contract broken by a value
        child's message
    ┌─ [INPUTS_PATH]/errors/subcontract_nested_custom_diagnostics.ncl:19:8
@@ -18,5 +54,3 @@ error: contract broken by a value
 
 note: from a parent contract violation: parent's message
  = parent's note
-
-
diff --git a/cli/tests/snapshot/snapshots/snapshot__export_stderr_non_serializable_print_path.ncl.snap b/cli/tests/snapshot/snapshots/snapshot__export_stderr_non_serializable_print_path.ncl.snap
index 24433abb..0957b1d5 100644
--- a/cli/tests/snapshot/snapshots/snapshot__export_stderr_non_serializable_print_path.ncl.snap
+++ b/cli/tests/snapshot/snapshots/snapshot__export_stderr_non_serializable_print_path.ncl.snap
@@ -2,6 +2,17 @@
 source: cli/tests/snapshot/main.rs
 expression: err
 ---
+warning: plain functions as contracts are deprecated
+   ┌─ [INPUTS_PATH]/errors/non_serializable_print_path.ncl:17:63
+   │
+ 8 │ let SomeParametricContract = fun parameter label value => value
+   │                              ---------------------------------- this function
+   ·
+17 │           inner = { qux_miss_param | SomeParametricContract = {} },
+   │                                                               ^^ applied to this term
+   │
+   = wrap this function using one of the constructors in `std.contract` instead, like `std.contract.from_validator` or `std.contract.custom`
+
 error: non serializable term
   ┌─ [INPUTS_PATH]/errors/non_serializable_print_path.ncl:8:30
   │
diff --git a/core/src/eval/tests.rs b/core/src/eval/tests.rs
index cf44485d..412ef1ff 100644
--- a/core/src/eval/tests.rs
+++ b/core/src/eval/tests.rs
@@ -1,7 +1,7 @@
 use super::cache::CacheImpl;
 use super::*;
 use crate::cache::resolvers::{DummyResolver, SimpleResolver};
-use crate::error::ImportError;
+use crate::error::{ImportError, NullReporter};
 use crate::files::Files;
 use crate::label::Label;
 use crate::parser::{grammar, lexer, ErrorTolerantParser};
@@ -14,14 +14,14 @@ use assert_matches::assert_matches;
 
 /// Evaluate a term without import support.
 fn eval_no_import(t: RichTerm) -> Result<Term, EvalError> {
-    VirtualMachine::<_, CacheImpl>::new(DummyResolver {}, std::io::sink())
+    VirtualMachine::<_, CacheImpl>::new(DummyResolver {}, std::io::sink(), NullReporter {})
         .eval(t)
         .map(Term::from)
 }
 
 /// Fully evaluate a term without import support.
 fn eval_full_no_import(t: RichTerm) -> Result<Term, EvalError> {
-    VirtualMachine::<_, CacheImpl>::new(DummyResolver {}, std::io::sink())
+    VirtualMachine::<_, CacheImpl>::new(DummyResolver {}, std::io::sink(), NullReporter {})
         .eval_full(t)
         .map(Term::from)
 }
@@ -122,7 +122,7 @@ fn asking_for_various_types() {
 
 #[test]
 fn imports() {
-    let mut vm = VirtualMachine::new(SimpleResolver::new(), std::io::sink());
+    let mut vm = VirtualMachine::new(SimpleResolver::new(), std::io::sink(), NullReporter {});
     vm.import_resolver_mut()
         .add_source(String::from("two"), String::from("1 + 1"));
     vm.import_resolver_mut()
@@ -273,29 +273,44 @@ fn initial_env() {
 
     let t = mk_term::let_one_in("x", mk_term::integer(2), mk_term::var("x"));
     assert_eq!(
-        VirtualMachine::new_with_cache(DummyResolver {}, eval_cache.clone(), std::io::sink())
-            .with_initial_env(initial_env.clone())
-            .eval(t)
-            .map(RichTerm::without_pos),
+        VirtualMachine::new_with_cache(
+            DummyResolver {},
+            eval_cache.clone(),
+            std::io::sink(),
+            NullReporter {}
+        )
+        .with_initial_env(initial_env.clone())
+        .eval(t)
+        .map(RichTerm::without_pos),
         Ok(mk_term::integer(2))
     );
 
     let t = mk_term::let_one_in("x", mk_term::integer(2), mk_term::var("g"));
     assert_eq!(
-        VirtualMachine::new_with_cache(DummyResolver {}, eval_cache.clone(), std::io::sink())
-            .with_initial_env(initial_env.clone())
-            .eval(t)
-            .map(RichTerm::without_pos),
+        VirtualMachine::new_with_cache(
+            DummyResolver {},
+            eval_cache.clone(),
+            std::io::sink(),
+            NullReporter {}
+        )
+        .with_initial_env(initial_env.clone())
+        .eval(t)
+        .map(RichTerm::without_pos),
         Ok(mk_term::integer(1))
     );
 
     // Shadowing of the initial environment
     let t = mk_term::let_one_in("g", mk_term::integer(2), mk_term::var("g"));
     assert_eq!(
-        VirtualMachine::new_with_cache(DummyResolver {}, eval_cache.clone(), std::io::sink())
-            .with_initial_env(initial_env.clone())
-            .eval(t)
-            .map(RichTerm::without_pos),
+        VirtualMachine::new_with_cache(
+            DummyResolver {},
+            eval_cache.clone(),
+            std::io::sink(),
+            NullReporter {}
+        )
+        .with_initial_env(initial_env.clone())
+        .eval(t)
+        .map(RichTerm::without_pos),
         Ok(mk_term::integer(2))
     );
 }
diff --git a/core/tests/examples/main.rs b/core/tests/examples/main.rs
index 05e863dd..df8d98ab 100644
--- a/core/tests/examples/main.rs
+++ b/core/tests/examples/main.rs
@@ -1,5 +1,5 @@
 use assert_matches::assert_matches;
-use nickel_lang_core::error::{Error, EvalError};
+use nickel_lang_core::error::{Error, EvalError, NullReporter};
 use nickel_lang_utils::{
     annotated_test::{read_annotated_test_case, TestCase},
     project_root::project_root,
@@ -14,8 +14,12 @@ fn check_example_file(path: &str) {
         read_annotated_test_case(path).expect("Failed to parse annotated program");
 
     // `test_resources` uses paths relative to the workspace manifesty
-    let mut p = TestProgram::new_from_file(project_root().join(path), std::io::stderr())
-        .expect("Failed to load program from file");
+    let mut p = TestProgram::new_from_file(
+        project_root().join(path),
+        std::io::stderr(),
+        NullReporter {},
+    )
+    .expect("Failed to load program from file");
 
     match test.annotation {
         Expectation::Pass => {
diff --git a/core/tests/integration/main.rs b/core/tests/integration/main.rs
index fceb748a..420afb81 100644
--- a/core/tests/integration/main.rs
+++ b/core/tests/integration/main.rs
@@ -2,7 +2,8 @@ use std::{io::Cursor, thread};
 
 use nickel_lang_core::{
     error::{
-        Error, EvalError, ExportError, ExportErrorData, ImportError, ParseError, TypecheckError,
+        Error, EvalError, ExportError, ExportErrorData, ImportError, NullReporter, ParseError,
+        TypecheckError,
     },
     term::Term,
     typecheck::TypecheckMode,
@@ -70,6 +71,7 @@ fn run_test(test_case: TestCase<Test>, path: String) {
             Cursor::new(program.clone()),
             path.as_str(),
             std::io::stderr(),
+            NullReporter {},
         )
         .expect("");
         if let Some(imports) = &test_case.annotation.nickel_path {
diff --git a/core/tests/integration/query.rs b/core/tests/integration/query.rs
index 21f487c8..4904f6f7 100644
--- a/core/tests/integration/query.rs
+++ b/core/tests/integration/query.rs
@@ -1,4 +1,5 @@
 use nickel_lang_core::{
+    error::NullReporter,
     eval::cache::lazy::CBNCache,
     program::Program,
     term::{
@@ -27,6 +28,7 @@ pub fn test_query_metadata_basic() {
         "{val | doc \"Test basic\" = (1 + 1)}".as_bytes(),
         "regr_tests",
         std::io::stderr(),
+        NullReporter {},
     )
     .unwrap()
     .with_field_path("val")
@@ -44,16 +46,26 @@ pub fn test_query_with_wildcard() {
     /// Checks whether `lhs` and `rhs` both evaluate to terms with the same static type
     #[track_caller]
     fn assert_types_eq(lhs: &str, rhs: &str, path: &str) {
-        let term1 = TestProgram::new_from_source(lhs.as_bytes(), "regr_tests", std::io::stderr())
-            .unwrap()
-            .with_field_path(path)
-            .query()
-            .unwrap();
-        let term2 = TestProgram::new_from_source(rhs.as_bytes(), "regr_tests", std::io::stderr())
-            .unwrap()
-            .with_field_path(path)
-            .query()
-            .unwrap();
+        let term1 = TestProgram::new_from_source(
+            lhs.as_bytes(),
+            "regr_tests",
+            std::io::stderr(),
+            NullReporter {},
+        )
+        .unwrap()
+        .with_field_path(path)
+        .query()
+        .unwrap();
+        let term2 = TestProgram::new_from_source(
+            rhs.as_bytes(),
+            "regr_tests",
+            std::io::stderr(),
+            NullReporter {},
+        )
+        .unwrap()
+        .with_field_path(path)
+        .query()
+        .unwrap();
         if let (
             Field {
                 metadata:
@@ -95,12 +107,16 @@ pub fn test_query_with_wildcard() {
     }
 
     // Without wildcard, the result has no type annotation
-    let result =
-        TestProgram::new_from_source("{value = 10}".as_bytes(), "regr_tests", std::io::stderr())
-            .unwrap()
-            .with_field_path(path)
-            .query()
-            .unwrap();
+    let result = TestProgram::new_from_source(
+        "{value = 10}".as_bytes(),
+        "regr_tests",
+        std::io::stderr(),
+        NullReporter {},
+    )
+    .unwrap()
+    .with_field_path(path)
+    .query()
+    .unwrap();
 
     assert!(matches!(
         result,
diff --git a/git/tests/main.rs b/git/tests/main.rs
new file mode 100644
index 00000000..5b91532a
--- /dev/null
+++ b/git/tests/main.rs
@@ -0,0 +1,103 @@
+use std::{path::Path, process::Command};
+
+use nickel_lang_git::Target;
+use tempfile::tempdir;
+
+fn write_contents(dir: &Path, branch: &str) {
+    let contents = dir.join("contents.txt");
+
+    let run = |cmd: &mut Command| {
+        let output = cmd.current_dir(dir).output().unwrap();
+        if !output.status.success() {
+            panic!(
+                "failed. stdout {}, stderr {}",
+                String::from_utf8_lossy(&output.stdout),
+                String::from_utf8_lossy(&output.stderr)
+            );
+        }
+    };
+
+    run(Command::new("git").args(["checkout", "-b", branch]));
+    std::fs::write(&contents, branch.as_bytes()).unwrap();
+    run(Command::new("git").args(["add", contents.as_os_str().to_str().unwrap()]));
+    run(Command::new("git").args(["commit", "-m", "foo"]));
+}
+
+fn check_contents(dir: &Path, target: Target, contents: &str) {
+    let out_dir = tempdir().unwrap();
+    let spec = nickel_lang_git::Spec {
+        url: dir.try_into().unwrap(),
+        target,
+    };
+    let _id = nickel_lang_git::fetch(&spec, out_dir.path()).unwrap();
+
+    let contents_path = out_dir.path().join("contents.txt");
+    assert_eq!(1, std::fs::read_dir(out_dir.path()).unwrap().count());
+    let actual = std::fs::read_to_string(contents_path).unwrap();
+    assert_eq!(contents, actual);
+}
+
+fn check_missing(dir: &Path, target: Target) {
+    let out_dir = tempdir().unwrap();
+    let spec = nickel_lang_git::Spec {
+        url: dir.try_into().unwrap(),
+        target,
+    };
+    let result = nickel_lang_git::fetch(&spec, out_dir.path());
+
+    assert!(matches!(
+        result,
+        Err(nickel_lang_git::Error::TargetNotFound(_))
+    ));
+}
+
+// Test that the various targets (branch, tag, commit) fetch the expected contents.
+#[test]
+fn fetch_targets() {
+    let repo = tempdir().unwrap();
+
+    let run = |cmd: &mut Command| {
+        let output = cmd.current_dir(repo.path()).output().unwrap();
+        assert!(output.status.success());
+        output.stdout
+    };
+
+    run(Command::new("git").arg("init"));
+    run(Command::new("git").args(["config", "user.email", "test-runner@example.com"]));
+    run(Command::new("git").args(["config", "user.name", "Test Runner"]));
+    run(Command::new("git").args(["branch", "-m", "main"]));
+
+    write_contents(repo.path(), "main");
+    write_contents(repo.path(), "other_branch");
+
+    run(Command::new("git").args(["tag", "a_tag"]));
+    let main_hash = run(Command::new("git").args(["rev-parse", "main"]));
+    let main_hash = std::str::from_utf8(main_hash.trim_ascii()).unwrap();
+    let branch_hash = run(Command::new("git").args(["rev-parse", "other_branch"]));
+    let branch_hash = std::str::from_utf8(branch_hash.trim_ascii()).unwrap();
+
+    check_contents(repo.path(), Target::Head, "other_branch");
+    check_contents(repo.path(), Target::Branch("main".to_owned()), "main");
+    check_contents(
+        repo.path(),
+        Target::Branch("other_branch".to_owned()),
+        "other_branch",
+    );
+    check_contents(
+        repo.path(),
+        Target::Commit(main_hash.parse().unwrap()),
+        "main",
+    );
+    check_contents(
+        repo.path(),
+        Target::Commit(branch_hash.parse().unwrap()),
+        "other_branch",
+    );
+    check_contents(repo.path(), Target::Tag("a_tag".to_owned()), "other_branch");
+
+    check_missing(repo.path(), Target::Tag("not_a_tag".to_owned()));
+    check_missing(repo.path(), Target::Branch("not_a_branch".to_owned()));
+    // We don't currently get a nice error for incorrect commit refs, because
+    // gix doesn't give us back a useful structured error: we get a
+    // FetchResponse(UploadPack(..)) with a string error message.
+}
diff --git a/utils/src/test_program.rs b/utils/src/test_program.rs
index 335d01d4..31a1136b 100644
--- a/utils/src/test_program.rs
+++ b/utils/src/test_program.rs
@@ -1,5 +1,5 @@
 use nickel_lang_core::{
-    error::{Error, ParseError},
+    error::{Error, NullReporter, ParseError},
     eval::cache::CacheImpl,
     files::Files,
     parser::{grammar, lexer, ErrorTolerantParser, ExtendedTerm},
@@ -13,9 +13,14 @@ use std::io::Cursor;
 pub type TestProgram = Program<CacheImpl>;
 
 /// Create a program from a Nickel expression provided as a string.
-pub fn program_from_expr(s: impl std::string::ToString) -> Program<CacheImpl> {
-    Program::<CacheImpl>::new_from_source(Cursor::new(s.to_string()), "test", std::io::stderr())
-        .unwrap()
+pub fn program_from_expr(s: impl std::string::ToString) -> TestProgram {
+    TestProgram::new_from_source(
+        Cursor::new(s.to_string()),
+        "test",
+        std::io::stderr(),
+        NullReporter {},
+    )
+    .unwrap()
 }
 
 pub fn eval(s: impl std::string::ToString) -> Result<Term, Error> {
@@ -50,6 +55,6 @@ pub fn typecheck_fixture(f: &str) -> Result<(), Error> {
 
 fn program_from_test_fixture(f: &str) -> Program<CacheImpl> {
     let path = format!("{}/../tests/integration/{}", env!("CARGO_MANIFEST_DIR"), f);
-    Program::new_from_file(&path, std::io::stderr())
+    Program::new_from_file(&path, std::io::stderr(), NullReporter {})
         .unwrap_or_else(|e| panic!("Could not create program from `{}`\n {}", path, e))
 }