elly_core/ctx.rs
1//! The realm, [`Ctx`]: the long-lived state of an evaluation, the code it
2//! holds in chunks, and the entry points that parse and evaluate in it.
3
4use alloc::boxed::Box;
5use alloc::string::{String, ToString};
6use alloc::vec::Vec;
7use core::cell::{Cell, OnceCell, Ref, RefCell};
8
9use crate::builtins::NMODULES;
10use crate::code::{
11 Builder, ChunkId, Id, Lambda, LambdaId, Node, NodeId, Pat, PatId, Run, StaticChunk, Text,
12};
13use crate::eval::env::Env;
14use crate::eval::error::{Raised, Unwind};
15use crate::eval::eval_env;
16use crate::module::{Instances, ModuleResolver};
17use crate::parse::{Error, Root};
18use crate::sym::Symbols;
19use crate::value::Value;
20
21/// The long-lived state of an evaluation, the equivalent of a JS realm: the
22/// unique-id source for closure identities, the builtin module cache, the
23/// environment free list, and the module loader — a resolver plus the cache of
24/// modules loaded through it ([`load`](Self::load)).
25///
26/// A host creates one per program run, per Python realm object or per
27/// playground session, and passes `&Ctx` to every call. Imports are still
28/// resolved before evaluation, and loaded modules are `Rc`-held values. See
29/// `docs/done/2026-10-02_elly-host.md`.
30pub struct Ctx {
31 pub(crate) ids: Cell<u64>,
32 /// The instantiated [builtin modules](crate::builtins::MODULES), one slot each, built on first
33 /// reference. Caching them here is what gives `__Int` an identity: every
34 /// reference in one evaluation yields the *same* module value, so
35 /// `__eq __Int Int` holds. Two different contexts hold non-identical `__Int`s —
36 /// v0's "two loads are unrelated modules" caveat, which a content-addressed
37 /// module brand would retire.
38 pub(crate) modules: [OnceCell<Value>; NMODULES],
39 /// Environment cells recycled by [`release`](Self::release), linked through
40 /// their own `next`, and how many there are (at most [`FREE_CAP`](crate::eval::env::FREE_CAP)).
41 pub(crate) free: Cell<Env>,
42 pub(crate) free_len: Cell<u32>,
43 /// The modules loaded through [`load`](Self::load) and
44 /// [`load_source`](Self::load_source), by canonical name. Keeping them here
45 /// is what makes two loads in one realm share an import.
46 pub(crate) instances: RefCell<Instances>,
47 /// Where an import spec's source comes from. `None` means every import fails
48 /// with [`LoadError::NotFound`](crate::LoadError::NotFound).
49 pub(crate) resolver: Option<Box<dyn ModuleResolver>>,
50 /// The symbols interned in this realm beyond the fixed ones (see
51 /// [`Sym`](crate::Sym)).
52 pub(crate) syms: RefCell<Symbols>,
53 /// The code of this realm, one chunk per compile, indexed by
54 /// [`ChunkId`](crate::code::ChunkId). Chunks live as long as the realm.
55 pub(crate) chunks: RefCell<Vec<StaticChunk>>,
56 /// How many names the realm has made for chunks (`__eval_N`, …).
57 pub(crate) made_names: Cell<u32>,
58 /// What an error left as it unwound, out of line: only errors read it.
59 pub(crate) unwind: Box<Unwind>,
60}
61
62impl Ctx {
63 /// A context with a fresh id source and no module resolver, so every import
64 /// fails.
65 pub fn new() -> Self {
66 Self::build(None)
67 }
68
69 /// A context whose imports are served by `resolver`.
70 pub fn with_resolver(resolver: Box<dyn ModuleResolver>) -> Self {
71 Self::build(Some(resolver))
72 }
73
74 fn build(resolver: Option<Box<dyn ModuleResolver>>) -> Self {
75 let ctx = Ctx {
76 ids: Cell::new(0),
77 modules: core::array::from_fn(|_| OnceCell::new()),
78 free: Cell::new(Env::EMPTY),
79 free_len: Cell::new(0),
80 instances: RefCell::new(Instances::new()),
81 resolver,
82 syms: RefCell::default(),
83 chunks: RefCell::default(),
84 made_names: Cell::new(0),
85 unwind: Box::default(),
86 };
87 ctx.add_elly_chunk();
88 ctx
89 }
90}
91
92/// Frees the environment free list iteratively (see `Ctx::free_cells`).
93impl Drop for Ctx {
94 fn drop(&mut self) {
95 self.free_cells();
96 }
97}
98
99impl Default for Ctx {
100 fn default() -> Self {
101 Self::new()
102 }
103}
104
105// ---------------------------------------------------------------------------
106// The realm's chunk table
107// ---------------------------------------------------------------------------
108
109impl Ctx {
110 /// The chunk `id` of this realm.
111 ///
112 /// Do not hold the reference across anything that may add a chunk: an
113 /// evaluation, an application or a load.
114 pub fn chunk(&self, id: ChunkId) -> Ref<'_, StaticChunk> {
115 Ref::map(self.chunks.borrow(), |chunks| &chunks[id.index()])
116 }
117
118 /// The s-expression of chunk `id`'s top module body, e.g.
119 /// `(app (name f) (name x))`: what the parse goldens assert on.
120 pub fn to_sexp(&self, id: ChunkId) -> String {
121 let chunk = self.chunk(id);
122 chunk.to_sexp(chunk.module(chunk.top()).body, self)
123 }
124
125 /// The tagged JSON of chunk `id`'s top module body, e.g.
126 /// `{"app":[{"name":"f"},{"name":"x"}]}`: the playground's AST view.
127 pub fn to_json(&self, id: ChunkId) -> String {
128 let chunk = self.chunk(id);
129 chunk.to_json(chunk.module(chunk.top()).body, self)
130 }
131
132 /// The builtin modules as the realm's first chunk, [`ChunkId::ELLY`].
133 pub(crate) fn add_elly_chunk(&self) {
134 let mut b = Builder::new();
135 b.builtin_modules();
136 let id = self.add_built(b, Some(Text::from_static("Elly")), "");
137 debug_assert_eq!(id, ChunkId::ELLY);
138 }
139
140 /// Finishes `b` into a chunk and adds it. The chunk is named `name`, or
141 /// else `prefix` and the next number of the realm's count of made names;
142 /// an anonymous document takes that number too, drawing one if the chunk
143 /// has a name.
144 pub(crate) fn add_built(&self, b: Builder, name: Option<Text>, prefix: &str) -> ChunkId {
145 let made = (name.is_none() || b.docs.has_anonymous()).then(|| {
146 let n = self.made_names.get();
147 self.made_names.set(n + 1);
148 n
149 });
150 let name = name.unwrap_or_else(|| {
151 let mut name = String::from(prefix);
152 name.push_str(&made.expect("drawn for an unnamed chunk").to_string());
153 Text::from(name.as_str())
154 });
155 self.add_chunk(b.finish(name, made))
156 }
157
158 /// Node `id` of chunk `at`, read through the realm's table: the evaluator's
159 /// one way to read code, which holds no borrow past the read.
160 #[inline]
161 pub(crate) fn node(&self, at: ChunkId, id: NodeId) -> Node {
162 self.chunks.borrow()[at.index()].node(id)
163 }
164
165 /// Pattern `id` of chunk `at`, read as [`node`](Self::node) is.
166 #[inline]
167 pub(crate) fn pat(&self, at: ChunkId, id: PatId) -> Pat {
168 self.chunks.borrow()[at.index()].pat(id)
169 }
170
171 /// Lambda `id` of chunk `at`, copied out.
172 #[inline]
173 pub(crate) fn lambda(&self, at: ChunkId, id: LambdaId) -> Lambda {
174 *self.chunks.borrow()[at.index()].lambda(id)
175 }
176
177 /// In debug builds, asserts that a run of `len` nodes of kind `run` starts
178 /// at `first` in chunk `at`.
179 #[inline]
180 pub(crate) fn check_run(&self, at: ChunkId, run: Run, first: NodeId, len: u32) {
181 #[cfg(debug_assertions)]
182 self.chunk(at).nodes(run, first, len);
183 let _ = (at, run, first, len);
184 }
185
186 /// [`check_run`](Self::check_run) for a run of patterns.
187 #[inline]
188 pub(crate) fn check_pat_run(&self, at: ChunkId, run: Run, first: PatId, len: u32) {
189 #[cfg(debug_assertions)]
190 self.chunk(at).pats(run, first, len);
191 let _ = (at, run, first, len);
192 }
193
194 fn add_chunk(&self, mut chunk: StaticChunk) -> ChunkId {
195 chunk.locs.base = self.next_base();
196 let mut chunks = self.chunks.borrow_mut();
197 let id = ChunkId::at(chunks.len());
198 chunks.push(chunk);
199 id
200 }
201}
202
203// ---------------------------------------------------------------------------
204// Parsing and evaluating
205// ---------------------------------------------------------------------------
206
207impl Ctx {
208 /// Parse Elly source into a new chunk named `__eval_N`, resolving its
209 /// names. Free variables cause [`ParseError::UnboundName`](crate::ParseError::UnboundName).
210 pub fn parse(&self, src: &str) -> Result<ChunkId, Error> {
211 self.compile_expr(src, Root::Closed)
212 }
213
214 /// Parse Elly source as an open term: a free name is bound into the root
215 /// frame, as if given. Used by tooling and the parse golden suite; for
216 /// evaluation, prefer [`parse`](Self::parse).
217 pub fn parse_open(&self, src: &str) -> Result<ChunkId, Error> {
218 self.compile_expr(src, Root::Open)
219 }
220
221 /// Parse Elly source resolved against a fixed outer frame (prelude):
222 /// `prelude[i]` is index `i` of the root frame. Unknown names cause
223 /// [`ParseError::UnboundName`](crate::ParseError::UnboundName).
224 pub fn parse_preluded(&self, src: &str, prelude: &[Text]) -> Result<ChunkId, Error> {
225 self.compile_expr(src, Root::Prelude(prelude))
226 }
227
228 /// Evaluate an expression chunk this realm parsed, in the empty environment.
229 pub fn eval(&self, code: ChunkId) -> Result<Value, Raised> {
230 eval_env(code, &Env::EMPTY, self)
231 }
232}
233
234/// Parse Elly source into a new realm, for a caller that has none: tests,
235/// benches and one-off tools. Returns the realm with the code, since the code
236/// is evaluated and rendered in it.
237pub fn parse_fresh(src: &str) -> Result<(Ctx, ChunkId), Error> {
238 let ctx = Ctx::new();
239 let code = ctx.parse(src)?;
240 Ok((ctx, code))
241}