mirror of https://github.com/vinc/moros.git
549 lines
17 KiB
Rust
549 lines
17 KiB
Rust
mod env;
|
|
mod eval;
|
|
mod expand;
|
|
mod number;
|
|
mod parse;
|
|
mod primitive;
|
|
|
|
pub use number::Number;
|
|
pub use env::Env;
|
|
|
|
use env::default_env;
|
|
use eval::{eval, eval_variable_args};
|
|
use expand::expand;
|
|
use parse::parse;
|
|
|
|
use crate::api;
|
|
use crate::api::console::Style;
|
|
use crate::api::process::ExitCode;
|
|
use crate::api::prompt::Prompt;
|
|
|
|
use alloc::boxed::Box;
|
|
use alloc::format;
|
|
use alloc::rc::Rc;
|
|
use alloc::string::String;
|
|
use alloc::string::ToString;
|
|
use alloc::vec::Vec;
|
|
use alloc::vec;
|
|
use core::cell::RefCell;
|
|
use core::convert::TryInto;
|
|
use core::fmt;
|
|
use lazy_static::lazy_static;
|
|
use spin::Mutex;
|
|
|
|
// Eval & Env adapted from Risp
|
|
// Copyright 2019 Stepan Parunashvili
|
|
// https://github.com/stopachka/risp
|
|
//
|
|
// Parser rewritten from scratch using Nom
|
|
// https://github.com/geal/nom
|
|
//
|
|
// See "Recursive Functions of Symic Expressions and Their Computation by Machine" by John McCarthy (1960)
|
|
// And "The Roots of Lisp" by Paul Graham (2002)
|
|
//
|
|
// MOROS Lisp is a lisp-1 like Scheme and Clojure
|
|
// See "Technical Issues of Separation in Function Cells and Value Cells" by Richard P. Gabriel (1982)
|
|
|
|
// Types
|
|
|
|
#[derive(Clone)]
|
|
pub enum Exp {
|
|
Primitive(fn(&[Exp]) -> Result<Exp, Err>),
|
|
Function(Box<Function>),
|
|
Macro(Box<Function>),
|
|
List(Vec<Exp>),
|
|
Bool(bool),
|
|
Num(Number),
|
|
Str(String),
|
|
Sym(String),
|
|
}
|
|
|
|
impl PartialEq for Exp {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
match (self, other) {
|
|
(Exp::Function(a), Exp::Function(b)) => a == b,
|
|
(Exp::Macro(a), Exp::Macro(b)) => a == b,
|
|
(Exp::List(a), Exp::List(b)) => a == b,
|
|
(Exp::Bool(a), Exp::Bool(b)) => a == b,
|
|
(Exp::Num(a), Exp::Num(b)) => a == b,
|
|
(Exp::Str(a), Exp::Str(b)) => a == b,
|
|
(Exp::Sym(a), Exp::Sym(b)) => a == b,
|
|
_ => false,
|
|
}
|
|
}
|
|
}
|
|
|
|
use core::cmp::Ordering;
|
|
impl PartialOrd for Exp {
|
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
match (self, other) {
|
|
(Exp::Function(a), Exp::Function(b)) => a.partial_cmp(b),
|
|
(Exp::Macro(a), Exp::Macro(b)) => a.partial_cmp(b),
|
|
(Exp::List(a), Exp::List(b)) => a.partial_cmp(b),
|
|
(Exp::Bool(a), Exp::Bool(b)) => a.partial_cmp(b),
|
|
(Exp::Num(a), Exp::Num(b)) => a.partial_cmp(b),
|
|
(Exp::Str(a), Exp::Str(b)) => a.partial_cmp(b),
|
|
(Exp::Sym(a), Exp::Sym(b)) => a.partial_cmp(b),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Exp {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
let out = match self {
|
|
Exp::Primitive(_) => format!("(function args)"),
|
|
Exp::Function(f) => format!("(function {})", f.params),
|
|
Exp::Macro(m) => format!("(macro {})", m.params),
|
|
Exp::Bool(a) => a.to_string(),
|
|
Exp::Num(n) => n.to_string(),
|
|
Exp::Sym(s) => s.clone(),
|
|
Exp::Str(s) => format!("{:?}", s),
|
|
Exp::List(list) => {
|
|
let xs: Vec<String> = list.iter().map(|x| x.to_string()).collect();
|
|
format!("({})", xs.join(" "))
|
|
},
|
|
};
|
|
write!(f, "{}", out)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, PartialEq, PartialOrd)]
|
|
pub struct Function {
|
|
params: Exp,
|
|
body: Exp,
|
|
doc: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum Err {
|
|
Reason(String),
|
|
}
|
|
|
|
lazy_static! {
|
|
pub static ref FUNCTIONS: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! ensure_length_eq {
|
|
($list:expr, $count:expr) => {
|
|
if $list.len() != $count {
|
|
let plural = if $count != 1 { "s" } else { "" };
|
|
return expected!("{} expression{}", $count, plural);
|
|
}
|
|
};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! ensure_length_gt {
|
|
($list:expr, $count:expr) => {
|
|
if $list.len() <= $count {
|
|
let plural = if $count != 1 { "s" } else { "" };
|
|
return expected!("more than {} expression{}", $count, plural);
|
|
}
|
|
};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! ensure_string {
|
|
($exp:expr) => {
|
|
match $exp {
|
|
Exp::Str(_) => {},
|
|
_ => return expected!("a string"),
|
|
}
|
|
};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! ensure_list {
|
|
($exp:expr) => {
|
|
match $exp {
|
|
Exp::List(_) => {},
|
|
_ => return expected!("a list"),
|
|
}
|
|
};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! expected {
|
|
($($arg:tt)*) => ({
|
|
use alloc::format;
|
|
Err(Err::Reason(format!("Expected {}", format_args!($($arg)*))))
|
|
});
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! could_not {
|
|
($($arg:tt)*) => ({
|
|
use alloc::format;
|
|
Err(Err::Reason(format!("Could not {}", format_args!($($arg)*))))
|
|
});
|
|
}
|
|
|
|
pub fn bytes(args: &[Exp]) -> Result<Vec<u8>, Err> {
|
|
args.iter().map(byte).collect()
|
|
}
|
|
|
|
pub fn strings(args: &[Exp]) -> Result<Vec<String>, Err> {
|
|
args.iter().map(string).collect()
|
|
}
|
|
|
|
pub fn numbers(args: &[Exp]) -> Result<Vec<Number>, Err> {
|
|
args.iter().map(number).collect()
|
|
}
|
|
|
|
pub fn string(exp: &Exp) -> Result<String, Err> {
|
|
match exp {
|
|
Exp::Str(s) => Ok(s.to_string()),
|
|
_ => expected!("a string"),
|
|
}
|
|
}
|
|
|
|
pub fn number(exp: &Exp) -> Result<Number, Err> {
|
|
match exp {
|
|
Exp::Num(num) => Ok(num.clone()),
|
|
_ => expected!("a number"),
|
|
}
|
|
}
|
|
|
|
pub fn float(exp: &Exp) -> Result<f64, Err> {
|
|
match exp {
|
|
Exp::Num(num) => Ok(num.into()),
|
|
_ => expected!("a float"),
|
|
}
|
|
}
|
|
|
|
pub fn byte(exp: &Exp) -> Result<u8, Err> {
|
|
number(exp)?.try_into()
|
|
}
|
|
|
|
// REPL
|
|
|
|
fn parse_eval(input: &str, env: &mut Rc<RefCell<Env>>) -> Result<(String, Exp), Err> {
|
|
let (rest, exp) = parse(input)?;
|
|
let exp = expand(&exp, env)?;
|
|
let exp = eval(&exp, env)?;
|
|
Ok((rest, exp))
|
|
}
|
|
|
|
fn lisp_completer(line: &str) -> Vec<String> {
|
|
let mut entries = Vec::new();
|
|
if let Some(last_word) = line.split_whitespace().next_back() {
|
|
if let Some(f) = last_word.strip_prefix('(') {
|
|
for function in &*FUNCTIONS.lock() {
|
|
if let Some(entry) = function.strip_prefix(f) {
|
|
entries.push(entry.into());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
entries
|
|
}
|
|
|
|
fn repl(env: &mut Rc<RefCell<Env>>) -> Result<(), ExitCode> {
|
|
let csi_color = Style::color("Cyan");
|
|
let csi_reset = Style::reset();
|
|
let prompt_string = format!("{}>{} ", csi_color, csi_reset);
|
|
|
|
println!("MOROS Lisp v0.5.0\n");
|
|
|
|
let mut prompt = Prompt::new();
|
|
let history_file = "~/.lisp-history";
|
|
prompt.history.load(history_file);
|
|
prompt.completion.set(&lisp_completer);
|
|
|
|
while let Some(input) = prompt.input(&prompt_string) {
|
|
if input == "(quit)" {
|
|
break;
|
|
}
|
|
if input.is_empty() {
|
|
println!();
|
|
continue;
|
|
}
|
|
match parse_eval(&input, env) {
|
|
Ok((_, exp)) => {
|
|
println!("{}\n", exp);
|
|
}
|
|
Err(e) => match e {
|
|
Err::Reason(msg) => error!("{}\n", msg),
|
|
},
|
|
}
|
|
prompt.history.add(&input);
|
|
prompt.history.save(history_file);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn main(args: &[&str]) -> Result<(), ExitCode> {
|
|
let env = &mut default_env();
|
|
|
|
// Store args in env
|
|
let key = Exp::Sym("args".to_string());
|
|
let list = Exp::List(if args.len() < 2 {
|
|
vec![]
|
|
} else {
|
|
args[2..].iter().map(|arg| Exp::Str(arg.to_string())).collect()
|
|
});
|
|
let quote = Exp::List(vec![Exp::Sym("quote".to_string()), list]);
|
|
if eval_variable_args(&[key, quote], env).is_err() {
|
|
error!("Could not parse args");
|
|
return Err(ExitCode::Failure);
|
|
}
|
|
|
|
if args.len() < 2 {
|
|
repl(env)
|
|
} else {
|
|
if args[1] == "-h" || args[1] == "--help" {
|
|
return help();
|
|
}
|
|
let path = args[1];
|
|
if let Ok(mut input) = api::fs::read_to_string(path) {
|
|
loop {
|
|
match parse_eval(&input, env) {
|
|
Ok((rest, _)) => {
|
|
if rest.is_empty() {
|
|
break;
|
|
}
|
|
input = rest;
|
|
}
|
|
Err(Err::Reason(msg)) => {
|
|
error!("{}", msg);
|
|
return Err(ExitCode::Failure);
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
} else {
|
|
error!("Could not find file '{}'", path);
|
|
Err(ExitCode::Failure)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn help() -> Result<(), ExitCode> {
|
|
let csi_option = Style::color("LightCyan");
|
|
let csi_title = Style::color("Yellow");
|
|
let csi_reset = Style::reset();
|
|
println!("{}Usage:{} lisp {}[<file> [<args>]]{}", csi_title, csi_reset, csi_option, csi_reset);
|
|
Ok(())
|
|
}
|
|
|
|
#[test_case]
|
|
fn test_lisp() {
|
|
use core::f64::consts::PI;
|
|
let env = &mut default_env();
|
|
|
|
macro_rules! eval {
|
|
($e:expr) => {
|
|
format!("{}", parse_eval($e, env).unwrap().1)
|
|
};
|
|
}
|
|
|
|
// quote
|
|
assert_eq!(eval!("(quote (1 2 3))"), "(1 2 3)");
|
|
assert_eq!(eval!("'(1 2 3)"), "(1 2 3)");
|
|
assert_eq!(eval!("(quote 1)"), "1");
|
|
assert_eq!(eval!("'1"), "1");
|
|
assert_eq!(eval!("(quote a)"), "a");
|
|
assert_eq!(eval!("'a"), "a");
|
|
assert_eq!(eval!("(quote '(a b c))"), "(quote (a b c))");
|
|
|
|
// atom?
|
|
assert_eq!(eval!("(atom? (quote a))"), "true");
|
|
assert_eq!(eval!("(atom? (quote (1 2 3)))"), "false");
|
|
assert_eq!(eval!("(atom? 1)"), "true");
|
|
|
|
// equal?
|
|
assert_eq!(eval!("(equal? (quote a) (quote a))"), "true");
|
|
assert_eq!(eval!("(equal? (quote a) (quote b))"), "false");
|
|
assert_eq!(eval!("(equal? (quote a) (quote ()))"), "false");
|
|
assert_eq!(eval!("(equal? (quote ()) (quote ()))"), "true");
|
|
assert_eq!(eval!("(equal? \"a\" \"a\")"), "true");
|
|
assert_eq!(eval!("(equal? \"a\" \"b\")"), "false");
|
|
assert_eq!(eval!("(equal? \"a\" 'b)"), "false");
|
|
assert_eq!(eval!("(equal? 1 1)"), "true");
|
|
assert_eq!(eval!("(equal? 1 2)"), "false");
|
|
assert_eq!(eval!("(equal? 1 1.0)"), "false");
|
|
assert_eq!(eval!("(equal? 1.0 1.0)"), "true");
|
|
|
|
// head
|
|
assert_eq!(eval!("(head (quote (1)))"), "1");
|
|
assert_eq!(eval!("(head (quote (1 2 3)))"), "1");
|
|
|
|
// tail
|
|
assert_eq!(eval!("(tail (quote (1)))"), "()");
|
|
assert_eq!(eval!("(tail (quote (1 2 3)))"), "(2 3)");
|
|
|
|
// cons
|
|
assert_eq!(eval!("(cons (quote 1) (quote (2 3)))"), "(1 2 3)");
|
|
assert_eq!(eval!("(cons (quote 1) (cons (quote 2) (cons (quote 3) (quote ()))))"), "(1 2 3)");
|
|
|
|
// cond
|
|
assert_eq!(eval!("(cond ((< 2 4) 1))"), "1");
|
|
assert_eq!(eval!("(cond ((> 2 4) 1))"), "()");
|
|
assert_eq!(eval!("(cond ((< 2 4) 1) (true 2))"), "1");
|
|
assert_eq!(eval!("(cond ((> 2 4) 1) (true 2))"), "2");
|
|
|
|
// if
|
|
assert_eq!(eval!("(if (< 2 4) 1)"), "1");
|
|
assert_eq!(eval!("(if (> 2 4) 1)"), "()");
|
|
assert_eq!(eval!("(if (< 2 4) 1 2)"), "1");
|
|
assert_eq!(eval!("(if (> 2 4) 1 2)"), "2");
|
|
|
|
// while
|
|
assert_eq!(eval!("(do (variable i 0) (while (< i 5) (set i (+ i 1))) i)"), "5");
|
|
|
|
// variable
|
|
eval!("(variable a 2)");
|
|
assert_eq!(eval!("(+ a 1)"), "3");
|
|
eval!("(variable add-one (function (b) (+ b 1)))");
|
|
assert_eq!(eval!("(add-one 2)"), "3");
|
|
eval!("(variable fibonacci (function (n) (if (< n 2) n (+ (fibonacci (- n 1)) (fibonacci (- n 2))))))");
|
|
assert_eq!(eval!("(fibonacci 6)"), "8");
|
|
|
|
// function
|
|
assert_eq!(eval!("((function (a) (+ 1 a)) 2)"), "3");
|
|
assert_eq!(eval!("((function (a) (* a a)) 2)"), "4");
|
|
assert_eq!(eval!("((function (x) (cons x '(b c))) 'a)"), "(a b c)");
|
|
|
|
// function definition shortcut
|
|
eval!("(define (double x) (* x 2))");
|
|
assert_eq!(eval!("(double 2)"), "4");
|
|
eval!("(define-function (triple x) (* x 3))");
|
|
assert_eq!(eval!("(triple 2)"), "6");
|
|
|
|
// addition
|
|
assert_eq!(eval!("(+)"), "0");
|
|
assert_eq!(eval!("(+ 2)"), "2");
|
|
assert_eq!(eval!("(+ 2 2)"), "4");
|
|
assert_eq!(eval!("(+ 2 3 4)"), "9");
|
|
assert_eq!(eval!("(+ 2 (+ 3 4))"), "9");
|
|
|
|
// subtraction
|
|
assert_eq!(eval!("(- 2)"), "-2");
|
|
assert_eq!(eval!("(- 2 1)"), "1");
|
|
assert_eq!(eval!("(- 1 2)"), "-1");
|
|
assert_eq!(eval!("(- 2 -1)"), "3");
|
|
assert_eq!(eval!("(- 8 4 2)"), "2");
|
|
|
|
// multiplication
|
|
assert_eq!(eval!("(*)"), "1");
|
|
assert_eq!(eval!("(* 2)"), "2");
|
|
assert_eq!(eval!("(* 2 2)"), "4");
|
|
assert_eq!(eval!("(* 2 3 4)"), "24");
|
|
assert_eq!(eval!("(* 2 (* 3 4))"), "24");
|
|
|
|
// division
|
|
assert_eq!(eval!("(/ 4)"), "0");
|
|
assert_eq!(eval!("(/ 4.0)"), "0.25");
|
|
assert_eq!(eval!("(/ 4 2)"), "2");
|
|
assert_eq!(eval!("(/ 1 2)"), "0");
|
|
assert_eq!(eval!("(/ 1 2.0)"), "0.5");
|
|
assert_eq!(eval!("(/ 8 4 2)"), "1");
|
|
|
|
// exponential
|
|
assert_eq!(eval!("(^ 2 4)"), "16");
|
|
assert_eq!(eval!("(^ 2 4 2)"), "256"); // Left to right
|
|
|
|
// remainder
|
|
assert_eq!(eval!("(rem 0 2)"), "0");
|
|
assert_eq!(eval!("(rem 1 2)"), "1");
|
|
assert_eq!(eval!("(rem 2 2)"), "0");
|
|
assert_eq!(eval!("(rem 3 2)"), "1");
|
|
assert_eq!(eval!("(rem -1 2)"), "-1");
|
|
|
|
// comparisons
|
|
assert_eq!(eval!("(< 6 4)"), "false");
|
|
assert_eq!(eval!("(> 6 4)"), "true");
|
|
assert_eq!(eval!("(> 6 4 2)"), "true");
|
|
assert_eq!(eval!("(> 6)"), "true");
|
|
assert_eq!(eval!("(>)"), "true");
|
|
assert_eq!(eval!("(= 6 4)"), "false");
|
|
assert_eq!(eval!("(= 6 6)"), "true");
|
|
assert_eq!(eval!("(= (+ 0.15 0.15) (+ 0.1 0.2))"), "false"); // FIXME?
|
|
|
|
// number
|
|
assert_eq!(eval!("(binary->number (number->binary 42) \"int\")"), "42");
|
|
assert_eq!(eval!("(binary->number (number->binary 42.0) \"float\")"), "42.0");
|
|
|
|
// string
|
|
assert_eq!(eval!("(parse \"9.75\")"), "9.75");
|
|
assert_eq!(eval!("(string \"a\" \"b\" \"c\")"), "\"abc\"");
|
|
assert_eq!(eval!("(string \"a\" \"\")"), "\"a\"");
|
|
assert_eq!(eval!("(string \"foo \" 3)"), "\"foo 3\"");
|
|
assert_eq!(eval!("(equal? \"foo\" \"foo\")"), "true");
|
|
assert_eq!(eval!("(equal? \"foo\" \"bar\")"), "false");
|
|
assert_eq!(eval!("(string.trim \"abc\n\")"), "\"abc\"");
|
|
assert_eq!(eval!("(string.split \"a\nb\nc\" \"\n\")"), "(\"a\" \"b\" \"c\")");
|
|
|
|
// apply
|
|
assert_eq!(eval!("(apply + '(1 2 3))"), "6");
|
|
assert_eq!(eval!("(apply + 1 '(2 3))"), "6");
|
|
assert_eq!(eval!("(apply + 1 2 '(3))"), "6");
|
|
assert_eq!(eval!("(apply + 1 2 3 '())"), "6");
|
|
|
|
// trigo
|
|
assert_eq!(eval!("(acos (cos pi))"), PI.to_string());
|
|
assert_eq!(eval!("(acos 0)"), (PI / 2.0).to_string());
|
|
assert_eq!(eval!("(asin 1)"), (PI / 2.0).to_string());
|
|
assert_eq!(eval!("(atan 0)"), "0.0");
|
|
assert_eq!(eval!("(cos pi)"), "-1.0");
|
|
assert_eq!(eval!("(sin (/ pi 2))"), "1.0");
|
|
assert_eq!(eval!("(tan 0)"), "0.0");
|
|
|
|
// list
|
|
assert_eq!(eval!("(list)"), "()");
|
|
assert_eq!(eval!("(list 1)"), "(1)");
|
|
assert_eq!(eval!("(list 1 2)"), "(1 2)");
|
|
assert_eq!(eval!("(list 1 2 (+ 1 2))"), "(1 2 3)");
|
|
|
|
// bigint
|
|
assert_eq!(eval!("9223372036854775807"), "9223372036854775807"); // -> int
|
|
assert_eq!(eval!("9223372036854775808"), "9223372036854775808"); // -> bigint
|
|
assert_eq!(eval!("(+ 9223372036854775807 0)"), "9223372036854775807"); // -> int
|
|
assert_eq!(eval!("(- 9223372036854775808 1)"), "9223372036854775807"); // -> bigint
|
|
assert_eq!(eval!("(+ 9223372036854775807 1)"), "9223372036854775808"); // -> bigint
|
|
assert_eq!(eval!("(+ 9223372036854775807 1.0)"), "9223372036854776000.0"); // -> float
|
|
assert_eq!(eval!("(+ 9223372036854775807 10)"), "9223372036854775817"); // -> bigint
|
|
assert_eq!(eval!("(* 9223372036854775807 10)"), "92233720368547758070"); // -> bigint
|
|
|
|
assert_eq!(eval!("(^ 2 16)"), "65536"); // -> int
|
|
assert_eq!(eval!("(^ 2 128)"), "340282366920938463463374607431768211456"); // -> bigint
|
|
assert_eq!(eval!("(^ 2.0 128)"), "340282366920938500000000000000000000000.0"); // -> float
|
|
|
|
assert_eq!(eval!("(number.type 9223372036854775807)"), "\"int\"");
|
|
assert_eq!(eval!("(number.type 9223372036854775808)"), "\"bigint\"");
|
|
assert_eq!(eval!("(number.type 9223372036854776000.0)"), "\"float\"");
|
|
|
|
// quasiquote
|
|
eval!("(variable x 'a)");
|
|
assert_eq!(eval!("`(x ,x y)"), "(x a y)");
|
|
assert_eq!(eval!("`(x ,x y ,(+ 1 2))"), "(x a y 3)");
|
|
assert_eq!(eval!("`(list ,(+ 1 2) 4)"), "(list 3 4)");
|
|
|
|
// unquote-splice
|
|
eval!("(variable x '(1 2 3))");
|
|
assert_eq!(eval!("`(+ ,x)"), "(+ (1 2 3))");
|
|
assert_eq!(eval!("`(+ ,@x)"), "(+ 1 2 3)");
|
|
|
|
// splice
|
|
assert_eq!(eval!("((function (a @b) a) 1 2 3)"), "1");
|
|
assert_eq!(eval!("((function (a @b) b) 1 2 3)"), "(2 3)");
|
|
|
|
// macro
|
|
eval!("(variable foo 42)");
|
|
eval!("(variable set-10 (macro (x) `(set ,x 10)))");
|
|
eval!("(set-10 foo)");
|
|
assert_eq!(eval!("foo"), "10");
|
|
|
|
// args
|
|
eval!("(variable list* (function args (concat args '())))");
|
|
assert_eq!(eval!("(list* 1 2 3)"), "(1 2 3)");
|
|
|
|
// comments
|
|
assert_eq!(eval!("# comment"), "()");
|
|
assert_eq!(eval!("# comment\n# comment"), "()");
|
|
assert_eq!(eval!("(+ 1 2 3) # comment"), "6");
|
|
assert_eq!(eval!("(+ 1 2 3) # comment\n# comment"), "6");
|
|
}
|