diff --git a/Cargo.toml b/Cargo.toml index 9609e61..0ac6da0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "compiler/cl-repl", "compiler/cl-typeck", + "compiler/cl-embed", "compiler/cl-interpret", "compiler/cl-structures", "compiler/cl-token", diff --git a/compiler/cl-embed/Cargo.toml b/compiler/cl-embed/Cargo.toml new file mode 100644 index 0000000..9e1157b --- /dev/null +++ b/compiler/cl-embed/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "cl-embed" +version = "0.1.0" +repository.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +cl-interpret = { path = "../cl-interpret" } +cl-ast = { path = "../cl-ast" } +cl-structures = { path = "../cl-structures" } +cl-lexer = { path = "../cl-lexer" } +cl-parser = { path = "../cl-parser" } + +[dev-dependencies] +repline = { path = "../../repline" } diff --git a/compiler/cl-embed/src/lib.rs b/compiler/cl-embed/src/lib.rs new file mode 100644 index 0000000..5312290 --- /dev/null +++ b/compiler/cl-embed/src/lib.rs @@ -0,0 +1,126 @@ +//! Embed Conlang code into your Rust project! +//! +//! # This crate is experimental, and has no guarantees of stability. +#![feature(decl_macro)] +#![cfg_attr(test, feature(assert_matches))] +#![allow(unused_imports)] + +pub use cl_interpret::{convalue::ConValue as Value, env::Environment}; + +use cl_ast::{Block, Module, ast_visitor::Fold}; +use cl_interpret::{convalue::ConValue, interpret::Interpret}; +use cl_lexer::Lexer; +use cl_parser::{Parser, error::Error as ParseError, inliner::ModuleInliner}; +use std::{path::Path, sync::OnceLock}; + +/// Constructs a function which evaluates a Conlang Block +/// +/// # Examples +/// +/// Bind and use a variable +/// ```rust +/// # fn main() -> Result<(), Box> { +/// use cl_embed::{conlang, Environment, Value}; +/// +/// let mut env = Environment::new(); +/// +/// // Bind a variable named `message` to "Hello, world!" +/// env.bind("message", "Hello, World!"); +/// +/// let print_hello = conlang!{ +/// println(message); +/// }; +/// +/// // Run the function +/// let ret = print_hello(&mut env)?; +/// +/// // `println` returns Empty +/// assert!(matches!(ret, Value::Empty)); +/// +/// # Ok(()) +/// # } +/// ``` +pub macro conlang ( + $($t:tt)* +) {{ + // Parse once + static FN: OnceLock> = OnceLock::new(); + + |env: &mut Environment| -> Result { + FN.get_or_init(|| { + // TODO: embed the full module tree at compile time + let path = AsRef::::as_ref(&concat!(env!("CARGO_MANIFEST_DIR"),"/../../", file!())).with_extension(""); + let mut mi = ModuleInliner::new(path); + let code = mi.fold_block( + Parser::new( + concat!(file!(), ":", line!(), ":"), + Lexer::new(stringify!({ $($t)* })), + ) + .parse::()?, + ); + if let Some((ie, pe)) = mi.into_errs() { + for (file, err) in ie { + eprintln!("{}: {err}", file.display()); + } + for (file, err) in pe { + eprintln!("{}: {err}", file.display()); + } + } + Ok(code) + }) + .as_ref() + .map_err(Clone::clone)? + .interpret(env) + .map_err(Into::into) + } +}} + +#[derive(Clone, Debug)] +pub enum EvalError { + Parse(cl_parser::error::Error), + Interpret(cl_interpret::error::Error), +} + +impl From for EvalError { + fn from(value: cl_parser::error::Error) -> Self { + Self::Parse(value) + } +} +impl From for EvalError { + fn from(value: cl_interpret::error::Error) -> Self { + Self::Interpret(value) + } +} +impl std::error::Error for EvalError {} +impl std::fmt::Display for EvalError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + EvalError::Parse(error) => error.fmt(f), + EvalError::Interpret(error) => error.fmt(f), + } + } +} + +#[cfg(test)] +mod tests { + use std::assert_matches::assert_matches; + + use super::*; + + #[test] + fn it_works() -> Result<(), EvalError> { + let mut env = Environment::new(); + + let result = conlang! { + fn add(left, right) -> isize { + left + right + } + + add(2, 2) + }(&mut env); + + assert_matches!(result, Ok(Value::Int(4))); + + Ok(()) + } +}