From b11f0aeb96d9443b0d7800bc635175ccf4b6a59b Mon Sep 17 00:00:00 2001 From: Vincent Ollivier Date: Thu, 25 Aug 2022 08:48:19 +0200 Subject: [PATCH] Add new forms to lisp (#385) * Add decode-float and encode-float forms * Add uptime form * Add write-bytes form * Add list form * Add new forms to move print to core lib * Rename file io forms * Update doc * Merge cat into string * Rename *code-float to *code-number * Add write-file and append-file forms * Add doc about the core library * Add regex-find * Add not * Add regex-match * Update doc * Bump lisp version * Add realtime * Update doc --- doc/lisp.md | 43 ++++++++---- dsk/ini/lisp/core.lsp | 26 ++++++- dsk/tmp/lisp/factorial.lsp | 7 +- dsk/tmp/lisp/fibonacci.lsp | 7 +- src/usr/lisp.rs | 137 ++++++++++++++++++++++++++++--------- 5 files changed, 166 insertions(+), 54 deletions(-) diff --git a/doc/lisp.md b/doc/lisp.md index 054f577d1..51c748b31 100644 --- a/doc/lisp.md +++ b/doc/lisp.md @@ -30,22 +30,32 @@ of strings to the language and reading from the filesystem. ## Additional Builtins - `defun` (aliased to `defn`) - `mapcar` (aliased to `map`) -- `print` -- `read` -- `read-bytes` -- `bytes` -- `str` -- `cat` -- `join` -- `lines` -- `parse` - `type` +- `string` +- `encode-string` and `decode-string` +- `encode-number` and `decode-number` +- `regex-find` +- `parse` - `system` +- `load` - Arithmetic operations: `+`, `-`, `*`, `/`, `%`, `^` - Trigonometric functions: `acos`, `asin`, `atan`, `cos`, `sin`, `tan` - Comparisons: `>`, `<`, `>=`, `<=`, `=` -- Boolean operations: `and`, `or` +- Boolean operations: `not`, `and`, `or` +- String operations: `lines`, `join` +- File IO: `read-file`, `read-file-bytes`, `write-file-bytes`, `append-file-bytes` + +## Core Library +- `null`, `null?`, `eq?` +- `atom?`, `string?`, `boolean?`, `symbol?`, `number?`, `list?`, `function?`, `lambda?` +- `first`, `second`, `third`, `rest` +- `append`, `reverse` +- `read-line`, `read-char` +- `print`, `println` +- `write-file`, `append-file` +- `uptime`, `realtime` +- `regex-match?` ## Usage @@ -61,21 +71,26 @@ MOROS Lisp v0.1.0 > (quit) ``` -And it can execute a file. For example a file located in `/tmp/fibonacci.lsp` +And it can execute a file. For example a file located in `/tmp/lisp/fibonacci.lsp` with the following content: ```lisp +(load "/ini/lisp/core.lsp") + (defn fib (n) (cond ((< n 2) n) (true (+ (fib (- n 1)) (fib (- n 2)))))) -(println (fib 10)) +(println + (cond + ((null? args) "Usage: fibonacci ") + (true (fib(parse (car args)))))) ``` Would produce the following output: ``` -> lisp /tmp/fibonacci.lsp -55 +> lisp /tmp/lisp/fibonacci.lsp 20 +6755 ``` diff --git a/dsk/ini/lisp/core.lsp b/dsk/ini/lisp/core.lsp index e310f3efb..c49ba30b6 100644 --- a/dsk/ini/lisp/core.lsp +++ b/dsk/ini/lisp/core.lsp @@ -30,6 +30,9 @@ (defn null? (x) (eq? x null)) +(defn not (x) + (eq? x false)) + (defn rest (a) (cdr a)) @@ -53,7 +56,28 @@ (true (append (reverse (rest a)) (cons (first a) '()))))) (defn read-line () - (str (reverse (rest (reverse (read-bytes "/dev/console" 256)))))) + (decode-string (reverse (rest (reverse (read-file-bytes "/dev/console" 256)))))) + +(defn read-char () + (decode-string (read-file-bytes "/dev/console" 4))) + +(defn print (exp) + (do (append-file-bytes "/dev/console" (encode-string (string exp))) '())) (defn println (exp) (do (print exp) (print "\n"))) + +(defn uptime () + (decode-number (read-file-bytes "/dev/clk/uptime" 8))) + +(defn realtime () + (decode-number (read-file-bytes "realtime" 8))) + +(defn write-file (path str) + (write-file-bytes path (encode-string str))) + +(defn append-file (path str) + (append-file-bytes path (encode-string str))) + +(defn regex-match? (pattern str) + (not (null? (regex-find pattern str)))) diff --git a/dsk/tmp/lisp/factorial.lsp b/dsk/tmp/lisp/factorial.lsp index f9ed765fc..8f0e265e9 100644 --- a/dsk/tmp/lisp/factorial.lsp +++ b/dsk/tmp/lisp/factorial.lsp @@ -9,7 +9,6 @@ (fact-acc n 1)) (println - (fact - (cond - ((null? args) 10) - (true (parse (car args)))))) + (cond + ((null? args) "Usage: factorial ") + (true (fact (parse (car args)))))) diff --git a/dsk/tmp/lisp/fibonacci.lsp b/dsk/tmp/lisp/fibonacci.lsp index 92ccdf3d0..67ce45100 100644 --- a/dsk/tmp/lisp/fibonacci.lsp +++ b/dsk/tmp/lisp/fibonacci.lsp @@ -6,7 +6,6 @@ (true (+ (fib (- n 1)) (fib (- n 2)))))) (println - (fib - (cond - ((null? args) 10) - (true (parse (car args)))))) + (cond + ((null? args) "Usage: fibonacci ") + (true (fib (parse (car args)))))) diff --git a/src/usr/lisp.rs b/src/usr/lisp.rs index 13ff16574..8acb131e0 100644 --- a/src/usr/lisp.rs +++ b/src/usr/lisp.rs @@ -3,6 +3,7 @@ use crate::api::fs; use crate::api::console::Style; use crate::api::process::ExitCode; use crate::api::prompt::Prompt; +use crate::api::regex::Regex; use alloc::collections::BTreeMap; use alloc::format; @@ -13,6 +14,7 @@ use alloc::vec::Vec; use alloc::vec; use core::borrow::Borrow; use core::cell::RefCell; +use core::convert::TryInto; use core::f64::consts::PI; use core::fmt; use float_cmp::approx_eq; @@ -78,12 +80,12 @@ impl PartialEq for Exp { impl fmt::Display for Exp { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let out = match self { - Exp::Lambda(_) => "Lambda {}".to_string(), - Exp::Func(_) => "Function {}".to_string(), + Exp::Lambda(_) => "".to_string(), + Exp::Func(_) => "".to_string(), Exp::Bool(a) => a.to_string(), Exp::Num(n) => n.to_string(), Exp::Sym(s) => s.clone(), - Exp::Str(s) => format!("\"{}\"", s.replace('"', "\\\"")), + Exp::Str(s) => format!("{:?}", s), Exp::List(list) => { let xs: Vec = list.iter().map(|x| x.to_string()).collect(); format!("({})", xs.join(" ")) @@ -324,21 +326,13 @@ fn default_env() -> Rc> { Err(code) => Ok(Exp::Num(code 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) => print!("{}", s), - exp => print!("{}", exp), - } - Ok(Exp::List(Vec::new())) - })); - data.insert("read".to_string(), Exp::Func(|args: &[Exp]| -> Result { + data.insert("read-file".to_string(), Exp::Func(|args: &[Exp]| -> Result { ensure_length_eq!(args, 1); let path = string(&args[0])?; let contents = fs::read_to_string(&path).or(Err(Err::Reason("Could not read file".to_string())))?; Ok(Exp::Str(contents)) })); - data.insert("read-bytes".to_string(), Exp::Func(|args: &[Exp]| -> Result { + data.insert("read-file-bytes".to_string(), Exp::Func(|args: &[Exp]| -> Result { ensure_length_eq!(args, 2); let path = string(&args[0])?; let len = float(&args[1])?; @@ -347,25 +341,81 @@ fn default_env() -> Rc> { buf.resize(bytes, 0); Ok(Exp::List(buf.iter().map(|b| Exp::Num(*b as f64)).collect())) })); - data.insert("bytes".to_string(), Exp::Func(|args: &[Exp]| -> Result { + data.insert("write-file-bytes".to_string(), Exp::Func(|args: &[Exp]| -> Result { + ensure_length_eq!(args, 2); + let path = string(&args[0])?; + match &args[1] { + Exp::List(list) => { + let buf = list_of_bytes(list)?; + let bytes = fs::write(&path, &buf).or(Err(Err::Reason("Could not write file".to_string())))?; + Ok(Exp::Num(bytes as f64)) + } + _ => Err(Err::Reason("Expected second arg to be a list".to_string())) + } + })); + data.insert("append-file-bytes".to_string(), Exp::Func(|args: &[Exp]| -> Result { + ensure_length_eq!(args, 2); + let path = string(&args[0])?; + match &args[1] { + Exp::List(list) => { + let buf = list_of_bytes(list)?; + let bytes = fs::append(&path, &buf).or(Err(Err::Reason("Could not write file".to_string())))?; + Ok(Exp::Num(bytes as f64)) + } + _ => Err(Err::Reason("Expected second arg to be a list".to_string())) + } + })); + data.insert("string".to_string(), Exp::Func(|args: &[Exp]| -> Result { + let args: Vec = args.iter().map(|arg| match arg { + Exp::Str(s) => format!("{}", s), + exp => format!("{}", exp), + }).collect(); + Ok(Exp::Str(args.join(""))) + })); + data.insert("encode-string".to_string(), Exp::Func(|args: &[Exp]| -> Result { ensure_length_eq!(args, 1); let s = string(&args[0])?; let buf = s.as_bytes(); Ok(Exp::List(buf.iter().map(|b| Exp::Num(*b as f64)).collect())) })); - data.insert("str".to_string(), Exp::Func(|args: &[Exp]| -> Result { + data.insert("decode-string".to_string(), Exp::Func(|args: &[Exp]| -> Result { ensure_length_eq!(args, 1); match &args[0] { Exp::List(list) => { - let buf = list_of_floats(list)?.iter().map(|b| *b as u8).collect(); + let buf = list_of_bytes(list)?; let s = String::from_utf8(buf).or(Err(Err::Reason("Could not convert to valid UTF-8 string".to_string())))?; Ok(Exp::Str(s)) } _ => 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("decode-number".to_string(), Exp::Func(|args: &[Exp]| -> Result { + ensure_length_eq!(args, 1); + match &args[0] { + Exp::List(list) => { + let bytes = list_of_bytes(list)?; + ensure_length_eq!(bytes, 8); + Ok(Exp::Num(f64::from_be_bytes(bytes[0..8].try_into().unwrap()))) + } + _ => Err(Err::Reason("Expected arg to be a list".to_string())) + } + })); + data.insert("encode-number".to_string(), Exp::Func(|args: &[Exp]| -> Result { + ensure_length_eq!(args, 1); + let f = float(&args[0])?; + Ok(Exp::List(f.to_be_bytes().iter().map(|b| Exp::Num(*b as f64)).collect())) + })); + data.insert("regex-find".to_string(), Exp::Func(|args: &[Exp]| -> Result { + ensure_length_eq!(args, 2); + match (&args[0], &args[1]) { + (Exp::Str(regex), Exp::Str(s)) => { + let res = Regex::new(regex).find(s).map(|(a, b)| { + vec![Exp::Num(a as f64), Exp::Num(b as f64)] + }).unwrap_or(vec![]); + Ok(Exp::List(res)) + } + _ => Err(Err::Reason("Expected args to be a regex and a string".to_string())) + } })); data.insert("join".to_string(), Exp::Func(|args: &[Exp]| -> Result { ensure_length_eq!(args, 2); @@ -399,6 +449,9 @@ fn default_env() -> Rc> { }; Ok(Exp::Str(exp.to_string())) })); + data.insert("list".to_string(), Exp::Func(|args: &[Exp]| -> Result { + Ok(Exp::List(args.to_vec())) + })); // Setup autocompletion let mut forms: Vec = data.keys().map(|k| k.to_string()).collect(); @@ -428,12 +481,23 @@ fn list_of_symbols(form: &Exp) -> Result, Err> { } } +fn list_of_strings(args: &[Exp]) -> Result, Err> { + args.iter().map(string).collect() +} + 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 list_of_bytes(args: &[Exp]) -> Result, Err> { + args.iter().map(byte).collect() +} + +fn string(exp: &Exp) -> Result { + match exp { + Exp::Str(s) => Ok(s.to_string()), + _ => Err(Err::Reason("Expected a string".to_string())), + } } fn float(exp: &Exp) -> Result { @@ -443,10 +507,12 @@ fn float(exp: &Exp) -> Result { } } -fn string(exp: &Exp) -> Result { - match exp { - Exp::Str(s) => Ok(s.to_string()), - _ => Err(Err::Reason("Expected a string".to_string())), +fn byte(exp: &Exp) -> Result { + let num = float(exp)?; + if num >= 0.0 && num < u8::MAX.into() && (num - libm::trunc(num) == 0.0) { + Ok(num as u8) + } else { + Err(Err::Reason(format!("Expected an integer between 0 and {}", u8::MAX))) } } @@ -698,7 +764,7 @@ fn repl(env: &mut Rc>) -> Result<(), ExitCode> { let csi_reset = Style::reset(); let prompt_string = format!("{}>{} ", csi_color, csi_reset); - println!("MOROS Lisp v0.2.0\n"); + println!("MOROS Lisp v0.3.0\n"); let mut prompt = Prompt::new(); let history_file = "~/.lisp-history"; @@ -904,10 +970,17 @@ fn test_lisp() { assert_eq!(eval!("(or false true)"), "true"); assert_eq!(eval!("(or false false)"), "false"); + // number + assert_eq!(eval!("(decode-number (encode-number 42))"), "42"); + // string - assert_eq!(eval!("(eq \"Hello, World!\" \"foo\")"), "false"); - assert_eq!(eval!("(lines \"a\nb\nc\")"), "(\"a\" \"b\" \"c\")"); 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!("(eq \"foo\" \"foo\")"), "true"); + assert_eq!(eval!("(eq \"foo\" \"bar\")"), "false"); + assert_eq!(eval!("(lines \"a\nb\nc\")"), "(\"a\" \"b\" \"c\")"); // map eval!("(defun inc (a) (+ a 1))"); @@ -915,10 +988,6 @@ 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\""); - assert_eq!(eval!("(cat \"a\" \"\")"), "\"a\""); - // join assert_eq!(eval!("(join '(\"a\" \"b\" \"c\") \" \")"), "\"a b c\""); @@ -933,4 +1002,10 @@ fn test_lisp() { eval!("(defn apply2 (f arg1 arg2) (f arg1 arg2))"); assert_eq!(eval!("(apply2 + 1 2)"), "3"); + + // 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)"); }