Improve Lisp with Advent of Code 2023 🎄 (#556)

* Add contains? to Lisp

* Use ls everywhere instead of lst

* Rename system command to shell

* Exec '/ini/lisp.lsp' at startup

* Check if init file exist

* Add missing lisp init file

* Remove contains?

* Add empty? function

* Add reject? function

* Update doc

* Add dict type to Lisp

* Add push function

* Add binary literals to doc

* Move push function from primitive to core lib

* Update lisp doc

* Add escape character to parser

* Add backspace to parser

* Update doc

* Add host function

* Fix regex.match?

* Test if and while for truthiness instead of comparing with true

* Add tests for empty string and 0

* Mention truthiness in changelog

* Check for unspecified IPv4 in usr::host::resolv

* Transform host error into nil

* Update ntp.lsp to support hostnames

* Fix typo

* Fix escape display

* Use '/' instead of '.' as namespace separator

* Add doc.lsp

* Fix comment color

* Update changelog
This commit is contained in:
Vincent Ollivier 2023-12-17 17:24:55 +01:00 committed by GitHub
parent 525b1dcca3
commit 9f525990a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 357 additions and 162 deletions

View File

@ -2,6 +2,10 @@
## Unreleased
- Improve lisp with Advent of Code 2023 (#556)
- Improve system information (#553)
- Add hash command (#554)
- Improve binary support (#552)
- Add basic userspace shell (#548)
- Add basic userspace allocator (#544)
- Improve documentation (#547)

View File

@ -12,8 +12,9 @@ MOROS Lisp is a Lisp-1 dialect inspired by Scheme, Clojure, and Ruby!
- Number: `float`, `int`, `bigint`
### Literals
- Number: `2.5`, `-25`, `255`, `0xFF`, `0xDEAD_C0DE`, `0b101010`
- String: `"Hello, World!"`
- Number: `2.5`, `-25`, `255`, `0xFF`, `0xDEAD_C0DE`
- Escape: `\b`, `\e`, `\n`, `\r`, `\t`, `\"`, `\\`
### Built-in Operators
- `quote` (abbreviated with `'`)
@ -44,32 +45,34 @@ MOROS Lisp is a Lisp-1 dialect inspired by Scheme, Clojure, and Ruby!
- `load`
### Primitive Operators
- `type`, `number.type` (aliased to `num.type`)
- `type`, `number/type` (aliased to `num/type`), `parse`
- `string` (aliased to `str`)
- `string->number` (aliased to to `str->num`)
- `string->binary` and `binary->string` (aliased to `str->bin` and `bin->str`)
- `number->binary` and `binary->number` (aliased to `num->bin` and `bin->num`)
- `regex.find`
- `system`
- Arithmetic operations: `+`, `-`, `*`, `/`, `^`, `abs`, `mod`, `rem` (aliased to `%`)
- `regex/find`
- `shell` (aliased to `sh`)
- Arithmetic operations: `+`, `-`, `*`, `/`, `^`, `rem` (aliased to `%`), `trunc`
- Trigonometric functions: `acos`, `asin`, `atan`, `cos`, `sin`, `tan`
- Comparisons: `>`, `<`, `>=`, `<=`, `=`
- Enumerable: `length` (aliased to `len`), `nth`, `first`, `second`, `third`, `last`, `rest`, `slice`
- String: `string.trim` and `string.split` (aliased to `str.trim` and `str.split`)
- List: `concat`, `chunks`, `sort`, `unique` (aliased to `uniq`), `min`, `max`
- File: `file.size`, `file.open`, `file.close`, `file.read`, `file.write`
- Enumerable: `length` (aliased to `len`), `put`, `get`, `slice`, `contains?`
- String: `string/trim` and `string/split` (aliased to `str/trim` and `str/split`)
- List: `list`, `concat`, `chunks`, `sort`, `unique` (aliased to `uniq`)
- Dict: `dict`
- File: `file/size`, `file/open`, `file/close`, `file/read`, `file/write`
- Net: `host`, `socket/connect`, `socket/listen`, `socket/accept`
### Core Library
- `nil`, `nil?`, `list?`
- `nil`, `nil?`, `list?`, `empty?`
- `boolean?` (aliased to `bool?`), `string?` (aliased to `str?`), `symbol?` (aliased to `sym?`), `number?` (aliased to `num?`)
- `function?` (aliased to `fun?`), `macro?` (aliased to `mac?`)
- `first`, `second`, `third`, `rest`
- `map`, `reduce`, `reverse` (aliased to `rev`), `range`, `filter`, `intersection`
- `abs`, `mod`, `min`, `max`
- `first`, `second`, `third`, `last`, `rest`, `push`
- `map`, `reduce`, `reverse` (aliased to `rev`), `range`, `filter`, `reject`, `intersection`
- `not`, `and`, `or`
- `let`
- `string.join` (aliased to `str.join`), `lines`, `words`, `chars`
- `regex.match?`
- `socket.connect`, `socket.listen`, `socket.accept`
- `string/join` (aliased to `str/join`), `lines`, `words`, `chars`
- `regex/match?`
### File Library
- `read`, `write`, `append`
@ -207,4 +210,9 @@ language and reading from the filesystem.
- Add socket functions
### Unreleased
- Add hexadecimal number literals
- Add binary and hexadecimal number literals
- Test for truthiness (neither `false` nor `nil`) in conditions of `if` and `while`
- Rename `nth` to `get`
- Add `empty?`, `reject`, `put`, `push`, and `host` functions`
- Add `dict` type
- Use `/` instead of `.` as namespace separator

1
dsk/ini/lisp.lsp Normal file
View File

@ -0,0 +1 @@
(load "/lib/lisp/core.lsp")

View File

@ -30,11 +30,16 @@
(if x false true))
(def-mac (or x y)
`(if ,x true (if ,y true false)))
`(if ,x ,x (if ,y ,y false)))
(def-mac (and x y)
`(if ,x (if ,y true false) false))
(def (empty? x)
(or
(equal? x nil)
(equal? x "")))
(def-mac (let params values body)
`((fun ,params ,body) ,@values))
@ -57,27 +62,34 @@
(cons (head ls) (filter f (tail ls)))
(filter f (tail ls)))))
(def (reject f ls)
"Reject the elements of the list with the function"
(if (nil? ls) nil
(if (not (f (head ls)))
(cons (head ls) (reject f (tail ls)))
(reject f (tail ls)))))
(def (intersection a b)
"Return elements found in both lists"
(filter (fun (x) (contains? b x)) a))
(def (reverse x)
(def (reverse ls)
"Reverse list"
(if (nil? x) x
(concat (reverse (tail x)) (cons (head x) '()))))
(if (nil? ls) ls
(concat (reverse (tail ls)) (cons (head ls) '()))))
(def (range start stop)
"Return a list of integers from start to stop excluded"
(if (= start stop) nil
(concat (list start) (range (+ start 1) stop))))
(def (min lst)
(def (min ls)
"Return the minimum element of the list"
(head (sort lst)))
(head (sort ls)))
(def (max lst)
(def (max ls)
"Return the maximum element of the list"
(head (reverse (sort lst))))
(head (reverse (sort ls))))
(def (abs x)
(if (> x 0) x (- x)))
@ -85,46 +97,52 @@
(def (mod a b)
(rem (+ (rem a b) b) b))
(def (string.join ls s)
(def (string/join ls s)
"Join the elements of the list with the string"
(reduce (fun (x y) (string x s y)) ls))
(def (regex.match? pattern s)
(not (nil? (regex.find pattern str))))
(def (regex/match? r s)
"Return true if the string match the pattern"
(not (nil? (regex/find r s))))
(def (lines text)
"Split text into a list of lines"
(string.split (string.trim text) "\n"))
(string/split (string/trim text) "\n"))
(def (words text)
"Split text into a list of words"
(string.split text " "))
(string/split text " "))
(def (chars text)
"Split text into a list of chars"
(string.split text ""))
(string/split text ""))
(def (first lst)
(nth lst 0))
(def (push ls x)
"Push element to the end of the list"
(put ls (len ls) x))
(def (second lst)
(nth lst 1))
(def (first ls)
(get ls 0))
(def (third lst)
(nth lst 2))
(def (second ls)
(get ls 1))
(def (last lst)
(nth lst
(if (= (length lst) 0) 0 (- (length lst) 1))))
(def (third ls)
(get ls 2))
(def (last ls)
(get ls
(if (= (length ls) 0) 0 (- (length ls) 1))))
# Short aliases
(var sh shell)
(var % rem)
(var str string)
(var str.split string.split)
(var str.join string.join)
(var str.trim string.trim)
(var num.type number.type)
(var str/split string/split)
(var str/join string/join)
(var str/trim string/trim)
(var num/type number/type)
(var str->num string->number)
(var str->bin string->binary)
(var num->bin number->binary)

View File

@ -7,10 +7,10 @@
(def (read-binary path)
"Read binary file"
(do
(var size (file.size path))
(var file (file.open path "r"))
(var data (file.read file size))
(file.close file)
(var size (file/size path))
(var file (file/open path "r"))
(var data (file/read file size))
(file/close file)
data))
(def (read path)
@ -22,9 +22,9 @@
(def (write-binary path data)
"Write binary to file"
(do
(var file (file.open path "w"))
(file.write file data)
(file.close file)))
(var file (file/open path "w"))
(file/write file data)
(file/close file)))
(def (write path text)
"Write text to file"
@ -35,9 +35,9 @@
(def (append-binary path data)
"Append binary to file"
(do
(var file (file.open path "a"))
(file.write file data)
(file.close file)))
(var file (file/open path "a"))
(file/write file data)
(file/close file)))
(def (append path text)
"Append text to file"
@ -47,16 +47,16 @@
(def (read-line)
"Read line from the console"
(string.trim (binary->string (file.read stdin 256))))
(string/trim (binary->string (file/read stdin 256))))
(def (read-char)
"Read char from the console"
(binary->string (file.read stdin 4)))
(binary->string (file/read stdin 4)))
(def (p exp)
"Print expression to the console"
(do
(file.write stdout (string->binary (string exp)))
(file/write stdout (string->binary (string exp)))
'()))
(def (print exp)

View File

@ -1,9 +1,7 @@
(load "/lib/lisp/core.lsp")
(var esc (bin->str '(27)))
(def (ansi-color x y)
(str esc "[" x ";" y "m"))
(str "\e[" x ";" y "m"))
(def (fg c)
(ansi-color c 40))
@ -15,7 +13,7 @@
(str " " (f c) (if (< c 100) " " "") c (ansi-color 0 0)))
(def (colors fs i j)
(str.join (map (fun (c) (color fs c)) (range i j)) ""))
(str/join (map (fun (c) (color fs c)) (range i j)) ""))
(print (colors fg 30 38))
(print (colors fg 90 98))

18
dsk/tmp/lisp/doc.lsp Normal file
View File

@ -0,0 +1,18 @@
(load "/lib/lisp/core.lsp")
(def (print-doc f) (do
(var s (second (parse (str (eval f)))))
(var d (doc (eval f)))
(print (str
"("
(if (function? (eval f)) "\e[96m" "\e[92m") f "\e[0m" # name
(if (nil? s) "" (str " " (if (list? s) (str/join s " ") s))) # args
")"
"\e[90m" (if (empty? d) "" " # ") d "\e[0m")))) # desc
(var fs
(filter
(fun (f) (or (fun? (eval f)) (mac? (eval f))))
(rev (env))))
(map print-doc fs)

View File

@ -6,10 +6,10 @@
(var proxy-port 8888)
(var stdout 1)
(var socket (socket.connect "tcp" proxy-host proxy-port))
(file.write socket (str->bin (str "GET " (nth args 0) "\n")))
(var socket (socket/connect "tcp" proxy-host proxy-port))
(file/write socket (str->bin (str "GET " (get args 0) "\n")))
(var open true)
(while open (do
(var buf (file.read socket 2048))
(file.write stdout buf)
(var buf (file/read socket 2048))
(file/write stdout buf)
(set open (not (nil? buf)))))

View File

@ -1,12 +1,12 @@
(load "/lib/lisp/core.lsp")
(var addr (nth args 0)) # Run `host pool.ntp.org` to get an address
(var addr (or (host (head args)) (head args)))
(var port 123)
(var socket (socket.connect "udp" addr port))
(var socket (socket/connect "udp" addr port))
(var req (map (fun (i) (if (eq? i 0) 0x33 0)) (range 0 48)))
(file.write socket req)
(var res (file.read socket 48))
(file/write socket req)
(var res (file/read socket 48))
(var buf (slice res 40 4))
(var time (- (bin->num (concat '(0 0 0 0) buf) "int") 2208988800))

View File

@ -160,13 +160,19 @@ pub fn resolve(name: &str) -> Result<IpAddress, ResponseCode> {
let message = Message::from(&data);
if message.id() == query.id() && message.is_response() {
syscall::close(handle);
//usr::hex::print_hex(&message.datagram);
return match message.code() {
ResponseCode::NoError => {
// TODO: Parse the datagram instead of extracting
// the last 4 bytes
let n = message.datagram.len();
let data = &message.datagram[(n - 4)..];
Ok(IpAddress::from(Ipv4Address::from_bytes(data)))
let ipv4 = Ipv4Address::from_bytes(data);
if ipv4.is_unspecified() {
Err(ResponseCode::NameError) // FIXME
} else {
Ok(IpAddress::from(ipv4))
}
}
code => {
Err(code)

View File

@ -41,6 +41,7 @@ pub fn copy_files(verbose: bool) {
copy_file("/ini/banner.txt", include_bytes!("../../dsk/ini/banner.txt"), verbose);
copy_file("/ini/boot.sh", include_bytes!("../../dsk/ini/boot.sh"), verbose);
copy_file("/ini/lisp.lsp", include_bytes!("../../dsk/ini/lisp.lsp"), verbose);
copy_file("/ini/shell.sh", include_bytes!("../../dsk/ini/shell.sh"), verbose);
copy_file("/ini/version.txt", include_bytes!("../../dsk/ini/version.txt"), verbose);
@ -64,11 +65,12 @@ pub fn copy_files(verbose: bool) {
create_dir("/tmp/lisp", verbose);
copy_file("/tmp/lisp/colors.lsp", include_bytes!("../../dsk/tmp/lisp/colors.lsp"), verbose);
copy_file("/tmp/lisp/doc.lsp", include_bytes!("../../dsk/tmp/lisp/doc.lsp"), verbose);
copy_file("/tmp/lisp/factorial.lsp", include_bytes!("../../dsk/tmp/lisp/factorial.lsp"), verbose);
//copy_file("/tmp/lisp/fetch.lsp", include_bytes!("../../dsk/tmp/lisp/fetch.lsp"), verbose);
copy_file("/tmp/lisp/fibonacci.lsp", include_bytes!("../../dsk/tmp/lisp/fibonacci.lsp"), verbose);
copy_file("/tmp/lisp/geotime.lsp", include_bytes!("../../dsk/tmp/lisp/geotime.lsp"), verbose);
//copy_file("/tmp/lisp/ntp.lsp", include_bytes!("../../dsk/tmp/lisp/ntp.lsp"), verbose);
copy_file("/tmp/lisp/ntp.lsp", include_bytes!("../../dsk/tmp/lisp/ntp.lsp"), verbose);
copy_file("/tmp/lisp/pi.lsp", include_bytes!("../../dsk/tmp/lisp/pi.lsp"), verbose);
copy_file("/tmp/lisp/sum.lsp", include_bytes!("../../dsk/tmp/lisp/sum.lsp"), verbose);

View File

@ -44,7 +44,7 @@ pub fn default_env() -> Rc<RefCell<Env>> {
data.insert("sin".to_string(), Exp::Primitive(primitive::lisp_sin));
data.insert("tan".to_string(), Exp::Primitive(primitive::lisp_tan));
data.insert("trunc".to_string(), Exp::Primitive(primitive::lisp_trunc));
data.insert("system".to_string(), Exp::Primitive(primitive::lisp_system));
data.insert("shell".to_string(), Exp::Primitive(primitive::lisp_shell));
data.insert("string".to_string(), Exp::Primitive(primitive::lisp_string));
data.insert("string->binary".to_string(), Exp::Primitive(primitive::lisp_string_binary));
data.insert("binary->string".to_string(), Exp::Primitive(primitive::lisp_binary_string));
@ -56,26 +56,32 @@ pub fn default_env() -> Rc<RefCell<Env>> {
data.insert("list".to_string(), Exp::Primitive(primitive::lisp_list));
data.insert("sort".to_string(), Exp::Primitive(primitive::lisp_sort));
data.insert("unique".to_string(), Exp::Primitive(primitive::lisp_unique));
data.insert("nth".to_string(), Exp::Primitive(primitive::lisp_nth));
data.insert("contains?".to_string(), Exp::Primitive(primitive::lisp_contains));
data.insert("slice".to_string(), Exp::Primitive(primitive::lisp_slice));
data.insert("chunks".to_string(), Exp::Primitive(primitive::lisp_chunks));
data.insert("length".to_string(), Exp::Primitive(primitive::lisp_length));
data.insert("concat".to_string(), Exp::Primitive(primitive::lisp_concat));
data.insert("number.type".to_string(), Exp::Primitive(primitive::lisp_number_type));
data.insert("regex.find".to_string(), Exp::Primitive(primitive::lisp_regex_find));
data.insert("string.split".to_string(), Exp::Primitive(primitive::lisp_string_split));
data.insert("string.trim".to_string(), Exp::Primitive(primitive::lisp_string_trim));
data.insert("number/type".to_string(), Exp::Primitive(primitive::lisp_number_type));
data.insert("regex/find".to_string(), Exp::Primitive(primitive::lisp_regex_find));
data.insert("string/split".to_string(), Exp::Primitive(primitive::lisp_string_split));
data.insert("string/trim".to_string(), Exp::Primitive(primitive::lisp_string_trim));
data.insert("file.size".to_string(), Exp::Primitive(primitive::lisp_file_size));
data.insert("file.open".to_string(), Exp::Primitive(primitive::lisp_file_open));
data.insert("file.read".to_string(), Exp::Primitive(primitive::lisp_file_read));
data.insert("file.write".to_string(), Exp::Primitive(primitive::lisp_file_write));
data.insert("file.close".to_string(), Exp::Primitive(primitive::lisp_file_close));
data.insert("socket.connect".to_string(), Exp::Primitive(primitive::lisp_socket_connect));
data.insert("socket.listen".to_string(), Exp::Primitive(primitive::lisp_socket_listen));
data.insert("socket.accept".to_string(), Exp::Primitive(primitive::lisp_socket_accept));
data.insert("file/size".to_string(), Exp::Primitive(primitive::lisp_file_size));
data.insert("file/open".to_string(), Exp::Primitive(primitive::lisp_file_open));
data.insert("file/read".to_string(), Exp::Primitive(primitive::lisp_file_read));
data.insert("file/write".to_string(), Exp::Primitive(primitive::lisp_file_write));
data.insert("file/close".to_string(), Exp::Primitive(primitive::lisp_file_close));
data.insert("socket/connect".to_string(), Exp::Primitive(primitive::lisp_socket_connect));
data.insert("socket/listen".to_string(), Exp::Primitive(primitive::lisp_socket_listen));
data.insert("socket/accept".to_string(), Exp::Primitive(primitive::lisp_socket_accept));
data.insert("host".to_string(), Exp::Primitive(primitive::lisp_host));
data.insert("dict".to_string(), Exp::Primitive(primitive::lisp_dict));
data.insert("get".to_string(), Exp::Primitive(primitive::lisp_get));
data.insert("put".to_string(), Exp::Primitive(primitive::lisp_put));
// Setup autocompletion
*FUNCTIONS.lock() = data.keys().cloned().chain(BUILT_INS.map(String::from)).collect();

View File

@ -101,7 +101,7 @@ fn eval_while_args(args: &[Exp], env: &mut Rc<RefCell<Env>>) -> Result<Exp, Err>
ensure_length_gt!(args, 1);
let cond = &args[0];
let mut res = Exp::List(vec![]);
while eval(cond, env)? == Exp::Bool(true) {
while eval(cond, env)?.is_truthy() {
for arg in &args[1..] {
res = eval(arg, env)?;
}
@ -212,7 +212,7 @@ pub fn eval(exp: &Exp, env: &mut Rc<RefCell<Env>>) -> Result<Exp, Err> {
}
Exp::Sym(s) if s == "if" => {
ensure_length_gt!(args, 1);
if eval(&args[0], env)? == Exp::Bool(true) { // consequent
if eval(&args[0], env)?.is_truthy() { // consequent
exp_tmp = args[1].clone();
} else if args.len() > 2 { // alternate
exp_tmp = args[2].clone();

View File

@ -13,7 +13,7 @@ use eval::{eval, eval_variable_args};
use expand::expand;
use parse::parse;
use crate::api;
use crate::api::fs;
use crate::api::console::Style;
use crate::api::process::ExitCode;
use crate::api::prompt::Prompt;
@ -24,9 +24,11 @@ use alloc::rc::Rc;
use alloc::string::String;
use alloc::string::ToString;
use alloc::vec::Vec;
use alloc::collections::btree_map::BTreeMap;
use alloc::vec;
use core::cell::RefCell;
use core::convert::TryInto;
use core::cmp;
use core::fmt;
use lazy_static::lazy_static;
use spin::Mutex;
@ -52,18 +54,30 @@ pub enum Exp {
Function(Box<Function>),
Macro(Box<Function>),
List(Vec<Exp>),
Dict(BTreeMap<String, Exp>),
Bool(bool),
Num(Number),
Str(String),
Sym(String),
}
impl Exp {
pub fn is_truthy(&self) -> bool {
match self {
Exp::Bool(b) => *b,
Exp::List(l) => !l.is_empty(),
_ => true,
}
}
}
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::Dict(a), Exp::Dict(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,
@ -73,13 +87,13 @@ impl PartialEq for Exp {
}
}
use core::cmp::Ordering;
impl PartialOrd for Exp {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
fn partial_cmp(&self, other: &Self) -> Option<cmp::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::Dict(a), Exp::Dict(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),
@ -98,11 +112,15 @@ impl fmt::Display for Exp {
Exp::Bool(a) => a.to_string(),
Exp::Num(n) => n.to_string(),
Exp::Sym(s) => s.clone(),
Exp::Str(s) => format!("{:?}", s),
Exp::Str(s) => format!("{:?}", s).replace("\\u{8}", "\\b").replace("\\u{1b}", "\\e"),
Exp::List(list) => {
let xs: Vec<String> = list.iter().map(|x| x.to_string()).collect();
format!("({})", xs.join(" "))
},
Exp::Dict(dict) => {
let xs: Vec<String> = dict.iter().map(|(k, v)| format!("{} {}", k, v)).collect();
format!("(dict {})", xs.join(" "))
},
};
write!(f, "{}", out)
}
@ -274,6 +292,29 @@ fn repl(env: &mut Rc<RefCell<Env>>) -> Result<(), ExitCode> {
Ok(())
}
fn exec(env: &mut Rc<RefCell<Env>>, path: &str) -> Result<(), ExitCode> {
if let Ok(mut input) = 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)
}
}
pub fn main(args: &[&str]) -> Result<(), ExitCode> {
let env = &mut default_env();
@ -291,32 +332,16 @@ pub fn main(args: &[&str]) -> Result<(), ExitCode> {
}
if args.len() < 2 {
let init = "/ini/lisp.lsp";
if fs::exists(init) {
exec(env, init)?;
}
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)
}
exec(env, args[1])
}
}
@ -328,6 +353,14 @@ fn help() -> Result<(), ExitCode> {
Ok(())
}
#[test_case]
fn test_exp() {
assert_eq!(Exp::Bool(true).is_truthy(), true);
assert_eq!(Exp::Bool(false).is_truthy(), false);
assert_eq!(Exp::Num(Number::Int(42)).is_truthy(), true);
assert_eq!(Exp::List(vec![]).is_truthy(), false);
}
#[test_case]
fn test_lisp() {
use core::f64::consts::PI;
@ -411,6 +444,12 @@ fn test_lisp() {
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");
assert_eq!(eval!("(if true 1 2)"), "1");
assert_eq!(eval!("(if false 1 2)"), "2");
assert_eq!(eval!("(if '() 1 2)"), "2");
assert_eq!(eval!("(if 0 1 2)"), "1");
assert_eq!(eval!("(if 42 1 2)"), "1");
assert_eq!(eval!("(if \"\" 1 2)"), "1");
// while
assert_eq!(eval!("(do (variable i 0) (while (< i 5) (set i (+ i 1))) i)"), "5");
@ -495,8 +534,8 @@ fn test_lisp() {
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\")");
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");
@ -536,9 +575,9 @@ fn test_lisp() {
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\"");
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)");
@ -570,4 +609,23 @@ fn test_lisp() {
assert_eq!(eval!("# comment\n# comment"), "()");
assert_eq!(eval!("(+ 1 2 3) # comment"), "6");
assert_eq!(eval!("(+ 1 2 3) # comment\n# comment"), "6");
// list
assert_eq!(eval!("(list 1 2 3)"), "(1 2 3)");
// dict
assert_eq!(eval!("(dict \"a\" 1 \"b\" 2 \"c\" 3)"), "(dict \"a\" 1 \"b\" 2 \"c\" 3)");
// get
assert_eq!(eval!("(get \"Hello\" 0)"), "\"H\"");
assert_eq!(eval!("(get \"Hello\" 6)"), "\"\"");
assert_eq!(eval!("(get (list 1 2 3) 0)"), "1");
assert_eq!(eval!("(get (list 1 2 3) 3)"), "()");
assert_eq!(eval!("(get (dict \"a\" 1 \"b\" 2 \"c\" 3) \"a\")"), "1");
assert_eq!(eval!("(get (dict \"a\" 1 \"b\" 2 \"c\" 3) \"d\")"), "()");
// put
assert_eq!(eval!("(put (dict \"a\" 1 \"b\" 2) \"c\" 3)"), "(dict \"a\" 1 \"b\" 2 \"c\" 3)");
assert_eq!(eval!("(put (list 1 3) 1 2)"), "(1 2 3)");
assert_eq!(eval!("(put \"Heo\" 2 \"ll\")"), "\"Hello\"");
}

View File

@ -96,17 +96,19 @@ fn float(input: &str) -> IResult<&str, &str> {
}
fn is_symbol_letter(c: char) -> bool {
let chars = "<>=-+*/%^?.";
let chars = "$<>=-+*/%^?.";
c.is_alphanumeric() || chars.contains(c)
}
fn parse_str(input: &str) -> IResult<&str, Exp> {
let escaped = map(opt(escaped_transform(is_not("\\\""), '\\', alt((
value("\\", tag("\\")),
value("\"", tag("\"")),
value("\n", tag("n")),
value("\r", tag("r")),
value("\t", tag("t")),
value("\\", tag("\\")),
value("\"", tag("\"")),
value("\n", tag("n")),
value("\r", tag("r")),
value("\t", tag("t")),
value("\x08", tag("b")),
value("\x1B", tag("e")),
)))), |inner| inner.unwrap_or("".to_string()));
let (input, s) = delimited(char('"'), escaped, char('"'))(input)?;
Ok((input, Exp::Str(s)))

View File

@ -8,7 +8,9 @@ use crate::api::regex::Regex;
use crate::api::syscall;
use crate::sys::fs::OpenFlag;
use crate::usr::shell;
use crate::usr::host;
use alloc::collections::btree_map::BTreeMap;
use alloc::format;
use alloc::string::String;
use alloc::string::ToString;
@ -156,7 +158,7 @@ pub fn lisp_trunc(args: &[Exp]) -> Result<Exp, Err> {
Ok(Exp::Num(number(&args[0])?.trunc()))
}
pub fn lisp_system(args: &[Exp]) -> Result<Exp, Err> {
pub fn lisp_shell(args: &[Exp]) -> Result<Exp, Err> {
ensure_length_gt!(args, 0);
let cmd = strings(args)?.join(" ");
match shell::exec(&cmd) {
@ -228,6 +230,7 @@ pub fn lisp_type(args: &[Exp]) -> Result<Exp, Err> {
Exp::Function(_) => "function",
Exp::Macro(_) => "macro",
Exp::List(_) => "list",
Exp::Dict(_) => "dict",
Exp::Bool(_) => "boolean",
Exp::Str(_) => "string",
Exp::Sym(_) => "symbol",
@ -278,28 +281,6 @@ pub fn lisp_contains(args: &[Exp]) -> Result<Exp, Err> {
}
}
pub fn lisp_nth(args: &[Exp]) -> Result<Exp, Err> {
ensure_length_eq!(args, 2);
let i = usize::try_from(number(&args[1])?)?;
match &args[0] {
Exp::List(l) => {
if let Some(e) = l.get(i) {
Ok(e.clone())
} else {
Ok(Exp::List(Vec::new()))
}
}
Exp::Str(s) => {
if let Some(c) = s.chars().nth(i) {
Ok(Exp::Str(c.to_string()))
} else {
Ok(Exp::Str("".to_string()))
}
}
_ => expected!("first argument to be a list or a string")
}
}
pub fn lisp_slice(args: &[Exp]) -> Result<Exp, Err> {
let (a, b) = match args.len() {
2 => (usize::try_from(number(&args[1])?)?, 1),
@ -339,8 +320,8 @@ pub fn lisp_length(args: &[Exp]) -> Result<Exp, Err> {
}
}
// TODO: This could also concat strings
pub fn lisp_concat(args: &[Exp]) -> Result<Exp, Err> {
// TODO: This could also concat strings
let mut res = vec![];
for arg in args {
if let Exp::List(list) = arg {
@ -520,3 +501,88 @@ pub fn lisp_socket_accept(args: &[Exp]) -> Result<Exp, Err> {
could_not!("accept connections")
}
}
pub fn lisp_dict(args: &[Exp]) -> Result<Exp, Err> {
let mut dict = BTreeMap::new();
for chunk in args.chunks(2) {
match chunk {
[k, v] => dict.insert(format!("{}", k), v.clone()),
[k] => dict.insert(format!("{}", k), Exp::List(vec![])),
_ => unreachable!(),
};
}
Ok(Exp::Dict(dict))
}
pub fn lisp_get(args: &[Exp]) -> Result<Exp, Err> {
ensure_length_eq!(args, 2);
match &args[0] {
Exp::Dict(dict) => {
let key = format!("{}", args[1]);
if let Some(val) = dict.get(&key) {
Ok(val.clone())
} else {
Ok(Exp::List(vec![]))
}
}
Exp::List(l) => {
let i = usize::try_from(number(&args[1])?)?;
if let Some(e) = l.get(i) {
Ok(e.clone())
} else {
Ok(Exp::List(Vec::new()))
}
}
Exp::Str(s) => {
let i = usize::try_from(number(&args[1])?)?;
if let Some(c) = s.chars().nth(i) {
Ok(Exp::Str(c.to_string()))
} else {
Ok(Exp::Str("".to_string()))
}
}
_ => expected!("first argument to be a dict, a list, or a string")
}
}
pub fn lisp_put(args: &[Exp]) -> Result<Exp, Err> {
ensure_length_eq!(args, 3);
match &args[0] {
Exp::Dict(dict) => {
let mut dict = dict.clone();
let key = format!("{}", args[1]);
let val = args[2].clone();
dict.insert(key, val);
Ok(Exp::Dict(dict))
}
Exp::List(list) => {
let i = usize::try_from(number(&args[1])?)?;
let val = args[2].clone();
let mut list = list.clone();
list.insert(i, val);
Ok(Exp::List(list))
}
Exp::Str(s) => {
let i = usize::try_from(number(&args[1])?)?;
let v: Vec<char> = string(&args[2])?.chars().collect();
let mut s: Vec<char> = s.chars().collect();
s.splice(i..i, v);
let s: String = s.into_iter().collect();
Ok(Exp::Str(s))
}
_ => expected!("first argument to be a dict, a list, or a string")
}
}
pub fn lisp_host(args: &[Exp]) -> Result<Exp, Err> {
ensure_length_eq!(args, 1);
let hostname = string(&args[0])?;
match host::resolve(&hostname) {
Ok(addr) => {
Ok(Exp::Str(format!("{}", addr)))
}
Err(_) => {
Ok(Exp::List(vec![]))
}
}
}

View File

@ -25,8 +25,9 @@ of the Shell.</p>
<h3>Literals</h3>
<ul>
<li>Number: <code>2.5</code>, <code>-25</code>, <code>255</code>, <code>0xFF</code>, <code>0xDEAD_C0DE</code>, <code>0b101010</code></li>
<li>String: <code>&quot;Hello, World!&quot;</code></li>
<li>Number: <code>2.5</code>, <code>-25</code>, <code>255</code>, <code>0xFF</code>, <code>0xDEAD_C0DE</code></li>
<li>Escape: <code>\b</code>, <code>\e</code>, <code>\n</code>, <code>\r</code>, <code>\t</code>, <code>\&quot;</code>, <code>\\</code></li>
</ul>
<h3>Built-in Operators</h3>
@ -63,35 +64,37 @@ of the Shell.</p>
<h3>Primitive Operators</h3>
<ul>
<li><code>type</code>, <code>number.type</code> (aliased to <code>num.type</code>)</li>
<li><code>type</code>, <code>number/type</code> (aliased to <code>num/type</code>), <code>parse</code></li>
<li><code>string</code> (aliased to <code>str</code>)</li>
<li><code>string-&gt;number</code> (aliased to to <code>str-&gt;num</code>)</li>
<li><code>string-&gt;binary</code> and <code>binary-&gt;string</code> (aliased to <code>str-&gt;bin</code> and <code>bin-&gt;str</code>)</li>
<li><code>number-&gt;binary</code> and <code>binary-&gt;number</code> (aliased to <code>num-&gt;bin</code> and <code>bin-&gt;num</code>)</li>
<li><code>regex.find</code></li>
<li><code>system</code></li>
<li>Arithmetic operations: <code>+</code>, <code>-</code>, <code>*</code>, <code>/</code>, <code>^</code>, <code>abs</code>, <code>mod</code>, <code>rem</code> (aliased to <code>%</code>)</li>
<li><code>regex/find</code></li>
<li><code>shell</code> (aliased to <code>sh</code>)</li>
<li>Arithmetic operations: <code>+</code>, <code>-</code>, <code>*</code>, <code>/</code>, <code>^</code>, <code>rem</code> (aliased to <code>%</code>), <code>trunc</code></li>
<li>Trigonometric functions: <code>acos</code>, <code>asin</code>, <code>atan</code>, <code>cos</code>, <code>sin</code>, <code>tan</code></li>
<li>Comparisons: <code>&gt;</code>, <code>&lt;</code>, <code>&gt;=</code>, <code>&lt;=</code>, <code>=</code></li>
<li>Enumerable: <code>length</code> (aliased to <code>len</code>), <code>nth</code>, <code>first</code>, <code>second</code>, <code>third</code>, <code>last</code>, <code>rest</code>, <code>slice</code></li>
<li>String: <code>string.trim</code> and <code>string.split</code> (aliased to <code>str.trim</code> and <code>str.split</code>)</li>
<li>List: <code>concat</code>, <code>chunks</code>, <code>sort</code>, <code>unique</code> (aliased to <code>uniq</code>), <code>min</code>, <code>max</code></li>
<li>File: <code>file.size</code>, <code>file.open</code>, <code>file.close</code>, <code>file.read</code>, <code>file.write</code></li>
<li>Enumerable: <code>length</code> (aliased to <code>len</code>), <code>put</code>, <code>get</code>, <code>slice</code>, <code>contains?</code></li>
<li>String: <code>string/trim</code> and <code>string/split</code> (aliased to <code>str/trim</code> and <code>str/split</code>)</li>
<li>List: <code>list</code>, <code>concat</code>, <code>chunks</code>, <code>sort</code>, <code>unique</code> (aliased to <code>uniq</code>)</li>
<li>Dict: <code>dict</code></li>
<li>File: <code>file/size</code>, <code>file/open</code>, <code>file/close</code>, <code>file/read</code>, <code>file/write</code></li>
<li>Net: <code>host</code>, <code>socket/connect</code>, <code>socket/listen</code>, <code>socket/accept</code></li>
</ul>
<h3>Core Library</h3>
<ul>
<li><code>nil</code>, <code>nil?</code>, <code>list?</code></li>
<li><code>nil</code>, <code>nil?</code>, <code>list?</code>, <code>empty?</code></li>
<li><code>boolean?</code> (aliased to <code>bool?</code>), <code>string?</code> (aliased to <code>str?</code>), <code>symbol?</code> (aliased to <code>sym?</code>), <code>number?</code> (aliased to <code>num?</code>)</li>
<li><code>function?</code> (aliased to <code>fun?</code>), <code>macro?</code> (aliased to <code>mac?</code>)</li>
<li><code>first</code>, <code>second</code>, <code>third</code>, <code>rest</code></li>
<li><code>map</code>, <code>reduce</code>, <code>reverse</code> (aliased to <code>rev</code>), <code>range</code>, <code>filter</code>, <code>intersection</code></li>
<li><code>abs</code>, <code>mod</code>, <code>min</code>, <code>max</code></li>
<li><code>first</code>, <code>second</code>, <code>third</code>, <code>last</code>, <code>rest</code>, <code>push</code></li>
<li><code>map</code>, <code>reduce</code>, <code>reverse</code> (aliased to <code>rev</code>), <code>range</code>, <code>filter</code>, <code>reject</code>, <code>intersection</code></li>
<li><code>not</code>, <code>and</code>, <code>or</code></li>
<li><code>let</code></li>
<li><code>string.join</code> (aliased to <code>str.join</code>), <code>lines</code>, <code>words</code>, <code>chars</code></li>
<li><code>regex.match?</code></li>
<li><code>socket.connect</code>, <code>socket.listen</code>, <code>socket.accept</code></li>
<li><code>string/join</code> (aliased to <code>str/join</code>), <code>lines</code>, <code>words</code>, <code>chars</code></li>
<li><code>regex/match?</code></li>
</ul>
<h3>File Library</h3>
@ -253,7 +256,12 @@ language and reading from the filesystem.</p>
<h3>Unreleased</h3>
<ul>
<li>Add hexadecimal number literals</li>
<li>Add binary and hexadecimal number literals</li>
<li>Test for truthiness (neither <code>false</code> nor <code>nil</code>) in conditions of <code>if</code> and <code>while</code></li>
<li>Rename <code>nth</code> to <code>get</code></li>
<li>Add <code>empty?</code>, <code>reject</code>, <code>put</code>, <code>push</code>, and <code>host</code> functions`</li>
<li>Add <code>dict</code> type</li>
<li>Use <code>/</code> instead of <code>.</code> as namespace separator</li>
</ul>
<footer><p><a href="/">MOROS</a></footer>
</body>