Skip to main content

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}