cl_embed/
lib.rs

1//! Embed Conlang code into your Rust project!
2//!
3//! # This crate is experimental, and has no guarantees of stability.
4#![feature(decl_macro)]
5#![cfg_attr(test, feature(assert_matches))]
6#![allow(unused_imports)]
7
8pub use cl_interpret::{convalue::ConValue as Value, env::Environment};
9
10use cl_ast::{Block, File, Module, ast_visitor::Fold};
11use cl_interpret::{convalue::ConValue, interpret::Interpret};
12use cl_lexer::Lexer;
13use cl_parser::{Parser, error::Error as ParseError, inliner::ModuleInliner};
14use std::{path::Path, sync::OnceLock};
15
16/// Constructs a function which evaluates a Conlang Block
17///
18/// # Examples
19///
20/// Bind and use a variable
21/// ```rust
22/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
23/// use cl_embed::{conlang, Environment, Value};
24///
25/// let mut env = Environment::new();
26///
27/// // Bind a variable named `message` to "Hello, world!"
28/// env.bind("message", "Hello, World!");
29///
30/// let print_hello = conlang!{
31///     println(message);
32/// };
33///
34/// // Run the function
35/// let ret = print_hello(&mut env)?;
36///
37/// // `println` returns Empty
38/// assert!(matches!(ret, Value::Empty));
39///
40/// # Ok(())
41/// # }
42/// ```
43pub macro conlang(
44    $($t:tt)*
45) {{
46    // Parse once
47    static FN: OnceLock<Result<Block, ParseError>> = OnceLock::new();
48
49    |env: &mut Environment| -> Result<ConValue, EvalError> {
50        FN.get_or_init(|| {
51            // TODO: embed the full module tree at compile time
52            let path =
53                AsRef::<Path>::as_ref(&concat!(env!("CARGO_MANIFEST_DIR"), "/../../", file!()))
54                    .with_extension("");
55            let mut mi = ModuleInliner::new(path);
56            let code = mi.fold_block(
57                Parser::new(
58                    concat!(file!(), ":", line!(), ":", column!()),
59                    Lexer::new(stringify!({ $($t)* })),
60                )
61                .parse::<Block>()?,
62            );
63            if let Some((ie, pe)) = mi.into_errs() {
64                for (file, err) in ie {
65                    eprintln!("{}: {err}", file.display());
66                }
67                for (file, err) in pe {
68                    eprintln!("{}: {err}", file.display());
69                }
70            }
71            Ok(code)
72        })
73        .as_ref()
74        .map_err(Clone::clone)?
75        .interpret(env)
76        .map_err(Into::into)
77    }
78}}
79
80pub macro conlang_include{
81    ($path:literal, $name:ident) => {
82        |env: &mut Environment| -> Result<ConValue, EvalError> {
83            // TODO: embed the full module tree at compile time
84            let path = AsRef::<Path>::as_ref(&concat!(env!("CARGO_MANIFEST_DIR"), "/../../", file!()))
85                .with_file_name(concat!($path));
86            let mut mi = ModuleInliner::new(path);
87            let code = mi.fold_module(Module {
88                name: stringify!($name).into(),
89                file: Some(
90                    Parser::new(
91                        concat!(file!(), ":", line!(), ":", column!()),
92                        Lexer::new(include_str!($path)),
93                    )
94                    .parse()?,
95                ),
96            });
97            if let Some((ie, pe)) = mi.into_errs() {
98                for (file, err) in ie {
99                    eprintln!("{}: {err}", file.display());
100                }
101                for (file, err) in pe {
102                    eprintln!("{}: {err}", file.display());
103                }
104            }
105            code.interpret(env).map_err(Into::into)
106        }
107    },
108    ($path:literal) => {
109        |env: &mut Environment| -> Result<ConValue, EvalError> {
110            // TODO: embed the full module tree at compile time
111            let path = AsRef::<Path>::as_ref(&concat!(env!("CARGO_MANIFEST_DIR"), "/../../", file!()))
112                .with_file_name(concat!($path));
113            let mut mi = ModuleInliner::new(path);
114            let code = mi.fold_file(
115                Parser::new(
116                    concat!(file!(), ":", line!(), ":", column!()),
117                    Lexer::new(include_str!($path)),
118                )
119                .parse()?,
120            );
121            if let Some((ie, pe)) = mi.into_errs() {
122                for (file, err) in ie {
123                    eprintln!("{}: {err}", file.display());
124                }
125                for (file, err) in pe {
126                    eprintln!("{}: {err}", file.display());
127                }
128            }
129            code.interpret(env).map_err(Into::into)
130        }
131    }
132}
133
134#[derive(Clone, Debug)]
135pub enum EvalError {
136    Parse(cl_parser::error::Error),
137    Interpret(cl_interpret::error::Error),
138}
139
140impl From<cl_parser::error::Error> for EvalError {
141    fn from(value: cl_parser::error::Error) -> Self {
142        Self::Parse(value)
143    }
144}
145impl From<cl_interpret::error::Error> for EvalError {
146    fn from(value: cl_interpret::error::Error) -> Self {
147        Self::Interpret(value)
148    }
149}
150impl std::error::Error for EvalError {}
151impl std::fmt::Display for EvalError {
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        match self {
154            EvalError::Parse(error) => error.fmt(f),
155            EvalError::Interpret(error) => error.fmt(f),
156        }
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use std::assert_matches::assert_matches;
163
164    use super::*;
165
166    #[test]
167    fn it_works() -> Result<(), EvalError> {
168        let mut env = Environment::new();
169
170        let result = conlang! {
171            fn add(left, right) -> isize {
172                left + right
173            }
174
175            add(2, 2)
176        }(&mut env);
177
178        assert_matches!(result, Ok(Value::Int(4)));
179
180        Ok(())
181    }
182}