Skip to main content

elly_core/
lib.rs

1//! Elly — a small language layered on Muon (see `docs/elly-spec.md`).
2//!
3//! This crate implements the first, deliberately small subset: references,
4//! application, `&` abstraction, symbols, and positional lists with
5//! projection. It reads the syntax with the `muon` crate, parses that notation
6//! tree into an [`Expr`], and evaluates it with a naive tree-walking
7//! [`eval`](fn@eval)uator.
8//!
9//! ```
10//! let expr = elly_core::parse("(&x x) .foo").unwrap();
11//! assert_eq!(elly_core::eval(&expr).unwrap().to_display(), ".foo");
12//! ```
13
14#![no_std]
15
16extern crate alloc;
17
18mod ast;
19mod eval;
20mod load;
21mod parse;
22mod resolve;
23
24pub use ast::{Expr, HomeLeaf, Pattern, ProtoRef, Text, TyKind};
25pub use eval::{
26    apply, apply_with, eval, eval_env, eval_main, instantiate, Builtin, Ctx, Env, EnvNode,
27    HostCall, HostFn, ModuleData, ObjectData, Raised, Value,
28};
29pub use load::{load_module, load_module_source, Instances, LoadError, ModuleResolver, Resolved};
30pub use parse::{
31    is_bindable_name, parse_module, parse_program, ImportDecl, ImportSpec, ModuleName,
32    ModuleSyntax, ParseError,
33};
34pub use resolve::{resolve, resolve_module_bodies, resolve_open, resolve_prelude};
35
36use alloc::rc::Rc;
37use alloc::vec::Vec;
38
39// TODO: use thiserror?
40// TODO: errors with spans
41/// A failure at the syntax or parse stage.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum Error {
44    /// The syntax layer (Muon) rejected the input.
45    Syntax(muon::ParseError),
46    /// The input is a valid Muon tree but not a valid Elly program (this subset).
47    Parse(ParseError),
48}
49
50/// Parse Elly source into an [`Expr`]: read the Muon syntax, parse that notation
51/// tree into the AST, then resolve names — a free variable is rejected here (a
52/// parse-time [`ParseError::UnboundName`]) rather than raising at eval.
53pub fn parse(src: &str) -> Result<Expr, Error> {
54    let mut expr = parse_open(src)?;
55    resolve(&mut expr).map_err(Error::Parse)?;
56    Ok(expr)
57}
58
59/// Parse Elly source into an [`Expr`] **without** name resolution, so free
60/// variables are left in place (an *open* term). Used by tooling that wants the
61/// raw AST and by the parse golden suite, which resolves open terms with
62/// [`resolve_open`] (auto-binding their free names). Prefer [`parse`](fn@parse) for
63/// evaluating a whole program.
64pub fn parse_open(src: &str) -> Result<Expr, Error> {
65    let seq = muon::parse(src).map_err(Error::Syntax)?;
66    parse_program(&seq).map_err(Error::Parse)
67}
68
69/// Compile Elly **module** source into a frozen [`ModuleData`]: read the Muon
70/// syntax, parse it into `import`s and `name = body` items ([`parse_module`]),
71/// then resolve each body against the module's item set
72/// ([`resolve_module_bodies`], turning sibling references into
73/// [`Expr::ModItem`]). Compiling is purely syntactic — no body is evaluated, and
74/// no import is resolved. See `docs/done/2026-08-07_elly-modules-v0.md`.
75///
76/// The module's imports become its leading frame slots, so source that imports
77/// compiles fine here but has to be [instantiated](instantiate) against the
78/// instances. Use [`load_module`] to compile and instantiate a whole import graph.
79pub fn compile_module(src: &str) -> Result<Rc<ModuleData>, Error> {
80    compile_module_framed(src, &[])
81}
82
83/// Compile Elly module source against a **frame**: an ordered list of names the
84/// module is instantiated against — a host prelude, module-minted identities. A
85/// body's reference to one resolves to a de Bruijn local whose index walks past
86/// the module terminal into the frame, so it costs one indexed lookup and nothing
87/// is evaluated on access.
88///
89/// A name in a body is looked for in the enclosing `&`/`let` binders, then the
90/// module's imports, then its own items, then `frame`; a name in none of them is
91/// [`ParseError::UnboundName`]. The module's **imports take the leading frame
92/// slots**, so the returned [`ModuleData`]'s
93/// [`frame_names`](ModuleData::frame_names) is its imports followed by `frame`,
94/// and [`instantiate`] wants the import instances ahead of the values for `frame`.
95/// [`ModuleData::imports`] says how many lead and what they were imported from;
96/// [`load_module`] is what fills them.
97///
98/// [`compile_module`] is this with an empty frame.
99pub fn compile_module_framed(src: &str, frame: &[Text]) -> Result<Rc<ModuleData>, Error> {
100    compile_module_named(src, frame, None)
101}
102
103/// [`compile_module_framed`], recording `name` as the canonical name the code came
104/// from — what `__Mod.name` reports and what [`load_module`] deduplicates
105/// instances by. Only a host that resolved the source knows the name, which is why
106/// the plain entry points leave it unset.
107pub fn compile_module_named(
108    src: &str,
109    frame: &[Text],
110    name: Option<ModuleName>,
111) -> Result<Rc<ModuleData>, Error> {
112    let seq = muon::parse(src).map_err(Error::Syntax)?;
113    let syntax = parse_module(&seq).map_err(Error::Parse)?;
114    let mut bindings = syntax.items;
115    let mut locals = syntax.locals;
116    let import_names: Vec<Text> = syntax.imports.iter().map(|i| i.name.clone()).collect();
117    // The iota table is name-sorted, as the item and local tables are: the three
118    // orders concatenated are the member index space a `ModItem` counts against.
119    let mut iotas = syntax.iotas;
120    iotas.sort_by(|a, b| a.as_str().cmp(b.as_str()));
121    // `__main` is resolved with the item bodies and against the same tiers — it
122    // is written in the module's own scope, so it reaches every member and every
123    // frame slot. It takes no member index of its own (see `ModuleData::body`).
124    let mut main = syntax.main;
125    resolve_module_bodies(
126        &mut bindings,
127        &mut locals,
128        main.as_mut(),
129        &import_names,
130        &iotas,
131        frame,
132    )
133    .map_err(Error::Parse)?;
134    let mut frame_names = import_names;
135    frame_names.extend_from_slice(frame);
136    Ok(Rc::new(ModuleData::from_bindings(
137        bindings,
138        locals,
139        iotas.into_boxed_slice(),
140        frame_names.into_boxed_slice(),
141        syntax
142            .imports
143            .into_iter()
144            .map(|i| i.spec)
145            .collect::<Vec<_>>()
146            .into_boxed_slice(),
147        main,
148        name,
149    )))
150}
151
152/// Parse Elly source into an [`Expr`] resolved against a named `prelude`: names
153/// in the prelude resolve to de Bruijn locals, unknown names are rejected with
154/// [`ParseError::UnboundName`]. The prelude is an ordered slice of names
155/// (insertion order) that forms a fixed outer frame below all lambda scopes.
156/// Prefer [`parse`](fn@parse) for a whole program without a prelude.
157pub fn parse_preluded(src: &str, prelude: &[Text]) -> Result<Expr, Error> {
158    let mut expr = parse_open(src)?;
159    resolve_prelude(&mut expr, prelude).map_err(Error::Parse)?;
160    Ok(expr)
161}