Merge branch 'dev'

This commit is contained in:
teo3300
2024-06-22 23:08:16 +09:00
24 changed files with 749 additions and 452 deletions

5
.gitignore vendored
View File

@ -7,4 +7,7 @@ src/#*
src/*~
# history file
.mal-history
.mal-history
# stupid macos
.DS_Store

23
Makefile Normal file
View File

@ -0,0 +1,23 @@
default: build-release
build-release:
@echo "Build release"
@cargo build --release
test:
@echo "Test release"
@cargo test --release
conf:
@echo "Copy core and libraries"
@mkdir -p ${HOME}/.config/mal
cp -f core/core.mal ${HOME}/.config/mal/
@mkdir -p ${HOME}/.config/mal/libs
cp -f libs/* ${HOME}/.config/mal/libs/
install: build-release test conf
@echo "Install mal"
sudo cp target/release/rust-mal /usr/local/bin/mal
@sudo chown ${USER} /usr/local/bin/mal
@echo "To start mal run:"
@printf "\tmal [path/module.mal ...]\n\n"

View File

@ -1,21 +1,25 @@
;; Previously in core.rs
; Logic
; Identity function
(def! I (fn* [x] x))
(def! not (fn* [x]
(if x nil true)))
(def! and (fn* [a b]
(if a
(if b true))))
b)))
(def! or (fn* [a b]
(if a
true
(if b true))))
a
b)))
(def! xor (fn* [a b]
(if a
(not b)
(if b true))))
b)))
; Arithmetic
(def! > (fn* [a b]
@ -27,57 +31,158 @@
(def! <= (fn* [a b]
(>= b a)))
(def! + (fn* [a b]
(- a (- 0 b))))
(def! abs (fn* [a]
(if (> a 0)
a
(- 0 a))))
(def! sign-product (fn* [op a b acc]
(let* [res (op (abs a) (abs b) acc)]
(if (xor (< 0 a) (< 0 b))
(- 0 res)
res))))
(def! * (fn* [a b]
"Recursive product based on sum"
(sign-product *-rec a b 0)))
(def! *-rec (fn* [a b acc]
(if (= b 0)
acc
(*-rec a (- b 1) (+ acc a)))))
(def! / (fn* [a b]
"recursive division based on subtraction"
(if (= b 0)
(raise "Attempting division by 0")
(sign-product /-rec a b 0))))
(def! /-rec (fn* [a b acc]
(if (>= (- a b) 0)
(/-rec (- a b) b (+ acc 1))
acc)))
(def! mod (fn* [a b]
(- a (* (/ a b) b))))
; Other functions in core.rs
(def! assert-eq (fn* [a b]
(assert (= a b))))
(def! int? (fn* [a]
(= (type a) :int)))
(def! sym? (fn* [a]
(= (type a) :symbol)))
(def! list? (fn* [a]
(= (type a) :list)))
(def! atom? (fn* [a]
(= (type a) :atom)))
(def! empty? (fn* [l]
(= (count l) 0)))
(def! assert-msg (fn* [e m]
(if (not e)
(raise m))))
(def! assert (fn* [e] (assert-msg e "Assertion failed")))
(def! assert-eq (fn* [a b]
"returns an error if arguments are not equals, NIL otherwise"
(assert-msg (= a b) (str "Expected " b (type b) ", found " a (type a)))))
(def! assert-fail (fn* [x]
"returns NIL if evaluation of argument fails, error otherwise"
(assert-msg (not (ok? (eval x))) (str "Expected failure, but " x " is correct"))))
;; Since thread safety is not required, I will leave it like this,
;; to make thread safe just guard the function
(def! swap! (fn* [a f]
(reset! a (f @a))))
;; File-interaction functions
(def! load-file (fn* [f]
"open a mal file and evaluate its content"
(eval (read-string (str "(do " (slurp f) "\nnil)")))))
(def! conf-reload (fn* []
"reload mal config file"
(load-file (str MAL_HOME "/" "config.mal"))))
;; Shorthand
(def! quit (fn* []
"Quit the program with status '0'"
(exit 0)))
;; variables
(def! MAL_HISTORY (str MAL_HOME "/" ".mal-history"))
;; helper functions
; these functions are never called, their symbols should always resolve
; in a special form, they are here only to provide informations through
; the "find" and "help" functions
(def! def! (fn* [symbol value] "==SPECIAL FORM=="
"<symbol>: Sym"
"assign <value> to <symbol> in the current environment"
"#returns: <value>"))
(def! let* (fn* [binding statement...] "==SPECIAL FORM=="
"<bindings>: Vec"
"create a new environment and assign values to symbols according"
"to the <binding> vector then evaluate each <statement>"
"#returns: result of the last evaluation"))
(def! do (fn* [statement...] "==SPECIAL FORM=="
"evaluate each <statement> in the current environment"
"#returns: result of the last evalutaion"))
(def! if (fn* [condition if-true if-false] "==SPECIAL FORM=="
"first evaluate <condition>, based on the result of evaluation"
"evaluates one of the two conditional branches, a missing branch"
"evaluates to NIL"
"#returns: result of the last evaluation"))
(def! fn* (fn* [arguments statement...] "==SPECIAL FORM=="
"arguments: Vec"
"#alias: λ" ; >:3
"#returns: new lambda that accepts <arguments>, evaluates each"
" : <statement> and returns the last evaluation's result"))
(def! help (fn* [symbol] "==SPECIAL FORM=="
"symbol: Sym"
"display an helper f or the specified symbol"
"#returns: NIL"))
(def! find (fn* [substring...] "==SPECIAL FORM=="
"print all the known symbols partially matching <substring> in"
"the current environment"
"#returns: NIL"))
(def! quote (fn* [statement] "==SPECIAL FORM=="
"prevents <statement> from being evaluated, it's possible to use"
"the ' symbol: 'sym is equivalent to (quote sym)"))
(def! ok? (fn* [statement] "==SPECIAL FORM=="
"evaluate <statement>"
"#returns: true if evaluation succeeds, NIL otherwise"))
(def! eval (fn* [statement] "==SPECIAL FORM=="
"evaluate <statement>"
"#returns: the result of the evaluation"))
(def! BANNER
(str
"; rust-mal: a toy lisp interpreter written in rust\n"
"; $ mal [filename ...] : load specified modules at start\n"
"; (load-file <name>) : load specified module while mal is running\n"
"; (find [pattern...]) : list symbols matching all patterns\n"
"; (help <symbol>) : print information about a symbol\n"
";\n"
"; enjoy ^.^\n"))
(def! reverse (fn* [x]
"Reverses order of elements in the arg"
(def! reverse-r (fn* [x t]
(if (empty? x)
t
(reverse-r (cdr x) (cons (car x) t)))))
(reverse-r x '())))
(def! map (fn* [f l]
"Apply function f to all elements of l"
(def! map-r (fn* [l p]
(if (empty? l)
p
(map-r (cdr l) (cons (f (car l)) p)))))
(reverse (map-r l '()))))
(def! filter (fn* [f l]
"Remove all elements of l that don't satisfy f"
(def! filter-r (fn* [l p]
(if (empty? l)
p
(if (f (def! t (car l)))
(filter-r (cdr l) (cons t p))
(filter-r (cdr l) p)))))
(reverse (filter-r l '()))))
(def! map-if (fn* [c f l]
"Apply function f to all elements of l that satisfy c"
(map (fn* [x] (if (c x) (f x) x)) l)))
(def! collect (fn* [f i l]
"Apply collector function f to list l"
"Collector function must accept two parameters:"
"collector and current element, result is the collector in the next iteration"
"Collector is initialized as i"
(def! collect-r (fn* [c l]
(if (empty? l)
c
(collect-r (f c (car l)) (cdr l)))))
(collect-r i l)))

19
libs/lists.mal Normal file
View File

@ -0,0 +1,19 @@
(def! map-nil (fn* [v l]
"Map nil values of l to the specified value"
(map-if v nil l)))
(def! concat (fn* [x y] (def! concat-r (fn* [x y t]
"Concatenate arguments, keeping their order"
(if (car x)
(concat-r (cdr x) y (cons (car x) t))
(if (car y)
(concat-r '() (cdr y) (cons (car y) t))
t))))
(concat-r (reverse y) (reverse x) '())))
(def! distribute (fn* [x] (def! distribute-r (fn* [p n t]
(if (empty? n)
t
(let* [c (car n) n (cdr n)]
(distribute-r (cons c p) n (cons (cons c (concat p n)) t))))))
(distribute-r '() x '())))

23
libs/math.mal Normal file
View File

@ -0,0 +1,23 @@
(def! abs (fn* [a]
(if (> a 0)
a
(- 0 a))))
(def! mod (fn* [a b]
(- a (* (/ a b) b))))
(def! max (fn* [a b]
(if (> a b)
a
b)))
(def! min (fn* [a b]
(if (< a b)
a
b)))
(def! fact (fn* [a]
(def! fact-r (fn* [a b]
(if (not (> a 1)) b
(fact-r (- a 1) (* a b)))))
(fact-r a 1)))

40
libs/string.mal Normal file
View File

@ -0,0 +1,40 @@
(def! char (fn* [l]
(car (boom l))))
(def! char? (fn* [a]
(= (type a) :char)))
(def! string? (fn* [a]
(= (type a) :string)))
(def! strc (fn* [l]
(def! strc-r (fn* [l s]
(if (empty? l)
s
(strc-r (cdr l) (str s (car l))))))
(strc-r l "")))
(def! split (fn* [s c]
"Split the string at every occurrence of character sc"
(if (and (string? s)
(char? c))
(def! split-r (fn* [l t r]
(if (empty? l)
(cons t r)
(do (def! cc (car l))
(if (= cc c)
(split-r (cdr l) "" (cons t r))
(split-r (cdr l) (str t cc) r))))))
(raise "split: accepts a string and a char as arguments"))
(reverse (split-r (boom s) "" '()))))
(def! join (fn* [l s]
"Join element of list l to a stiring, using s as separator"
(def! join-r (fn* [l t]
(if (empty? l)
t
(join-r (cdr l) (str t s (car l))))))
(join-r (cdr l) (car l))))
(def! chsub (fn* [s c1 c2]
(strc (map-if (fn* [x] (= x c1)) (fn* [x] c2) (boom s)))))

View File

@ -1,6 +1,9 @@
use std::env;
use std::{cell::RefCell, env, rc::Rc};
use crate::env::{car, env_new, env_set, mal_exit, num_op, Env};
use crate::env::{
any_zero, arithmetic_op, car, comparison_op, env_new, env_set, mal_boom, mal_car, mal_cdr,
mal_cons, mal_exit, Env,
};
// This is the first time I implement a macro, and I'm copying it
// so I will comment this a LOT
@ -32,32 +35,56 @@ macro_rules! env_init {
}
use crate::parse_tools::read_file;
use crate::printer::pr_str;
use crate::printer::{pr_str, prt};
use crate::reader::{read_str, Reader};
use crate::types::MalType::{Bool, Fun, Int, List, Nil, Str};
use crate::types::{mal_assert, mal_comp, MalArgs, MalErr};
use crate::types::MalType::{Atom, Fun, Int, List, Nil, Str};
use crate::types::{mal_equals, reset_bang, MalErr};
macro_rules! if_atom {
($val:expr) => {{
match $val {
Atom(a) => Ok(a.borrow().clone()),
_ => Err(MalErr::unrecoverable(
format!("{:?} is not an atom", prt($val)).as_str(),
)),
}
}};
}
pub fn ns_init() -> Env {
env_init!(None,
// That's it, you are all going to be simpler functions
"exit" => Fun(mal_exit, "Quits the program with specified status"),
"raise" => Fun(|a| Err(MalErr::unrecoverable(car(a)?.if_string()?)), "Raise an unrecoverable error with the specified message"),
"-" => Fun(|a| num_op(|a, b| Int(a - b), a), "Returns the difference of the arguments"),
"*" => Fun(|a| num_op(|a, b| Int(a * b), a), "Returns the product of the arguments"),
"<" => Fun(|a| num_op(|a, b| Bool(a < b), a), "Returns true if the first argument is strictly smallerthan the second one, nil otherwise"),
"pr-str" => Fun(|a| Ok(Str(a.iter().map(|i| pr_str(i, true)).collect::<Vec<String>>().join(" "))), "Print readably all arguments"),
"str" => Fun(|a| Ok(Str(a.iter().map(|i| pr_str(i, false)).collect::<Vec<String>>().join(""))), "Print non readably all arguments"),
"prn" => Fun(|a| {a.iter().for_each(|a| print!("{} ", pr_str(a, false))); Ok(Nil) }, "Print readably all the arguments"),
"println" => Fun(|a| {a.iter().for_each(|a| print!("{} ", pr_str(a, false))); println!(); Ok(Nil) }, "Print readably all the arguments"),
"list" => Fun(|a| Ok(List(MalArgs::new(a.to_vec()))), "Return the arguments as a list"),
"list?" => Fun(|a| Ok(Bool(a.iter().all(|el| matches!(el, List(_))))), "Return true if the first argument is a list, false otherwise"),
// Ok, keep * and / here because computing basic math operators recursively is fun but not too convenient
"+" => Fun(|a| arithmetic_op(0, |a, b| a + b, a), "Returns the sum of the arguments"),
"-" => Fun(|a| arithmetic_op(0, |a, b| a - b, a), "Returns the difference of the arguments"),
"*" => Fun(|a| arithmetic_op(1, |a, b| a * b, a), "Returns the product of the arguments"),
"/" => Fun(|a| arithmetic_op(1, |a, b| a / b, any_zero(a)?), "Returns the quotient of the arguments (not checking for division by 0)"),
"<" => Fun(|a| comparison_op( |a, b| a < b, a), "Returns true if the first argument is strictly smaller than the second one, nil otherwise"),
">" => Fun(|a| comparison_op( |a, b| a > b, a), "Returns true if the first argument is strictly greater than the second one, nil otherwise"),
"<=" => Fun(|a| comparison_op( |a, b| a <= b, a), "Returns true if the first argument is smaller than or equal to the second one, nil otherwise"),
">=" => Fun(|a| comparison_op( |a, b| a >= b, a), "Returns true if the first argument is greater than or equal to the second one, nil otherwise"),
"pr-str" => Fun(|a| Ok(Str(a.iter().map(|i| pr_str(i, true)).collect::<Vec<String>>().join("").into())), "Print readably all arguments"),
"str" => Fun(|a| Ok(Str(a.iter().map(|i| pr_str(i, false)).collect::<Vec<String>>().join("").into())), "Print non readably all arguments"),
"prn" => Fun(|a| {a.iter().for_each(|a| print!("{}", pr_str(a, false))); Ok(Nil) }, "Print readably all the arguments"),
"println" => Fun(|a| {a.iter().for_each(|a| print!("{}", pr_str(a, false))); println!(); Ok(Nil) }, "Print readably all the arguments"),
"list" => Fun(|a| Ok(List(a.into())), "Return the arguments as a list"),
"type" => Fun(|a| Ok(car(a)?.label_type()), "Returns a label indicating the type of it's argument"),
"count" => Fun(|a| Ok(Int(car(a)?.if_list()?.len() as isize)), "Return the number of elements in the first argument"),
"=" => Fun(mal_comp, "Return true if the first two parameters are the same type and content, in case of lists propagate to all elements (NOT IMPLEMENTED for 'Map', 'Fun' and 'MalFun')"),
"assert" => Fun(mal_assert, "Return an error if assertion fails"),
"=" => Fun(mal_equals, "Return true if the first two parameters are the same type and content, in case of lists propagate to all elements (NOT IMPLEMENTED for 'Map', 'Fun' and 'MalFun')"),
"car" => Fun(|a| mal_car(car(a)?), "Returns the first element of the list, NIL if its empty"),
"cdr" => Fun(|a| mal_cdr(car(a)?), "Returns all the list but the first element"),
// A tribute to PHP's explode (PHP, a language I never used)
"boom" => Fun(mal_boom, "Split a string into a list of string\n; BE CAREFUL WHEN USING"),
"read-string" => Fun(|a| read_str(Reader::new().push(car(a)?.if_string()?)).map_err(MalErr::severe), "Tokenize and read the first argument"),
"slurp" => Fun(|a| Ok(Str(read_file(car(a)?.if_string()?)?)), "Read a file and return the content as a string"),
"atom" => Fun(|a| Ok(Atom(Rc::new(RefCell::new(car(a).unwrap_or_default().clone())))), "Return an atom pointing to the given arg"),
"deref" => Fun(|a| if_atom!(car(a)?), "Return the content of the atom argumet"),
"reset!" => Fun(reset_bang, "Change the value of the Atom (frist argument) to the second argument"),
"cons" => Fun(mal_cons, "Push to front if second element is a list"),
"env" => Fun(|a| match env::var(car(a)?.if_string()?) {
Ok(s) => Ok(Str(s)),
Ok(s) => Ok(Str(s.into())),
_ => Ok(Nil),
}, "Retrieve the specified environment variable, returns NIL if that variable does not exist")
)

View File

@ -1,3 +1,4 @@
use crate::eval::eval;
use crate::types::MalErr;
use crate::types::{MalMap, MalRet, MalType};
use std::cell::RefCell;
@ -15,7 +16,7 @@ impl EnvType {
.data
.borrow()
.iter()
.map(|(k, _)| k.clone())
.map(|(k, _)| k.to_string())
.collect::<Vec<String>>();
keys.sort_unstable();
keys
@ -26,27 +27,31 @@ pub type Env = Rc<EnvType>;
// Following rust implementation, using shorthand to always pas Reference count
pub fn env_new(outer: Option<Env>) -> Env {
Rc::new(EnvType {
Env::new(EnvType {
data: RefCell::new(MalMap::new()),
outer,
})
}
pub fn env_set(env: &Env, sym: &str, val: &MalType) -> MalType {
env.data.borrow_mut().insert(sym.to_string(), val.clone());
val.clone()
pub fn env_set(env: &Env, sym: &str, val: &MalType) {
env.data.borrow_mut().insert(sym.into(), val.clone());
}
pub fn env_get(env: &Env, sym: &str) -> MalRet {
match env.data.borrow().get(sym) {
Some(val) => Ok(val.clone()),
None => match env.outer.clone() {
Some(outer) => env_get(&outer, sym),
None => Err(MalErr::unrecoverable(
format!("symbol {:?} not defined", sym).as_str(),
)),
},
let mut iter_env = env;
loop {
if let Some(val) = iter_env.data.borrow().get(sym) {
return Ok(val.clone());
}
if let Some(outer) = &iter_env.outer {
iter_env = &outer;
continue;
}
return Err(MalErr::unrecoverable(
format!("symbol {:?} not defined", sym).as_str(),
));
}
// Recursive was prettier, but we hate recursion
}
pub fn env_binds(outer: Env, binds: &MalType, exprs: &[MalType]) -> Result<Env, MalErr> {
@ -93,34 +98,88 @@ pub fn call_func(func: &MalType, args: &[MalType]) -> CallRet {
// It's fine to clone the environment here
// since this is when the function is actually called
match ast.as_ref() {
M::List(list) => Ok(CallFunc::MalFun(
list.last().unwrap_or(&Nil).clone(),
inner_env,
)),
M::List(list) => {
for x in &list[0..list.len() - 1] {
eval(x, inner_env.clone())?;
}
Ok(CallFunc::MalFun(
list.last().unwrap_or_default().clone(),
inner_env,
))
}
_ => scream!(),
}
}
M::Map(m) => {
if args.is_empty() {
return Err(MalErr::unrecoverable("No key provided to Map construct"));
}
match &args[0] {
M::Str(s) | M::Key(s) => {
Ok(CallFunc::Builtin(m.get(s).unwrap_or_default().clone()))
}
_ => Err(MalErr::unrecoverable("Map argument must be string or key")),
}
}
M::Vector(v) => {
if args.is_empty() {
return Err(MalErr::unrecoverable("No key provided to Vector construct"));
}
match &args[0] {
M::Int(i) => {
if { 0..v.len() as isize }.contains(i) {
Ok(CallFunc::Builtin(v[*i as usize].clone()))
} else {
Ok(CallFunc::Builtin(M::Nil))
}
}
_ => Err(MalErr::unrecoverable("Map argument must be string or key")),
}
}
_ => Err(MalErr::unrecoverable(
format!("{:?} is not a function", prt(func)).as_str(),
)),
}
}
pub fn bin_unpack(list: &[MalType]) -> Result<(&MalType, &MalType), MalErr> {
if list.len() != 2 {
return Err(MalErr::unrecoverable("Two arguments required"));
pub fn any_zero(list: &[MalType]) -> Result<&[MalType], MalErr> {
if list.iter().any(|x| matches!(x, M::Int(0))) {
return Err(MalErr::unrecoverable("Attempting division by 0"));
}
Ok((&list[0], &list[1]))
Ok(list)
}
pub fn num_op(f: fn(isize, isize) -> MalType, args: &[MalType]) -> MalRet {
let (car, cdr) = bin_unpack(args)?;
let car = car.if_number()?;
let cdr = cdr.if_number()?;
Ok(f(car, cdr))
pub fn arithmetic_op(set: isize, f: fn(isize, isize) -> isize, args: &[MalType]) -> MalRet {
Ok(M::Int(match args.len() {
0 => set,
1 => f(set, args[0].if_number()?),
_ => {
// TODO: Maybe an accumulator
let mut left = args[0].if_number()?;
for el in &args[1..] {
left = f(left, el.if_number()?);
}
left
}
}))
}
use MalType::Nil;
use MalType::{Bool, Nil};
pub fn comparison_op(f: fn(isize, isize) -> bool, args: &[MalType]) -> MalRet {
if args.is_empty() {
return Ok(Nil);
}
let (left, rights) = car_cdr(args)?;
let mut left = left.if_number()?;
for right in rights {
let right = right.if_number()?;
if !f(left, right) {
return Ok(Nil);
}
left = right;
}
Ok(Bool(true))
}
pub fn car(list: &[MalType]) -> Result<&MalType, MalErr> {
match list.len() {
@ -137,11 +196,39 @@ pub fn cdr(list: &[MalType]) -> &[MalType] {
}
}
pub fn mal_cdr(arg: &MalType) -> MalRet {
let list = arg.if_list()?;
Ok(MalType::List(cdr(list).into()))
}
pub fn mal_car(arg: &MalType) -> MalRet {
let list = arg.if_list()?;
if list.is_empty() {
Ok(Nil)
} else {
Ok(list[0].clone())
}
}
/// Extract the car and cdr from a list
pub fn car_cdr(list: &[MalType]) -> Result<(&MalType, &[MalType]), MalErr> {
Ok((car(list)?, cdr(list)))
}
// TODO: fix these chonky functions
pub fn mal_cons(args: &[MalType]) -> MalRet {
match args.len() {
2 => {
let mut car = vec![args[0].clone()];
let cdr = args[1].if_list()?;
car.extend_from_slice(cdr);
Ok(M::List(car.into()))
}
_ => Err(MalErr::unrecoverable("cons: requires 2 arguments")),
}
}
fn first(list: &[MalType]) -> &[MalType] {
if list.len() > 1 {
&list[..list.len() - 1]
@ -153,7 +240,7 @@ fn first(list: &[MalType]) -> &[MalType] {
// FIXME: Treat as result for now, change later
fn last(list: &[MalType]) -> Result<&MalType, MalErr> {
match list.len() {
0 => Err(MalErr::unrecoverable("Mi sono cacato le mutande")),
0 => Ok(&MalType::Nil),
_ => Ok(&list[list.len() - 1]),
}
}
@ -170,3 +257,9 @@ pub fn mal_exit(list: &[MalType]) -> MalRet {
_ => exit(-1),
}
}
// TODO: find another way to process strings
pub fn mal_boom(args: &[MalType]) -> MalRet {
let string = car(args)?.if_string()?;
Ok(M::List(string.chars().map(M::Ch).collect()))
}

View File

@ -4,8 +4,28 @@ use crate::env::{first_last, Env};
use crate::printer::prt;
use crate::types::MalType as M;
use crate::types::{MalArgs, MalErr, MalMap, MalRet, MalType};
use std::borrow::Borrow;
use std::rc::Rc;
macro_rules! forms {
($($name:ident : $value:expr),*) => {
$(
const $name: &'static str = $value;
)*
};
}
forms!(NAME_DEF : "def!",
NAME_LET : "let*",
NAME_DO : "do",
NAME_IF : "if",
NAME_FN : "fn*",
NAME_FN_ALT : "λ",
NAME_HELP : "help",
NAME_FIND : "find",
NAME_QUOTE : "quote",
NAME_OK : "ok?",
NAME_EVAL : "eval");
/// Resolve the first element of the list as the function name and call it
/// with the other elements as arguments
fn eval_func(list: &MalType) -> CallRet {
@ -42,7 +62,9 @@ fn def_bang_form(list: &[MalType], env: Env) -> MalRet {
}
let (car, _) = car_cdr(list)?;
let sym = car.if_symbol()?;
Ok(env_set(&env, sym, &eval(&list[1], env.clone())?))
let val = &eval(&list[1], env.clone())?;
env_set(&env, sym, val);
Ok(val.clone())
}
/// let* special form:
@ -96,7 +118,7 @@ fn fn_star_form(list: &[MalType], env: Env) -> MalRet {
Ok(M::MalFun {
// eval: eval_ast,
params: Rc::new(binds.clone()),
ast: Rc::new(M::List(MalArgs::new(exprs.to_vec()))),
ast: Rc::new(M::List(exprs.into())),
env,
})
}
@ -132,6 +154,21 @@ pub fn outermost(env: &Env) -> Env {
env.clone()
}
macro_rules! apply {
($ast:expr, $env:expr) => {{
let apply_list = &eval_ast(&$ast, $env.clone())?;
let eval_ret = eval_func(apply_list)?;
match eval_ret {
CallFunc::Builtin(ret) => return Ok(ret),
CallFunc::MalFun(fun_ast, fun_env) => {
$ast = fun_ast;
$env = fun_env;
}
}
}};
}
/// Intermediate function to discern special forms from defined symbols
pub fn eval(ast: &MalType, env: Env) -> MalRet {
let mut ast = ast.clone();
@ -141,75 +178,59 @@ pub fn eval(ast: &MalType, env: Env) -> MalRet {
M::List(list) if list.is_empty() => return Ok(ast.clone()),
M::List(list) => {
let (symbol, args) = car_cdr(list)?;
match symbol {
M::Sym(sym) if sym == "def!" => return def_bang_form(args, env.clone()), // Set for env
M::Sym(sym) if sym == "let*" => (ast, env) = let_star_form(args, env.clone())?,
M::Sym(sym) if sym == "do" => ast = do_form(args, env.clone())?,
M::Sym(sym) if sym == "if" => ast = if_form(args, env.clone())?,
M::Sym(sym) if sym == "fn*" => return fn_star_form(args, env.clone()),
M::Sym(sym) if sym == "help" => return help_form(args, env.clone()),
M::Sym(sym) if sym == "find" => return find_form(args, env.clone()),
// Oh God, what have I done
M::Sym(sym) if sym == "quote" => return Ok(car(args)?.clone()),
M::Sym(sym) if sym == "ok?" => {
return match eval(car(args)?, env.clone()) {
Err(_) => Ok(M::Nil),
_ => Ok(M::Bool(true)),
if let M::Sym(sym) = symbol {
match sym.borrow() {
// I don't like to borrow tho
NAME_DEF => return def_bang_form(args, env.clone()), // Set for env
NAME_LET => {(ast, env) = let_star_form(args, env.clone())?; continue;},
NAME_DO => {ast = do_form(args, env.clone())?; continue;},
NAME_IF => {ast = if_form(args, env.clone())?; continue;},
NAME_FN | NAME_FN_ALT /* :) */ => {
return fn_star_form(args, env.clone())
}
}
// Special form, sad
// Bruh, is basically double eval
M::Sym(sym) if sym == "eval" => {
ast = eval(env::car(args)?, env.clone())?;
// Climb to the outermost environment (The repl env)
env = outermost(&env);
}
// Filter out special forms
// "apply"/invoke
_ => {
let apply_list = &eval_ast(&ast, env.clone())?;
let eval_ret = eval_func(apply_list)?;
match eval_ret {
CallFunc::Builtin(ret) => return Ok(ret),
CallFunc::MalFun(fun_ast, fun_env) => {
ast = fun_ast;
env = fun_env;
NAME_HELP => return help_form(args, env.clone()),
NAME_FIND => return find_form(args, env.clone()),
// Oh God, what have I done
NAME_QUOTE => return Ok(car(args)?.clone()),
NAME_OK => {
return match eval(car(args)?, env.clone()) {
Err(_) => Ok(M::Nil),
_ => Ok(M::Bool(true)),
}
}
// Special form, sad
// Bruh, is basically double eval
NAME_EVAL => {
ast = eval(env::car(args)?, env.clone())?;
// Climb to the outermost environment (The repl env)
env = outermost(&env);
continue;
}
_ => {}
}
};
}
// "apply"/invoke
apply!(ast, env)
}
_ => return eval_ast(&ast, env),
}
}
}
/// Switch ast evaluation depending on it being a list or not and return the
/// result of the evaluation, this function calls "eval_ast" to recursively
/// evaluate asts
/*pub fn eval(ast: &MalType, env: Env) -> MalRet {
match &ast {
M::List(list) if list.is_empty() => Ok(ast.clone()),
M::List(list) if !list.is_empty() => apply(list, env),
_ => eval_ast(ast, env),
}
}*/
/// Separately evaluate all elements in a collection (list or vector)
fn eval_collection(list: &MalArgs, env: Env) -> Result<MalArgs, MalErr> {
let mut ret = Vec::new();
for el in list.as_ref() {
ret.push(eval(el, env.clone())?);
}
Ok(MalArgs::new(ret))
Ok(ret.into())
}
/// Evaluate the values of a map
fn eval_map(map: &MalMap, env: Env) -> MalRet {
let mut ret = MalMap::new();
for (k, v) in map {
ret.insert(k.to_string(), eval(v, env.clone())?);
ret.insert(k.clone(), eval(v, env.clone())?);
}
Ok(M::Map(ret))
}
@ -225,248 +246,4 @@ fn eval_ast(ast: &MalType, env: Env) -> MalRet {
}
}
////////////////////////////////////////////////////////////////////////////////
// Tests //
////////////////////////////////////////////////////////////////////////////////
#[cfg(test)]
mod tests {
use crate::env::Env;
macro_rules! load2 {
($input:expr) => {{
use crate::reader::{read_str, Reader};
let r = Reader::new();
r.push($input);
&match read_str(&r) {
Ok(v) => match v {
MalType::List(v) => v,
_ => panic!("Not a list"),
},
_ => panic!("Bad command"),
}
}};
}
macro_rules! load {
($input:expr) => {{
use crate::env::cdr;
cdr(load2!($input))
}};
}
/*macro_rules! load_f {
($input:expr, $env:expr) => {{
use crate::reader::{read_str, Reader};
use std::rc::Rc;
let r = Reader::new();
r.push($input);
let args = match read_str(&r) {
Ok(v) => match v {
MalType::List(v) => v,
_ => panic!("Bad command"),
},
_ => panic!("Bad command"),
};
&MalType::List(Rc::new(if args.is_empty() {
Vec::new()
} else {
let f_str = match &args[0] {
MalType::Sym(s) => s.as_str(),
_ => panic!("Can't solve function"),
};
let f = match env_get(&$env.clone(), f_str) {
Ok(v) => v,
_ => panic!("No such function in env"),
};
[&[f], &args[1..]].concat()
}))
}};
}*/
fn _env_empty() -> Env {
use crate::env::env_new;
env_new(None)
}
mod forms {
use crate::env::env_get;
use crate::eval::tests::_env_empty;
use crate::eval::{def_bang_form, fn_star_form, if_form, let_star_form};
use crate::types::MalType;
#[test]
fn def_bang() {
let env = _env_empty();
assert!(matches!( //x empty
def_bang_form(
load!("(def!) ; empty"),
env.clone()),
Err(e)
if !e.is_recoverable()));
assert!(matches!( //x 1 arg
def_bang_form(
load!("(def! a) ; 1 arg"),
env.clone()),
Err(e)
if !e.is_recoverable()));
assert!(matches!( //x 3 args
def_bang_form(
load!("(def! a 1 2) ; 3 args"),
env.clone()),
Err(e)
if !e.is_recoverable()));
assert!(matches!(
//v 2 args
def_bang_form(load!("(def! a 1) ; correct a = 1"), env.clone()),
Ok(MalType::Int(1))
));
assert!(matches!(env_get(&env, "a"), Ok(MalType::Int(1))));
}
#[test]
fn let_star() {
let env = _env_empty();
assert!(
matches!(let_star_form(load!("(let*)"), env.clone()), Err(e) if !e.is_recoverable())
);
assert!(
matches!(let_star_form(load!("(let* 1)"), env.clone()), Err(e) if !e.is_recoverable())
);
assert!(
matches!(let_star_form(load!("(let* [a])"), env.clone()), Err(e) if !e.is_recoverable())
); /*
assert!(matches!(
let_star_form(load!("(let* ())"), env.clone()),
Ok(MalType::Nil)
));
assert!(matches!(
let_star_form(load!("(let* (a 1))"), env.clone()),
Ok(MalType::Nil)
));
assert!(matches!(env_get(&env.clone(), "a"), Err(e) if !e.is_recoverable()));
assert!(matches!(
let_star_form(load!("(let* (a 1 b 2) a b)"), env.clone()),
Ok(MalType::Int(2))
));
assert!(matches!(env_get(&env.clone(), "a"), Err(e) if !e.is_recoverable()));
assert!(matches!(env_get(&env.clone(), "b"), Err(e) if !e.is_recoverable()));
assert!(matches!(
let_star_form(load!("(let* (a 1 b 2) (def! c 1) a b)"), env.clone()),
Ok(MalType::Int(2))
));*/
assert!(matches!(env_get(&env.clone(), "c"), Err(e) if !e.is_recoverable()));
}
/*#[test]
fn _do_form() {
let env = _env_empty();
assert!(matches!(
do_form(load!("(do)"), env.clone()),
Ok(MalType::Nil)
));
assert!(matches!(
do_form(load!("(do true)"), env.clone()),
Ok(MalType::Bool(true))
));
assert!(matches!(
do_form(load!("(do (def! a 1) 2)"), env.clone()),
Ok(MalType::Int(2))
));
assert!(matches!(env_get(&env.clone(), "a"), Ok(MalType::Int(1))));
}*/
#[test]
fn _if_form() {
let env = _env_empty();
assert!(matches!(
if_form(load!("(if)"), env.clone()),
Err(e) if !e.is_recoverable()));
assert!(matches!(
if_form(load!("(if 1)"), env.clone()),
Err(e) if !e.is_recoverable()));
assert!(matches!(
if_form(load!("(if 1 2 3 4)"), env.clone()),
Err(e) if !e.is_recoverable()));
assert!(matches!(
if_form(load!("(if nil 1)"), env.clone()),
Ok(MalType::Nil)
));
assert!(matches!(
if_form(load!("(if nil 1 2)"), env.clone()),
Ok(MalType::Int(2))
));
assert!(matches!(
if_form(load!("(if true 1)"), env.clone()),
Ok(MalType::Int(1))
));
assert!(matches!(
if_form(load!("(if true 1 2)"), env.clone()),
Ok(MalType::Int(1))
));
}
#[test]
fn fn_star() {
let env = _env_empty();
assert!(matches!(
fn_star_form(load!("(fn* [a b] 1 2)"), env.clone()),
Ok(MalType::MalFun {params, ast, .. })
if matches!((*params).clone(), MalType::Vector(v)
if matches!(&v[0], MalType::Sym(v) if v == "a")
&& matches!(&v[1], MalType::Sym(v) if v == "b")
&& matches!((*ast).clone(), MalType::List(v)
if matches!(&v[0], MalType::Int(1))
&& matches!(&v[1], MalType::Int(2))))));
// We trust the fact that the env does not do silly stuff
assert!(matches!(
fn_star_form(load!("(fn*)"), env.clone()),
Err(e) if !e.is_recoverable()));
assert!(matches!(
fn_star_form(load!("(fn* 1)"), env.clone()),
Err(e) if !e.is_recoverable()));
}
/*
#[test]
fn _eval_func() {
let env = _env_empty();
assert!(matches!(
def_bang_form(load!("(def! or (fn* (a b) (if a a b)))"), env.clone()),
Ok(_)
));
assert!(matches!(
eval_func(&MalType::Int(1)),
Err(e) if !e.is_recoverable()));
assert!(matches!(
eval_func(load_f!("()", env.clone())),
Err(e) if !e.is_recoverable()));
assert!(matches!(
eval_func(load_f!("(or nil nil)", env.clone())),
Ok(v) if matches!(v, MalType::Nil)));
assert!(matches!(
eval_func(load_f!("(or 1 nil)", env.clone())),
Ok(MalType::Int(1))
));
assert!(matches!(
eval_func(load_f!("(or nil 1)", env.clone())),
Ok(MalType::Int(1))
));
}*/
/*#[test]
fn _apply() {
let env = _env_empty();
assert!(matches!(
apply(load2!("(def! a 1)"), env.clone()),
Ok(MalType::Int(1))
));
assert!(matches!(env_get(&env, "a"), Ok(MalType::Int(1))));
}*/
}
}
// all tests moved to mal

View File

@ -12,7 +12,7 @@ mod step6_file;
mod types;
use core::ns_init;
use parse_tools::{interactive, load_file, load_home_file, set_home_path};
use parse_tools::{interactive, load_file, load_home_file, print_banner, set_home_path};
fn main() {
// Initialize ns environment
@ -33,5 +33,7 @@ fn main() {
}
});
print_banner(&reply_env);
interactive(reply_env);
}

View File

@ -17,7 +17,7 @@ mod functional {
}
#[test]
fn assert_fail() {
fn assert() {
test!("assert")
}
@ -28,7 +28,7 @@ mod functional {
#[test]
fn builtin_equals() {
test!("equals");
test!("equals")
}
#[test]
@ -38,6 +38,36 @@ mod functional {
#[test]
fn fibonacci() {
test!("fibonacci");
test!("fibonacci")
}
#[test]
fn forms() {
test!("forms")
}
#[test]
fn lists() {
test!("lists")
}
#[test]
fn atoms() {
test!("atoms")
}
#[test]
fn car_cdr() {
test!("car-cdr")
}
#[test]
fn map() {
test!("map")
}
#[test]
fn fil() {
test!("fil")
}
}

View File

@ -2,7 +2,7 @@ use crate::env::Env;
use crate::eval::eval;
use crate::reader::{read_str, Reader};
use crate::step6_file::rep;
use crate::types::{MalErr, MalRet};
use crate::types::{MalErr, MalRet, MalStr};
use std::fs::File;
use std::io::Read;
use std::path::Path;
@ -22,6 +22,10 @@ pub fn set_home_path(env: &Env) {
.unwrap();
}
pub fn print_banner(env: &Env) {
let _ = eval_str("(prn BANNER)", env);
}
fn get_home_path(env: &Env) -> Result<String, MalErr> {
Ok(eval_str("MAL_HOME", env)?.if_string()?.to_string())
}
@ -39,7 +43,7 @@ pub fn load_home_file(filename: &str, env: &Env, warn: bool) {
}
}
pub fn read_file(filename: &str) -> Result<String, MalErr> {
pub fn read_file(filename: &str) -> Result<MalStr, MalErr> {
let mut file = File::open(filename).map_err(|_| {
MalErr::unrecoverable(format!("Failed to open file '{}'", filename).as_str())
})?;
@ -49,7 +53,7 @@ pub fn read_file(filename: &str) -> Result<String, MalErr> {
MalErr::unrecoverable(format!("Failed to read content of '{}'", filename).as_str())
})?;
Ok(content)
Ok(content.into())
}
pub fn load_file(filename: &str, env: &Env) -> MalRet {
@ -63,7 +67,6 @@ pub fn load_file(filename: &str, env: &Env) -> MalRet {
)
} // WTF this is becoming ever less like rust and more like lisp, did I really completely skip the file reading?
extern crate rustyline;
use rustyline::error::ReadlineError;
use rustyline::DefaultEditor;
@ -71,6 +74,7 @@ pub fn interactive(env: Env) {
const HISTORY: &str = ".mal-history";
let home = get_home_path(&env).unwrap();
let history = home + "/" + HISTORY;
eval_str(format!("(def! MAL_HISTORY \"{}\")", history).as_str(), &env).unwrap();
// Using "Editor" instead of the standard I/O because I hate myself but not this much
// TODO: remove unwrap and switch to a better error handling
@ -92,7 +96,7 @@ pub fn interactive(env: Env) {
// // Read line to compose program input
// let mut line = String::new();
// io::stdin().read_line(&mut line).unwrap();
let line = rl.readline("user> ");
let line = rl.readline("; mal> ");
match line {
Ok(line) => {
@ -105,7 +109,7 @@ pub fn interactive(env: Env) {
// Perform rep on whole available input
match rep(&parser, &env) {
Ok(output) => output.iter().for_each(|el| eprintln!("[{}]> {}", num, el)),
Ok(output) => output.iter().for_each(|el| println!("; [{}]> {}", num, el)),
Err(error) => {
if error.is_recoverable() {
// && line != "\n" {

View File

@ -5,9 +5,9 @@ use crate::types::{escape_str, MalType};
fn key_str(val: &str) -> MalType {
if val.starts_with('ʞ') {
M::Key(val.to_string())
M::Key(val.into())
} else {
M::Str(val.to_string())
M::Str(val.into())
}
}
@ -53,6 +53,14 @@ pub fn pr_str(ast: &MalType, print_readably: bool) -> String {
),
M::Fun(..) => "#<builtin>".to_string(),
M::MalFun { .. } => "#<function>".to_string(),
M::Atom(sub) => format!("Atom({})", pr_str(&sub.borrow(), print_readably)),
M::Ch(c) => {
if print_readably {
format!("#\\{}", c)
} else {
c.to_string()
}
}
}
}
@ -61,7 +69,7 @@ pub fn prt(ast: &MalType) -> String {
}
pub fn print_malfun(sym: &str, params: Rc<MalType>, ast: Rc<MalType>) {
println!("{}\t[function]: {}", sym, prt(&params));
println!("; {}\t[function]: {}", sym, prt(&params));
ast.as_ref()
.if_list()
.unwrap_or(&[])

View File

@ -79,9 +79,9 @@ impl Reader {
self.next()?;
match terminator {
")" => Ok(List(MalArgs::new(vector))),
"]" => Ok(Vector(MalArgs::new(vector))),
"}" => make_map(MalArgs::new(vector)),
")" => Ok(List(vector.into())),
"]" => Ok(Vector(vector.into())),
"}" => make_map(vector.into()),
t => Err(MalErr::unrecoverable(
format!("Unknown collection terminator: {}", t).as_str(),
)),
@ -100,15 +100,15 @@ impl Reader {
return Ok(Int(tk.parse::<isize>().unwrap()));
} else if tk.starts_with('\"') {
if tk.len() > 1 && tk.ends_with('\"') {
return Ok(Str(unescape_str(tk)));
return Ok(Str(unescape_str(tk).into()));
}
return Err(MalErr::unrecoverable(
"End of line reached without closing string",
));
} else if tk.starts_with(':') {
return Ok(Key(format!("ʞ{}", tk)));
return Ok(Key(format!("ʞ{}", tk).into()));
}
Ok(Sym(tk.to_string()))
Ok(Sym(tk.into()))
}
}
}
@ -128,8 +128,15 @@ impl Reader {
// Ugly quote transformation for quote expansion
"'" => {
self.next()?;
Ok(List(Rc::new(vec![
MalType::Sym("quote".to_string()),
Ok(List(Rc::new([
MalType::Sym("quote".into()),
self.read_form()?,
])))
}
"@" => {
self.next()?;
Ok(List(Rc::new([
MalType::Sym("deref".into()),
self.read_form()?,
])))
}
@ -168,6 +175,8 @@ fn tokenize(input: &str) -> Tokens {
#[cfg(test)]
mod tests {
use std::borrow::Borrow;
use crate::{
reader::read_str,
types::{MalMap, MalType as M},
@ -262,9 +271,15 @@ mod tests {
assert!(matches!(r.read_atom(), Ok(x) if matches!(x, M::Nil)));
assert!(matches!(r.read_atom(), Ok(x) if matches!(x, M::Int(1))));
assert!(matches!(r.read_atom(), Ok(x) if matches!(x, M::Bool(true))));
assert!(matches!(r.read_atom(), Ok(x) if matches!(x.clone(), M::Sym(v) if v == "a")));
assert!(matches!(r.read_atom(), Ok(x) if matches!(x.clone(), M::Str(v) if v == "s")));
assert!(matches!(r.read_atom(), Ok(x) if matches!(x.clone(), M::Key(v) if v == "ʞ:a")));
assert!(
matches!(r.read_atom(), Ok(x) if matches!(x.clone(), M::Sym(v) if matches!(v.borrow(), "a")))
);
assert!(
matches!(r.read_atom(), Ok(x) if matches!(x.clone(), M::Str(v) if matches!(v.borrow(), "s")))
);
assert!(
matches!(r.read_atom(), Ok(x) if matches!(x.clone(), M::Key(v) if matches!(v.borrow(), "ʞ:a")))
);
assert!(matches!(r.read_atom(), Err(e) if !e.is_recoverable()));
assert!(matches!(r.read_atom(), Err(e) if !e.is_recoverable()));
assert!(matches!(r.read_atom(), Err(e) if !e.is_recoverable()));
@ -311,10 +326,14 @@ mod tests {
MalMap::new()
}
};
assert!(matches!(t.get("n"), Some(x) if matches!(x.clone(), M::Nil)));
assert!(matches!(t.get("t"), Some(x) if matches!(x.clone(), M::Bool(v) if v)));
assert!(matches!(t.get("i"), Some(x) if matches!(x.clone(), M::Int(v) if v == 1)));
assert!(matches!(t.get("s"), Some(x) if matches!(x.clone(), M::Str(v) if v == "str")));
assert!(matches!(t.get("ʞ:s"), Some(x) if matches!(x.clone(), M::Key(v) if v == "ʞ:sym")));
assert!(matches!(t.get("n"), Some(x) if matches!(&x, M::Nil)));
assert!(matches!(t.get("t"), Some(x) if matches!(&x, M::Bool(v) if *v)));
assert!(matches!(t.get("i"), Some(x) if matches!(&x, M::Int(v) if *v == 1)));
assert!(
matches!(t.get("s"), Some(x) if matches!(&x, M::Str(v) if matches!(v.borrow(), "str")))
);
assert!(
matches!(t.get("ʞ:s"), Some(x) if matches!(&x, M::Key(v) if matches!(v.borrow(), "ʞ:sym")))
);
}
}

View File

@ -4,6 +4,8 @@
// input, thus this can be referenced by the previous step without the need
// to allocate more memory
// FIXME: (?) multiple sentences per line, only last is kept
use crate::env::Env;
use crate::eval::eval;
use crate::printer::pr_str;

View File

@ -1,5 +1,10 @@
use crate::env::{bin_unpack, Env};
use std::{collections::HashMap, rc::Rc};
use crate::env::{car_cdr, Env};
use std::{cell::RefCell, collections::HashMap, rc::Rc};
pub type MalStr = Rc<str>;
pub type MalArgs = Rc<[MalType]>;
pub type MalMap = HashMap<MalStr, MalType>;
pub type MalRet = Result<MalType, MalErr>;
// All Mal types should inherit from this
#[derive(Clone)]
@ -15,14 +20,22 @@ pub enum MalType {
env: Env,
}, // Used for functions defined within mal
// Use Rc so I can now clone like there's no tomorrow
Sym(String),
Key(String),
Str(String),
Sym(MalStr),
Key(MalStr),
Str(MalStr),
Ch(char),
Int(isize),
Bool(bool),
Atom(Rc<RefCell<MalType>>),
Nil,
}
impl Default for &MalType {
fn default() -> Self {
&MalType::Nil
}
}
impl MalType {
pub fn if_number(&self) -> Result<isize, MalErr> {
match self {
@ -68,36 +81,68 @@ impl MalType {
)),
}
}
pub fn label_type(&self) -> MalType {
Key(("ʞ:".to_owned()
+ match self {
M::Nil => "nil",
M::Bool(_) => "bool",
M::Int(_) => "int",
M::Fun(_, _) | M::MalFun { .. } => "lambda",
M::Key(_) => "key",
M::Str(_) => "string",
M::Sym(_) => "symbol",
M::List(_) => "list",
M::Vector(_) => "vector",
M::Map(_) => "map",
M::Atom(_) => "atom",
M::Ch(_) => "char",
})
.into())
}
}
use crate::types::MalType as M;
// That's a quite chonky function
fn mal_eq(args: (&MalType, &MalType)) -> bool {
fn mal_compare(args: (&MalType, &MalType)) -> bool {
match (args.0, args.1) {
(M::Nil, M::Nil) => true,
(M::Bool(a), M::Bool(b)) => a == b,
(M::Int(a), M::Int(b)) => a == b,
(M::Ch(a), M::Ch(b)) => a == b,
(M::Key(a), M::Key(b)) | (M::Str(a), M::Str(b)) | (M::Sym(a), M::Sym(b)) => a == b,
(M::List(a), M::List(b)) | (M::Vector(a), M::Vector(b)) => {
a.len() == b.len() && a.iter().zip(b.iter()).all(mal_eq)
a.len() == b.len() && a.iter().zip(b.iter()).all(mal_compare)
}
_ => false,
}
}
pub fn mal_comp(args: &[MalType]) -> MalRet {
if args.len() != 2 {
return Err(MalErr::unrecoverable("Expected 2 arguments"));
}
Ok(M::Bool(mal_eq(bin_unpack(args)?)))
pub fn mal_equals(args: &[MalType]) -> MalRet {
Ok(M::Bool(match args.len() {
0 => true,
_ => {
let (car, cdr) = car_cdr(args)?;
cdr.iter().all(|x| mal_compare((car, x)))
}
}))
}
pub fn mal_assert(args: &[MalType]) -> MalRet {
if args.iter().any(|i| matches!(i, M::Nil | M::Bool(false))) {
return Err(MalErr::unrecoverable("Assertion failed"));
pub fn reset_bang(args: &[MalType]) -> MalRet {
if args.len() < 2 {
return Err(MalErr::unrecoverable("reset requires two arguments"));
}
let val = &args[1];
match &args[0] {
M::Atom(sym) => {
*std::cell::RefCell::<_>::borrow_mut(sym) = val.clone();
Ok(val.clone())
}
_ => Err(MalErr::unrecoverable(
format!("{:?} is not an atom", prt(&args[1])).as_str(),
)),
}
Ok(M::Nil)
}
#[derive(PartialEq, Clone, Copy, Debug)]
@ -143,10 +188,6 @@ impl MalErr {
}
}
pub type MalArgs = Rc<Vec<MalType>>;
pub type MalMap = HashMap<String, MalType>;
pub type MalRet = Result<MalType, MalErr>;
use crate::printer::prt;
use MalType::{Key, Map, Str};
@ -161,7 +202,7 @@ pub fn make_map(list: MalArgs) -> MalRet {
match &list[i] {
Key(k) | Str(k) => {
let v = &list[i + 1];
map.insert(k.to_string(), v.clone());
map.insert(k.clone(), v.clone());
}
_ => {
return Err(MalErr::unrecoverable(
@ -196,16 +237,6 @@ pub fn unescape_str(s: &str) -> String {
#[cfg(test)]
mod tests {
use crate::types::mal_assert;
use crate::types::MalType as M;
#[test]
fn _mal_assert() {
assert!(matches!(mal_assert(&[M::Nil]), Err(_)));
assert!(matches!(mal_assert(&[M::Bool(false)]), Err(_)));
assert!(matches!(mal_assert(&[M::Bool(true)]), Ok(_)));
assert!(matches!(mal_assert(&[M::Int(1)]), Ok(_)));
}
#[test]
fn _escape_str() {

View File

@ -18,10 +18,6 @@
(assert (= (/ -10 -2) 5))
(assert (not (ok? (/ 12 0))))
; mod
(assert (= (mod 10 4) 2))
(assert (= (mod 4 10) 4))
; >
(assert (> 3 2))
(assert (not (> 1 2)))
@ -40,4 +36,4 @@
; <=
(assert (<= 1 3))
(assert (not (<= 3 2)))
(assert (<= 1 1))
(assert (<= 1 1))

View File

@ -2,5 +2,7 @@
(assert (ok? 1))
(assert-eq nil (ok? (1)))
(assert-eq true (ok? 1))
(ok? (assert true))
(not (ok? (assert nil)))
(assert (ok? (assert true)))
(assert (not (ok? (assert nil))))
(assert (not (ok? (assert-fail '1))))
(assert-fail '(1))

19
tests/atoms.mal Normal file
View File

@ -0,0 +1,19 @@
; atom?
(assert (atom? (atom 1)))
(assert (not (atom? 1)))
; deref
(assert-eq (deref (atom 1)) 1)
; @ macro
(assert-eq @(atom 1) 1)
(def! ATOM (atom 1))
; reset!
(reset! ATOM 2)
(assert-eq @ATOM 2)
; swap!
(swap! ATOM (fn* [x] (* x 2)))
(assert-eq @ATOM 4)

7
tests/car-cdr.mal Normal file
View File

@ -0,0 +1,7 @@
;; Test car
(assert-eq (car '(1 2 3)) 1)
(assert-eq (car '()) nil)
;; Test cdr
(assert-eq (cdr '(1 2 3)) '(2 3))
(assert-eq (cdr '()) '())

4
tests/fil.mal Normal file
View File

@ -0,0 +1,4 @@
; filter with builtin function
(assert-eq (list '(1) '(2) '(3)) (filter car (list '(1) '() '(2) '() '(3))))
; filter with lambda function
(assert-eq (list 1 2 3) (filter int? (list 1 "string" 2 'symbol 3 :label)))

58
tests/forms.mal Normal file
View File

@ -0,0 +1,58 @@
; def!
(assert (not (def! FALSE nil)))
(assert (def! TRUE true))
(println :def)
(def! value-0 0)
(assert-eq value-0 0)
(assert-eq
((fn* [x]
(def! value-1 1) (+ x value-1))
2)
3)
(assert-fail '(def!))
(assert-fail '(def! a))
(println :do)
; do
(assert-eq
(do 1 2 3)
3)
(assert-eq
(do
(def! do-test-1 1)
(def! do-test-2 2))
2)
(assert-eq 2 do-test-2)
(let* [a 1 b 2]
(def! do-test-3 3)
(assert-eq do-test-3 3))
(assert-eq (do) nil)
; if
(assert (if true 1))
(assert (not (if false 1)))
(assert (not (if false nil)))
(assert (if false nil 1))
(assert (if true 1 nil))
(assert-fail '(if true))
(assert-fail '(if false))
(assert-fail '(if))
; let*
(assert-eq (let* [let-a 1
let-b 2]
(assert-eq let-a 1)
(assert-eq let-b 2)
4) 4)
(assert-fail 'let-a)
; fn*
(def! f (fn* [x] (def! func-var x) (+ func-var 1)))
(assert-fail 'func-var)
(assert-eq (f 1) 2)
(assert-fail '(f 1 2))
(assert-fail '(1 2 3))

View File

@ -5,5 +5,5 @@
; empty?
(assert (empty? ()))
(assert (empty? (list)))
(assert (not (empty? ())))
(assert (not (empty? (list (1 2 3)))))
(assert (not (empty? '(1 2 3))))
(assert (not (empty? (list 1 2 3))))

5
tests/map.mal Normal file
View File

@ -0,0 +1,5 @@
; builtin map
(assert-eq '((1) (2) (3)) (map list '(1 2 3)))
; lambda map
(assert-eq '(2 3 4) (map (fn* [x] (+ x 1)) '(1 2 3)))