Repository: tweag/nickel
Pull request #2094 (2024-11-15 04:21:01)

Add support for packages to nickel-lang-core
These are the changes to the core crate, picked out from the `npm` branch.

Essentially, this enables the `import pkg` syntax, and introduces `PackageMap` for figuring out how to interpret those import statements.

It also includes deserialization support for enum variants, because I ran into that while working on manifest files. That's unrelated to most of the changes here, so I could break it out as a separate PR if you want.

github-actions[bot]: <h2><a href="https://bencher.dev/perf/nickel/reports/69ff9ff8-153d-4016-96eb-ae83ad94498d?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/npm-core?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">npm-core</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=ccf38d81-11c5-48ef-b2f5-a065c5fde0a9&heads=947ed994-08c0-44a7-879c-1fd43ea92d78&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=3cfaf19e-c9c5-4341-95cd-703456d0fc9f&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1729053218000&end_time=1731645218000&report=69ff9ff8-153d-4016-96eb-ae83ad94498d&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/342ce920-58bc-4ca0-a746-688b0fd57178?model=fb26b8f8-063f-4c74-8d15-0d2505b8b3fd&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>500,150.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=ccf38d81-11c5-48ef-b2f5-a065c5fde0a9&heads=947ed994-08c0-44a7-879c-1fd43ea92d78&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=95048806-9bf2-49e4-88f2-dbcd66cbf68a&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1729053218000&end_time=1731645218000&report=69ff9ff8-153d-4016-96eb-ae83ad94498d&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/342ce920-58bc-4ca0-a746-688b0fd57178?model=fb26b8f8-063f-4c74-8d15-0d2505b8b3fd&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,500.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=ccf38d81-11c5-48ef-b2f5-a065c5fde0a9&heads=947ed994-08c0-44a7-879c-1fd43ea92d78&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=15373c31-496e-4f43-bbb4-466ef49fdd87&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1729053218000&end_time=1731645218000&report=69ff9ff8-153d-4016-96eb-ae83ad94498d&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/342ce920-58bc-4ca0-a746-688b0fd57178?model=fb26b8f8-063f-4c74-8d15-0d2505b8b3fd&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,673,700.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=ccf38d81-11c5-48ef-b2f5-a065c5fde0a9&heads=947ed994-08c0-44a7-879c-1fd43ea92d78&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=87746452-c4df-48ea-b5e9-8d66f82ca556&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1729053218000&end_time=1731645218000&report=69ff9ff8-153d-4016-96eb-ae83ad94498d&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/342ce920-58bc-4ca0-a746-688b0fd57178?model=fb26b8f8-063f-4c74-8d15-0d2505b8b3fd&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,995,800.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=ccf38d81-11c5-48ef-b2f5-a065c5fde0a9&heads=947ed994-08c0-44a7-879c-1fd43ea92d78&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=7f141ea0-9fbb-4305-bb63-78f163e546d6&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1729053218000&end_time=1731645218000&report=69ff9ff8-153d-4016-96eb-ae83ad94498d&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/342ce920-58bc-4ca0-a746-688b0fd57178?model=fb26b8f8-063f-4c74-8d15-0d2505b8b3fd&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,136,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=ccf38d81-11c5-48ef-b2f5-a065c5fde0a9&heads=947ed994-08c0-44a7-879c-1fd43ea92d78&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=8c154d79-dc68-4d93-abb3-83183a62f6a2&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1729053218000&end_time=1731645218000&report=69ff9ff8-153d-4016-96eb-ae83ad94498d&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/342ce920-58bc-4ca0-a746-688b0fd57178?model=fb26b8f8-063f-4c74-8d15-0d2505b8b3fd&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>40,879,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=ccf38d81-11c5-48ef-b2f5-a065c5fde0a9&heads=947ed994-08c0-44a7-879c-1fd43ea92d78&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=6d0222f6-0b85-4eaf-8c5c-e8957c522ef9&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1729053218000&end_time=1731645218000&report=69ff9ff8-153d-4016-96eb-ae83ad94498d&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/342ce920-58bc-4ca0-a746-688b0fd57178?model=fb26b8f8-063f-4c74-8d15-0d2505b8b3fd&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,881,600.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=ccf38d81-11c5-48ef-b2f5-a065c5fde0a9&heads=947ed994-08c0-44a7-879c-1fd43ea92d78&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=be0d3f87-d089-4b81-af44-3bbc6e831697&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1729053218000&end_time=1731645218000&report=69ff9ff8-153d-4016-96eb-ae83ad94498d&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/342ce920-58bc-4ca0-a746-688b0fd57178?model=fb26b8f8-063f-4c74-8d15-0d2505b8b3fd&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,347,200.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=ccf38d81-11c5-48ef-b2f5-a065c5fde0a9&heads=947ed994-08c0-44a7-879c-1fd43ea92d78&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=2946df33-45e4-4282-b749-040582021fe3&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1729053218000&end_time=1731645218000&report=69ff9ff8-153d-4016-96eb-ae83ad94498d&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/342ce920-58bc-4ca0-a746-688b0fd57178?model=fb26b8f8-063f-4c74-8d15-0d2505b8b3fd&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>760,570.00</td></tr><tr><td><a href="https://bencher.dev/perf/nickel/benchmarks/pidigits-100?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">pidigits 100</a></td><td>📈 <a href="https://bencher.dev/perf/nickel?branches=ccf38d81-11c5-48ef-b2f5-a065c5fde0a9&heads=947ed994-08c0-44a7-879c-1fd43ea92d78&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=5be25c57-d46f-4b48-b6f0-dee01b84220a&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1729053218000&end_time=1731645218000&report=69ff9ff8-153d-4016-96eb-ae83ad94498d&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/342ce920-58bc-4ca0-a746-688b0fd57178?model=fb26b8f8-063f-4c74-8d15-0d2505b8b3fd&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,218,800.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=ccf38d81-11c5-48ef-b2f5-a065c5fde0a9&heads=947ed994-08c0-44a7-879c-1fd43ea92d78&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=7178a975-b548-4e78-8c84-4f3802668825&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1729053218000&end_time=1731645218000&report=69ff9ff8-153d-4016-96eb-ae83ad94498d&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/342ce920-58bc-4ca0-a746-688b0fd57178?model=fb26b8f8-063f-4c74-8d15-0d2505b8b3fd&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,500,100.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=ccf38d81-11c5-48ef-b2f5-a065c5fde0a9&heads=947ed994-08c0-44a7-879c-1fd43ea92d78&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=6884f292-8796-44a8-a30c-d3cb2ed987f6&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1729053218000&end_time=1731645218000&report=69ff9ff8-153d-4016-96eb-ae83ad94498d&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/342ce920-58bc-4ca0-a746-688b0fd57178?model=fb26b8f8-063f-4c74-8d15-0d2505b8b3fd&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>9,880,100.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=ccf38d81-11c5-48ef-b2f5-a065c5fde0a9&heads=947ed994-08c0-44a7-879c-1fd43ea92d78&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=2f5f7f6a-4e54-4542-aa09-da5671953fbb&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1729053218000&end_time=1731645218000&report=69ff9ff8-153d-4016-96eb-ae83ad94498d&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/342ce920-58bc-4ca0-a746-688b0fd57178?model=fb26b8f8-063f-4c74-8d15-0d2505b8b3fd&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,090.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=ccf38d81-11c5-48ef-b2f5-a065c5fde0a9&heads=947ed994-08c0-44a7-879c-1fd43ea92d78&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=211bb66a-58f8-488b-97e4-7ea032870038&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1729053218000&end_time=1731645218000&report=69ff9ff8-153d-4016-96eb-ae83ad94498d&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/342ce920-58bc-4ca0-a746-688b0fd57178?model=fb26b8f8-063f-4c74-8d15-0d2505b8b3fd&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,516,900.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=ccf38d81-11c5-48ef-b2f5-a065c5fde0a9&heads=947ed994-08c0-44a7-879c-1fd43ea92d78&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=3f1710c5-ce75-4dfb-aadb-1435fed8a093&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1729053218000&end_time=1731645218000&report=69ff9ff8-153d-4016-96eb-ae83ad94498d&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/342ce920-58bc-4ca0-a746-688b0fd57178?model=fb26b8f8-063f-4c74-8d15-0d2505b8b3fd&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,430.00</td></tr></tbody></table></details><a href="https://bencher.dev/perf/nickel/reports/69ff9ff8-153d-4016-96eb-ae83ad94498d?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/npm-core/ubuntu-latest/rust_criterion"></div>

Patch:
diff --git a/core/src/bytecode/ast/compat.rs b/core/src/bytecode/ast/compat.rs
index e1de29bd..83ebcd79 100644
--- a/core/src/bytecode/ast/compat.rs
+++ b/core/src/bytecode/ast/compat.rs
@@ -414,7 +414,10 @@ impl<'ast> FromMainline<'ast, term::Term> for Node<'ast> {
             Term::Annotated(annot, term) => {
                 alloc.annotated(annot.to_ast(alloc), term.to_ast(alloc))
             }
-            Term::Import { path, format } => alloc.import(path.clone(), *format),
+            Term::Import(term::Import::Path { path, format }) => {
+                alloc.import_path(path.clone(), *format)
+            }
+            Term::Import(term::Import::Package { id }) => alloc.import_package(*id),
             Term::ResolvedImport(_) => panic!("didn't expect a resolved import at parsing stage"),
             Term::Type { typ, .. } => alloc.typ(typ.to_ast(alloc)),
             Term::CustomContract(_) => panic!("didn't expect a custom contract at parsing stage"),
@@ -1170,10 +1173,11 @@ impl<'ast> FromAst<Node<'ast>> for term::Term {
             Node::Annotated { annot, inner } => {
                 Term::Annotated((*annot).to_mainline(), inner.to_mainline())
             }
-            Node::Import { path, format } => Term::Import {
-                path: (*path).clone(),
+            Node::Import(Import::Path { path, format }) => Term::Import(term::Import::Path {
+                path: (*path).to_owned(),
                 format: *format,
-            },
+            }),
+            Node::Import(Import::Package { id }) => Term::Import(term::Import::Package { id: *id }),
             Node::Type(typ) => {
                 let typ: mline_type::Type = (*typ).to_mainline();
 
diff --git a/core/src/bytecode/ast/mod.rs b/core/src/bytecode/ast/mod.rs
index 5f7a3244..39aa4d2e 100644
--- a/core/src/bytecode/ast/mod.rs
+++ b/core/src/bytecode/ast/mod.rs
@@ -9,12 +9,21 @@
 //! The corresponding lifetime of all the nodes - and thus of the arena as well - is consistently
 //! called `'ast`.
 
-use std::{ffi::OsString, fmt::Debug, rc};
+use std::{
+    ffi::{OsStr, OsString},
+    fmt::Debug,
+    rc,
+};
 
 use pattern::Pattern;
 use record::Record;
 
-use crate::{cache::InputFormat, error::ParseError, identifier::LocIdent, position::TermPos};
+use crate::{
+    cache::InputFormat,
+    error::ParseError,
+    identifier::{Ident, LocIdent},
+    position::TermPos,
+};
 
 // For now, we reuse those types from the term module.
 pub use crate::term::{Number, StrChunk};
@@ -142,10 +151,7 @@ pub enum Node<'ast> {
     },
 
     /// An import.
-    Import {
-        path: &'ast OsString,
-        format: InputFormat,
-    },
+    Import(Import<'ast>),
 
     /// A type in term position, such as in `let my_contract = Number -> Number in ...`.
     ///
@@ -219,6 +225,18 @@ impl<'ast> Annotation<'ast> {
     }
 }
 
+#[derive(Clone, Debug, PartialEq, Eq)]
+/// Specifies where something should be imported from.
+pub enum Import<'ast> {
+    Path {
+        path: &'ast OsStr,
+        format: InputFormat,
+    },
+    /// Importing packges requires a [`crate::package::PackageMap`] to translate the location
+    /// to a path. The format is always Nickel.
+    Package { id: Ident },
+}
+
 /// Owns the arenas required to allocate new AST nodes and provide builder methods to create them.
 ///
 /// # Drop and arena allocation
@@ -396,11 +414,15 @@ impl AstAlloc {
         }
     }
 
-    pub fn import(&self, path: OsString, format: InputFormat) -> Node<'_> {
-        Node::Import {
+    pub fn import_path(&self, path: OsString, format: InputFormat) -> Node<'_> {
+        Node::Import(Import::Path {
             path: self.generic_arena.alloc(path),
             format,
-        }
+        })
+    }
+
+    pub fn import_package(&self, id: Ident) -> Node<'_> {
+        Node::Import(Import::Package { id })
     }
 
     /// As opposed to [Self::typ], this method takes an already constructed type and move it into
diff --git a/core/src/cache.rs b/core/src/cache.rs
index b02fe03d..6c9818f2 100644
--- a/core/src/cache.rs
+++ b/core/src/cache.rs
@@ -8,12 +8,13 @@ use crate::files::{FileId, Files};
 use crate::metrics::measure_runtime;
 #[cfg(feature = "nix-experimental")]
 use crate::nix_ffi;
+use crate::package::PackageMap;
 use crate::parser::{lexer::Lexer, ErrorTolerantParser};
 use crate::position::TermPos;
 use crate::program::FieldPath;
 use crate::stdlib::{self as nickel_stdlib, StdlibModule};
 use crate::term::record::{Field, RecordData};
-use crate::term::{RichTerm, SharedTerm, Term};
+use crate::term::{Import, RichTerm, SharedTerm, Term};
 use crate::transform::import_resolution;
 use crate::typ::UnboundTypeVariableError;
 use crate::typecheck::{self, type_check, TypecheckMode, Wildcards};
@@ -120,12 +121,19 @@ pub struct Cache {
     rev_imports: HashMap<FileId, HashSet<FileId>>,
     /// The table storing parsed terms corresponding to the entries of the file database.
     terms: HashMap<FileId, TermEntry>,
+    /// A table mapping FileIds to the package that they belong to.
+    ///
+    /// Path dependencies have already been canonicalized to absolute paths.
+    packages: HashMap<FileId, PathBuf>,
     /// The inferred type of wildcards for each `FileId`.
     wildcards: HashMap<FileId, Wildcards>,
     /// Whether processing should try to continue even in case of errors. Needed by the NLS.
     error_tolerance: ErrorTolerance,
     import_paths: Vec<PathBuf>,
 
+    /// The map used to resolve package imports.
+    package_map: Option<PackageMap>,
+
     #[cfg(debug_assertions)]
     /// Skip loading the stdlib, used for debugging purpose
     pub skip_stdlib: bool,
@@ -372,8 +380,10 @@ impl Cache {
             wildcards: HashMap::new(),
             imports: HashMap::new(),
             rev_imports: HashMap::new(),
+            packages: HashMap::new(),
             error_tolerance,
             import_paths: Vec::new(),
+            package_map: None,
 
             #[cfg(debug_assertions)]
             skip_stdlib: false,
@@ -387,6 +397,11 @@ impl Cache {
         self.import_paths.extend(paths.map(PathBuf::from));
     }
 
+    /// Sets the package map to use for package import resolution.
+    pub fn set_package_map(&mut self, map: PackageMap) {
+        self.package_map = Some(map);
+    }
+
     /// Same as [Self::add_file], but assume that the path is already normalized, and take the
     /// timestamp as a parameter.
     fn add_file_(
@@ -1410,8 +1425,7 @@ pub trait ImportResolver {
     /// already transformed in the cache and do not need further processing.
     fn resolve(
         &mut self,
-        path: &OsStr,
-        format: InputFormat,
+        import: &Import,
         parent: Option<FileId>,
         pos: &TermPos,
     ) -> Result<(ResolvedTerm, FileId), ImportError>;
@@ -1425,21 +1439,47 @@ pub trait ImportResolver {
 impl ImportResolver for Cache {
     fn resolve(
         &mut self,
-        path: &OsStr,
-        format: InputFormat,
+        import: &Import,
         parent: Option<FileId>,
         pos: &TermPos,
     ) -> Result<(ResolvedTerm, FileId), ImportError> {
-        // `parent` is the file that did the import. We first look in its containing directory.
-        let mut parent_path = parent
-            .and_then(|p| self.get_path(p))
-            .map(PathBuf::from)
-            .unwrap_or_default();
-        parent_path.pop();
+        let (possible_parents, path, pkg_id, format) = match import {
+            Import::Path { path, format } => {
+                // `parent` is the file that did the import. We first look in its containing directory, followed by
+                // the directories in the import path.
+                let mut parent_path = parent
+                    .and_then(|p| self.get_path(p))
+                    .map(PathBuf::from)
+                    .unwrap_or_default();
+                parent_path.pop();
 
-        let possible_parents: Vec<PathBuf> = std::iter::once(parent_path)
-            .chain(self.import_paths.iter().cloned())
-            .collect();
+                (
+                    std::iter::once(parent_path)
+                        .chain(self.import_paths.iter().cloned())
+                        .collect(),
+                    Path::new(path),
+                    None,
+                    *format,
+                )
+            }
+            Import::Package { id } => {
+                let package_map = self
+                    .package_map
+                    .as_ref()
+                    .ok_or(ImportError::NoPackageMap { pos: *pos })?;
+                let parent_path = parent
+                    .and_then(|p| self.packages.get(&p))
+                    .map(PathBuf::as_path);
+                let pkg_path = package_map.get(parent_path, *id, *pos)?;
+                (
+                    vec![pkg_path.to_owned()],
+                    Path::new("main.ncl"),
+                    Some(pkg_path.to_owned()),
+                    // Packages are always in nickel format
+                    InputFormat::Nickel,
+                )
+            }
+        };
 
         // Try to import from all possibilities, taking the first one that succeeds.
         let (id_op, path_buf) = possible_parents
@@ -1476,6 +1516,10 @@ impl ImportResolver for Cache {
         self.parse(file_id, format)
             .map_err(|err| ImportError::ParseErrors(err, *pos))?;
 
+        if let Some(pkg_id) = pkg_id {
+            self.packages.insert(file_id, pkg_id);
+        }
+
         Ok((result, file_id))
     }
 
@@ -1516,7 +1560,7 @@ pub fn normalize_path(path: impl Into<PathBuf>) -> std::io::Result<PathBuf> {
 /// [`std::fs::canonicalize`] can be hard to use correctly, since it can often
 /// fail, or on Windows returns annoying device paths. This is a problem Cargo
 /// needs to improve on.
-fn normalize_abs_path(path: &Path) -> PathBuf {
+pub fn normalize_abs_path(path: &Path) -> PathBuf {
     use std::path::Component;
 
     let mut components = path.components().peekable();
@@ -1545,6 +1589,43 @@ fn normalize_abs_path(path: &Path) -> PathBuf {
     ret
 }
 
+/// Normalize a relative path, removing mid-path `..`s.
+///
+/// Like [`normalize_abs_path`], this works only on the path itself
+/// (i.e. not the filesystem) and does not follow symlinks.
+pub fn normalize_rel_path(path: &Path) -> PathBuf {
+    use std::path::Component;
+
+    let mut components = path.components().peekable();
+    let mut parents = PathBuf::new();
+    let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
+        components.next();
+        PathBuf::from(c.as_os_str())
+    } else {
+        PathBuf::new()
+    };
+
+    for component in components {
+        match component {
+            Component::Prefix(..) => unreachable!(),
+            Component::RootDir => {
+                ret.push(component.as_os_str());
+            }
+            Component::CurDir => {}
+            Component::ParentDir => {
+                if !ret.pop() {
+                    parents.push(Component::ParentDir);
+                }
+            }
+            Component::Normal(c) => {
+                ret.push(c);
+            }
+        }
+    }
+    parents.extend(ret.components());
+    parents
+}
+
 /// Return the timestamp of a file. Return `None` if an IO error occurred.
 pub fn timestamp(path: impl AsRef<OsStr>) -> io::Result<SystemTime> {
     fs::metadata(path.as_ref())?.modified()
@@ -1561,8 +1642,7 @@ pub mod resolvers {
     impl ImportResolver for DummyResolver {
         fn resolve(
             &mut self,
-            _path: &OsStr,
-            _format: InputFormat,
+            _import: &Import,
             _parent: Option<FileId>,
             _pos: &TermPos,
         ) -> Result<(ResolvedTerm, FileId), ImportError> {
@@ -1603,11 +1683,14 @@ pub mod resolvers {
     impl ImportResolver for SimpleResolver {
         fn resolve(
             &mut self,
-            path: &OsStr,
-            _format: InputFormat,
+            import: &Import,
             _parent: Option<FileId>,
             pos: &TermPos,
         ) -> Result<(ResolvedTerm, FileId), ImportError> {
+            let Import::Path { path, .. } = import else {
+                panic!("simple resolver doesn't support packages");
+            };
+
             let file_id = self
                 .file_cache
                 .get(path.to_string_lossy().as_ref())
@@ -1646,3 +1729,21 @@ pub mod resolvers {
         }
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use std::path::Path;
+
+    use super::normalize_rel_path;
+    #[test]
+    fn normalize_rel() {
+        assert_eq!(
+            &normalize_rel_path(Path::new("../a/../b")),
+            Path::new("../b")
+        );
+        assert_eq!(
+            &normalize_rel_path(Path::new("../../a/../b")),
+            Path::new("../../b")
+        );
+    }
+}
diff --git a/core/src/deserialize.rs b/core/src/deserialize.rs
index 3feef79d..05336ec1 100644
--- a/core/src/deserialize.rs
+++ b/core/src/deserialize.rs
@@ -59,6 +59,10 @@ impl<'de> serde::Deserializer<'de> for RichTerm {
                 variant: v.into_label(),
                 rich_term: None,
             }),
+            Term::EnumVariant { tag, arg, .. } => visitor.visit_enum(EnumDeserializer {
+                variant: tag.into_label(),
+                rich_term: Some(arg),
+            }),
             Term::Record(record) => visit_record(record.fields, visitor),
             Term::Array(v, _) => visit_array(v, visitor),
             // unreachable(): `unwrap_term` recursively unwraps `Annotated` nodes until it
@@ -107,6 +111,7 @@ impl<'de> serde::Deserializer<'de> for RichTerm {
     {
         let (variant, rich_term) = match unwrap_term(self)? {
             Term::Enum(ident) => (ident.into_label(), None),
+            Term::EnumVariant { tag, arg, .. } => (tag.into_label(), Some(arg)),
             Term::Record(record) => {
                 let mut iter = record.fields.into_iter();
                 let (variant, value) = match iter.next() {
diff --git a/core/src/error/mod.rs b/core/src/error/mod.rs
index 58090fc4..8fff0ea6 100644
--- a/core/src/error/mod.rs
+++ b/core/src/error/mod.rs
@@ -14,7 +14,7 @@ use crate::{
     cache::Cache,
     eval::callstack::CallStack,
     files::{FileId, Files},
-    identifier::LocIdent,
+    identifier::{Ident, LocIdent},
     label::{
         self,
         ty_path::{self, PathSpan},
@@ -590,6 +590,17 @@ pub enum ImportError {
         /* error */ ParseErrors,
         /* import position */ TermPos,
     ),
+    /// A package dependency was not found.
+    MissingDependency {
+        /// The package that tried to import the missing dependency, if there was one.
+        /// This will be `None` if the missing dependency was from the top-level
+        parent: Option<std::path::PathBuf>,
+        /// The name of the package that could not be resolved.
+        missing: Ident,
+        pos: TermPos,
+    },
+    /// They tried to import a file from a package, but no package manifest was supplied.
+    NoPackageMap { pos: TermPos },
 }
 
 #[derive(Debug, PartialEq, Clone)]
@@ -2562,6 +2573,36 @@ impl IntoDiagnostics for ImportError {
 
                 diagnostic
             }
+            ImportError::MissingDependency {
+                parent,
+                missing,
+                pos,
+            } => {
+                let labels = pos
+                    .as_opt_ref()
+                    .map(|span| vec![primary(span).with_message("imported here")])
+                    .unwrap_or_default();
+                let msg = if let Some(parent_path) = parent.as_deref() {
+                    format!(
+                        "unknown package {missing}, imported from package {}",
+                        parent_path.display()
+                    )
+                } else {
+                    format!("unknown package {missing}")
+                };
+
+                vec![Diagnostic::error().with_message(msg).with_labels(labels)]
+            }
+            ImportError::NoPackageMap { pos } => {
+                let labels = pos
+                    .as_opt_ref()
+                    .map(|span| vec![primary(span).with_message("imported here")])
+                    .unwrap_or_default();
+                vec![Diagnostic::error()
+                    .with_message("tried to import from a package, but no package manifest found")
+                    .with_labels(labels)
+                    .with_notes(vec!["did you forget a --manifest-path argument?".to_owned()])]
+            }
         }
     }
 }
diff --git a/core/src/eval/mod.rs b/core/src/eval/mod.rs
index c6896598..a4f329cd 100644
--- a/core/src/eval/mod.rs
+++ b/core/src/eval/mod.rs
@@ -79,8 +79,7 @@ use crate::{
     environment::Environment as GenericEnvironment,
     error::{Error, EvalError},
     files::FileId,
-    identifier::Ident,
-    identifier::LocIdent,
+    identifier::{Ident, LocIdent},
     match_sharedterm,
     metrics::{increment, measure_runtime},
     position::TermPos,
@@ -91,7 +90,7 @@ use crate::{
         pattern::compile::Compile,
         record::{Field, RecordData},
         string::NickelString,
-        BinaryOp, BindingType, LetAttrs, MatchBranch, MatchData, RecordOpKind, RichTerm,
+        BinaryOp, BindingType, Import, LetAttrs, MatchBranch, MatchData, RecordOpKind, RichTerm,
         RuntimeContract, StrChunk, Term, UnaryOp,
     },
 };
@@ -799,12 +798,18 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
                         ));
                     }
                 }
-                Term::Import { path, .. } => {
+                Term::Import(Import::Path { path, .. }) => {
                     break Err(EvalError::InternalError(
                         format!("Unresolved import ({})", path.to_string_lossy()),
                         pos,
                     ));
                 }
+                Term::Import(Import::Package { id }) => {
+                    return Err(EvalError::InternalError(
+                        format!("Unresolved package import ({})", id),
+                        pos,
+                    ));
+                }
                 // Closurize the array if it's not already done.
                 // This *should* make it unnecessary to call closurize in [operation].
                 // See the comment on the `BinaryOp::ArrayConcat` match arm.
@@ -1190,7 +1195,7 @@ pub fn subst<C: Cache>(
         | v @ Term::ForeignId(_)
         | v @ Term::SealingKey(_)
         | v @ Term::Enum(_)
-        | v @ Term::Import{..}
+        | v @ Term::Import(_)
         | v @ Term::ResolvedImport(_)
         // We could recurse here, because types can contain terms which would then be subject to
         // substitution. Not recursing should be fine, though, because a type in term position
diff --git a/core/src/lib.rs b/core/src/lib.rs
index 895eb483..dc38f338 100644
--- a/core/src/lib.rs
+++ b/core/src/lib.rs
@@ -12,6 +12,7 @@ pub mod identifier;
 pub mod label;
 #[cfg(feature = "nix-experimental")]
 pub mod nix_ffi;
+pub mod package;
 pub mod parser;
 pub mod position;
 pub mod pretty;
diff --git a/core/src/package.rs b/core/src/package.rs
new file mode 100644
index 00000000..53ab8043
--- /dev/null
+++ b/core/src/package.rs
@@ -0,0 +1,85 @@
+//! This module contains the part of package management that's needed by the evaluator.
+//!
+//! In particular, it doesn't contain any support for version resolution or
+//! dependency fetching, but defines package maps and allows them to be used to
+//! resolve package dependencies to paths.
+
+use std::{
+    collections::HashMap,
+    path::{Path, PathBuf},
+};
+
+use crate::{error::ImportError, identifier::Ident, position::TermPos};
+
+/// Maps package imports to filesystem locations.
+///
+/// Providing one of these to the import resolver enables importing named
+/// packages, like `import foo`; this map tells the import resolver where
+/// to find the `foo` package on the filesystem.
+///
+/// Package names are package-local, in the sense that if you import `foo`
+/// and `bar`, and then `foo` also imports `bar`, then those two `bar`s
+/// are not necessarily the same package.
+#[derive(Clone, Debug, Default)]
+pub struct PackageMap {
+    /// The top-level name-to-path map. If the main file imports the name `foo`,
+    /// it gets looked up here.
+    pub top_level: HashMap<Ident, PathBuf>,
+    /// The name-to-path map for imported packages. If the package located at
+    /// `/my-path` imports `foo`, we look up `("/my-path", foo)` in this map.
+    pub packages: HashMap<(PathBuf, Ident), PathBuf>,
+}
+
+impl PackageMap {
+    /// Look up a package in the map.
+    ///
+    /// `parent` is the path of the package doing the import (if `None`, it was
+    /// the top-level package doing the import).
+    pub fn get(
+        &self,
+        parent: Option<&Path>,
+        name: Ident,
+        pos: TermPos,
+    ) -> Result<&Path, ImportError> {
+        let result = match parent {
+            Some(parent) => self.packages.get(&(parent.to_owned(), name)),
+            None => self.top_level.get(&name),
+        };
+        result
+            .map(PathBuf::as_path)
+            .ok_or_else(|| ImportError::MissingDependency {
+                parent: parent.map(Path::to_owned),
+                missing: name,
+                pos,
+            })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use std::io::Cursor;
+
+    use super::*;
+    use crate::{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
+    // files in our integration test folder. More thorough integration tests
+    // will be possible once the cli is wired up with lock-files.
+    #[test]
+    fn package_import() {
+        let pkg1 = project_root().join("core/tests/integration/inputs/imports/imported/pkg1");
+        let pkg2 = project_root().join("core/tests/integration/inputs/imports/imported/pkg2");
+
+        let map = PackageMap {
+            top_level: std::iter::once((Ident::new("pkg"), pkg1.clone())).collect(),
+            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();
+        p.set_package_map(map);
+
+        assert_eq!(p.eval_full().unwrap().as_ref(), &Term::Num(44.into()));
+    }
+}
diff --git a/core/src/parser/grammar.lalrpop b/core/src/parser/grammar.lalrpop
index 6ca0a543..d8a9495a 100644
--- a/core/src/parser/grammar.lalrpop
+++ b/core/src/parser/grammar.lalrpop
@@ -258,6 +258,9 @@ UniTerm: UniTerm = {
     "import" <s: StandardStaticString> "as" <l: @L> <t: EnumTag> <r: @R> =>? {
         Ok(UniTerm::from(mk_import_explicit(s, t, mk_span(src_id, l, r))?))
     },
+    "import" <pkg: Ident> => {
+        UniTerm::from(Term::Import(Import::Package { id: pkg.ident() }))
+    }
 };
 
 AnnotatedInfixExpr: UniTerm = {
diff --git a/core/src/parser/utils.rs b/core/src/parser/utils.rs
index f7f6cfbd..0cedb31a 100644
--- a/core/src/parser/utils.rs
+++ b/core/src/parser/utils.rs
@@ -708,7 +708,7 @@ pub fn mk_import_based_on_filename(path: String, _span: RawSpan) -> Result<Term,
     // Fall back to InputFormat::Nickel in case of unknown filename extension for backwards compatiblilty.
     let format = format.unwrap_or_default();
 
-    Ok(Term::Import { path, format })
+    Ok(Term::Import(Import::Path { path, format }))
 }
 
 pub fn mk_import_explicit(
@@ -720,7 +720,7 @@ pub fn mk_import_explicit(
     let Some(format) = InputFormat::from_tag(format.label()) else {
         return Err(ParseError::InvalidImportFormat { span });
     };
-    Ok(Term::Import { path, format })
+    Ok(Term::Import(Import::Path { path, format }))
 }
 
 /// Determine the minimal level of indentation of a multi-line string.
diff --git a/core/src/pretty.rs b/core/src/pretty.rs
index d126079b..7a04b1e3 100644
--- a/core/src/pretty.rs
+++ b/core/src/pretty.rs
@@ -9,7 +9,7 @@ use crate::term::{
     record::{Field, FieldMetadata, RecordData},
     *,
 };
-use crate::typ::*;
+use crate::{term, typ::*};
 
 use malachite::num::{basic::traits::Zero, conversion::traits::ToSci};
 use once_cell::sync::Lazy;
@@ -1110,7 +1110,7 @@ impl<'a> Pretty<'a, Allocator> for &Term {
             SealingKey(sym) => allocator.text(format!("%<sealing key: {sym}>")),
             Sealed(_i, _rt, _lbl) => allocator.text("%<sealed>"),
             Annotated(annot, rt) => allocator.atom(rt).append(annot.pretty(allocator)),
-            Import { path, format } => {
+            Import(term::Import::Path { path, format }) => {
                 docs![
                     allocator,
                     "import",
@@ -1132,6 +1132,9 @@ impl<'a> Pretty<'a, Allocator> for &Term {
                     },
                 ]
             }
+            Import(term::Import::Package { id }) => {
+                allocator.text("import ").append(id.to_string())
+            }
             ResolvedImport(id) => allocator.text(format!("import <file_id: {id:?}>")),
             // This type is in term position, so we don't need to add parentheses.
             Type { typ, contract: _ } => typ.pretty(allocator),
diff --git a/core/src/program.rs b/core/src/program.rs
index 01839725..da006db5 100644
--- a/core/src/program.rs
+++ b/core/src/program.rs
@@ -32,10 +32,12 @@ use crate::{
     identifier::LocIdent,
     label::Label,
     metrics::increment,
+    package::PackageMap,
+    position::TermPos,
     term::{
         make::{self as mk_term, builder},
         record::Field,
-        BinaryOp, MergePriority, RichTerm, Term,
+        BinaryOp, Import, MergePriority, RichTerm, RuntimeContract, Term,
     },
     typecheck::TypecheckMode,
 };
@@ -192,6 +194,9 @@ pub struct Program<EC: EvalCache> {
     /// be evaluated, but it can be set by the user (for example by the `--field` argument of the
     /// CLI) to evaluate only a specific field.
     pub field: FieldPath,
+    /// Extra contracts to apply to the main program source. Note that the contract is applied to
+    /// the whole value before fields are extracted.
+    pub contracts: Vec<RuntimeContract>,
 }
 
 /// The Possible Input Sources, anything that a Nickel program can be created from
@@ -236,6 +241,7 @@ impl<EC: EvalCache> Program<EC> {
             color_opt: colorchoice::ColorChoice::Auto,
             overrides: Vec::new(),
             field: FieldPath::new(),
+            contracts: Vec::new(),
         })
     }
 
@@ -254,19 +260,19 @@ impl<EC: EvalCache> Program<EC> {
         let merge_term = inputs
             .into_iter()
             .map(|input| match input {
-                Input::Path(path) => RichTerm::from(Term::Import {
+                Input::Path(path) => RichTerm::from(Term::Import(Import::Path {
                     path: path.into(),
                     format: InputFormat::Nickel,
-                }),
+                })),
                 Input::Source(source, name) => {
                     let path = PathBuf::from(name.into());
                     cache
                         .add_source(SourcePath::Path(path.clone(), InputFormat::Nickel), source)
                         .unwrap();
-                    RichTerm::from(Term::Import {
+                    RichTerm::from(Term::Import(Import::Path {
                         path: path.into(),
                         format: InputFormat::Nickel,
-                    })
+                    }))
                 }
             })
             .reduce(|acc, f| mk_term::op2(BinaryOp::Merge(Label::default().into()), acc, f))
@@ -285,6 +291,7 @@ impl<EC: EvalCache> Program<EC> {
             color_opt: colorchoice::ColorChoice::Auto,
             overrides: Vec::new(),
             field: FieldPath::new(),
+            contracts: Vec::new(),
         })
     }
 
@@ -367,6 +374,10 @@ impl<EC: EvalCache> Program<EC> {
         self.overrides.extend(overrides);
     }
 
+    pub fn add_contract(&mut self, contract: RuntimeContract) {
+        self.contracts.push(contract);
+    }
+
     /// Adds import paths to the end of the list.
     pub fn add_import_paths<P>(&mut self, paths: impl Iterator<Item = P>)
     where
@@ -375,6 +386,10 @@ impl<EC: EvalCache> Program<EC> {
         self.vm.import_resolver_mut().add_import_paths(paths);
     }
 
+    pub fn set_package_map(&mut self, map: PackageMap) {
+        self.vm.import_resolver_mut().set_package_map(map)
+    }
+
     /// Only parse the program, don't typecheck or evaluate. returns the [`RichTerm`] AST
     pub fn parse(&mut self) -> Result<RichTerm, Error> {
         self.vm
@@ -427,7 +442,7 @@ impl<EC: EvalCache> Program<EC> {
     fn prepare_eval_impl(&mut self, for_query: bool) -> Result<Closure, Error> {
         // If there are no overrides, we avoid the boilerplate of creating an empty record and
         // merging it with the current program
-        let prepared_body = if self.overrides.is_empty() {
+        let mut prepared_body = if self.overrides.is_empty() {
             self.vm.prepare_eval(self.main_id)?
         } else {
             let mut record = builder::Record::new();
@@ -456,6 +471,14 @@ impl<EC: EvalCache> Program<EC> {
             mk_term::op2(BinaryOp::Merge(Label::default().into()), t, built_record)
         };
 
+        if !self.contracts.is_empty() {
+            prepared_body = RuntimeContract::apply_all(
+                prepared_body,
+                self.contracts.iter().cloned(),
+                TermPos::None,
+            );
+        }
+
         let prepared = Closure::atomic_closure(prepared_body);
 
         let result = if for_query {
diff --git a/core/src/term/mod.rs b/core/src/term/mod.rs
index c893dd73..d8c0c277 100644
--- a/core/src/term/mod.rs
+++ b/core/src/term/mod.rs
@@ -25,7 +25,7 @@ use crate::{
     error::{EvalError, ParseError},
     eval::{cache::CacheIndex, Environment},
     files::FileId,
-    identifier::LocIdent,
+    identifier::{Ident, LocIdent},
     impl_display_from_pretty,
     label::{Label, MergeLabel},
     match_sharedterm,
@@ -207,7 +207,7 @@ pub enum Term {
 
     /// An unresolved import.
     #[serde(skip)]
-    Import { path: OsString, format: InputFormat },
+    Import(Import),
 
     /// A resolved import (which has already been loaded and parsed).
     #[serde(skip)]
@@ -365,16 +365,7 @@ impl PartialEq for Term {
                 l0 == r0 && l1 == r1 && l2 == r2
             }
             (Self::Annotated(l0, l1), Self::Annotated(r0, r1)) => l0 == r0 && l1 == r1,
-            (
-                Self::Import {
-                    path: l0,
-                    format: l1,
-                },
-                Self::Import {
-                    path: r0,
-                    format: r1,
-                },
-            ) => l0 == r0 && l1 == r1,
+            (Self::Import(l), Self::Import(r)) => l == r,
             (Self::ResolvedImport(l0), Self::ResolvedImport(r0)) => l0 == r0,
             (
                 Self::Type {
@@ -399,6 +390,20 @@ impl PartialEq for Term {
     }
 }
 
+#[derive(Clone, Debug, PartialEq, Eq)]
+/// Specifies where something should be imported from.
+pub enum Import {
+    Path {
+        path: OsString,
+        format: InputFormat,
+    },
+    /// Importing packges requires a [`crate::package::PackageMap`] to translate the location
+    /// to a path. The format is always Nickel.
+    Package {
+        id: Ident,
+    },
+}
+
 /// A unique sealing key, introduced by polymorphic contracts.
 pub type SealingKey = i32;
 
@@ -1000,7 +1005,7 @@ impl Term {
             | Term::Op1(_, _)
             | Term::Op2(_, _, _)
             | Term::OpN(..)
-            | Term::Import { .. }
+            | Term::Import(_)
             | Term::ResolvedImport(_)
             | Term::StrChunks(_)
             | Term::ParseError(_)
@@ -1053,7 +1058,7 @@ impl Term {
             | Term::OpN(..)
             | Term::Sealed(..)
             | Term::Annotated(..)
-            | Term::Import{..}
+            | Term::Import(_)
             | Term::ResolvedImport(_)
             | Term::StrChunks(_)
             | Term::RecRecord(..)
@@ -1115,7 +1120,7 @@ impl Term {
             | Term::OpN(..)
             | Term::Sealed(..)
             | Term::Annotated(..)
-            | Term::Import { .. }
+            | Term::Import(_)
             | Term::ResolvedImport(_)
             | Term::StrChunks(_)
             | Term::RecRecord(..)
@@ -1171,7 +1176,7 @@ impl Term {
             | Term::OpN(..)
             | Term::Sealed(..)
             | Term::Annotated(..)
-            | Term::Import{..}
+            | Term::Import(_)
             | Term::ResolvedImport(..)
             | Term::Closure(_)
             | Term::ParseError(_)
@@ -2418,7 +2423,7 @@ impl Traverse<RichTerm> for RichTerm {
             | Term::Var(_)
             | Term::Closure(_)
             | Term::Enum(_)
-            | Term::Import { .. }
+            | Term::Import(_)
             | Term::ResolvedImport(_)
             | Term::SealingKey(_)
             | Term::ForeignId(_)
@@ -2889,10 +2894,10 @@ pub mod make {
     where
         S: Into<OsString>,
     {
-        Term::Import {
+        Term::Import(Import::Path {
             path: path.into(),
             format,
-        }
+        })
         .into()
     }
 
diff --git a/core/src/transform/import_resolution.rs b/core/src/transform/import_resolution.rs
index 40b20531..6741fbb9 100644
--- a/core/src/transform/import_resolution.rs
+++ b/core/src/transform/import_resolution.rs
@@ -144,8 +144,7 @@ pub mod tolerant {
     {
         let term = rt.as_ref();
         match term {
-            Term::Import { path, format } => match resolver.resolve(path, *format, parent, &rt.pos)
-            {
+            Term::Import(import) => match resolver.resolve(import, parent, &rt.pos) {
                 Ok((_, file_id)) => (RichTerm::new(Term::ResolvedImport(file_id), rt.pos), None),
                 Err(err) => (rt, Some(err)),
             },
diff --git a/core/src/typecheck/mod.rs b/core/src/typecheck/mod.rs
index b87b26b7..c3a9e987 100644
--- a/core/src/typecheck/mod.rs
+++ b/core/src/typecheck/mod.rs
@@ -1446,7 +1446,7 @@ fn walk<V: TypecheckVisitor>(
         | Term::SealingKey(_)
         // This function doesn't recursively typecheck imports: this is the responsibility of the
         // caller.
-        | Term::Import{..}
+        | Term::Import(_)
         | Term::ResolvedImport(_) => Ok(()),
         Term::Var(x) => ctxt.type_env
             .get(&x.ident())
@@ -2402,7 +2402,7 @@ fn check<V: TypecheckVisitor>(
             .unify(mk_uniftype::sym(), state, &ctxt)
             .map_err(|err| err.into_typecheck_err(state, rt.pos)),
         Term::Sealed(_, t, _) => check(state, ctxt, visitor, t, ty),
-        Term::Import { .. } => ty
+        Term::Import(_) => ty
             .unify(mk_uniftype::dynamic(), state, &ctxt)
             .map_err(|err| err.into_typecheck_err(state, rt.pos)),
         // We use the apparent type of the import for checking. This function doesn't recursively
diff --git a/lsp/nls/src/requests/completion.rs b/lsp/nls/src/requests/completion.rs
index 792c7433..d994422d 100644
--- a/lsp/nls/src/requests/completion.rs
+++ b/lsp/nls/src/requests/completion.rs
@@ -7,7 +7,7 @@ use nickel_lang_core::{
     identifier::Ident,
     position::RawPos,
     pretty::Allocator,
-    term::{record::FieldMetadata, RichTerm, Term, UnaryOp},
+    term::{record::FieldMetadata, Import, RichTerm, Term, UnaryOp},
     typ::Type,
 };
 use pretty::{DocBuilder, Pretty};
@@ -300,7 +300,9 @@ pub fn handle_completion(
     let term = server.world.lookup_term_by_position(pos)?.cloned();
     let ident = server.world.lookup_ident_by_position(pos)?;
 
-    if let Some(Term::Import { path: import, .. }) = term.as_ref().map(|t| t.term.as_ref()) {
+    if let Some(Term::Import(Import::Path { path: import, .. })) =
+        term.as_ref().map(|t| t.term.as_ref())
+    {
         // Don't respond with anything if trigger is a `.`, as that may be the
         // start of a relative file path `./`, or the start of a file extension
         if !matches!(trigger, Some(".")) {


Test patch:
diff --git a/core/tests/integration/inputs/imports/imported/pkg1/main.ncl b/core/tests/integration/inputs/imports/imported/pkg1/main.ncl
new file mode 100644
index 00000000..810845d2
--- /dev/null
+++ b/core/tests/integration/inputs/imports/imported/pkg1/main.ncl
@@ -0,0 +1,2 @@
+# test.type = 'skip'
+1 + (import dep)
diff --git a/core/tests/integration/inputs/imports/imported/pkg2/main.ncl b/core/tests/integration/inputs/imports/imported/pkg2/main.ncl
new file mode 100644
index 00000000..e60d8649
--- /dev/null
+++ b/core/tests/integration/inputs/imports/imported/pkg2/main.ncl
@@ -0,0 +1,2 @@
+# test.type = 'skip'
+43