Crates.io | osyris |
lib.rs | osyris |
version | 0.1.0 |
source | src |
created_at | 2022-03-20 16:14:14.30309 |
updated_at | 2022-03-20 16:14:14.30309 |
description | A highly embeddable LISP-like language with no depedencies |
homepage | https://github.com/mortie/osyris |
repository | https://github.com/mortie/osyris |
max_upload_size | |
id | 553627 |
size | 48,562 |
Osyris is a pure Rust programming language with no dependencies. It's a LISP, and is intended to be easily embeddable. It's not extremely fast to execute, but it's very fast to parse and code starts executing in no time.
The main concepts you need to know about when using Osyris as a library are
the Reader, the Scope, and the eval
function. The Reader parses an input file
into expressions, the Scope is the map from names to variables, and the
eval function takes an expression and a scope and produces a value.
Here's a simple sample program:
use osyris::{eval, parse, stdlib};
use std::cell::RefCell;
use std::rc::Rc;
// The code we want to execute
static CODE: &'static str = r#"
(def 'fib {bind args 'num
{if (<= num 1)
1
{+ (fib (- num 1))
(fib (- num 2))}}})
(print "fib of 20 is" (fib 20))
"#;
fn main() -> Result<(), String> {
// Create a reader which will give us expressions
let mut reader = parse::Reader::new(CODE.as_bytes());
// Create a scope, and populate it with the stdlib
let scope = Rc::new(RefCell::new(eval::Scope::new()));
stdlib::init(&scope);
// Read and eval expressions
loop {
// We get a ParseError if the syntax is wrong
let expr = match parse::parse(&mut reader) {
Ok(expr) => expr,
Err(err) => {
println!("Parse error: {}:{}: {}", err.line, err.col, err.msg);
break;
}
};
// If the returned expression is None, we reached EOF
let expr = match expr {
Some(expr) => expr,
None => break,
};
// Evaluate the expression
match eval::eval(&expr, &scope) {
Ok(_value) => (), // Ignore the return value
Err(err) => {
println!("Eval error: {}", err);
break;
}
};
}
Ok(())
}
Like most LISPs, the grammar is extremely simple. There are really only strings, numbers, identifiers, function calls, and quoted lists.
The basics are:
10
, 20
"hello world"
, "with \"escapes\""
, 'identifier-strings
look-like-this
,Function calls are an opening brace, followed by expressions, followed by a closing brace,
like this: (print 10 "hello" name)
. The value of the first expression is expected to be
a quoted list (or a built-in function).
Quoted lists are like function calls but preceded by a quote: '(print 10 "hello" name)
.
When evaluated, a quoted list becomes a list of expressions. It's common for functions
to take a quoted list as an argument as a kind of callback function. Because they're
so common, Osyris lets you write them with braces instead: {print 10 "hello" name}
.
if
is a function which takes a condition and one or two quotes:
(if (> 10 20)
{print "10 is greater than 20"}
{print "10 is not greater than 20"})
def
introduces a value in the current scope:
(def 'age 32)
(print "Your age is" age)
You can define functions by defining variables whose bodies are quotes:
(def 'say-hello {print "Hello!"})
(say-hello)
And functions get an implicit args
value which contains function arguments:
(def 'say-hello {print "Hello," (args 0)})
(say-hello "Bob")
You can use the bind
function to give arguments names:
(def 'say-hello {bind 'name 'age {print "Hello," age "year old" name}})
(say-hello "Annie" 41)
These values are populated when you call stdlib::init
:
none
: A variable of type None.(print [values...])
: A function to print stuff to stdout.(+ [numbers...])
: Add together numbers.(- [head] [tail...])
: Subtract the numbers in the tail from the head.(* [numbers...])
: Multiply together numbers.(/ [head] [tail...])
: Divide the number in the head by the numbers in the tail.(== [values...])
: Return 1 if all values are equal, 0 otherwise.(!= [values...])
: Opposite of ==
.(<= [values...])
: Return 1 if each value is smaller than or equal to the next, 0 otherwise.(< [values...])
: Return 1 if each value is smaller than the next, 0 otherwise.(>= [values...])
: Return 1 if each value is greater than or equal to the next, 0 otherwise.(> [values...])
: Return 1 if each value is greater than the next, 0 otherwise.(|| [values...])
: Return the first truthy value, or the last value if all are falsy.(&& [values...])
: Return the first falsy value, or the last value if all are truthy.(def <name> <value>)
: Create a new variable called name
in the current scope.(set <name> <value>)
: Replace the existing variable called name
.(if <condition> <body> [else-body])
: Execute body
if condition
is truthy,
otherwise execute else-body
if it exists.(match [pairs...] [default])
: Execute one out of a set of options,
based on predicates. Example:(def x 55)
(match {> x 10} {print "It's greater than 10"}
{< x 10} {print "It's smaller than 10"}
{print "It's neither greater nor smaller than 10"})
(while <condition> <body>)
: Execute body
while condition
executes to something truthy.(do [values...])
: Return the last value.(bind <array> [names...] <body>)
: Bind values in the array
to names, then execute body
.(with [pairs...] <body>)
: Bind values to names, then execute body
. Example:(with 'x 10
'y 20
{print "x + y is" (+ x y)})
(list [values...])
: Create a list.(dict [pairs...])
: Create a dictionary. Example:(dict 'name "Bob"
'age 34
'profession "Programmer")
(lazy <quoted list>)
: Create a lazy variable.(read <port> [size])
: Read from port
. If size
is provided, read a chunk.(write <port> <data>)
: Write data
to port
.(seek <port> <offset> [whence])
: Seek to offset
. By default, offset
is relative
to the start, but whence
can be one of: 'set
, 'end
or 'current
to change that.These values are populated when you call iolib::init
.
(open <path>)
: Open the file at path
in read-only mode.(create <path>)
: Create or truncate the file at path
, open it in write-only mode.(exec <argv>)
: Execute system command.