From b2da75189ffb7794d3297ab9b63f64b7f6431a95 Mon Sep 17 00:00:00 2001 From: Vincent Ollivier Date: Mon, 6 Jun 2022 16:02:34 +0200 Subject: [PATCH] Improve Lisp (#344) * Add println * Rewrite fib * Refactor parse * Update lisp completer * Store args in env * Add 'or' and 'and' builtins * Add factorial.lsp * Update docs * Add cat operation * Add join operation * Add system command * Add time command (#346) * Add time command * Fix merge artefact * Fix call to realtime * Replace clock syscalls with device files (#345) * Replace clock syscalls with device files * Add missing newline to read * Update time command * Use Rc> * Add first TCO * Remove Box * Change result of env_for_lambda * Run clippy * Remove env clone * Remove TCO * Change return type of env_for_lambda --- doc/lisp.md | 17 ++- dsk/ini/lisp/core.lsp | 3 + dsk/tmp/fibonacci.lsp | 7 - dsk/tmp/lisp/factorial.lsp | 15 +++ dsk/tmp/lisp/fibonacci.lsp | 12 ++ src/usr/install.rs | 5 +- src/usr/lisp.rs | 269 ++++++++++++++++++++++++------------- 7 files changed, 217 insertions(+), 111 deletions(-) delete mode 100644 dsk/tmp/fibonacci.lsp create mode 100644 dsk/tmp/lisp/factorial.lsp create mode 100644 dsk/tmp/lisp/fibonacci.lsp diff --git a/doc/lisp.md b/doc/lisp.md index 8d2cfb7..4cd2f8a 100644 --- a/doc/lisp.md +++ b/doc/lisp.md @@ -3,13 +3,13 @@ A minimalist Lisp interpreter is available in MOROS to extend the capabilities of the Shell. +MOROS Lisp is a Lisp-1 dialect inspired by Scheme and Clojure. + It started from [Risp](https://github.com/stopachka/risp) and was extended to include the seven primitive operators and the two special forms of John McCarthy's paper "Recursive Functions of Symbolic Expressions and Their Computation by Machine" (1960) and "The Roots of Lisp" (2002) by Paul Graham. -MOROS Lisp dialect is also inspired by Scheme and Clojure. - In version 0.2.0 the whole implementation was refactored and the parser was rewritten to use [Nom](https://github.com/Geal/nom). This allowed the addition of strings to the language and reading from the filesystem. @@ -53,18 +53,17 @@ And it can execute a file. For example a file located in `/tmp/fibonacci.lsp` with the following content: ```lisp -(label fib - (lambda (n) - (cond - ((< n 2) n) - (true (+ (fib (- n 1)) (fib (- n 2))))))) +(defn fib (n) + (cond + ((< n 2) n) + (true (+ (fib (- n 1)) (fib (- n 2)))))) -(print (fib 6)) +(println (fib 10)) ``` Would produce the following output: ``` > lisp /tmp/fibonacci.lsp -8 +55 ``` diff --git a/dsk/ini/lisp/core.lsp b/dsk/ini/lisp/core.lsp index 4bb6cee..8231614 100644 --- a/dsk/ini/lisp/core.lsp +++ b/dsk/ini/lisp/core.lsp @@ -41,3 +41,6 @@ (defn third (lst) (second (rest lst))) + +(defn println (exp) + (do (print exp) (print "\n"))) diff --git a/dsk/tmp/fibonacci.lsp b/dsk/tmp/fibonacci.lsp deleted file mode 100644 index ce8a613..0000000 --- a/dsk/tmp/fibonacci.lsp +++ /dev/null @@ -1,7 +0,0 @@ -(label fib - (lambda (n) - (cond - ((< n 2) n) - (true (+ (fib (- n 1)) (fib (- n 2))))))) - -(print (fib 6)) diff --git a/dsk/tmp/lisp/factorial.lsp b/dsk/tmp/lisp/factorial.lsp new file mode 100644 index 0000000..f9ed765 --- /dev/null +++ b/dsk/tmp/lisp/factorial.lsp @@ -0,0 +1,15 @@ +(load "/ini/lisp/core.lsp") + +(defn fact-acc (n acc) + (cond + ((< n 2) acc) + (true (fact-acc (- n 1) (* acc n))))) + +(defn fact (n) + (fact-acc n 1)) + +(println + (fact + (cond + ((null? args) 10) + (true (parse (car args)))))) diff --git a/dsk/tmp/lisp/fibonacci.lsp b/dsk/tmp/lisp/fibonacci.lsp new file mode 100644 index 0000000..92ccdf3 --- /dev/null +++ b/dsk/tmp/lisp/fibonacci.lsp @@ -0,0 +1,12 @@ +(load "/ini/lisp/core.lsp") + +(defn fib (n) + (cond + ((< n 2) n) + (true (+ (fib (- n 1)) (fib (- n 2)))))) + +(println + (fib + (cond + ((null? args) 10) + (true (parse (car args)))))) diff --git a/src/usr/install.rs b/src/usr/install.rs index b687689..991d177 100644 --- a/src/usr/install.rs +++ b/src/usr/install.rs @@ -42,7 +42,10 @@ pub fn copy_files(verbose: bool) { copy_file("/ini/fonts/zap-vga-8x16.psf", include_bytes!("../../dsk/ini/fonts/zap-vga-8x16.psf"), verbose); copy_file("/tmp/alice.txt", include_bytes!("../../dsk/tmp/alice.txt"), verbose); - copy_file("/tmp/fibonacci.lsp", include_bytes!("../../dsk/tmp/fibonacci.lsp"), verbose); + + create_dir("/tmp/lisp", verbose); + copy_file("/tmp/lisp/factorial.lsp", include_bytes!("../../dsk/tmp/lisp/factorial.lsp"), verbose); + copy_file("/tmp/lisp/fibonacci.lsp", include_bytes!("../../dsk/tmp/lisp/fibonacci.lsp"), verbose); create_dir("/tmp/beep", verbose); copy_file("/tmp/beep/tetris.sh", include_bytes!("../../dsk/tmp/beep/tetris.sh"), verbose); diff --git a/src/usr/lisp.rs b/src/usr/lisp.rs index ed7613d..80a3b2d 100644 --- a/src/usr/lisp.rs +++ b/src/usr/lisp.rs @@ -2,13 +2,16 @@ use crate::{api, usr}; use crate::api::fs; use crate::api::console::Style; use crate::api::prompt::Prompt; -use alloc::string::ToString; -use alloc::string::String; -use alloc::vec::Vec; -use alloc::format; -use alloc::vec; + use alloc::collections::BTreeMap; +use alloc::format; use alloc::rc::Rc; +use alloc::string::String; +use alloc::string::ToString; +use alloc::vec::Vec; +use alloc::vec; +use core::borrow::Borrow; +use core::cell::RefCell; use core::fmt; use float_cmp::approx_eq; @@ -96,9 +99,47 @@ enum Err { } #[derive(Clone)] -struct Env<'a> { +struct Env { data: BTreeMap, - outer: Option<&'a Env<'a>>, + outer: Option>>, +} + +const COMPLETER_FORMS: [&str; 21] = [ + "atom", + "bytes", + "car", + "cdr", + "cond", + "cons", + "defun", + "eq", + "label", + "lambda", + "lines", + "load", + "mapcar", + "parse", + "print", + "progn", + "quote", + "read", + "read-bytes", + "str", + "type", +]; + +fn lisp_completer(line: &str) -> Vec { + let mut entries = Vec::new(); + if let Some(last_word) = line.split_whitespace().next_back() { + if let Some(f) = last_word.strip_prefix('(') { + for form in COMPLETER_FORMS { + if let Some(entry) = form.strip_prefix(f) { + entries.push(entry.into()); + } + } + } + } + entries } // Parser @@ -149,10 +190,9 @@ fn parse_exp(input: &str) -> IResult<&str, Exp> { } fn parse(input: &str)-> Result<(String, Exp), Err> { - if let Ok((input, exp)) = parse_exp(input) { - Ok((input.to_string(), exp)) - } else { - Err(Err::Reason("Could not parse input".to_string())) + match parse_exp(input) { + Ok((input, exp)) => Ok((input.to_string(), exp)), + Err(_) => Err(Err::Reason("Could not parse input".to_string())), } } @@ -194,7 +234,7 @@ macro_rules! ensure_length_gt { }; } -fn default_env<'a>() -> Env<'a> { +fn default_env() -> Rc> { let mut data: BTreeMap = BTreeMap::new(); data.insert("=".to_string(), Exp::Func(ensure_tonicity!(|a, b| approx_eq!(f64, a, b)))); data.insert(">".to_string(), Exp::Func(ensure_tonicity!(|a, b| !approx_eq!(f64, a, b) && a > b))); @@ -237,15 +277,35 @@ fn default_env<'a>() -> Env<'a> { let res = args[1..].iter().fold(car, |acc, a| libm::pow(acc, *a)); Ok(Exp::Num(res)) })); + data.insert("or".to_string(), Exp::Func(|args: &[Exp]| -> Result { + ensure_length_eq!(args, 2); + match (args[0].clone(), args[1].clone()) { + (Exp::Bool(a), Exp::Bool(b)) => Ok(Exp::Bool(a || b)), + _ => Err(Err::Reason("Expected booleans".to_string())), + } + })); + data.insert("and".to_string(), Exp::Func(|args: &[Exp]| -> Result { + ensure_length_eq!(args, 2); + match (args[0].clone(), args[1].clone()) { + (Exp::Bool(a), Exp::Bool(b)) => Ok(Exp::Bool(a && b)), + _ => Err(Err::Reason("Expected booleans".to_string())), + } + })); + data.insert("system".to_string(), Exp::Func(|args: &[Exp]| -> Result { + ensure_length_eq!(args, 1); + let cmd = string(&args[0])?; + let res = usr::shell::exec(&cmd); + Ok(Exp::Num(res as u8 as f64)) + })); data.insert("print".to_string(), Exp::Func(|args: &[Exp]| -> Result { ensure_length_eq!(args, 1); match args[0].clone() { Exp::Str(s) => { - println!("{}", s); + print!("{}", s); Ok(Exp::Str(s)) } exp => { - println!("{}", exp); + print!("{}", exp); Ok(exp) } } @@ -282,6 +342,16 @@ fn default_env<'a>() -> Env<'a> { _ => Err(Err::Reason("Expected arg to be a list".to_string())) } })); + data.insert("cat".to_string(), Exp::Func(|args: &[Exp]| -> Result { + Ok(Exp::Str(list_of_strings(args)?.join(""))) + })); + data.insert("join".to_string(), Exp::Func(|args: &[Exp]| -> Result { + ensure_length_eq!(args, 2); + match (&args[0], &args[1]) { + (Exp::List(list), Exp::Str(s)) => Ok(Exp::Str(list_of_strings(list)?.join(s))), + _ => Err(Err::Reason("Expected args to be a list and a string".to_string())) + } + })); data.insert("lines".to_string(), Exp::Func(|args: &[Exp]| -> Result { ensure_length_eq!(args, 1); let s = string(&args[0])?; @@ -307,7 +377,7 @@ fn default_env<'a>() -> Env<'a> { }; Ok(Exp::Str(exp.to_string())) })); - Env { data, outer: None } + Rc::new(RefCell::new(Env { data, outer: None })) } fn list_of_symbols(form: &Exp) -> Result, Err> { @@ -328,6 +398,10 @@ fn list_of_floats(args: &[Exp]) -> Result, Err> { args.iter().map(float).collect() } +fn list_of_strings(args: &[Exp]) -> Result, Err> { + args.iter().map(string).collect() +} + fn float(exp: &Exp) -> Result { match exp { Exp::Num(num) => Ok(*num), @@ -349,7 +423,7 @@ fn eval_quote_args(args: &[Exp]) -> Result { Ok(args[0].clone()) } -fn eval_atom_args(args: &[Exp], env: &mut Env) -> Result { +fn eval_atom_args(args: &[Exp], env: &mut Rc>) -> Result { ensure_length_eq!(args, 1); match eval(&args[0], env)? { Exp::List(_) => Ok(Exp::Bool(false)), @@ -357,14 +431,14 @@ fn eval_atom_args(args: &[Exp], env: &mut Env) -> Result { } } -fn eval_eq_args(args: &[Exp], env: &mut Env) -> Result { +fn eval_eq_args(args: &[Exp], env: &mut Rc>) -> Result { ensure_length_eq!(args, 2); let a = eval(&args[0], env)?; let b = eval(&args[1], env)?; Ok(Exp::Bool(a == b)) } -fn eval_car_args(args: &[Exp], env: &mut Env) -> Result { +fn eval_car_args(args: &[Exp], env: &mut Rc>) -> Result { ensure_length_eq!(args, 1); match eval(&args[0], env)? { Exp::List(list) => { @@ -375,7 +449,7 @@ fn eval_car_args(args: &[Exp], env: &mut Env) -> Result { } } -fn eval_cdr_args(args: &[Exp], env: &mut Env) -> Result { +fn eval_cdr_args(args: &[Exp], env: &mut Rc>) -> Result { ensure_length_eq!(args, 1); match eval(&args[0], env)? { Exp::List(list) => { @@ -386,7 +460,7 @@ fn eval_cdr_args(args: &[Exp], env: &mut Env) -> Result { } } -fn eval_cons_args(args: &[Exp], env: &mut Env) -> Result { +fn eval_cons_args(args: &[Exp], env: &mut Rc>) -> Result { ensure_length_eq!(args, 2); match eval(&args[1], env)? { Exp::List(mut list) => { @@ -397,7 +471,7 @@ fn eval_cons_args(args: &[Exp], env: &mut Env) -> Result { } } -fn eval_cond_args(args: &[Exp], env: &mut Env) -> Result { +fn eval_cond_args(args: &[Exp], env: &mut Rc>) -> Result { ensure_length_gt!(args, 0); for arg in args { match arg { @@ -416,12 +490,12 @@ fn eval_cond_args(args: &[Exp], env: &mut Env) -> Result { Ok(Exp::List(Vec::new())) } -fn eval_label_args(args: &[Exp], env: &mut Env) -> Result { +fn eval_label_args(args: &[Exp], env: &mut Rc>) -> Result { ensure_length_eq!(args, 2); match &args[0] { Exp::Sym(key) => { let exp = eval(&args[1], env)?; - env.data.insert(key.clone(), exp); + env.borrow_mut().data.insert(key.clone(), exp); Ok(Exp::Sym(key.clone())) } _ => Err(Err::Reason("Expected first argument to be a symbol".to_string())) @@ -436,7 +510,7 @@ fn eval_lambda_args(args: &[Exp]) -> Result { })) } -fn eval_defun_args(args: &[Exp], env: &mut Env) -> Result { +fn eval_defun_args(args: &[Exp], env: &mut Rc>) -> Result { ensure_length_eq!(args, 3); let name = args[0].clone(); let params = args[1].clone(); @@ -446,7 +520,7 @@ fn eval_defun_args(args: &[Exp], env: &mut Env) -> Result { eval_label_args(&label_args, env) } -fn eval_mapcar_args(args: &[Exp], env: &mut Env) -> Result { +fn eval_mapcar_args(args: &[Exp], env: &mut Rc>) -> Result { ensure_length_eq!(args, 2); match eval(&args[1], env) { Ok(Exp::List(list)) => { @@ -458,7 +532,7 @@ fn eval_mapcar_args(args: &[Exp], env: &mut Env) -> Result { } } -fn eval_progn_args(args: &[Exp], env: &mut Env) -> Result { +fn eval_progn_args(args: &[Exp], env: &mut Rc>) -> Result { let mut res = Ok(Exp::List(vec![])); for arg in args { res = Ok(eval(arg, env)?); @@ -466,7 +540,7 @@ fn eval_progn_args(args: &[Exp], env: &mut Env) -> Result { res } -fn eval_load_args(args: &[Exp], env: &mut Env) -> Result { +fn eval_load_args(args: &[Exp], env: &mut Rc>) -> Result { ensure_length_eq!(args, 1); let path = string(&args[0])?; let mut code = fs::read_to_string(&path).or(Err(Err::Reason("Could not read file".to_string())))?; @@ -481,7 +555,7 @@ fn eval_load_args(args: &[Exp], env: &mut Env) -> Result { Ok(Exp::Bool(true)) } -fn eval_built_in_form(exp: &Exp, args: &[Exp], env: &mut Env) -> Option> { +fn eval_built_in_form(exp: &Exp, args: &[Exp], env: &mut Rc>) -> Option> { match exp { Exp::Sym(s) => { match s.as_ref() { @@ -509,19 +583,20 @@ fn eval_built_in_form(exp: &Exp, args: &[Exp], env: &mut Env) -> Option Result { +fn env_get(key: &str, env: &Rc>) -> Result { + let env = env.borrow_mut(); match env.data.get(key) { Some(exp) => Ok(exp.clone()), None => { match &env.outer { - Some(outer_env) => env_get(key, outer_env), + Some(outer_env) => env_get(key, outer_env.borrow()), None => Err(Err::Reason(format!("Unexpected symbol '{}'", key))), } } } } -fn env_for_lambda<'a>(params: Rc, args: &[Exp], outer: &'a mut Env) -> Result, Err> { +fn env_for_lambda(params: Rc, args: &[Exp], outer: &mut Rc>) -> Result>, Err> { let ks = list_of_symbols(¶ms)?; if ks.len() != args.len() { let plural = if ks.len() == 1 { "" } else { "s" }; @@ -532,16 +607,16 @@ fn env_for_lambda<'a>(params: Rc, args: &[Exp], outer: &'a mut Env) -> Resu for (k, v) in ks.iter().zip(vs.iter()) { data.insert(k.clone(), v.clone()); } - Ok(Env { data, outer: Some(outer) }) + Ok(Rc::new(RefCell::new(Env { data, outer: Some(Rc::new(RefCell::new(outer.borrow_mut().clone()))) }))) } -fn eval_args(args: &[Exp], env: &mut Env) -> Result, Err> { +fn eval_args(args: &[Exp], env: &mut Rc>) -> Result, Err> { args.iter().map(|x| eval(x, env)).collect() } -fn eval(exp: &Exp, env: &mut Env) -> Result { +fn eval(exp: &Exp, env: &mut Rc>) -> Result { match exp { - Exp::Sym(key) => env_get(key, env), + Exp::Sym(key) => env_get(&key, &env), Exp::Bool(_) => Ok(exp.clone()), Exp::Num(_) => Ok(exp.clone()), Exp::Str(_) => Ok(exp.clone()), @@ -558,8 +633,8 @@ fn eval(exp: &Exp, env: &mut Env) -> Result { func(&eval_args(args, env)?) }, Exp::Lambda(lambda) => { - let env = &mut env_for_lambda(lambda.params, args, env)?; - eval(&lambda.body, env) + let mut env = env_for_lambda(lambda.params, args, env)?; + eval(&lambda.body, &mut env) }, _ => Err(Err::Reason("First form must be a function".to_string())), } @@ -573,7 +648,7 @@ fn eval(exp: &Exp, env: &mut Env) -> Result { // REPL -fn parse_eval(exp: &str, env: &mut Env) -> Result { +fn parse_eval(exp: &str, env: &mut Rc>) -> Result { let (_, exp) = parse(exp)?; let exp = eval(&exp, env)?; Ok(exp) @@ -583,27 +658,7 @@ fn strip_comments(s: &str) -> String { s.split('#').next().unwrap().into() } - -const COMPLETER_FORMS: [&str; 11] = [ - "quote", "atom?", "eq?", "first", "rest", "cons", "cond", "def", "fn", - "defn", "print", -]; - -fn lisp_completer(line: &str) -> Vec { - let mut entries = Vec::new(); - if let Some(last_word) = line.split_whitespace().next_back() { - if let Some(f) = last_word.strip_prefix('(') { - for form in COMPLETER_FORMS { - if let Some(entry) = form.strip_prefix(f) { - entries.push(entry.into()); - } - } - } - } - entries -} - -fn repl(env: &mut Env) -> usr::shell::ExitCode { +fn repl(env: &mut Rc>) -> usr::shell::ExitCode { let csi_color = Style::color("Cyan"); let csi_error = Style::color("LightRed"); let csi_reset = Style::reset(); @@ -644,48 +699,56 @@ pub fn main(args: &[&str]) -> usr::shell::ExitCode { let reset = Style::reset(); let env = &mut default_env(); - match args.len() { - 1 => { - repl(env) - }, - 2 => { - let pathname = args[1]; - if let Ok(code) = api::fs::read_to_string(pathname) { - let mut block = String::new(); - let mut opened = 0; - let mut closed = 0; - for (i, line) in code.split('\n').enumerate() { - let line = strip_comments(line); - if !line.is_empty() { - opened += line.matches('(').count(); - closed += line.matches(')').count(); - block.push_str(&line); - if closed >= opened { - if let Err(e) = parse_eval(&block, env) { - match e { - Err::Reason(msg) => { - eprintln!("{}Error:{} {}", error_color, reset, msg); - eprintln!(); - eprintln!(" {}{}:{} {}", line_color, i, reset, line); - return usr::shell::ExitCode::CommandError; - } + + // 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_label_args(&[key, quote], env).is_err() { + error!("Could not parse args"); + return usr::shell::ExitCode::CommandError; + } + + if args.len() < 2 { + repl(env) + } else { + let pathname = args[1]; + if let Ok(code) = api::fs::read_to_string(pathname) { + let mut block = String::new(); + let mut opened = 0; + let mut closed = 0; + for (i, line) in code.split('\n').enumerate() { + let line = strip_comments(line); + if !line.is_empty() { + opened += line.matches('(').count(); + closed += line.matches(')').count(); + block.push_str(&line); + if closed >= opened { + if let Err(e) = parse_eval(&block, env) { + match e { + Err::Reason(msg) => { + eprintln!("{}Error:{} {}", error_color, reset, msg); + eprintln!(); + eprintln!(" {}{}:{} {}", line_color, i, reset, line); + return usr::shell::ExitCode::CommandError; } } - block.clear(); - opened = 0; - closed = 0; } + block.clear(); + opened = 0; + closed = 0; } } - usr::shell::ExitCode::CommandSuccessful - } else { - error!("File not found '{}'", pathname); - usr::shell::ExitCode::CommandError } - }, - _ => { + usr::shell::ExitCode::CommandSuccessful + } else { + error!("File not found '{}'", pathname); usr::shell::ExitCode::CommandError - }, + } } } @@ -795,6 +858,18 @@ fn test_lisp() { assert_eq!(eval!("(= 6 6)"), "true"); assert_eq!(eval!("(= (+ 0.15 0.15) (+ 0.1 0.2))"), "true"); + // and + assert_eq!(eval!("(and true true)"), "true"); + assert_eq!(eval!("(and true false)"), "false"); + assert_eq!(eval!("(and false true)"), "false"); + assert_eq!(eval!("(and false false)"), "false"); + + // or + assert_eq!(eval!("(or true true)"), "true"); + assert_eq!(eval!("(or true false)"), "true"); + assert_eq!(eval!("(or false true)"), "true"); + assert_eq!(eval!("(or false false)"), "false"); + // string assert_eq!(eval!("(eq \"Hello, World!\" \"foo\")"), "false"); assert_eq!(eval!("(lines \"a\nb\nc\")"), "(\"a\" \"b\" \"c\")"); @@ -806,6 +881,12 @@ fn test_lisp() { assert_eq!(eval!("(map parse '(\"1\" \"2\" \"3\"))"), "(1 2 3)"); assert_eq!(eval!("(map (fn (n) (* n 2)) '(1 2 3))"), "(2 4 6)"); + // cat + assert_eq!(eval!("(cat \"a\" \"b\" \"c\")"), "\"abc\""); + + // join + assert_eq!(eval!("(join '(\"a\" \"b\" \"c\") \" \")"), "\"a b c\""); + eval!("(defn apply2 (f arg1 arg2) (f arg1 arg2))"); assert_eq!(eval!("(apply2 + 1 2)"), "3"); }