lambda-throw-cat 0.1.0

Lambda calculus with records, prototype chains, ref cells, GC, and non-local control flow via throw/try/catch. Outcome::Normal/Thrown is threaded purely-functionally through every reduction. Spike 4 of a web-engine reformulation targeting Tauri.
Documentation
//! # lambda-throw-cat
//!
//! Lambda calculus with records, prototype chains, ref cells, a tracing GC,
//! and non-local control flow via `throw` and `try`/`catch`.  Spike 4 of a
//! comp-cat-rs web-engine reformulation targeting Tauri.
//!
//! ## Quick start
//!
//! ```
//! # fn main() -> Result<(), lambda_throw_cat::error::Error> {
//! use lambda_throw_cat::run;
//!
//! let source = r"
//!     try
//!         throw (\msg. msg)
//!     catch e. e
//! ";
//! let value = run(source).run()?;
//! assert_eq!(format!("{value}"), "\\msg. msg");
//! # Ok(())
//! # }
//! ```
//!
//! ## Grammar
//!
//! ```text
//! expr        ::= seq_expr
//! seq_expr    ::= right_expr (";" right_expr)*
//! right_expr  ::= lambda | let | fix | extend | throw_expr | try_expr | assign_expr
//! lambda      ::= "\" ident "." expr
//! let         ::= "let" ident "=" expr "in" expr
//! fix         ::= "fix" ident "." expr
//! extend      ::= "extend" atom object_lit
//! throw_expr  ::= "throw" right_expr
//! try_expr    ::= "try" right_expr "catch" ident "." right_expr
//! assign_expr ::= app_expr (":=" right_expr)?
//! app_expr    ::= atom atom*
//! atom        ::= base_atom ("." ident)*
//! base_atom   ::= ident | "(" expr ")" | "ref" atom | "!" atom | object_lit
//! object_lit  ::= "{" props? "}"
//! props       ::= property ("," property)*
//! property    ::= ident "=" right_expr
//! ```

pub mod env;
pub mod error;
pub mod eval;
pub mod gc;
pub mod heap;
pub mod lexer;
pub mod parser;
pub mod syntax;
pub mod value;

use comp_cat_rs::effect::io::Io;

use crate::env::Env;
use crate::error::Error;
use crate::eval::Fuel;
use crate::heap::Heap;
use crate::value::Value;

/// Default step budget used by [`run`].
pub const DEFAULT_FUEL: u64 = 10_000;

/// Lex, parse, and evaluate `source` with the default fuel budget.
///
/// # Errors
///
/// See [`Error`].  An uncaught `throw` surfaces as [`Error::UncaughtException`].
///
/// [`Error::UncaughtException`]: crate::error::Error::UncaughtException
///
/// # Examples
///
/// ```
/// # fn main() -> Result<(), lambda_throw_cat::error::Error> {
/// use lambda_throw_cat::run;
///
/// let value = run(r"try throw (\x. x) catch e. e").run()?;
/// assert_eq!(format!("{value}"), "\\x. x");
/// # Ok(())
/// # }
/// ```
#[must_use]
pub fn run(source: &str) -> Io<Error, Value> {
    run_with_fuel(source, Fuel::new(DEFAULT_FUEL))
}

/// Lex, parse, and evaluate `source` with a caller-supplied fuel budget.
///
/// # Errors
///
/// See [`Error`].
///
/// # Examples
///
/// ```
/// # fn main() -> Result<(), lambda_throw_cat::error::Error> {
/// use lambda_throw_cat::eval::Fuel;
/// use lambda_throw_cat::run_with_fuel;
///
/// let value = run_with_fuel(r"try throw (\x. x) catch e. e", Fuel::new(100)).run()?;
/// assert_eq!(format!("{value}"), "\\x. x");
/// # Ok(())
/// # }
/// ```
#[must_use]
pub fn run_with_fuel(source: &str, fuel: Fuel) -> Io<Error, Value> {
    let owned = source.to_owned();
    Io::suspend(move || {
        let (value, _heap) = pipeline(&owned, fuel)?;
        Ok(value)
    })
}

/// Lex, parse, and evaluate `source`, returning both the value and the final
/// heap.  Useful for tests that need to verify GC behaviour.
///
/// # Errors
///
/// See [`Error`].
///
/// # Examples
///
/// ```
/// # fn main() -> Result<(), lambda_throw_cat::error::Error> {
/// use lambda_throw_cat::eval::Fuel;
/// use lambda_throw_cat::run_inspecting;
///
/// let (_value, heap) = run_inspecting(r"{}", Fuel::new(100)).run()?;
/// assert_eq!(heap.len(), 1);
/// # Ok(())
/// # }
/// ```
#[must_use]
pub fn run_inspecting(source: &str, fuel: Fuel) -> Io<Error, (Value, Heap)> {
    let owned = source.to_owned();
    Io::suspend(move || pipeline(&owned, fuel))
}

fn pipeline(source: &str, fuel: Fuel) -> Result<(Value, Heap), Error> {
    let tokens = lexer::lex(source)?;
    let expr = parser::parse(&tokens)?;
    match eval::eval(&expr, &Env::empty(), Heap::empty(), fuel) {
        Ok((value, heap, _fuel)) => Ok((value, heap)),
        Err(Error::Thrown(payload)) => Err(Error::UncaughtException {
            value: payload.into_parts().0,
        }),
        Err(other) => Err(other),
    }
}