Repository: tweag/nickel
Pull request #2084 (2024-10-31 07:55:36)

Make files copy-on-write
This introduces our own `Files` database, and uses it instead of the one in `codespan`. The main point of this change is that our `Files` is cheaply clonable and copy-on-write (using `Vector` under the hood). Also, the loading of the stdlib text has been moved from `Cache` to `Files`. Together, this means that errors needing files for reporting can take a cloned `Files` instead of a `Program`. (I haven't actually made this change to the errors yet.)

There are a lot of changes caused by the fact that we're moving from `codespan::{Files, FileId}` to `nickel_lang_core::files::{Files, FileId}` everywhere, but most of the interesting changes are to `core/src/cache.rs`.

github-actions[bot]: <h2><a href="https://bencher.dev/perf/nickel/reports/83e623b8-8981-4170-8a87-7a3a767931ca?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/2084-merge?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">2084/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=7a02328a-2d54-4cbb-a7f0-e401707ab45b&heads=2257872a-884f-477e-8aa6-caccb0c8d676&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=3cfaf19e-c9c5-4341-95cd-703456d0fc9f&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727770102000&end_time=1730362102000&report=83e623b8-8981-4170-8a87-7a3a767931ca&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>474,280.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=7a02328a-2d54-4cbb-a7f0-e401707ab45b&heads=2257872a-884f-477e-8aa6-caccb0c8d676&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=95048806-9bf2-49e4-88f2-dbcd66cbf68a&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727770102000&end_time=1730362102000&report=83e623b8-8981-4170-8a87-7a3a767931ca&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,722,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=7a02328a-2d54-4cbb-a7f0-e401707ab45b&heads=2257872a-884f-477e-8aa6-caccb0c8d676&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=15373c31-496e-4f43-bbb4-466ef49fdd87&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727770102000&end_time=1730362102000&report=83e623b8-8981-4170-8a87-7a3a767931ca&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,860,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=7a02328a-2d54-4cbb-a7f0-e401707ab45b&heads=2257872a-884f-477e-8aa6-caccb0c8d676&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=87746452-c4df-48ea-b5e9-8d66f82ca556&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727770102000&end_time=1730362102000&report=83e623b8-8981-4170-8a87-7a3a767931ca&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,293,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=7a02328a-2d54-4cbb-a7f0-e401707ab45b&heads=2257872a-884f-477e-8aa6-caccb0c8d676&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=7f141ea0-9fbb-4305-bb63-78f163e546d6&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727770102000&end_time=1730362102000&report=83e623b8-8981-4170-8a87-7a3a767931ca&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>64,449,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=7a02328a-2d54-4cbb-a7f0-e401707ab45b&heads=2257872a-884f-477e-8aa6-caccb0c8d676&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=8c154d79-dc68-4d93-abb3-83183a62f6a2&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727770102000&end_time=1730362102000&report=83e623b8-8981-4170-8a87-7a3a767931ca&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>46,335,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=7a02328a-2d54-4cbb-a7f0-e401707ab45b&heads=2257872a-884f-477e-8aa6-caccb0c8d676&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=6d0222f6-0b85-4eaf-8c5c-e8957c522ef9&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727770102000&end_time=1730362102000&report=83e623b8-8981-4170-8a87-7a3a767931ca&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,239,400.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=7a02328a-2d54-4cbb-a7f0-e401707ab45b&heads=2257872a-884f-477e-8aa6-caccb0c8d676&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=be0d3f87-d089-4b81-af44-3bbc6e831697&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727770102000&end_time=1730362102000&report=83e623b8-8981-4170-8a87-7a3a767931ca&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,540,800.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=7a02328a-2d54-4cbb-a7f0-e401707ab45b&heads=2257872a-884f-477e-8aa6-caccb0c8d676&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=2946df33-45e4-4282-b749-040582021fe3&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727770102000&end_time=1730362102000&report=83e623b8-8981-4170-8a87-7a3a767931ca&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>795,660.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=7a02328a-2d54-4cbb-a7f0-e401707ab45b&heads=2257872a-884f-477e-8aa6-caccb0c8d676&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=5be25c57-d46f-4b48-b6f0-dee01b84220a&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727770102000&end_time=1730362102000&report=83e623b8-8981-4170-8a87-7a3a767931ca&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,197,400.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=7a02328a-2d54-4cbb-a7f0-e401707ab45b&heads=2257872a-884f-477e-8aa6-caccb0c8d676&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=7178a975-b548-4e78-8c84-4f3802668825&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727770102000&end_time=1730362102000&report=83e623b8-8981-4170-8a87-7a3a767931ca&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,535,600.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=7a02328a-2d54-4cbb-a7f0-e401707ab45b&heads=2257872a-884f-477e-8aa6-caccb0c8d676&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=6884f292-8796-44a8-a30c-d3cb2ed987f6&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727770102000&end_time=1730362102000&report=83e623b8-8981-4170-8a87-7a3a767931ca&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,598,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=7a02328a-2d54-4cbb-a7f0-e401707ab45b&heads=2257872a-884f-477e-8aa6-caccb0c8d676&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=2f5f7f6a-4e54-4542-aa09-da5671953fbb&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727770102000&end_time=1730362102000&report=83e623b8-8981-4170-8a87-7a3a767931ca&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>825,460.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=7a02328a-2d54-4cbb-a7f0-e401707ab45b&heads=2257872a-884f-477e-8aa6-caccb0c8d676&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=211bb66a-58f8-488b-97e4-7ea032870038&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727770102000&end_time=1730362102000&report=83e623b8-8981-4170-8a87-7a3a767931ca&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,499,700.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=7a02328a-2d54-4cbb-a7f0-e401707ab45b&heads=2257872a-884f-477e-8aa6-caccb0c8d676&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=3f1710c5-ce75-4dfb-aadb-1435fed8a093&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1727770102000&end_time=1730362102000&report=83e623b8-8981-4170-8a87-7a3a767931ca&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>824,870.00</td></tr></tbody></table></details><a href="https://bencher.dev/perf/nickel/reports/83e623b8-8981-4170-8a87-7a3a767931ca?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/2084-merge/ubuntu-latest/rust_criterion"></div>

Patch:
diff --git a/cli/src/error.rs b/cli/src/error.rs
index 28de2308..f2ec8e57 100644
--- a/cli/src/error.rs
+++ b/cli/src/error.rs
@@ -3,9 +3,10 @@
 use nickel_lang_core::{
     error::{
         report::{ColorOpt, ErrorFormat},
-        Diagnostic, FileId, Files, IntoDiagnostics, ParseError,
+        Diagnostic, IntoDiagnostics, ParseError,
     },
     eval::cache::lazy::CBNCache,
+    files::{FileId, Files},
     program::{FieldOverride, FieldPath, Program},
 };
 
@@ -62,12 +63,8 @@ pub enum Error {
     FailedTests,
 }
 
-impl IntoDiagnostics<FileId> for CliUsageError {
-    fn into_diagnostics(
-        self,
-        files: &mut Files<String>,
-        stdlib_ids: Option<&Vec<FileId>>,
-    ) -> Vec<Diagnostic<FileId>> {
+impl IntoDiagnostics for CliUsageError {
+    fn into_diagnostics(self, files: &mut Files) -> Vec<Diagnostic<FileId>> {
         fn mk_unknown_diags<FileId>(
             data: UnknownFieldData,
             method: &str,
@@ -120,7 +117,7 @@ impl IntoDiagnostics<FileId> for CliUsageError {
                     ])]
             }
             CliUsageError::AssignmentParseError { error } => {
-                let mut diags = IntoDiagnostics::into_diagnostics(error, files, stdlib_ids);
+                let mut diags = IntoDiagnostics::into_diagnostics(error, files);
                 diags.push(
                     Diagnostic::note()
                         .with_message("when parsing a field assignment on the command line")
@@ -135,7 +132,7 @@ impl IntoDiagnostics<FileId> for CliUsageError {
                 diags
             }
             CliUsageError::FieldPathParseError { error } => {
-                let mut diags = IntoDiagnostics::into_diagnostics(error, files, stdlib_ids);
+                let mut diags = IntoDiagnostics::into_diagnostics(error, files);
                 diags.push(
                     Diagnostic::note()
                         .with_message("when parsing a field path on the command line")
@@ -164,12 +161,8 @@ pub enum Warning {
     EmptyQueryPath,
 }
 
-impl<FileId> IntoDiagnostics<FileId> for Warning {
-    fn into_diagnostics(
-        self,
-        _files: &mut Files<String>,
-        _stdlib_ids: Option<&Vec<FileId>>,
-    ) -> Vec<Diagnostic<FileId>> {
+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![
diff --git a/core/src/bytecode/ast/compat.rs b/core/src/bytecode/ast/compat.rs
index 3a26c9d8..e1de29bd 100644
--- a/core/src/bytecode/ast/compat.rs
+++ b/core/src/bytecode/ast/compat.rs
@@ -26,7 +26,7 @@ impl<'ast> FromMainline<'ast, term::pattern::Pattern> for &'ast Pattern<'ast> {
         alloc: &'ast AstAlloc,
         pattern: &term::pattern::Pattern,
     ) -> &'ast Pattern<'ast> {
-        alloc.move_pattern(pattern.to_ast(alloc))
+        alloc.pattern(pattern.to_ast(alloc))
     }
 }
 
@@ -108,7 +108,11 @@ impl<'ast> FromMainline<'ast, term::pattern::ArrayPattern> for PatternData<'ast>
 impl<'ast> FromMainline<'ast, term::pattern::EnumPattern> for PatternData<'ast> {
     fn from_mainline(alloc: &'ast AstAlloc, enum_pat: &term::pattern::EnumPattern) -> Self {
         let pattern = enum_pat.pattern.as_ref().map(|pat| (**pat).to_ast(alloc));
-        PatternData::Enum(alloc.enum_pattern(enum_pat.tag, pattern, enum_pat.pos))
+        PatternData::Enum(alloc.enum_pattern(EnumPattern {
+            tag: enum_pat.tag,
+            pattern,
+            pos: enum_pat.pos,
+        }))
     }
 }
 
@@ -125,7 +129,10 @@ impl<'ast> FromMainline<'ast, term::pattern::ConstantPattern> for PatternData<'a
             term::pattern::ConstantPatternData::Null => ConstantPatternData::Null,
         };
 
-        PatternData::Constant(alloc.constant_pattern(data, pattern.pos))
+        PatternData::Constant(alloc.constant_pattern(ConstantPattern {
+            data,
+            pos: pattern.pos,
+        }))
     }
 }
 
@@ -241,13 +248,13 @@ impl<'ast> FromMainline<'ast, MainlineRecordRowsUnr> for RecordRowsUnr<'ast> {
     }
 }
 
-impl<'ast> FromMainline<'ast, term::Term> for &'ast Node<'ast> {
+impl<'ast> FromMainline<'ast, term::Term> for Node<'ast> {
     fn from_mainline(alloc: &'ast AstAlloc, term: &term::Term) -> Self {
         use term::Term;
 
         match term {
-            Term::Null => alloc.null(),
-            Term::Bool(b) => alloc.bool(*b),
+            Term::Null => Node::Null,
+            Term::Bool(b) => Node::Bool(*b),
             Term::Num(n) => alloc.number(n.clone()),
             Term::Str(s) => alloc.string(s),
             Term::StrChunks(chunks) => alloc.str_chunks(
@@ -261,10 +268,7 @@ impl<'ast> FromMainline<'ast, term::Term> for &'ast Node<'ast> {
                     })
                     .rev(),
             ),
-            Term::Fun(id, body) => alloc.fun(
-                alloc.generic_arena.alloc(Pattern::any(*id)),
-                body.to_ast(alloc),
-            ),
+            Term::Fun(id, body) => alloc.fun(Pattern::any(*id), body.to_ast(alloc)),
             Term::FunPattern(pat, body) => alloc.fun(pat.to_ast(alloc), body.to_ast(alloc)),
             Term::Let(bindings, body, attrs) => alloc.let_binding(
                 bindings
@@ -312,7 +316,7 @@ impl<'ast> FromMainline<'ast, term::Term> for &'ast Node<'ast> {
 
                 alloc.app(fun.to_ast(alloc), args.into_iter().rev())
             }
-            Term::Var(id) => alloc.var(*id),
+            Term::Var(id) => Node::Var(*id),
             Term::Enum(id) => alloc.enum_variant(*id, None),
             Term::EnumVariant { tag, arg, attrs: _ } => {
                 alloc.enum_variant(*tag, Some(arg.to_ast(alloc)))
@@ -412,7 +416,7 @@ impl<'ast> FromMainline<'ast, term::Term> for &'ast Node<'ast> {
             }
             Term::Import { path, format } => alloc.import(path.clone(), *format),
             Term::ResolvedImport(_) => panic!("didn't expect a resolved import at parsing stage"),
-            Term::Type { typ, .. } => alloc.typ_move(typ.to_ast(alloc)),
+            Term::Type { typ, .. } => alloc.typ(typ.to_ast(alloc)),
             Term::CustomContract(_) => panic!("didn't expect a custom contract at parsing stage"),
             Term::ParseError(error) => alloc.parse_error(error.clone()),
             Term::RuntimeError(_) => panic!("didn't expect a runtime error at parsing stage"),
@@ -425,7 +429,16 @@ impl<'ast> FromMainline<'ast, term::Term> for &'ast Node<'ast> {
 
 impl<'ast> FromMainline<'ast, term::RichTerm> for Ast<'ast> {
     fn from_mainline(alloc: &'ast AstAlloc, rterm: &term::RichTerm) -> Self {
-        alloc.ast(rterm.as_ref().to_ast(alloc), rterm.pos)
+        Ast {
+            node: rterm.as_ref().to_ast(alloc),
+            pos: rterm.pos,
+        }
+    }
+}
+
+impl<'ast> FromMainline<'ast, term::RichTerm> for &'ast Ast<'ast> {
+    fn from_mainline(alloc: &'ast AstAlloc, rterm: &term::RichTerm) -> Self {
+        alloc.ast(rterm.to_ast(alloc))
     }
 }
 
@@ -869,7 +882,7 @@ impl<'ast> FromAst<MatchBranch<'ast>> for term::MatchBranch {
     fn from_ast(branch: &MatchBranch<'ast>) -> Self {
         term::MatchBranch {
             pattern: branch.pattern.to_mainline(),
-            guard: branch.guard.map(|ast| ast.to_mainline()),
+            guard: branch.guard.as_ref().map(|ast| ast.to_mainline()),
             body: branch.body.to_mainline(),
         }
     }
@@ -1050,9 +1063,9 @@ impl<'ast> FromAst<Node<'ast>> for term::Term {
 
                 Term::StrChunks(chunks)
             }
-            Node::Fun(pat, body) => match pat.data {
+            Node::Fun { arg, body } => match arg.data {
                 PatternData::Any(id) => Term::Fun(id, body.to_mainline()),
-                _ => Term::FunPattern((*pat).to_mainline(), body.to_mainline()),
+                _ => Term::FunPattern((*arg).to_mainline(), body.to_mainline()),
             },
             Node::Let {
                 bindings,
@@ -1184,7 +1197,7 @@ impl<'ast> FromAst<Node<'ast>> for term::Term {
 
 impl<'ast> FromAst<Ast<'ast>> for term::RichTerm {
     fn from_ast(ast: &Ast<'ast>) -> Self {
-        let mut result = term::RichTerm::new(ast.node.to_mainline(), *ast.pos);
+        let mut result = term::RichTerm::new(ast.node.to_mainline(), ast.pos);
         // See [^merge-label-span]
         if let term::Term::Op2(term::BinaryOp::Merge(ref mut label), _, _) =
             term::SharedTerm::make_mut(&mut result.term)
@@ -1197,3 +1210,9 @@ impl<'ast> FromAst<Ast<'ast>> for term::RichTerm {
         result
     }
 }
+
+impl<'ast> FromAst<&'ast Ast<'ast>> for term::RichTerm {
+    fn from_ast(ast: &&'ast Ast<'ast>) -> Self {
+        FromAst::from_ast(*ast)
+    }
+}
diff --git a/core/src/bytecode/ast/mod.rs b/core/src/bytecode/ast/mod.rs
index 9005db43..7d7ede49 100644
--- a/core/src/bytecode/ast/mod.rs
+++ b/core/src/bytecode/ast/mod.rs
@@ -33,13 +33,12 @@ use typ::*;
 
 /// A Nickel AST. Contains a root node and a span.
 ///
-/// Both members are references so that `Ast` is cheap to clone - it's in fact `Copy` - and to
-/// store. Additionally, we don't expect to access the span much on the happy path, so the
-/// indirection should be ok.
-#[derive(Clone, Copy, Debug, PartialEq)]
+//TODO: we don't expect to access the span much on the happy path. Should we add an indirection
+//through a reference?
+#[derive(Clone, Debug, PartialEq)]
 pub struct Ast<'ast> {
-    node: &'ast Node<'ast>,
-    pos: &'ast TermPos,
+    node: Node<'ast>,
+    pos: TermPos,
 }
 
 /// A node of the Nickel AST.
@@ -78,18 +77,21 @@ pub enum Node<'ast> {
     StrChunks(&'ast [StrChunk<Ast<'ast>>]),
 
     /// A function.
-    Fun(&'ast Pattern<'ast>, Ast<'ast>),
+    Fun {
+        arg: &'ast Pattern<'ast>,
+        body: &'ast Ast<'ast>,
+    },
 
     /// A let-binding.
     Let {
         bindings: &'ast [(Pattern<'ast>, Ast<'ast>)],
-        body: Ast<'ast>,
+        body: &'ast Ast<'ast>,
         rec: bool,
     },
 
     /// An application to one or more arguments.
     App {
-        fun: Ast<'ast>,
+        fun: &'ast Ast<'ast>,
         args: &'ast [Ast<'ast>],
     },
 
@@ -103,7 +105,7 @@ pub enum Node<'ast> {
     /// multiple arguments.
     EnumVariant {
         tag: LocIdent,
-        arg: Option<Ast<'ast>>,
+        arg: Option<&'ast Ast<'ast>>,
     },
 
     /// A record.
@@ -111,9 +113,9 @@ pub enum Node<'ast> {
 
     /// An if-then-else expression.
     IfThenElse {
-        cond: Ast<'ast>,
-        then_branch: Ast<'ast>,
-        else_branch: Ast<'ast>,
+        cond: &'ast Ast<'ast>,
+        then_branch: &'ast Ast<'ast>,
+        else_branch: &'ast Ast<'ast>,
     },
 
     /// A match expression. This expression is still to be applied to an argument to match on.
@@ -134,7 +136,7 @@ pub enum Node<'ast> {
     /// A term with a type and/or contract annotation.
     Annotated {
         annot: &'ast Annotation<'ast>,
-        inner: Ast<'ast>,
+        inner: &'ast Ast<'ast>,
     },
 
     /// An import.
@@ -244,79 +246,68 @@ impl AstAlloc {
         }
     }
 
-    pub fn null(&self) -> &Node<'_> {
-        self.generic_arena.alloc(Node::Null)
+    pub fn node<'ast>(&'ast self, node: Node<'ast>) -> &'ast Node<'ast> {
+        self.generic_arena.alloc(node)
     }
 
-    pub fn bool(&self, value: bool) -> &Node<'_> {
-        self.generic_arena.alloc(Node::Bool(value))
+    pub fn number(&self, number: Number) -> Node<'_> {
+        Node::Number(self.number_arena.alloc(number))
     }
 
-    pub fn number(&self, number: Number) -> &Node<'_> {
-        let number = self.number_arena.alloc(number);
-        self.generic_arena.alloc(Node::Number(number))
+    pub fn string<'ast>(&'ast self, s: &str) -> Node<'ast> {
+        Node::String(self.generic_arena.alloc_str(s))
     }
 
-    pub fn string<'ast>(&'ast self, s: &str) -> &'ast Node<'ast> {
-        let s = self.generic_arena.alloc_str(s);
-        self.generic_arena.alloc(Node::String(s))
-    }
-
-    pub fn str_chunks<'ast, I>(&'ast self, chunks: I) -> &'ast Node<'ast>
+    pub fn str_chunks<'ast, I>(&'ast self, chunks: I) -> Node<'ast>
     where
         I: IntoIterator<Item = StrChunk<Ast<'ast>>>,
         I::IntoIter: ExactSizeIterator,
     {
-        let chunks = self.generic_arena.alloc_slice_fill_iter(chunks);
-        self.generic_arena.alloc(Node::StrChunks(chunks))
+        Node::StrChunks(self.generic_arena.alloc_slice_fill_iter(chunks))
     }
 
-    pub fn fun<'ast>(&'ast self, pat: &'ast Pattern<'ast>, body: Ast<'ast>) -> &'ast Node<'ast> {
-        self.generic_arena.alloc(Node::Fun(pat, body))
+    pub fn fun<'ast>(&'ast self, pat: Pattern<'ast>, body: Ast<'ast>) -> Node<'ast> {
+        let arg = self.generic_arena.alloc(pat);
+        let body = self.generic_arena.alloc(body);
+        Node::Fun { arg, body }
     }
 
-    pub fn let_binding<'ast, I>(
-        &'ast self,
-        bindings: I,
-        body: Ast<'ast>,
-        rec: bool,
-    ) -> &'ast Node<'ast>
+    pub fn let_binding<'ast, I>(&'ast self, bindings: I, body: Ast<'ast>, rec: bool) -> Node<'ast>
     where
         I: IntoIterator<Item = (Pattern<'ast>, Ast<'ast>)>,
         I::IntoIter: ExactSizeIterator,
     {
         let bindings = self.generic_arena.alloc_slice_fill_iter(bindings);
-        self.generic_arena.alloc(Node::Let {
+        let body = self.generic_arena.alloc(body);
+
+        Node::Let {
             bindings,
             body,
             rec,
-        })
+        }
     }
 
-    pub fn app<'ast, I>(&'ast self, fun: Ast<'ast>, args: I) -> &'ast Node<'ast>
+    pub fn app<'ast, I>(&'ast self, fun: Ast<'ast>, args: I) -> Node<'ast>
     where
         I: IntoIterator<Item = Ast<'ast>>,
         I::IntoIter: ExactSizeIterator,
     {
-        let args = self.generic_arena.alloc_slice_fill_iter(args);
-        self.generic_arena.alloc(Node::App { fun, args })
-    }
-
-    pub fn var(&self, ident: LocIdent) -> &Node<'_> {
-        self.generic_arena.alloc(Node::Var(ident))
+        Node::App {
+            fun: self.generic_arena.alloc(fun),
+            args: self.generic_arena.alloc_slice_fill_iter(args),
+        }
     }
 
-    pub fn enum_variant<'ast>(
-        &'ast self,
-        tag: LocIdent,
-        arg: Option<Ast<'ast>>,
-    ) -> &'ast Node<'ast> {
-        self.generic_arena.alloc(Node::EnumVariant { tag, arg })
+    pub fn enum_variant<'ast>(&'ast self, tag: LocIdent, arg: Option<Ast<'ast>>) -> Node<'ast> {
+        Node::EnumVariant {
+            tag,
+            arg: arg.map(|arg| &*self.generic_arena.alloc(arg)),
+        }
     }
 
-    pub fn record<'ast>(&'ast self, record: Record<'ast>) -> &'ast Node<'ast> {
+    pub fn record<'ast>(&'ast self, record: Record<'ast>) -> Node<'ast> {
         let record = self.generic_arena.alloc(record);
-        self.generic_arena.alloc(Node::Record(record))
+        Node::Record(record)
     }
 
     pub fn record_data<'ast, Ss, Ds>(
@@ -345,71 +336,64 @@ impl AstAlloc {
         cond: Ast<'ast>,
         then_branch: Ast<'ast>,
         else_branch: Ast<'ast>,
-    ) -> &'ast Node<'ast> {
-        self.generic_arena.alloc(Node::IfThenElse {
-            cond,
-            then_branch,
-            else_branch,
-        })
+    ) -> Node<'ast> {
+        Node::IfThenElse {
+            cond: self.generic_arena.alloc(cond),
+            then_branch: self.generic_arena.alloc(then_branch),
+            else_branch: self.generic_arena.alloc(else_branch),
+        }
     }
 
-    pub fn match_expr<'ast, I>(&'ast self, branches: I) -> &'ast Node<'ast>
+    pub fn match_expr<'ast, I>(&'ast self, branches: I) -> Node<'ast>
     where
         I: IntoIterator<Item = MatchBranch<'ast>>,
         I::IntoIter: ExactSizeIterator,
     {
-        let branches = self.generic_arena.alloc_slice_fill_iter(branches);
-        self.generic_arena.alloc(Node::Match(Match { branches }))
+        Node::Match(Match {
+            branches: self.generic_arena.alloc_slice_fill_iter(branches),
+        })
     }
 
-    pub fn array<'ast, I>(&'ast self, elts: I) -> &'ast Node<'ast>
+    pub fn array<'ast, I>(&'ast self, elts: I) -> Node<'ast>
     where
         I: IntoIterator<Item = Ast<'ast>>,
         I::IntoIter: ExactSizeIterator,
     {
-        let elts = self.generic_arena.alloc_slice_fill_iter(elts);
-        self.generic_arena.alloc(Node::Array(elts))
+        Node::Array(self.generic_arena.alloc_slice_fill_iter(elts))
     }
 
-    pub fn prim_op<'ast, I>(&'ast self, op: PrimOp, args: I) -> &'ast Node<'ast>
+    pub fn prim_op<'ast, I>(&'ast self, op: PrimOp, args: I) -> Node<'ast>
     where
         I: IntoIterator<Item = Ast<'ast>>,
         I::IntoIter: ExactSizeIterator,
     {
         let op = self.generic_arena.alloc(op);
         let args = self.generic_arena.alloc_slice_fill_iter(args);
-        self.generic_arena.alloc(Node::PrimOpApp { op, args })
+        Node::PrimOpApp { op, args }
     }
 
-    pub fn annotated<'ast>(
-        &'ast self,
-        annot: Annotation<'ast>,
-        inner: Ast<'ast>,
-    ) -> &'ast Node<'ast> {
-        let annot = self.generic_arena.alloc(annot);
-        self.generic_arena.alloc(Node::Annotated { annot, inner })
-    }
-
-    pub fn import(&self, path: OsString, format: InputFormat) -> &Node<'_> {
-        let path = self.generic_arena.alloc(path);
-        self.generic_arena.alloc(Node::Import { path, format })
+    pub fn annotated<'ast>(&'ast self, annot: Annotation<'ast>, inner: Ast<'ast>) -> Node<'ast> {
+        Node::Annotated {
+            annot: self.generic_arena.alloc(annot),
+            inner: self.generic_arena.alloc(inner),
+        }
     }
 
-    pub fn typ<'ast>(&'ast self, typ: TypeUnr<'ast>, pos: TermPos) -> &'ast Node<'ast> {
-        let type_full = self.generic_arena.alloc(Type { typ, pos });
-        self.generic_arena.alloc(Node::Type(type_full))
+    pub fn import(&self, path: OsString, format: InputFormat) -> Node<'_> {
+        Node::Import {
+            path: self.generic_arena.alloc(path),
+            format,
+        }
     }
 
     /// As opposed to [Self::typ], this method takes an already constructed type and move it into
     /// the arena, instead of taking each constituent separately.
-    pub fn typ_move<'ast>(&'ast self, typ: Type<'ast>) -> &'ast Node<'ast> {
-        let typ = self.generic_arena.alloc(typ);
-        self.generic_arena.alloc(Node::Type(typ))
+    pub fn typ<'ast>(&'ast self, typ: Type<'ast>) -> Node<'ast> {
+        Node::Type(self.generic_arena.alloc(typ))
     }
 
-    pub fn typ_unr_move<'ast>(&'ast self, typ: TypeUnr<'ast>, pos: TermPos) -> &'ast Node<'ast> {
-        let type_full = self.generic_arena.alloc(Type { typ, pos });
-        self.generic_arena.alloc(Node::Type(type_full))
+    pub fn typ_from_unr<'ast>(&'ast self, typ: TypeUnr<'ast>, pos: TermPos) -> Node<'ast> {
+        Node::Type(self.generic_arena.alloc(Type { typ, pos }))
     }
 
     pub fn types<'ast, I>(&'ast self, types: I) -> &'ast [Type<'ast>]
@@ -428,53 +412,30 @@ impl AstAlloc {
         self.generic_arena.alloc(RecordRows(rrows))
     }
 
-    pub fn parse_error(&self, error: ParseError) -> &Node<'_> {
-        let error = self.error_arena.alloc(error);
-        self.generic_arena.alloc(Node::ParseError(error))
-    }
-
-    pub fn ast<'ast>(&'ast self, node: &'ast Node<'ast>, pos: TermPos) -> Ast<'ast> {
-        let pos = self.generic_arena.alloc(pos);
-        Ast { node, pos }
+    pub fn parse_error(&self, error: ParseError) -> Node<'_> {
+        Node::ParseError(self.error_arena.alloc(error))
     }
 
-    pub fn pattern<'ast>(
-        &'ast self,
-        data: PatternData<'ast>,
-        alias: Option<LocIdent>,
-        pos: TermPos,
-    ) -> &'ast Pattern<'ast> {
-        self.generic_arena.alloc(Pattern { data, alias, pos })
+    pub fn ast<'ast>(&'ast self, ast: Ast<'ast>) -> &'ast Ast<'ast> {
+        self.generic_arena.alloc(ast)
     }
 
-    pub fn move_pattern<'ast>(&'ast self, pattern: Pattern<'ast>) -> &'ast Pattern<'ast> {
+    pub fn pattern<'ast>(&'ast self, pattern: Pattern<'ast>) -> &'ast Pattern<'ast> {
         self.generic_arena.alloc(pattern)
     }
 
     pub fn enum_pattern<'ast>(
         &'ast self,
-        tag: LocIdent,
-        pattern: Option<Pattern<'ast>>,
-        pos: TermPos,
+        enum_pattern: EnumPattern<'ast>,
     ) -> &'ast EnumPattern<'ast> {
-        self.generic_arena.alloc(EnumPattern { tag, pattern, pos })
+        self.generic_arena.alloc(enum_pattern)
     }
 
     pub fn field_pattern<'ast>(
         &'ast self,
-        matched_id: LocIdent,
-        annotation: Annotation<'ast>,
-        default: Option<Ast<'ast>>,
-        pattern: Pattern<'ast>,
-        pos: TermPos,
+        field_pat: FieldPattern<'ast>,
     ) -> &'ast FieldPattern<'ast> {
-        self.generic_arena.alloc(FieldPattern {
-            matched_id,
-            annotation,
-            default,
-            pattern,
-            pos,
-        })
+        self.generic_arena.alloc(field_pat)
     }
 
     pub fn record_pattern<'ast, I>(
@@ -517,10 +478,9 @@ impl AstAlloc {
 
     pub fn constant_pattern<'ast>(
         &'ast self,
-        data: ConstantPatternData<'ast>,
-        pos: TermPos,
+        cst_pat: ConstantPattern<'ast>,
     ) -> &'ast ConstantPattern<'ast> {
-        self.generic_arena.alloc(ConstantPattern { data, pos })
+        self.generic_arena.alloc(cst_pat)
     }
 
     pub fn or_pattern<'ast, I>(&'ast self, patterns: I, pos: TermPos) -> &'ast OrPattern<'ast>
diff --git a/core/src/bytecode/ast/typ.rs b/core/src/bytecode/ast/typ.rs
index bf6ee4d0..252999dc 100644
--- a/core/src/bytecode/ast/typ.rs
+++ b/core/src/bytecode/ast/typ.rs
@@ -6,7 +6,7 @@ pub use mainline_typ::{EnumRowF, EnumRowsF, RecordRowF, RecordRowsF, TypeF};
 
 /// The recursive unrolling of a type, that is when we "peel off" the top-level layer to find the actual
 /// structure represented by an instantiation of `TypeF`.
-pub type TypeUnr<'ast> = TypeF<&'ast Type<'ast>, RecordRows<'ast>, EnumRows<'ast>, Ast<'ast>>;
+pub type TypeUnr<'ast> = TypeF<&'ast Type<'ast>, RecordRows<'ast>, EnumRows<'ast>, &'ast Ast<'ast>>;
 
 /// The recursive unrolling of a enum rows.
 pub type EnumRowsUnr<'ast> = EnumRowsF<&'ast Type<'ast>, &'ast EnumRows<'ast>>;
diff --git a/core/src/cache.rs b/core/src/cache.rs
index 2b3fe218..b02fe03d 100644
--- a/core/src/cache.rs
+++ b/core/src/cache.rs
@@ -4,6 +4,7 @@ use crate::closurize::Closurize as _;
 use crate::error::{Error, ImportError, ParseError, ParseErrors, TypecheckError};
 use crate::eval::cache::Cache as EvalCache;
 use crate::eval::Closure;
+use crate::files::{FileId, Files};
 use crate::metrics::measure_runtime;
 #[cfg(feature = "nix-experimental")]
 use crate::nix_ffi;
@@ -18,7 +19,6 @@ use crate::typ::UnboundTypeVariableError;
 use crate::typecheck::{self, type_check, TypecheckMode, Wildcards};
 use crate::{eval, parser, transform};
 
-use codespan::{FileId, Files};
 use io::Read;
 use serde::Deserialize;
 use std::collections::hash_map;
@@ -110,7 +110,7 @@ impl InputFormat {
 #[derive(Debug, Clone)]
 pub struct Cache {
     /// The content of the program sources plus imports.
-    files: Files<String>,
+    files: Files,
     file_paths: HashMap<FileId, SourcePath>,
     /// The name-id table, holding file ids stored in the database indexed by source names.
     file_ids: HashMap<SourcePath, NameIdEntry>,
@@ -120,8 +120,6 @@ 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>,
-    /// The list of ids corresponding to the stdlib modules
-    stdlib_ids: Option<HashMap<StdlibModule, FileId>>,
     /// 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.
@@ -374,7 +372,6 @@ impl Cache {
             wildcards: HashMap::new(),
             imports: HashMap::new(),
             rev_imports: HashMap::new(),
-            stdlib_ids: None,
             error_tolerance,
             import_paths: Vec::new(),
 
@@ -458,6 +455,9 @@ impl Cache {
         Ok(self.add_string(source_name, buffer))
     }
 
+    /// Returns the source code of a file.
+    ///
+    /// Panics if the file id is invalid.
     pub fn source(&self, id: FileId) -> &str {
         self.files.source(id)
     }
@@ -561,9 +561,7 @@ impl Cache {
         format: InputFormat,
     ) -> Result<(RichTerm, ParseErrors), ParseError> {
         let attach_pos = |t: RichTerm| -> RichTerm {
-            let pos: TermPos =
-                crate::position::RawSpan::from_codespan(file_id, self.files.source_span(file_id))
-                    .into();
+            let pos: TermPos = self.files.source_span(file_id).into();
             t.with_pos(pos)
         };
 
@@ -1124,16 +1122,10 @@ impl Cache {
 
     /// Get a reference to the underlying files. Required by
     /// the WASM REPL error reporting code and LSP functions.
-    pub fn files(&self) -> &Files<String> {
+    pub fn files(&self) -> &Files {
         &self.files
     }
 
-    /// Get a mutable reference to the underlying files. Required by
-    /// [crate::error::IntoDiagnostics::into_diagnostics].
-    pub fn files_mut(&mut self) -> &mut Files<String> {
-        &mut self.files
-    }
-
     /// Get an immutable reference to the cached term roots
     pub fn terms(&self) -> &HashMap<FileId, TermEntry> {
         &self.terms
@@ -1202,16 +1194,15 @@ impl Cache {
     /// Returns true if a particular file id represents a Nickel standard library file, false
     /// otherwise.
     pub fn is_stdlib_module(&self, file: FileId) -> bool {
-        let Some(table) = &self.stdlib_ids else {
-            return false;
-        };
-        table.values().any(|stdlib_file| *stdlib_file == file)
+        self.files.is_stdlib(file)
     }
 
     /// Retrieve the FileId for a given standard libray module.
     pub fn get_submodule_file_id(&self, module: StdlibModule) -> Option<FileId> {
-        let file = self.stdlib_ids.as_ref()?.get(&module).copied()?;
-        Some(file)
+        self.files
+            .stdlib_modules()
+            .find(|(m, _id)| m == &module)
+            .map(|(_, id)| id)
     }
 
     /// Returns the set of files that this file imports.
@@ -1248,34 +1239,17 @@ impl Cache {
         ret
     }
 
-    /// Retrieve the FileIds for all the stdlib modules
-    pub fn get_all_stdlib_modules_file_id(&self) -> Option<Vec<FileId>> {
-        let ids = self.stdlib_ids.as_ref()?;
-        Some(ids.values().copied().collect())
-    }
-
     /// Load and parse the standard library in the cache.
     pub fn load_stdlib(&mut self) -> Result<CacheOp<()>, Error> {
-        if self.stdlib_ids.is_some() {
-            return Ok(CacheOp::Cached(()));
-        }
+        let mut ret = CacheOp::Cached(());
 
-        let file_ids: HashMap<StdlibModule, FileId> = nickel_stdlib::modules()
-            .into_iter()
-            .map(|module| {
-                let content = module.content();
-                (
-                    module,
-                    self.add_string(SourcePath::Std(module), String::from(content)),
-                )
-            })
-            .collect();
-
-        for (_, file_id) in file_ids.iter() {
-            self.parse(*file_id, InputFormat::Nickel)?;
+        for (_, file_id) in self.files.stdlib_modules() {
+            let op = self.parse(file_id, InputFormat::Nickel)?;
+            if matches!(op, CacheOp::Done(_)) {
+                ret = CacheOp::Done(());
+            }
         }
-        self.stdlib_ids.replace(file_ids);
-        Ok(CacheOp::Done(()))
+        Ok(ret)
     }
 
     /// Typecheck the standard library. Currently only used in the test suite.
@@ -1301,17 +1275,14 @@ impl Cache {
         &mut self,
         initial_ctxt: &typecheck::Context,
     ) -> Result<CacheOp<()>, CacheError<TypecheckError>> {
-        if let Some(ids) = self.stdlib_ids.as_ref().cloned() {
-            ids.iter()
-                .try_fold(CacheOp::Cached(()), |cache_op, (_, file_id)| {
-                    match self.typecheck(*file_id, initial_ctxt, TypecheckMode::Walk)? {
-                        done @ CacheOp::Done(()) => Ok(done),
-                        _ => Ok(cache_op),
-                    }
-                })
-        } else {
-            Err(CacheError::NotParsed)
-        }
+        self.files
+            .stdlib_modules()
+            .try_fold(CacheOp::Cached(()), |cache_op, (_, file_id)| {
+                match self.typecheck(file_id, initial_ctxt, TypecheckMode::Walk)? {
+                    done @ CacheOp::Done(()) => Ok(done),
+                    _ => Ok(cache_op),
+                }
+            })
     }
 
     /// Load, parse, and apply program transformations to the standard library. Do not typecheck for
@@ -1326,11 +1297,8 @@ impl Cache {
         self.load_stdlib()?;
         let type_ctxt = self.mk_type_ctxt().unwrap();
 
-        self.stdlib_ids
-            .as_ref()
-            .cloned()
-            .expect("cache::prepare_stdlib(): stdlib has been loaded but stdlib_ids is None")
-            .into_iter()
+        self.files
+            .stdlib_modules()
             // We need to handle the internals module separately. Each field
             // is bound directly in the environment without evaluating it first, so we can't
             // tolerate top-level let bindings that would be introduced by `transform`.
@@ -1361,19 +1329,17 @@ impl Cache {
     /// Generate the initial typing context from the list of `file_ids` corresponding to the
     /// standard library parts.
     pub fn mk_type_ctxt(&self) -> Result<typecheck::Context, CacheError<Void>> {
-        let stdlib_terms_vec: Vec<(StdlibModule, RichTerm)> =
-            self.stdlib_ids
-                .as_ref()
-                .map_or(Err(CacheError::NotParsed), |ids| {
-                    Ok(ids
-                        .iter()
-                        .map(|(module, file_id)| {
-                            (*module, self.get_owned(*file_id).expect(
-                                "cache::mk_type_env(): can't build environment, stdlib not parsed",
-                            ))
-                        })
-                        .collect())
-                })?;
+        let stdlib_terms_vec: Vec<(StdlibModule, RichTerm)> = self
+            .files
+            .stdlib_modules()
+            .map(|(module, file_id)| {
+                (
+                    module,
+                    self.get_owned(file_id)
+                        .expect("cache::mk_type_env(): can't build environment, stdlib not parsed"),
+                )
+            })
+            .collect();
         Ok(typecheck::mk_initial_ctxt(&stdlib_terms_vec).unwrap())
     }
 
@@ -1383,44 +1349,39 @@ impl Cache {
         &self,
         eval_cache: &mut EC,
     ) -> Result<eval::Environment, CacheError<Void>> {
-        if let Some(ids) = self.stdlib_ids.as_ref().cloned() {
-            let mut eval_env = eval::Environment::new();
-            ids.iter().for_each(|(module, file_id)| {
-                // The internals module needs special treatment: it's required to be a record
-                // literal, and its bindings are added directly to the environment
-                if let nickel_stdlib::StdlibModule::Internals = module {
-                    let result = eval::env_add_record(
-                        eval_cache,
-                        &mut eval_env,
-                        Closure::atomic_closure(self.get_owned(*file_id).expect(
-                            "cache::mk_eval_env(): can't build environment, stdlib not parsed",
-                        )),
-                    );
-                    if let Err(eval::EnvBuildError::NotARecord(rt)) = result {
-                        panic!(
-                            "cache::load_stdlib(): \
+        let mut eval_env = eval::Environment::new();
+        self.files.stdlib_modules().for_each(|(module, file_id)| {
+            // The internals module needs special treatment: it's required to be a record
+            // literal, and its bindings are added directly to the environment
+            if let nickel_stdlib::StdlibModule::Internals = module {
+                let result = eval::env_add_record(
+                    eval_cache,
+                    &mut eval_env,
+                    Closure::atomic_closure(self.get_owned(file_id).expect(
+                        "cache::mk_eval_env(): can't build environment, stdlib not parsed",
+                    )),
+                );
+                if let Err(eval::EnvBuildError::NotARecord(rt)) = result {
+                    panic!(
+                        "cache::load_stdlib(): \
                             expected the stdlib module {} to be a record, got {:?}",
-                            self.name(*file_id).to_string_lossy().as_ref(),
-                            rt
-                        )
-                    }
-                } else {
-                    eval::env_add(
-                        eval_cache,
-                        &mut eval_env,
-                        module.name().into(),
-                        self.get_owned(*file_id).expect(
-                            "cache::mk_eval_env(): can't build environment, stdlib not parsed",
-                        ),
-                        eval::Environment::new(),
-                    );
+                        self.name(file_id).to_string_lossy().as_ref(),
+                        rt
+                    )
                 }
-            });
+            } else {
+                eval::env_add(
+                    eval_cache,
+                    &mut eval_env,
+                    module.name().into(),
+                    self.get_owned(file_id)
+                        .expect("cache::mk_eval_env(): can't build environment, stdlib not parsed"),
+                    eval::Environment::new(),
+                );
+            }
+        });
 
-            Ok(eval_env)
-        } else {
-            Err(CacheError::NotParsed)
-        }
+        Ok(eval_env)
     }
 }
 
@@ -1622,7 +1583,7 @@ pub mod resolvers {
     /// when needed: don't use this resolver with source code that import non UTF-8 paths.
     #[derive(Clone, Default)]
     pub struct SimpleResolver {
-        files: Files<String>,
+        files: Files,
         file_cache: HashMap<String, FileId>,
         term_cache: HashMap<FileId, RichTerm>,
     }
diff --git a/core/src/error/mod.rs b/core/src/error/mod.rs
index 6376af46..58090fc4 100644
--- a/core/src/error/mod.rs
+++ b/core/src/error/mod.rs
@@ -2,9 +2,10 @@
 //!
 //! Define error types for different phases of the execution, together with functions to generate a
 //! [codespan](https://crates.io/crates/codespan-reporting) diagnostic from them.
-pub use codespan::{FileId, Files};
+use codespan::ByteIndex;
 pub use codespan_reporting::diagnostic::{Diagnostic, Label, LabelStyle};
 
+use codespan_reporting::files::Files as _;
 use codespan_reporting::term::termcolor::{ColorChoice, StandardStream, WriteColor};
 use lalrpop_util::ErrorRecovery;
 use malachite::num::conversion::traits::ToSci;
@@ -12,6 +13,7 @@ use malachite::num::conversion::traits::ToSci;
 use crate::{
     cache::Cache,
     eval::callstack::CallStack,
+    files::{FileId, Files},
     identifier::LocIdent,
     label::{
         self,
@@ -456,15 +458,11 @@ impl From<Vec<ParseError>> for ParseErrors {
     }
 }
 
-impl IntoDiagnostics<FileId> for ParseErrors {
-    fn into_diagnostics(
-        self,
-        files: &mut Files<String>,
-        stdlib_ids: Option<&Vec<FileId>>,
-    ) -> Vec<Diagnostic<FileId>> {
+impl IntoDiagnostics for ParseErrors {
+    fn into_diagnostics(self, files: &mut Files) -> Vec<Diagnostic<FileId>> {
         self.errors
             .into_iter()
-            .flat_map(|e| e.into_diagnostics(files, stdlib_ids))
+            .flat_map(|e| e.into_diagnostics(files))
             .collect()
     }
 }
@@ -815,11 +813,7 @@ impl ParseError {
         }
     }
 
-    pub fn from_serde_json(
-        error: serde_json::Error,
-        file_id: FileId,
-        files: &Files<String>,
-    ) -> Self {
+    pub fn from_serde_json(error: serde_json::Error, file_id: FileId, files: &Files) -> Self {
         use codespan::ByteOffset;
 
         // error.line() should start at `1` according to the documentation, but in practice, it may
@@ -829,10 +823,11 @@ impl ParseError {
         let line_span = if error.line() == 0 {
             None
         } else {
-            files.line_span(file_id, (error.line() - 1) as u32).ok()
+            files.line_index(file_id, error.line() - 1).ok()
         };
 
-        let start = line_span.map(|ls| ls.start() + ByteOffset::from(error.column() as i64 - 1));
+        let start =
+            line_span.map(|ls| ByteIndex::from(((ls + error.column()) as u32).saturating_sub(1)));
         ParseError::ExternalFormatError(
             String::from("json"),
             error.to_string(),
@@ -889,16 +884,15 @@ pub const INTERNAL_ERROR_MSG: &str =
  reporting it at https://github.com/tweag/nickel/issues with the above error message.";
 
 /// A trait for converting an error to a diagnostic.
-pub trait IntoDiagnostics<FileId> {
+pub trait IntoDiagnostics {
     /// Convert an error to a list of printable formatted diagnostic.
     ///
     /// # Arguments
     ///
-    /// - `files`: to know why it takes a mutable reference to `Files<String>`, see
-    ///   `label_alt`.
-    /// - `stdlib_ids` is required to format the callstack when reporting blame errors. For some
-    ///   errors (such as [`ParseError`])), contracts may not have been loaded yet, hence the
-    ///   optional. See also [`crate::eval::callstack::CallStack::group_by_calls`].
+    /// - `files`: this is a mutable reference to allow insertion of temporary snippets. Note that
+    ///   `Files` is cheaply clonable and copy-on-write, so you can easily get a mutable `Files` from
+    ///    a non-mutable one, but bear in mind that the returned diagnostics may contains file ids that
+    ///    refer to your mutated files.
     ///
     /// # Return
     ///
@@ -906,20 +900,12 @@ pub trait IntoDiagnostics<FileId> {
     /// ordered requires to sidestep a limitation of codespan. The current solution is to generate
     /// one diagnostic per callstack element. See issue
     /// [#285](https://github.com/brendanzab/codespan/issues/285).
-    fn into_diagnostics(
-        self,
-        files: &mut Files<String>,
-        stdlib_ids: Option<&Vec<FileId>>,
-    ) -> Vec<Diagnostic<FileId>>;
+    fn into_diagnostics(self, files: &mut Files) -> Vec<Diagnostic<FileId>>;
 }
 
 // Allow the use of a single `Diagnostic` directly as an error that can be reported by Nickel.
-impl<FileId> IntoDiagnostics<FileId> for Diagnostic<FileId> {
-    fn into_diagnostics(
-        self,
-        _files: &mut Files<String>,
-        _stdlib_ids: Option<&Vec<FileId>>,
-    ) -> Vec<Diagnostic<FileId>> {
+impl IntoDiagnostics for Diagnostic<FileId> {
+    fn into_diagnostics(self, _files: &mut Files) -> Vec<Diagnostic<FileId>> {
         vec![self]
     }
 }
@@ -986,7 +972,7 @@ fn label_alt(
     span_opt: Option<RawSpan>,
     alt_term: String,
     style: LabelStyle,
-    files: &mut Files<String>,
+    files: &mut Files,
 ) -> Label<FileId> {
     match span_opt {
         Some(span) => Label::new(
@@ -1005,11 +991,7 @@ fn label_alt(
 /// snippet `alt_term` if the span is `None`.
 ///
 /// See [`label_alt`].
-fn primary_alt(
-    span_opt: Option<RawSpan>,
-    alt_term: String,
-    files: &mut Files<String>,
-) -> Label<FileId> {
+fn primary_alt(span_opt: Option<RawSpan>, alt_term: String, files: &mut Files) -> Label<FileId> {
     label_alt(span_opt, alt_term, LabelStyle::Primary, files)
 }
 
@@ -1017,7 +999,7 @@ fn primary_alt(
 /// term if its span is `None`.
 ///
 /// See [`label_alt`].
-fn primary_term(term: &RichTerm, files: &mut Files<String>) -> Label<FileId> {
+fn primary_term(term: &RichTerm, files: &mut Files) -> Label<FileId> {
     primary_alt(term.pos.into_opt(), term.to_string(), files)
 }
 
@@ -1025,7 +1007,7 @@ fn primary_term(term: &RichTerm, files: &mut Files<String>) -> Label<FileId> {
 /// snippet `alt_term` if the span is `None`.
 ///
 /// See [`label_alt`].
-fn secondary_alt(span_opt: TermPos, alt_term: String, files: &mut Files<String>) -> Label<FileId> {
+fn secondary_alt(span_opt: TermPos, alt_term: String, files: &mut Files) -> Label<FileId> {
     label_alt(span_opt.into_opt(), alt_term, LabelStyle::Secondary, files)
 }
 
@@ -1033,7 +1015,7 @@ fn secondary_alt(span_opt: TermPos, alt_term: String, files: &mut Files<String>)
 /// this term if its span is `None`.
 ///
 /// See [`label_alt`].
-fn secondary_term(term: &RichTerm, files: &mut Files<String>) -> Label<FileId> {
+fn secondary_term(term: &RichTerm, files: &mut Files) -> Label<FileId> {
     secondary_alt(term.pos, term.to_string(), files)
 }
 
@@ -1050,47 +1032,32 @@ fn cardinal(number: usize) -> String {
     format!("{number}{suffix}")
 }
 
-impl IntoDiagnostics<FileId> for Error {
-    fn into_diagnostics(
-        self,
-        files: &mut Files<String>,
-        stdlib_ids: Option<&Vec<FileId>>,
-    ) -> Vec<Diagnostic<FileId>> {
+impl IntoDiagnostics for Error {
+    fn into_diagnostics(self, files: &mut Files) -> Vec<Diagnostic<FileId>> {
         match self {
             Error::ParseErrors(errs) => errs
                 .errors
                 .into_iter()
-                .flat_map(|e| e.into_diagnostics(files, stdlib_ids))
+                .flat_map(|e| e.into_diagnostics(files))
                 .collect(),
-            Error::TypecheckError(err) => err.into_diagnostics(files, stdlib_ids),
-            Error::EvalError(err) => err.into_diagnostics(files, stdlib_ids),
-            Error::ImportError(err) => err.into_diagnostics(files, stdlib_ids),
-            Error::ExportError(err) => err.into_diagnostics(files, stdlib_ids),
-            Error::IOError(err) => err.into_diagnostics(files, stdlib_ids),
-            Error::ReplError(err) => err.into_diagnostics(files, stdlib_ids),
+            Error::TypecheckError(err) => err.into_diagnostics(files),
+            Error::EvalError(err) => err.into_diagnostics(files),
+            Error::ImportError(err) => err.into_diagnostics(files),
+            Error::ExportError(err) => err.into_diagnostics(files),
+            Error::IOError(err) => err.into_diagnostics(files),
+            Error::ReplError(err) => err.into_diagnostics(files),
         }
     }
 }
 
-impl IntoDiagnostics<FileId> for EvalError {
-    fn into_diagnostics(
-        self,
-        files: &mut Files<String>,
-        stdlib_ids: Option<&Vec<FileId>>,
-    ) -> Vec<Diagnostic<FileId>> {
+impl IntoDiagnostics for EvalError {
+    fn into_diagnostics(self, files: &mut Files) -> Vec<Diagnostic<FileId>> {
         match self {
             EvalError::BlameError {
                 evaluated_arg,
                 label,
                 call_stack,
-            } => blame_error::blame_diagnostics(
-                files,
-                stdlib_ids,
-                label,
-                evaluated_arg,
-                &call_stack,
-                "",
-            ),
+            } => blame_error::blame_diagnostics(files, label, evaluated_arg, &call_stack, ""),
             EvalError::MissingFieldDef {
                 id,
                 metadata,
@@ -1163,7 +1130,7 @@ impl IntoDiagnostics<FileId> for EvalError {
                     .with_labels(labels)
                     .with_notes(vec![msg])]
             }
-            EvalError::ParseError(parse_error) => parse_error.into_diagnostics(files, stdlib_ids),
+            EvalError::ParseError(parse_error) => parse_error.into_diagnostics(files),
             EvalError::NotAFunc(t, arg, pos_opt) => vec![Diagnostic::error()
                 .with_message("not a function")
                 .with_labels(vec![
@@ -1373,7 +1340,7 @@ impl IntoDiagnostics<FileId> for EvalError {
                     .with_labels(labels)
                     .with_notes(vec![String::from(INTERNAL_ERROR_MSG)])]
             }
-            EvalError::SerializationError(err) => err.into_diagnostics(files, stdlib_ids),
+            EvalError::SerializationError(err) => err.into_diagnostics(files),
             EvalError::DeserializationError(format, msg, span_opt) => {
                 let labels = span_opt
                     .as_opt_ref()
@@ -1491,7 +1458,6 @@ impl IntoDiagnostics<FileId> for EvalError {
                 call_stack,
             } => blame_error::blame_diagnostics(
                 files,
-                stdlib_ids,
                 contract_label,
                 evaluated_arg,
                 &call_stack,
@@ -1508,7 +1474,7 @@ impl IntoDiagnostics<FileId> for EvalError {
                 arg_pos,
                 arg_evaluated,
             )
-            .into_diagnostics(files, stdlib_ids),
+            .into_diagnostics(files),
             EvalError::NAryPrimopTypeError {
                 primop,
                 expected,
@@ -1524,7 +1490,7 @@ impl IntoDiagnostics<FileId> for EvalError {
                 arg_pos,
                 arg_evaluated,
             )
-            .into_diagnostics(files, stdlib_ids),
+            .into_diagnostics(files),
             EvalError::QueryNonRecord { pos, id, value } => {
                 let label = format!(
                     "tried to query field `{}`, but the expression has type {}",
@@ -1551,11 +1517,11 @@ impl IntoDiagnostics<FileId> for EvalError {
 
 /// Common functionality for formatting blame errors.
 mod blame_error {
-    use codespan::{FileId, Files};
     use codespan_reporting::diagnostic::{Diagnostic, Label};
 
     use crate::{
         eval::callstack::CallStack,
+        files::{FileId, Files},
         label::{
             self,
             ty_path::{self, PathSpan},
@@ -1597,8 +1563,7 @@ mod blame_error {
         evaluated_arg: Option<RichTerm>,
         blame_label: &label::Label,
         path_label: Label<FileId>,
-        files: &mut Files<String>,
-        stdlib_ids: Option<&Vec<FileId>>,
+        files: &mut Files,
     ) -> Vec<Label<FileId>> {
         let mut labels = vec![path_label];
 
@@ -1608,11 +1573,7 @@ mod blame_error {
             // point to the builtin implementation contract like `func` or `record`, so
             // there's no good reason to show it. Note than even in that case, the
             // information contained at the argument index can still be useful.
-            if stdlib_ids
-                .as_ref()
-                .map(|ctrs_id| !ctrs_id.contains(&arg_pos.src_id))
-                .unwrap_or(true)
-            {
+            if !files.is_stdlib(arg_pos.src_id) {
                 labels.push(primary(arg_pos).with_message("applied to this expression"));
             }
         }
@@ -1621,14 +1582,10 @@ mod blame_error {
         // we can try to show more information about the final, evaluated value that is
         // responsible for the blame.
         if let Some(mut evaluated_arg) = evaluated_arg {
-            match (
-                evaluated_arg.pos,
-                blame_label.arg_pos.as_opt_ref(),
-                stdlib_ids,
-            ) {
+            match (evaluated_arg.pos, blame_label.arg_pos.as_opt_ref()) {
                 // Avoid showing a position inside builtin contracts, it's rarely
                 // informative.
-                (TermPos::Original(val_pos), _, Some(c_id)) if c_id.contains(&val_pos.src_id) => {
+                (TermPos::Original(val_pos), _) if files.is_stdlib(val_pos.src_id) => {
                     evaluated_arg.pos = TermPos::None;
                     labels.push(
                         secondary_term(&evaluated_arg, files)
@@ -1637,14 +1594,14 @@ mod blame_error {
                 }
                 // Do not show the same thing twice: if arg_pos and val_pos are the same,
                 // the first label "applied to this value" is sufficient.
-                (TermPos::Original(ref val_pos), Some(arg_pos), _) if val_pos == arg_pos => {}
-                (TermPos::Original(ref val_pos), ..) => {
+                (TermPos::Original(ref val_pos), Some(arg_pos)) if val_pos == arg_pos => {}
+                (TermPos::Original(ref val_pos), _) => {
                     labels.push(secondary(val_pos).with_message("evaluated to this expression"))
                 }
                 // If the final element is a direct reduct of the original value, rather
                 // print the actual value than referring to the same position as
                 // before.
-                (TermPos::Inherited(ref val_pos), Some(arg_pos), _) if val_pos == arg_pos => {
+                (TermPos::Inherited(ref val_pos), Some(arg_pos)) if val_pos == arg_pos => {
                     evaluated_arg.pos = TermPos::None;
                     labels.push(
                         secondary_term(&evaluated_arg, files)
@@ -1653,12 +1610,8 @@ mod blame_error {
                 }
                 // Finally, if the parameter reduced to a value which originates from a
                 // different expression, show both the expression and the value.
-                (TermPos::Inherited(ref val_pos), _, ids) => {
-                    if ids
-                        .as_ref()
-                        .map(|cids| !cids.contains(&val_pos.src_id))
-                        .unwrap_or(true)
-                    {
+                (TermPos::Inherited(ref val_pos), _) => {
+                    if !files.is_stdlib(val_pos.src_id) {
                         labels
                             .push(secondary(val_pos).with_message("evaluated to this expression"));
                     }
@@ -1669,7 +1622,7 @@ mod blame_error {
                             .with_message("evaluated to this value"),
                     );
                 }
-                (TermPos::None, ..) => labels.push(
+                (TermPos::None, _) => labels.push(
                     secondary_term(&evaluated_arg, files).with_message("evaluated to this value"),
                 ),
             }
@@ -1679,12 +1632,12 @@ mod blame_error {
     }
 
     pub trait ExtendWithCallStack {
-        fn extend_with_call_stack(&mut self, stdlib_ids: &[FileId], call_stack: &CallStack);
+        fn extend_with_call_stack(&mut self, files: &Files, call_stack: &CallStack);
     }
 
     impl ExtendWithCallStack for Vec<Diagnostic<FileId>> {
-        fn extend_with_call_stack(&mut self, stdlib_ids: &[FileId], call_stack: &CallStack) {
-            let (calls, curr_call) = call_stack.group_by_calls(stdlib_ids);
+        fn extend_with_call_stack(&mut self, files: &Files, call_stack: &CallStack) {
+            let (calls, curr_call) = call_stack.group_by_calls(files);
             let diag_curr_call = curr_call.map(|cdescr| {
                 let name = cdescr
                     .head
@@ -1713,7 +1666,7 @@ mod blame_error {
     /// subtype isn't defined), [path_span] pretty-prints the type inside a new source, parses it,
     /// and calls `ty_path::span`. This new type is guaranteed to have all of its positions set,
     /// providing a definite `PathSpan`. This is similar to the behavior of [`super::primary_alt`].
-    pub fn path_span(files: &mut Files<String>, path: &[ty_path::Elem], ty: &Type) -> PathSpan {
+    pub fn path_span(files: &mut Files, path: &[ty_path::Elem], ty: &Type) -> PathSpan {
         use crate::parser::{grammar::FixedTypeParser, lexer::Lexer, ErrorTolerantParser};
 
         ty_path::span(path.iter().peekable(), ty)
@@ -1735,7 +1688,7 @@ mod blame_error {
 
     /// Generate a codespan label that describes the [type path][crate::label::ty_path::Path] of a
     /// (Nickel) label.
-    pub fn report_ty_path(files: &mut Files<String>, l: &label::Label) -> Label<FileId> {
+    pub fn report_ty_path(files: &mut Files, l: &label::Label) -> Label<FileId> {
         let PathSpan {
             span,
             last,
@@ -1812,8 +1765,7 @@ is None but last_arrow_elem is Some"
     /// leading "contract broken by .." and the custom contract diagnostic message in tail
     /// position.
     pub fn blame_diagnostics(
-        files: &mut Files<String>,
-        stdlib_ids: Option<&Vec<FileId>>,
+        files: &mut Files,
         mut label: label::Label,
         evaluated_arg: Option<RichTerm>,
         call_stack: &CallStack,
@@ -1857,7 +1809,7 @@ is None but last_arrow_elem is Some"
             .unwrap_or_default();
         let path_label = report_ty_path(files, &label);
 
-        let labels = build_diagnostic_labels(evaluated_arg, &label, path_label, files, stdlib_ids);
+        let labels = build_diagnostic_labels(evaluated_arg, &label, path_label, files);
 
         // If there are notes in the head contract diagnostic, we build the first
         // diagnostic using them and will put potential generated notes on higher-order
@@ -1888,26 +1840,19 @@ is None but last_arrow_elem is Some"
             );
         }
 
-        match stdlib_ids {
-            Some(id) if !ty_path::has_no_dom(&label.path) => {
-                diagnostics.extend_with_call_stack(id, call_stack)
-            }
-            _ => (),
-        };
+        if !ty_path::has_no_dom(&label.path) {
+            diagnostics.extend_with_call_stack(files, call_stack);
+        }
 
         diagnostics
     }
 }
 
-impl IntoDiagnostics<FileId> for ParseError {
-    fn into_diagnostics(
-        self,
-        files: &mut Files<String>,
-        _stdlib_ids: Option<&Vec<FileId>>,
-    ) -> Vec<Diagnostic<FileId>> {
+impl IntoDiagnostics for ParseError {
+    fn into_diagnostics(self, files: &mut Files) -> Vec<Diagnostic<FileId>> {
         let diagnostic = match self {
             ParseError::UnexpectedEOF(file_id, _expected) => {
-                let end = files.source_span(file_id).end();
+                let end = files.source_span(file_id).end;
                 Diagnostic::error()
                     .with_message(format!(
                         "unexpected end of file when parsing {}",
@@ -2122,12 +2067,8 @@ impl IntoDiagnostics<FileId> for ParseError {
     }
 }
 
-impl IntoDiagnostics<FileId> for TypecheckError {
-    fn into_diagnostics(
-        self,
-        files: &mut Files<String>,
-        stdlib_ids: Option<&Vec<FileId>>,
-    ) -> Vec<Diagnostic<FileId>> {
+impl IntoDiagnostics for TypecheckError {
+    fn into_diagnostics(self, files: &mut Files) -> Vec<Diagnostic<FileId>> {
         fn mk_expr_label(span_opt: &TermPos) -> Vec<Label<FileId>> {
             span_opt
                 .as_opt_ref()
@@ -2147,7 +2088,7 @@ impl IntoDiagnostics<FileId> for TypecheckError {
             TypecheckError::UnboundIdentifier { id, pos } =>
             // Use the same diagnostic as `EvalError::UnboundIdentifier` for consistency.
             {
-                EvalError::UnboundIdentifier(id, pos).into_diagnostics(files, stdlib_ids)
+                EvalError::UnboundIdentifier(id, pos).into_diagnostics(files)
             }
             TypecheckError::MissingRow {
                 id,
@@ -2327,12 +2268,10 @@ impl IntoDiagnostics<FileId> for TypecheckError {
                 // We generate a diagnostic for the underlying error, but append a prefix to the
                 // error message to make it clear that this is not a separate error but a more
                 // precise description of why the unification of a row failed.
-                diags.extend((*err).into_diagnostics(files, stdlib_ids).into_iter().map(
-                    |mut diag| {
-                        diag.message = format!("while typing field `{}`: {}", field, diag.message);
-                        diag
-                    },
-                ));
+                diags.extend((*err).into_diagnostics(files).into_iter().map(|mut diag| {
+                    diag.message = format!("while typing field `{}`: {}", field, diag.message);
+                    diag
+                }));
                 diags
             }
             TypecheckError::EnumRowMismatch {
@@ -2383,13 +2322,10 @@ impl IntoDiagnostics<FileId> for TypecheckError {
                 // the error message to make it clear that this is not a separate error but a more
                 // precise description of why the unification of a row failed.
                 if let Some(err) = cause {
-                    diags.extend((*err).into_diagnostics(files, stdlib_ids).into_iter().map(
-                        |mut diag| {
-                            diag.message =
-                                format!("while typing enum row `{id}`: {}", diag.message);
-                            diag
-                        },
-                    ));
+                    diags.extend((*err).into_diagnostics(files).into_iter().map(|mut diag| {
+                        diag.message = format!("while typing enum row `{id}`: {}", diag.message);
+                        diag
+                    }));
                 }
 
                 diags
@@ -2504,13 +2440,11 @@ impl IntoDiagnostics<FileId> for TypecheckError {
                     // information, so we just ignore it.
                     TypecheckError::TypeMismatch { .. } => (),
                     err => {
-                        diags.extend(err.into_diagnostics(files, stdlib_ids).into_iter().map(
-                            |mut diag| {
-                                diag.message =
-                                    format!("while matching function types: {}", diag.message);
-                                diag
-                            },
-                        ));
+                        diags.extend(err.into_diagnostics(files).into_iter().map(|mut diag| {
+                            diag.message =
+                                format!("while matching function types: {}", diag.message);
+                            diag
+                        }));
                     }
                 }
 
@@ -2600,12 +2534,8 @@ impl IntoDiagnostics<FileId> for TypecheckError {
     }
 }
 
-impl IntoDiagnostics<FileId> for ImportError {
-    fn into_diagnostics(
-        self,
-        files: &mut Files<String>,
-        stdlib_ids: Option<&Vec<FileId>>,
-    ) -> Vec<Diagnostic<FileId>> {
+impl IntoDiagnostics for ImportError {
+    fn into_diagnostics(self, files: &mut Files) -> Vec<Diagnostic<FileId>> {
         match self {
             ImportError::IOError(path, error, span_opt) => {
                 let labels = span_opt
@@ -2621,7 +2551,7 @@ impl IntoDiagnostics<FileId> for ImportError {
                 let mut diagnostic: Vec<Diagnostic<FileId>> = error
                     .errors
                     .into_iter()
-                    .flat_map(|e| e.into_diagnostics(files, stdlib_ids))
+                    .flat_map(|e| e.into_diagnostics(files))
                     .collect();
 
                 if let Some(span) = span_opt.as_opt_ref() {
@@ -2636,12 +2566,8 @@ impl IntoDiagnostics<FileId> for ImportError {
     }
 }
 
-impl IntoDiagnostics<FileId> for ExportError {
-    fn into_diagnostics(
-        self,
-        files: &mut Files<String>,
-        _stdlib_ids: Option<&Vec<FileId>>,
-    ) -> Vec<Diagnostic<FileId>> {
+impl IntoDiagnostics for ExportError {
+    fn into_diagnostics(self, files: &mut Files) -> Vec<Diagnostic<FileId>> {
         let mut notes = if !self.path.0.is_empty() {
             vec![format!("When exporting field `{}`", self.path)]
         } else {
@@ -2715,31 +2641,23 @@ impl IntoDiagnostics<FileId> for ExportError {
     }
 }
 
-impl IntoDiagnostics<FileId> for IOError {
-    fn into_diagnostics(
-        self,
-        _files: &mut Files<String>,
-        _stdlib_ids: Option<&Vec<FileId>>,
-    ) -> Vec<Diagnostic<FileId>> {
+impl IntoDiagnostics for IOError {
+    fn into_diagnostics(self, _fil: &mut Files) -> Vec<Diagnostic<FileId>> {
         match self {
             IOError(msg) => vec![Diagnostic::error().with_message(msg)],
         }
     }
 }
 
-impl IntoDiagnostics<FileId> for ReplError {
-    fn into_diagnostics(
-        self,
-        files: &mut Files<String>,
-        stdlib_ids: Option<&Vec<FileId>>,
-    ) -> Vec<Diagnostic<FileId>> {
+impl IntoDiagnostics for ReplError {
+    fn into_diagnostics(self, files: &mut Files) -> Vec<Diagnostic<FileId>> {
         match self {
             ReplError::UnknownCommand(s) => vec![Diagnostic::error()
                 .with_message(format!("unknown command `{s}`"))
                 .with_notes(vec![String::from(
                     "type `:?` or `:help` for a list of available commands.",
                 )])],
-            ReplError::InvalidQueryPath(err) => err.into_diagnostics(files, stdlib_ids),
+            ReplError::InvalidQueryPath(err) => err.into_diagnostics(files),
             ReplError::MissingArg { cmd, msg_opt } => {
                 let mut notes = msg_opt
                     .as_ref()
diff --git a/core/src/error/report.rs b/core/src/error/report.rs
index 4b203d3b..ec8b02f6 100644
--- a/core/src/error/report.rs
+++ b/core/src/error/report.rs
@@ -60,7 +60,7 @@ impl Default for ColorOpt {
 ///
 /// - `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<FileId>>(
+pub fn report<E: IntoDiagnostics>(
     cache: &mut Cache,
     error: E,
     format: ErrorFormat,
@@ -68,11 +68,9 @@ pub fn report<E: IntoDiagnostics<FileId>>(
 ) {
     use std::io::{stderr, IsTerminal};
 
-    let stdlib_ids = cache.get_all_stdlib_modules_file_id();
     report_with(
         &mut StandardStream::stderr(color_opt.for_terminal(stderr().is_terminal())).lock(),
-        cache.files_mut(),
-        stdlib_ids.as_ref(),
+        &mut cache.files().clone(),
         error,
         format,
     )
@@ -84,7 +82,7 @@ pub fn report<E: IntoDiagnostics<FileId>>(
 ///
 /// - `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<FileId>>(
+pub fn report_to_stdout<E: IntoDiagnostics>(
     cache: &mut Cache,
     error: E,
     format: ErrorFormat,
@@ -92,26 +90,23 @@ pub fn report_to_stdout<E: IntoDiagnostics<FileId>>(
 ) {
     use std::io::{stdout, IsTerminal};
 
-    let stdlib_ids = cache.get_all_stdlib_modules_file_id();
     report_with(
         &mut StandardStream::stdout(color_opt.for_terminal(stdout().is_terminal())).lock(),
-        cache.files_mut(),
-        stdlib_ids.as_ref(),
+        &mut cache.files().clone(),
         error,
         format,
     )
 }
 
 /// Report an error on `stderr`, provided a file database and a list of stdlib file ids.
-pub fn report_with<E: IntoDiagnostics<FileId>>(
+pub fn report_with<E: IntoDiagnostics>(
     writer: &mut dyn WriteColor,
-    files: &mut Files<String>,
-    stdlib_ids: Option<&Vec<FileId>>,
+    files: &mut Files,
     error: E,
     format: ErrorFormat,
 ) {
     let config = codespan_reporting::term::Config::default();
-    let diagnostics = error.into_diagnostics(files, stdlib_ids);
+    let diagnostics = error.into_diagnostics(files);
     let stderr = std::io::stderr();
 
     let result = match format {
diff --git a/core/src/eval/callstack.rs b/core/src/eval/callstack.rs
index bc2cd56a..cfe24a02 100644
--- a/core/src/eval/callstack.rs
+++ b/core/src/eval/callstack.rs
@@ -2,10 +2,10 @@
 //! application is evaluated. Additional information about the history of function calls is thus
 //! stored in a call stack solely for better error reporting.
 use crate::{
+    files::Files,
     identifier::LocIdent,
     position::{RawSpan, TermPos},
 };
-use codespan::FileId;
 
 /// A call stack, saving the history of function calls.
 #[derive(PartialEq, Eq, Clone, Default, Debug)]
@@ -128,10 +128,7 @@ impl CallStack {
     ///
     /// - `stdlib_ids`: the `FileId`s of the sources containing standard contracts, to filter their
     ///   calls out.
-    pub fn group_by_calls(
-        self: &CallStack,
-        stdlib_ids: &[FileId],
-    ) -> (Vec<CallDescr>, Option<CallDescr>) {
+    pub fn group_by_calls(self: &CallStack, files: &Files) -> (Vec<CallDescr>, Option<CallDescr>) {
         // We filter out calls and accesses made from within the builtin contracts, as well as
         // generated variables introduced by program transformations.
         let it = self.0.iter().filter(|elem| match elem {
@@ -145,7 +142,7 @@ impl CallStack {
             // We avoid applications (Fun/App) with inherited positions. Such calls include
             // contracts applications which add confusing call items whose positions don't point to
             // an actual call in the source.
-                if !stdlib_ids.contains(src_id) =>
+                if !files.is_stdlib(*src_id) =>
             {
                 true
             }
diff --git a/core/src/eval/mod.rs b/core/src/eval/mod.rs
index 787e4d3f..c6896598 100644
--- a/core/src/eval/mod.rs
+++ b/core/src/eval/mod.rs
@@ -78,6 +78,7 @@ use crate::{
     closurize::{closurize_rec_record, Closurize},
     environment::Environment as GenericEnvironment,
     error::{Error, EvalError},
+    files::FileId,
     identifier::Ident,
     identifier::LocIdent,
     match_sharedterm,
@@ -105,7 +106,6 @@ pub mod operation;
 pub mod stack;
 
 use callstack::*;
-use codespan::FileId;
 use operation::OperationCont;
 use stack::{Stack, StrAccData};
 
diff --git a/core/src/files.rs b/core/src/files.rs
new file mode 100644
index 00000000..be132327
--- /dev/null
+++ b/core/src/files.rs
@@ -0,0 +1,240 @@
+//! This module provides [`Files`], a cheaply-clonable, persistent, `codespan`-compatible collection of files.
+
+use std::{
+    ffi::{OsStr, OsString},
+    path::PathBuf,
+    rc::Rc,
+};
+
+use codespan::ByteIndex;
+use codespan_reporting::files::Error;
+use nickel_lang_vector::Vector;
+
+use crate::{position::RawSpan, stdlib::StdlibModule};
+
+/// A file identifier, which can be used to access a file in a [`Files`].
+///
+/// Note that there is no protection against using a `FileId` for the wrong
+/// instance of `Files`.
+#[derive(
+    Copy, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
+)]
+pub struct FileId(u32);
+
+#[derive(Debug, Clone)]
+struct File {
+    /// The name of the file.
+    name: OsString,
+    /// The source code of the file.
+    source: Rc<str>,
+    /// The byte index of the start of each line. The first element of this array is always 0.
+    line_starts: Rc<[ByteIndex]>,
+}
+
+impl File {
+    fn new(name: impl Into<OsString>, source: impl Into<Rc<str>>) -> Self {
+        let source = source.into();
+        let line_starts: Vec<_> = std::iter::once(ByteIndex(0))
+            .chain(
+                source
+                    .match_indices('\n')
+                    .map(|(i, _)| ByteIndex(i as u32 + 1)),
+            )
+            .collect();
+
+        File {
+            name: name.into(),
+            line_starts: line_starts.into(),
+            source,
+        }
+    }
+
+    fn line_index(&self, byte_index: ByteIndex) -> usize {
+        match self.line_starts.binary_search(&byte_index) {
+            Ok(line) => line,
+            // unwrap: we always start off the `line_starts` array with a zero,
+            // so next_line must be at least 1.
+            Err(next_line) => next_line.checked_sub(1).unwrap(),
+        }
+    }
+}
+
+/// A cheaply-clonable, persistent, `codespan`-compatible collection of files.
+///
+/// `Files` knows about the nickel standard library, and automatically loads it on creation
+/// (but it doesn't do parsing, or anything particularly expensive).
+///
+/// Cloning a `Files` is cheap, and the underlying file data will be shared between clones
+/// until one of them wants to modify a file. In that case, only the modified file(s) will
+/// be duplicated.
+#[derive(Debug, Clone)]
+pub struct Files {
+    files: Vector<File, 8>,
+    first_non_stdlib: usize,
+}
+
+impl Files {
+    /// Creates a new `Files`, initialized with the nickel standard library.
+    pub fn new() -> Self {
+        let files: Vector<_, 8> = crate::stdlib::modules()
+            .iter()
+            .map(|m| File::new(m.file_name().to_owned(), m.content()))
+            .collect();
+
+        Files {
+            first_non_stdlib: files.len(),
+            files,
+        }
+    }
+
+    /// Does this file id point to a standard library file?
+    pub fn is_stdlib(&self, id: FileId) -> bool {
+        (id.0 as usize) < self.first_non_stdlib
+    }
+
+    /// Returns the list of all standard library modules and their file ids.
+    pub fn stdlib_modules(&self) -> impl Iterator<Item = (StdlibModule, FileId)> {
+        crate::stdlib::modules()
+            .into_iter()
+            .zip(0..)
+            .map(|(m, id)| (m, FileId(id)))
+    }
+
+    /// Adds a file to this collection, creating and returning a new file id.
+    ///
+    /// The name does not need to be unique, and this method does not affect any other files
+    /// with the same name.
+    pub fn add(&mut self, name: impl Into<OsString>, source: impl Into<Rc<str>>) -> FileId {
+        let file_id = FileId(self.files.len() as u32);
+        self.files.push(File::new(name, source));
+        file_id
+    }
+
+    /// Updates a source file in place.
+    ///
+    /// Panics if `file_id` is invalid.
+    pub fn update(&mut self, file_id: FileId, source: impl Into<Rc<str>>) {
+        // 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();
+        self.files.set(file_id.0 as usize, old);
+    }
+
+    /// Returns a span containing all of a source.
+    ///
+    /// Panics if `file_id` is invalid.
+    pub fn source_span(&self, file_id: FileId) -> RawSpan {
+        // unwrap: we're allowed to panic if file_id is invalid
+        let len = self.get(file_id).unwrap().source.len();
+
+        RawSpan {
+            src_id: file_id,
+            start: ByteIndex(0),
+            end: ByteIndex(len as u32),
+        }
+    }
+
+    /// Returns the source's name.
+    ///
+    /// Panics if `file_id` is invalid.
+    pub fn name(&self, id: FileId) -> &OsStr {
+        &self.get(id).unwrap().name
+    }
+
+    /// Returns a source's contents.
+    pub fn source(&self, id: FileId) -> &str {
+        self.get(id).unwrap().source.as_ref()
+    }
+
+    /// Returns a slice of the source's contents.
+    pub fn source_slice(&self, span: RawSpan) -> &str {
+        let start: usize = span.start.into();
+        let end: usize = span.end.into();
+        &self.source(span.src_id)[start..end]
+    }
+
+    /// Returns the `codespan::Location` (basically: line + col) corresponding
+    /// to a byte index.
+    ///
+    /// Returns an error if the byte index is out of bounds or fails to point to
+    /// a UTF-8 char boundary.
+    pub fn location(
+        &self,
+        id: FileId,
+        byte_index: impl Into<ByteIndex>,
+    ) -> Result<codespan::Location, Error> {
+        let file = self.get(id)?;
+        let byte_index = byte_index.into();
+        let idx = byte_index.to_usize();
+
+        if idx >= file.source.len() {
+            return Err(Error::IndexTooLarge {
+                given: idx,
+                max: file.source.len() - 1,
+            });
+        }
+
+        let line_idx = file.line_index(byte_index);
+        let line_start_idx = file.line_starts[line_idx];
+        let line = file
+            .source
+            .get(line_start_idx.to_usize()..idx)
+            .ok_or(Error::InvalidCharBoundary { given: idx })?;
+
+        Ok(codespan::Location {
+            line: codespan::LineIndex::from(line_idx as u32),
+            column: codespan::ColumnIndex::from(line.chars().count() as u32),
+        })
+    }
+
+    fn get(&self, id: FileId) -> Result<&File, Error> {
+        self.files.get(id.0 as usize).ok_or(Error::FileMissing)
+    }
+}
+
+impl Default for Files {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl<'a> codespan_reporting::files::Files<'a> for Files {
+    type FileId = FileId;
+    type Name = String;
+    type Source = &'a str;
+
+    fn name(&'a self, id: Self::FileId) -> Result<String, Error> {
+        Ok(PathBuf::from(&self.get(id)?.name).display().to_string())
+    }
+
+    fn source(
+        &'a self,
+        id: Self::FileId,
+    ) -> Result<Self::Source, codespan_reporting::files::Error> {
+        Ok(self.get(id)?.source.as_ref())
+    }
+
+    fn line_index(
+        &'a self,
+        id: Self::FileId,
+        byte_index: usize,
+    ) -> Result<usize, codespan_reporting::files::Error> {
+        let file = self.get(id)?;
+        Ok(file.line_index(ByteIndex(byte_index as u32)))
+    }
+
+    fn line_range(
+        &'a self,
+        id: Self::FileId,
+        line_index: usize,
+    ) -> Result<std::ops::Range<usize>, codespan_reporting::files::Error> {
+        let file = self.get(id)?;
+        let starts = &file.line_starts;
+        let end = starts
+            .get(line_index + 1)
+            .copied()
+            .unwrap_or(ByteIndex(file.source.len() as u32));
+        Ok(starts[line_index].into()..end.into())
+    }
+}
diff --git a/core/src/label.rs b/core/src/label.rs
index 16536809..5314bf2f 100644
--- a/core/src/label.rs
+++ b/core/src/label.rs
@@ -6,6 +6,7 @@ use std::{collections::HashMap, rc::Rc};
 
 use crate::{
     eval::cache::{Cache as EvalCache, CacheIndex},
+    files::Files,
     identifier::LocIdent,
     mk_uty_enum, mk_uty_record,
     position::{RawSpan, TermPos},
@@ -17,8 +18,6 @@ use crate::{
     typecheck::{ReifyAsUnifType, UnifType},
 };
 
-use codespan::Files;
-
 pub mod ty_path {
     //! Type paths.
     //!
diff --git a/core/src/lib.rs b/core/src/lib.rs
index 32ecbe27..895eb483 100644
--- a/core/src/lib.rs
+++ b/core/src/lib.rs
@@ -7,6 +7,7 @@ pub mod deserialize;
 pub mod environment;
 pub mod error;
 pub mod eval;
+pub mod files;
 pub mod identifier;
 pub mod label;
 #[cfg(feature = "nix-experimental")]
diff --git a/core/src/parser/error.rs b/core/src/parser/error.rs
index 817460d8..2a05bbc1 100644
--- a/core/src/parser/error.rs
+++ b/core/src/parser/error.rs
@@ -1,7 +1,6 @@
-use codespan::FileId;
 use codespan_reporting::diagnostic::Label;
 
-use crate::{identifier::LocIdent, position::RawSpan};
+use crate::{files::FileId, identifier::LocIdent, position::RawSpan};
 use std::ops::Range;
 
 #[derive(Clone, PartialEq, Eq, Debug)]
diff --git a/core/src/parser/grammar.lalrpop b/core/src/parser/grammar.lalrpop
index 5b1eebc9..6ca0a543 100644
--- a/core/src/parser/grammar.lalrpop
+++ b/core/src/parser/grammar.lalrpop
@@ -39,7 +39,6 @@ use std::{
     convert::TryFrom,
 };
 
-use codespan::FileId;
 use lalrpop_util::ErrorRecovery;
 
 use super::{
@@ -51,6 +50,7 @@ use super::{
 };
 
 use crate::{
+    files::FileId,
     mk_app,
     mk_opn,
     mk_fun,
diff --git a/core/src/parser/mod.rs b/core/src/parser/mod.rs
index de16cbed..d0a7279a 100644
--- a/core/src/parser/mod.rs
+++ b/core/src/parser/mod.rs
@@ -1,9 +1,9 @@
 use crate::error::{ParseError, ParseErrors};
+use crate::files::FileId;
 use crate::identifier::LocIdent;
 use crate::position::RawSpan;
 use crate::term::RichTerm;
 use crate::typ::Type;
-use codespan::FileId;
 use lalrpop_util::lalrpop_mod;
 
 lalrpop_mod!(
diff --git a/core/src/parser/utils.rs b/core/src/parser/utils.rs
index 1db0114f..f7f6cfbd 100644
--- a/core/src/parser/utils.rs
+++ b/core/src/parser/utils.rs
@@ -5,8 +5,6 @@ use std::ffi::OsString;
 use std::rc::Rc;
 use std::{collections::HashSet, fmt::Debug};
 
-use codespan::FileId;
-
 use self::pattern::bindings::Bindings as _;
 
 use super::error::ParseError;
@@ -18,6 +16,7 @@ use crate::{
         merge::{merge_doc, split},
         operation::RecPriority,
     },
+    files::FileId,
     identifier::LocIdent,
     label::{Label, MergeKind, MergeLabel},
     mk_app, mk_fun,
diff --git a/core/src/position.rs b/core/src/position.rs
index 298e831d..46645c71 100644
--- a/core/src/position.rs
+++ b/core/src/position.rs
@@ -3,7 +3,8 @@
 //! The positions defined in this module are represented by the id of the corresponding source and
 //! raw byte indices.  They are prefixed with Raw to differentiate them from codespan's types and
 //! indicate that they do not store human friendly data like lines and columns.
-use codespan::{self, ByteIndex, FileId};
+use crate::files::FileId;
+use codespan::{self, ByteIndex};
 use std::cmp::{max, min, Ordering};
 
 /// A position identified by a byte offset in a file.
diff --git a/core/src/pretty.rs b/core/src/pretty.rs
index a4492145..d126079b 100644
--- a/core/src/pretty.rs
+++ b/core/src/pretty.rs
@@ -1377,12 +1377,12 @@ pub trait PrettyPrintCap: ToString {
 
 #[cfg(test)]
 mod tests {
+    use crate::files::Files;
     use crate::parser::lexer::Lexer;
     use crate::parser::{
         grammar::{FixedTypeParser, TermParser},
         ErrorTolerantParser,
     };
-    use codespan::Files;
     use pretty::Doc;
 
     use super::*;
diff --git a/core/src/program.rs b/core/src/program.rs
index cab770e2..9dbf1893 100644
--- a/core/src/program.rs
+++ b/core/src/program.rs
@@ -28,6 +28,7 @@ use crate::{
         Error, EvalError, IOError, IntoDiagnostics, ParseError,
     },
     eval::{cache::Cache as EvalCache, Closure, VirtualMachine},
+    files::FileId,
     identifier::LocIdent,
     label::Label,
     metrics::increment,
@@ -40,7 +41,6 @@ use crate::{
 };
 
 use clap::ColorChoice;
-use codespan::FileId;
 use codespan_reporting::term::termcolor::{Ansi, NoColor, WriteColor};
 
 use std::{
@@ -160,10 +160,7 @@ impl FieldOverride {
                 )
             })?;
 
-        let value = cache
-            .files()
-            .source_slice(span_value.src_id, span_value)
-            .expect("the span coming from the parser must be valid");
+        let value = cache.files().source_slice(span_value);
 
         Ok(FieldOverride {
             path: FieldPath(path),
@@ -559,7 +556,7 @@ impl<EC: EvalCache> Program<EC> {
     /// Wrapper for [`report`].
     pub fn report<E>(&mut self, error: E, format: ErrorFormat)
     where
-        E: IntoDiagnostics<FileId>,
+        E: IntoDiagnostics,
     {
         report(self.vm.import_resolver_mut(), error, format, self.color_opt)
     }
@@ -567,7 +564,7 @@ impl<EC: EvalCache> Program<EC> {
     /// Wrapper for [`report_to_stdout`].
     pub fn report_to_stdout<E>(&mut self, error: E, format: ErrorFormat)
     where
-        E: IntoDiagnostics<FileId>,
+        E: IntoDiagnostics,
     {
         report_to_stdout(self.vm.import_resolver_mut(), error, format, self.color_opt)
     }
@@ -575,10 +572,9 @@ impl<EC: EvalCache> Program<EC> {
     /// Build an error report as a string and return it.
     pub fn report_as_str<E>(&mut self, error: E) -> String
     where
-        E: IntoDiagnostics<FileId>,
+        E: IntoDiagnostics,
     {
         let cache = self.vm.import_resolver_mut();
-        let stdlib_ids = cache.get_all_stdlib_modules_file_id();
 
         let mut buffer = Vec::new();
         let mut with_color;
@@ -591,13 +587,7 @@ impl<EC: EvalCache> Program<EC> {
             &mut with_color
         };
 
-        report_with(
-            writer,
-            cache.files_mut(),
-            stdlib_ids.as_ref(),
-            error,
-            ErrorFormat::Text,
-        );
+        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()
     }
diff --git a/core/src/repl/mod.rs b/core/src/repl/mod.rs
index b7c35d10..6b3812a7 100644
--- a/core/src/repl/mod.rs
+++ b/core/src/repl/mod.rs
@@ -13,6 +13,7 @@ use crate::error::{
 };
 use crate::eval::cache::Cache as EvalCache;
 use crate::eval::{Closure, VirtualMachine};
+use crate::files::FileId;
 use crate::identifier::LocIdent;
 use crate::parser::{grammar, lexer, ErrorTolerantParser, ExtendedTerm};
 use crate::program::FieldPath;
@@ -22,7 +23,6 @@ use crate::transform::import_resolution;
 use crate::typ::Type;
 use crate::typecheck::TypecheckMode;
 use crate::{eval, transform, typecheck};
-use codespan::FileId;
 use simple_counter::*;
 use std::convert::Infallible;
 use std::ffi::{OsStr, OsString};
@@ -217,7 +217,7 @@ impl<EC: EvalCache> ReplImpl<EC> {
         }
     }
 
-    fn report(&mut self, err: impl IntoDiagnostics<FileId>, color_opt: ColorOpt) {
+    fn report(&mut self, err: impl IntoDiagnostics, color_opt: ColorOpt) {
         report::report(self.cache_mut(), err, ErrorFormat::Text, color_opt);
     }
 }
diff --git a/core/src/repl/wasm_frontend.rs b/core/src/repl/wasm_frontend.rs
index b7160eb4..06a2dce7 100644
--- a/core/src/repl/wasm_frontend.rs
+++ b/core/src/repl/wasm_frontend.rs
@@ -4,8 +4,8 @@ use super::{Repl, ReplImpl};
 use crate::cache::Cache;
 use crate::error::IntoDiagnostics;
 use crate::eval::cache::CacheImpl;
+use crate::files::{FileId, Files};
 use crate::serialize::ExportFormat;
-use codespan::{FileId, Files};
 use codespan_reporting::{
     diagnostic::{Diagnostic, Label, LabelStyle, Severity},
     term::termcolor::Ansi,
@@ -84,7 +84,7 @@ pub struct WasmErrorDiagnostic {
 }
 
 impl WasmErrorDiagnostic {
-    fn from_codespan(files: &Files<String>, diag: Diagnostic<FileId>) -> Self {
+    fn from_codespan(files: &Files, diag: Diagnostic<FileId>) -> Self {
         WasmErrorDiagnostic {
             severity: diag.severity.into(),
             msg: diag.message,
@@ -110,7 +110,7 @@ pub struct WasmErrorLabel {
 }
 
 impl WasmErrorLabel {
-    fn from_codespan(files: &Files<String>, label: Label<FileId>) -> Self {
+    fn from_codespan(files: &Files, label: Label<FileId>) -> Self {
         let start_loc = files.location(label.file_id, label.range.start as u32);
         let end_loc = files.location(label.file_id, label.range.end as u32);
 
@@ -194,10 +194,10 @@ impl WasmInputResult {
     fn error(cache: &mut Cache, error: InputError) -> Self {
         let (msg, errors) = match error {
             InputError::NickelError(err) => {
-                let stdlib_ids = cache.get_all_stdlib_modules_file_id();
-                let diagnostics = err.into_diagnostics(cache.files_mut(), stdlib_ids.as_ref());
+                let mut files = cache.files().clone();
+                let diagnostics = err.into_diagnostics(&mut files);
 
-                let msg = diags_to_string(cache, &diagnostics);
+                let msg = diags_to_string(&files, &diagnostics);
                 let errors: Vec<WasmErrorDiagnostic> = diagnostics
                     .into_iter()
                     .map(|diag| WasmErrorDiagnostic::from_codespan(cache.files(), diag))
@@ -308,27 +308,25 @@ impl std::io::Write for CallbackWriter {
 }
 
 /// Render error diagnostics as a string.
-pub fn diags_to_string(cache: &mut Cache, diags: &[Diagnostic<FileId>]) -> String {
+pub fn diags_to_string(files: &Files, diags: &[Diagnostic<FileId>]) -> String {
     let mut buffer = Ansi::new(Cursor::new(Vec::new()));
     let config = codespan_reporting::term::Config::default();
 
     diags
         .iter()
-        .try_for_each(|d| {
-            codespan_reporting::term::emit(&mut buffer, &config, cache.files_mut(), d)
-        })
+        .try_for_each(|d| codespan_reporting::term::emit(&mut buffer, &config, files, d))
         .unwrap();
 
     String::from_utf8(buffer.into_inner().into_inner()).unwrap()
 }
 
 /// Render an error as a string (similar to [`diags_to_string`](./meth.diags_to_string.html)).
-pub fn err_to_string(cache: &mut Cache, error: InputError) -> String {
+pub fn err_to_string(cache: &Cache, error: InputError) -> String {
     match error {
         InputError::NickelError(nickel_err) => {
-            let stdlib_ids = cache.get_all_stdlib_modules_file_id();
-            let diags = nickel_err.into_diagnostics(cache.files_mut(), stdlib_ids.as_ref());
-            diags_to_string(cache, &diags)
+            let mut files = cache.files().clone();
+            let diags = nickel_err.into_diagnostics(&mut files);
+            diags_to_string(&files, &diags)
         }
         InputError::Other(msg) => msg,
     }
diff --git a/core/src/serialize.rs b/core/src/serialize.rs
index 80321735..aea1c209 100644
--- a/core/src/serialize.rs
+++ b/core/src/serialize.rs
@@ -474,6 +474,7 @@ pub fn to_string(format: ExportFormat, rt: &RichTerm) -> Result<String, ExportEr
 /// any new dependencies, because it's used by `toml` internally anyway.
 pub mod toml_deser {
     use crate::{
+        files::FileId,
         identifier::LocIdent,
         position::{RawSpan, TermPos},
         term::{
@@ -482,7 +483,7 @@ pub mod toml_deser {
             RichTerm, Term,
         },
     };
-    use codespan::{ByteIndex, FileId};
+    use codespan::ByteIndex;
     use malachite::{num::conversion::traits::ExactFrom as _, Rational};
     use std::ops::Range;
     use toml_edit::Value;
diff --git a/core/src/term/mod.rs b/core/src/term/mod.rs
index 974239bb..c893dd73 100644
--- a/core/src/term/mod.rs
+++ b/core/src/term/mod.rs
@@ -24,6 +24,7 @@ use crate::{
     cache::InputFormat,
     error::{EvalError, ParseError},
     eval::{cache::CacheIndex, Environment},
+    files::FileId,
     identifier::LocIdent,
     impl_display_from_pretty,
     label::{Label, MergeLabel},
@@ -36,8 +37,6 @@ use crate::{
 
 use crate::metrics::increment;
 
-use codespan::FileId;
-
 pub use malachite::{
     num::{
         basic::traits::Zero,
diff --git a/core/src/transform/import_resolution.rs b/core/src/transform/import_resolution.rs
index 6bf9d24c..40b20531 100644
--- a/core/src/transform/import_resolution.rs
+++ b/core/src/transform/import_resolution.rs
@@ -7,8 +7,8 @@ use super::ImportResolver;
 pub mod strict {
     use super::{tolerant, ImportResolver};
     use crate::error::ImportError;
+    use crate::files::FileId;
     use crate::term::RichTerm;
-    use codespan::FileId;
 
     /// The result of an import resolution transformation.
     #[derive(Debug)]
@@ -78,8 +78,8 @@ pub mod strict {
 pub mod tolerant {
     use super::ImportResolver;
     use crate::error::ImportError;
+    use crate::files::FileId;
     use crate::term::{RichTerm, Term, Traverse, TraverseOrder};
-    use codespan::FileId;
 
     /// The result of an error tolerant import resolution.
     #[derive(Debug)]
diff --git a/core/src/typ.rs b/core/src/typ.rs
index 04a80060..f443f7f8 100644
--- a/core/src/typ.rs
+++ b/core/src/typ.rs
@@ -1913,7 +1913,7 @@ mod tests {
 
     /// Parse a type represented as a string.
     fn parse_type(s: &str) -> Type {
-        use codespan::Files;
+        use crate::files::Files;
         let id = Files::new().add("<test>", s);
 
         FixedTypeParser::new()
diff --git a/core/src/typecheck/mod.rs b/core/src/typecheck/mod.rs
index def31528..b87b26b7 100644
--- a/core/src/typecheck/mod.rs
+++ b/core/src/typecheck/mod.rs
@@ -2846,7 +2846,7 @@ pub fn apparent_type(
     env: Option<&Environment>,
     resolver: Option<&dyn ImportResolver>,
 ) -> ApparentType {
-    use codespan::FileId;
+    use crate::files::FileId;
 
     // Check the apparent type while avoiding cycling through direct imports loops. Indeed,
     // `apparent_type` tries to see through imported terms. But doing so can lead to an infinite
diff --git a/lsp/nls/src/analysis.rs b/lsp/nls/src/analysis.rs
index 080d81d1..205c0931 100644
--- a/lsp/nls/src/analysis.rs
+++ b/lsp/nls/src/analysis.rs
@@ -1,7 +1,7 @@
 use std::collections::HashMap;
 
-use codespan::FileId;
 use nickel_lang_core::{
+    files::FileId,
     identifier::Ident,
     position::RawSpan,
     term::{BinaryOp, RichTerm, Term, Traverse, TraverseControl, UnaryOp},
@@ -434,8 +434,9 @@ impl TypeCollector {
 #[cfg(test)]
 mod tests {
     use assert_matches::assert_matches;
-    use codespan::{ByteIndex, Files};
+    use codespan::ByteIndex;
     use nickel_lang_core::{
+        files::Files,
         identifier::Ident,
         parser::{grammar, lexer, ErrorTolerantParser as _},
         term::Term,
diff --git a/lsp/nls/src/background.rs b/lsp/nls/src/background.rs
index e58b6419..389cdba4 100644
--- a/lsp/nls/src/background.rs
+++ b/lsp/nls/src/background.rs
@@ -5,13 +5,13 @@ use std::{
 };
 
 use anyhow::anyhow;
-use codespan::FileId;
 use crossbeam::channel::{bounded, Receiver, RecvTimeoutError, Sender};
 use log::warn;
 use lsp_types::Url;
 use nickel_lang_core::{
     cache::{InputFormat, SourcePath},
     eval::{cache::CacheImpl, VirtualMachine},
+    files::FileId,
 };
 use serde::{Deserialize, Serialize};
 
diff --git a/lsp/nls/src/cache.rs b/lsp/nls/src/cache.rs
index e774b4b8..995a2f98 100644
--- a/lsp/nls/src/cache.rs
+++ b/lsp/nls/src/cache.rs
@@ -1,10 +1,11 @@
-use codespan::{ByteIndex, FileId};
+use codespan::ByteIndex;
 use lsp_types::{TextDocumentPositionParams, Url};
 use nickel_lang_core::cache::InputFormat;
 use nickel_lang_core::term::{RichTerm, Term, Traverse};
 use nickel_lang_core::{
     cache::{Cache, CacheError, CacheOp, EntryState, SourcePath, TermEntry},
     error::{Error, ImportError},
+    files::FileId,
     position::RawPos,
     typecheck::{self},
 };
diff --git a/lsp/nls/src/diagnostic.rs b/lsp/nls/src/diagnostic.rs
index 5d387060..ac4083c5 100644
--- a/lsp/nls/src/diagnostic.rs
+++ b/lsp/nls/src/diagnostic.rs
@@ -1,8 +1,8 @@
 use std::ops::Range;
 
-use codespan::{FileId, Files};
 use codespan_reporting::diagnostic::{self, Diagnostic, LabelStyle};
 use lsp_types::{DiagnosticRelatedInformation, NumberOrString};
+use nickel_lang_core::files::{FileId, Files};
 use nickel_lang_core::{error::UNKNOWN_SOURCE_NAME, position::RawSpan};
 use serde::{Deserialize, Serialize};
 
@@ -93,19 +93,15 @@ pub trait DiagnosticCompat: Sized {
     //
     // We do use the `related_information` field for cross-file diagnostics, because the main
     // diagnostics notification assumes all the diagnostics are for the same file.
-    fn from_codespan(
-        file_id: FileId,
-        diagnostic: Diagnostic<FileId>,
-        files: &mut Files<String>,
-    ) -> Vec<Self>;
+    fn from_codespan(file_id: FileId, diagnostic: Diagnostic<FileId>, files: &Files) -> Vec<Self>;
 }
 
 /// Determine the position of a [codespan_reporting::diagnostic::Label] by looking it up
 /// in the file cache
 pub trait LocationCompat: Sized {
-    fn from_codespan(file_id: &FileId, range: &Range<usize>, files: &Files<String>) -> Self;
+    fn from_codespan(file_id: &FileId, range: &Range<usize>, files: &Files) -> Self;
 
-    fn from_span(span: &RawSpan, files: &Files<String>) -> Self {
+    fn from_span(span: &RawSpan, files: &Files) -> Self {
         Self::from_codespan(
             &span.src_id,
             &(span.start.to_usize()..span.end.to_usize()),
@@ -115,7 +111,7 @@ pub trait LocationCompat: Sized {
 }
 
 impl LocationCompat for lsp_types::Range {
-    fn from_codespan(file_id: &FileId, range: &Range<usize>, files: &Files<String>) -> Self {
+    fn from_codespan(file_id: &FileId, range: &Range<usize>, files: &Files) -> Self {
         byte_span_to_range(files, *file_id, range.clone()).unwrap_or(lsp_types::Range {
             start: Default::default(),
             end: Default::default(),
@@ -124,7 +120,7 @@ impl LocationCompat for lsp_types::Range {
 }
 
 impl LocationCompat for lsp_types::Location {
-    fn from_codespan(file_id: &FileId, range: &Range<usize>, files: &Files<String>) -> Self {
+    fn from_codespan(file_id: &FileId, range: &Range<usize>, files: &Files) -> Self {
         lsp_types::Location {
             uri: lsp_types::Url::from_file_path(files.name(*file_id)).unwrap(),
             range: lsp_types::Range::from_codespan(file_id, range, files),
@@ -133,11 +129,7 @@ impl LocationCompat for lsp_types::Location {
 }
 
 impl DiagnosticCompat for SerializableDiagnostic {
-    fn from_codespan(
-        file_id: FileId,
-        diagnostic: Diagnostic<FileId>,
-        files: &mut Files<String>,
-    ) -> Vec<Self> {
+    fn from_codespan(file_id: FileId, diagnostic: Diagnostic<FileId>, files: &Files) -> Vec<Self> {
         let severity = Some(match diagnostic.severity {
             diagnostic::Severity::Bug => lsp_types::DiagnosticSeverity::WARNING,
             diagnostic::Severity::Error => lsp_types::DiagnosticSeverity::ERROR,
@@ -218,11 +210,7 @@ impl DiagnosticCompat for SerializableDiagnostic {
 }
 
 impl DiagnosticCompat for lsp_types::Diagnostic {
-    fn from_codespan(
-        file_id: FileId,
-        diagnostic: Diagnostic<FileId>,
-        files: &mut Files<String>,
-    ) -> Vec<Self> {
+    fn from_codespan(file_id: FileId, diagnostic: Diagnostic<FileId>, files: &Files) -> Vec<Self> {
         SerializableDiagnostic::from_codespan(file_id, diagnostic, files)
             .into_iter()
             .map(From::from)
diff --git a/lsp/nls/src/position.rs b/lsp/nls/src/position.rs
index 332c91b4..09f44d5f 100644
--- a/lsp/nls/src/position.rs
+++ b/lsp/nls/src/position.rs
@@ -204,8 +204,9 @@ fn search<T>(vec: &[(Range<u32>, T)], index: ByteIndex) -> Option<&T> {
 #[cfg(test)]
 pub(crate) mod tests {
     use assert_matches::assert_matches;
-    use codespan::{ByteIndex, FileId, Files};
+    use codespan::ByteIndex;
     use nickel_lang_core::{
+        files::{FileId, Files},
         parser::{grammar, lexer, ErrorTolerantParser},
         term::{RichTerm, Term, UnaryOp},
     };
diff --git a/lsp/nls/src/requests/formatting.rs b/lsp/nls/src/requests/formatting.rs
index 4c8d1726..5d165c46 100644
--- a/lsp/nls/src/requests/formatting.rs
+++ b/lsp/nls/src/requests/formatting.rs
@@ -18,7 +18,7 @@ pub fn handle_format_document(
         .cache
         .id_of(&SourcePath::Path(path, InputFormat::Nickel))
         .unwrap();
-    let text = server.world.cache.files().source(file_id).clone();
+    let text = server.world.cache.files().source(file_id);
     let document_length = text.lines().count() as u32;
 
     let mut formatted: Vec<u8> = Vec::new();
diff --git a/lsp/nls/src/server.rs b/lsp/nls/src/server.rs
index 7bf9ae98..04bd43c0 100644
--- a/lsp/nls/src/server.rs
+++ b/lsp/nls/src/server.rs
@@ -1,5 +1,4 @@
 use anyhow::Result;
-use codespan::FileId;
 use crossbeam::select;
 use log::{debug, trace, warn};
 use lsp_server::{Connection, ErrorCode, Message, Notification, RequestId, Response};
@@ -14,6 +13,7 @@ use lsp_types::{
     TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, Url,
     WorkDoneProgressOptions,
 };
+use nickel_lang_core::files::FileId;
 
 use crate::{
     actions,
diff --git a/lsp/nls/src/term.rs b/lsp/nls/src/term.rs
index a12ce81f..2c2dbe38 100644
--- a/lsp/nls/src/term.rs
+++ b/lsp/nls/src/term.rs
@@ -1,7 +1,7 @@
 use std::{hash::Hash, ops::Range};
 
-use codespan::FileId;
 use nickel_lang_core::{
+    files::FileId,
     position::RawSpan,
     term::{RichTerm, SharedTerm, Term},
 };
diff --git a/lsp/nls/src/usage.rs b/lsp/nls/src/usage.rs
index 69463c63..588baccf 100644
--- a/lsp/nls/src/usage.rs
+++ b/lsp/nls/src/usage.rs
@@ -250,8 +250,7 @@ impl UsageLookup {
 #[cfg(test)]
 pub(crate) mod tests {
     use assert_matches::assert_matches;
-    use codespan::FileId;
-    use nickel_lang_core::{identifier::Ident, position::RawSpan, term::Term};
+    use nickel_lang_core::{files::FileId, identifier::Ident, position::RawSpan, term::Term};
 
     use crate::{
         identifier::LocIdent,
diff --git a/lsp/nls/src/world.rs b/lsp/nls/src/world.rs
index 5847e522..30646547 100644
--- a/lsp/nls/src/world.rs
+++ b/lsp/nls/src/world.rs
@@ -4,13 +4,13 @@ use std::{
     path::PathBuf,
 };
 
-use codespan::FileId;
 use log::warn;
 use lsp_server::{ErrorCode, ResponseError};
 use lsp_types::Url;
 use nickel_lang_core::{
     cache::{Cache, CacheError, ErrorTolerance, InputFormat, SourcePath},
     error::{ImportError, IntoDiagnostics},
+    files::FileId,
     position::{RawPos, RawSpan},
     term::{pattern::bindings::Bindings, record::FieldMetadata, RichTerm, Term, UnaryOp},
     typecheck::Context,
@@ -136,12 +136,12 @@ impl World {
     pub fn lsp_diagnostics(
         &mut self,
         file_id: FileId,
-        err: impl IntoDiagnostics<FileId>,
+        err: impl IntoDiagnostics,
     ) -> Vec<SerializableDiagnostic> {
-        let stdlib_ids = self.cache.get_all_stdlib_modules_file_id();
-        err.into_diagnostics(self.cache.files_mut(), stdlib_ids.as_ref())
+        let mut files = self.cache.files().clone();
+        err.into_diagnostics(&mut files)
             .into_iter()
-            .flat_map(|d| SerializableDiagnostic::from_codespan(file_id, d, self.cache.files_mut()))
+            .flat_map(|d| SerializableDiagnostic::from_codespan(file_id, d, &files))
             .collect()
     }
 
diff --git a/vector/src/slice.rs b/vector/src/slice.rs
index 510e730d..7ec5fa19 100644
--- a/vector/src/slice.rs
+++ b/vector/src/slice.rs
@@ -114,6 +114,20 @@ where
         self.start.checked_add(idx).and_then(|i| self.vec.get(i))
     }
 
+    /// Sets an element at a given index, panicking if `idx` is out-of-bounds.
+    ///
+    /// # Examples
+    ///
+    /// ```rust
+    /// # use nickel_lang_vector::Slice;
+    /// let mut arr = Slice::<_, 32>::from_iter([0, 1, 2, 3, 4, 5]);
+    /// arr.set(2, 6);
+    /// assert_eq!(arr.into_iter().collect::<Vec<_>>(), vec![0, 1, 6, 3, 4, 5]);
+    /// ```
+    pub fn set(&mut self, idx: usize, elt: T) {
+        self.vec.set(self.start.checked_add(idx).unwrap(), elt);
+    }
+
     /// Adds an element to the end of this array.
     ///
     /// Morally runs in time `O(log self.len())`, but see [`Slice::slice`]
diff --git a/vector/src/vector.rs b/vector/src/vector.rs
index 78b1b2a5..272de456 100644
--- a/vector/src/vector.rs
+++ b/vector/src/vector.rs
@@ -597,6 +597,19 @@ where
         self.root.as_ref().and_then(|r| r.get(self.height, idx))
     }
 
+    /// Sets an element at a given index.
+    ///
+    /// Panics if the index is out of bounds.
+    pub fn set(&mut self, idx: usize, elt: T) {
+        if idx >= self.length {
+            panic!("index {idx} out of bounds, length is {}", self.length);
+        }
+
+        if let Some(root) = self.root.as_mut() {
+            Rc::make_mut(root).set(self.height, idx, elt);
+        }
+    }
+
     // Increases the height of the tree by one, temporarily breaking the invariant that
     // the root must have at least two children.
     fn add_level(&mut self) {


Test patch:
diff --git a/core/src/eval/tests.rs b/core/src/eval/tests.rs
index 38945d9e..cf44485d 100644
--- a/core/src/eval/tests.rs
+++ b/core/src/eval/tests.rs
@@ -2,6 +2,7 @@ use super::cache::CacheImpl;
 use super::*;
 use crate::cache::resolvers::{DummyResolver, SimpleResolver};
 use crate::error::ImportError;
+use crate::files::Files;
 use crate::label::Label;
 use crate::parser::{grammar, lexer, ErrorTolerantParser};
 use crate::term::make as mk_term;
@@ -10,7 +11,6 @@ use crate::term::{BinaryOp, StrChunk, UnaryOp};
 use crate::transform::import_resolution::strict::resolve_imports;
 use crate::{mk_app, mk_fun, mk_record};
 use assert_matches::assert_matches;
-use codespan::Files;
 
 /// Evaluate a term without import support.
 fn eval_no_import(t: RichTerm) -> Result<Term, EvalError> {
diff --git a/core/src/parser/tests.rs b/core/src/parser/tests.rs
index c0ec48fc..952a77bd 100644
--- a/core/src/parser/tests.rs
+++ b/core/src/parser/tests.rs
@@ -1,6 +1,7 @@
 use super::lexer::{Lexer, MultiStringToken, NormalToken, StringToken, SymbolicStringStart, Token};
 use super::utils::{build_record, FieldPathElem};
 use crate::error::ParseError;
+use crate::files::Files;
 use crate::identifier::LocIdent;
 use crate::parser::{error::ParseError as InternalParseError, ErrorTolerantParser};
 use crate::term::Number;
@@ -10,7 +11,6 @@ use crate::term::{record, BinaryOp, RichTerm, StrChunk, UnaryOp};
 
 use crate::mk_app;
 use assert_matches::assert_matches;
-use codespan::Files;
 
 fn parse(s: &str) -> Result<RichTerm, ParseError> {
     let id = Files::new().add("<test>", String::from(s));
diff --git a/core/tests/integration/contract_label_path.rs b/core/tests/integration/contract_label_path.rs
index 2a3fd716..9e6d0634 100644
--- a/core/tests/integration/contract_label_path.rs
+++ b/core/tests/integration/contract_label_path.rs
@@ -1,6 +1,6 @@
 use assert_matches::assert_matches;
-use codespan::Files;
 use nickel_lang_core::error::{Error, EvalError, IntoDiagnostics};
+use nickel_lang_core::files::Files;
 
 use nickel_lang_utils::test_program::eval;
 
@@ -23,7 +23,7 @@ fn array_contracts_label_path_is_set_correctly() {
     // Check that reporting doesn't panic. Provide a dummy file database, as we won't report
     // the error message but just check that it can be built.
     let mut files = Files::new();
-    res.unwrap_err().into_diagnostics(&mut files, None);
+    res.unwrap_err().into_diagnostics(&mut files);
 
     let res = eval(
         "(%array/at% (\
@@ -41,7 +41,7 @@ fn array_contracts_label_path_is_set_correctly() {
         ),
         err => panic!("expected blame error, got {err:?}"),
     }
-    res.unwrap_err().into_diagnostics(&mut files, None);
+    res.unwrap_err().into_diagnostics(&mut files);
 }
 
 #[test]
diff --git a/core/tests/manual/main.rs b/core/tests/manual/main.rs
index 78acc9c0..a9bc1038 100644
--- a/core/tests/manual/main.rs
+++ b/core/tests/manual/main.rs
@@ -156,9 +156,8 @@ fn check_repl(content: String) {
                 (Ok(EvalResult::Bound(_)), ReplResult::Empty) => (),
                 (Err(e), ReplResult::Error(expected)) => {
                     let mut error = NoColor::new(Vec::<u8>::new());
-                    let stdlib_ids = repl.cache_mut().get_all_stdlib_modules_file_id();
-                    let files = repl.cache_mut().files_mut();
-                    report_with(&mut error, files, stdlib_ids.as_ref(), e, ErrorFormat::Text);
+                    let mut files = repl.cache_mut().files().clone();
+                    report_with(&mut error, &mut files, e, ErrorFormat::Text);
 
                     check_error_report(String::from_utf8(error.into_inner()).unwrap(), expected);
                 }
diff --git a/utils/src/test_program.rs b/utils/src/test_program.rs
index 8a076c84..335d01d4 100644
--- a/utils/src/test_program.rs
+++ b/utils/src/test_program.rs
@@ -1,8 +1,7 @@
-use codespan::Files;
-
 use nickel_lang_core::{
     error::{Error, ParseError},
     eval::cache::CacheImpl,
+    files::Files,
     parser::{grammar, lexer, ErrorTolerantParser, ExtendedTerm},
     program::Program,
     term::{RichTerm, Term},