x7 is a lisp I built to better understand programming languages and interpreters.
The standard library is being written in either x7 or rust for performance.
You have brackets. Lots of brackets. And lists. And functions.
The goal is to have every function describe itself, so you can live in the repl.
Use the doc
function on a symbol to see it’s documentation:
>>> (print (doc foreach)) Eagerly apply the given function to a sequence or list. Example: (foreach (fn (x) (println x)) (range 20)) ; prints 0 to 20. Returns (). (foreach (fn (x) (println x)) (take 5 (map (fn (x) (* x x x x x x)) (range)))) ; prints 0, 1, 64, 729, 4096
The general goal is to be as helpful as possible. So stacktraces include more information than you’d usually get, such as arguments.
For example, the following program will fail:
(defn bottom (x) (% x 2))
(defn middle (x) (bottom x))
(defn top () (middle "a"))
(top)
And give this helpful stacktrace:
Error: BadTypes Stacktrace: - Remainder requires left and right are num types, was given "a" % 2 - Error in Fn<%, 2, [ ]>, with args ("a" 2) - Error in Fn<bottom, 1, [ x ]>, with args ("a") - Error in Fn<middle, 1, [ x ]>, with args ("a") - Error in Fn<top, 0, [ ]>, with args ()
x7 offers easy and convenient embedding into other rust programs.
use x7::ffi::{X7Interpreter, ForeignData};
let interpreter = X7Interpreter::new();
let res = interpreter.run_program::<u64>("(+ 1 1)").unwrap();
assert_eq!(res, 2);
You can interface your own types in x7 with the ForeignData
trait, and
add foreign functions into the interpreter. To maximize convenience foreign functions
are typed in terms of their own datatypes - not x7’s Expr type.
let interpreter = X7Interpreter::new();
let my_sum_fn = |args: Vec<u64>| Ok(args.iter().sum());
// Add the my-sum to interpreter
interpreter.add_function("my-sum", 1, Arc::new(my_sum_fn));
// And verify we get u64 with value 6 out of it.
assert_eq!(interpreter.run_program::<u64>("(my-sum 1 2 3)").unwrap(), 6);
More interesting is the fact that functions added to the interpreter in a strongly typed way, allowing us to mix types!
// Recall that my-sum is a Fn(Vec<u64>) -> u64
let string_res = interpreter.run_program::<String>("(my-sum 1 2 3)").unwrap();
// And we get a string out it!
assert_eq!(string_res, "6".to_string());
The reason it works is we embed the type information into the function
added to the interpreter, and x7’s Expr
type acts as a bridge between types.
For more info see the ffi.rs
example in the examples folder!
You can run the example with:
cargo run --example ffi
Certain constructs like (range)
and map
are backed by lazy iterators, making them pretty fast.
We can print the first hundred fibonacci numbers in 14 milliseconds:
;; fib.x7
;; Run with: x7 fib.x7
;; Map (l, r) -> (r, l + r)
(defn fib-step (x)
(bind ((l r) x) ^(r (+ l r))))
;; Reduce (0 1) `num` times using fib-step to
;; generate the `num`'th fibonacci number
(defn fib (num)
(nth 0 (reduce
fib-step
(tuple 0 1)
(range num))))
;; Print one hundred fibonacci numbers
;;
;; Note: (take 100 (map fib (range)))
;; is an iterator which maps to Rust's iterators which
;; makes them very fast. No weird intermediate allocations.
(println (time (foreach
println
(take 100 (map fib (range))))))
Outputs:
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 ...truncated... 83621143489848422977 135301852344706746049 218922995834555169026
A recent addition to the language is the defrecord
and defmethod
functions,
which allow you to define records in x7
and add methods to the them respectively.
Here’s an example of defining Vec3
, and a way to add them together:
;; Define a record
(defrecord Vec3 "Three dimensional vector" x y z)
;; Add a method to it
(defmethod Vec3 +
"Add two vectors together"
(other)
(Vec3
(+ other.x self.x)
(+ other.y self.y)
(+ other.z self.z)))
This lets us encapsulate data, and access it in a nice structured way.
;; Instantiate a Vec3
(def my-vector (Vec3 1 1 1))
;; Call the + method
(.+ my-vector my-vector) ;; Record<Vec3, fields=[ x: 2 y: 2 z: 2 ]>
The process of adding this support added two new ways to interact with expressions - callable Records and field-access-sugar
To make record construction nice, you can treat records defined with defrecord
as constructor functions:
>>> (defrecord Point x y) Record<Point, uninitialized> >>> (Point 0 0) Record<Point, fields=[ x: 0 y: 0 ]>
By default, fields of a record are treated as zero-arity methods on that record, with self
being inserted with method_call syntax.
This meant that this got old after a while:
(+ (.x self) (.x other))
So I added some sugar in the form of self.x
:
>>> (defrecord Point x y) >>> (def origin (Point 0 0)) >>> origin.x 0
It works in a recursive way if you have deeply nested fields.
>>> (defrecord Point x y) >>> (defrecord Space origin) >>> (def space (Space (Point 0 0))) >>> space.origin Record<Point, fields=[ x: 0 y: 0 ]> >>> space.origin.x 0 >>> space.origin.y 0
The syntax immediately evaluates, as it gets transformed a nested list of function calls:
space.origin.y ;; (.y (.origin space))
You can do some tricks with this, like this:
(def file (fs::open "input.txt"))
(def re (re::compile "(\d+)-(\d+) (.): (.*)"))
(def captures (.captures re file.read_to_string))
Zero arity functions can also be referenced:
>>> (def v (Vec3 1 1 1)) nil >>> v.scale Fn<curried_method_call<Vec3<scale>; #args=1>, 0, [ ]> >>> (v.scale 3) Record<Vec3, fields=[ x: 3 y: 3 z: 3 ]> >>> v.length 1.73205080
You can easily create anonymous functions with #(...)
. Here’s an example:
(filter #(< $1 10) (range 100)) ;; (0 1 2 3 4 5 6 7 8 9)
(map #(+ 10 $1) (range 10)) ;; (10 11 12 13 14 15 16 17 18 19)
Fields are labelled $1, $2, ...
.
x7 is a quirky lisp which sort of evolved naturally. It has the following data-types:
pub enum Expr {
Num(Num),
Integer(Integer),
Symbol(Symbol),
List(Vector<Expr>),
Function(Arc<Function>),
Nil,
String(String),
Quote(Vector<Expr>),
Tuple(Vector<Expr>),
Bool(bool),
LazyIter(IterType),
Dict(Dict),
Record(crate::records::RecordType),
}
Numbers in x7 are arbitrary precision BigDecimal
types backed by the bigdecimal
crate.
Example:
0 0.0 1.1 1000000000000000000
A fast-path for integer heavy calculations.
If you can avoid non-whole numbers this is substantially faster than the Num
type.
Example:
1 2 -5
Symbols are references to some object in the symbol table. They can’t contain quotes or brackets.
Example:
+ sort doc
A list is a sequential collection of values. When evaluated, the first argument is called as a function with the rest of the elements as arguments.
Example:
(+ 1 2) (println "hello world!")
A function is a type defined by the fn
or defn
keywords. They accept a variable number
of arguments and carry a local scope. Variables shadow each other, and functions will close over arguments.
Example:
(defn is-odd?
(x)
(= 1 (% x 2))) ; add function is-odd? to symbol table
(map
(fn (num) (* num num)) ; anon func
(range 20))
(defn not=
(& args) ; excess variables can be captured into a list
(not (apply = args)))
Null type. Usually equal to an empty list.
A UTF-8 string of characters between two quotes: “hello world!”
An unevaluated list. When evaluated, it turns into a list.
Has special syntax: '(1 2 3)
And a keyword: (quote 1 2 3)
Same thing as a list, but always evals to itself.
Has special syntax: ^(1 2 3)
And a keyword: (tuple 1 2 3)
Classic boolean. True or false.
Example:
true false (= 1 0) ;; false
A sequence of values backed by a Rust iterator. These are useful for working with infinite sequences.
Currently, map
, filter
, take
, and range
can yield lazy iterators.
They are evaluated with doall
to make a list or foreach
to operate on it.
Example:
(doall (take 5 (map inc (range)))) ; (1 2 3 4 5) ; Or (foreach println (take 5 (map inc (range)))) ; prints one through five
Classic immutable dictionary. This is certainly a work in progress.
Example:
(def foo (dict "key1" "value1" 3 4)) (get foo 3) ;; 4 (get foo "key1") ;; "value1" (set foo 5 6) ;; {"key1": "value1", 3: 4, 5: 6} ;; This does not mutate `foo`! (get foo 5) ;; nil
Objects in x7
. See the record section above.
The x7 language has self-documenting features. The standard library reference is generated
with the script below, which org-mode
pastes into the list below:
(defn pretty-print
"Format doc strings into something org-mode will agree with."
(x)
(bind
((sym docu) x)
(do
(println "*** =" sym "=")
(println "")
(println "#+BEGIN_SRC elisp")
(println docu)
(println "#+END_SRC")
(println ""))))
(foreach
pretty-print
(zip (all-symbols) (map doc (all-symbols))))
Add two items together. Concatenates strings, lists, and tuples.
Example: (+ 1 1 1) ; 3
Example: (+ "Hello " "World") ; "Hello World"
Subtracts all items from the first. Only works with Nums.
Example: (- 2 1 1) ; 0
Multiply all items against the first. Works with Nums and (String Num*)
Example: (* 1 2 3) ; 6
(* "abc" 3) ; "abcabcabc"
Take the remainder of the first item against the second.
Example: (% 4 2) ; 0
Divide the first element by the rest.
Example: (/ 8 2 2 2) ; 1
Take the square root of a number. There's minor precision loss as it's way faster to convert to floats internally over using a bigdecimal.
Example: (sqrt 9) ; 3
Test if all items are equal.
Example: (= 1 1) ; true
(= 1) ; true
Test if the first item is strictly smaller than the rest.
Example: (< 0 1 2) ; true
Test if the first item is smaller or equal to the rest.
Example: (<= 0 0 0.05 1) ; true
Test if the first item is strictly greater than the rest.
Example: (> 10 0 1 2 3 4) ; true
Test if the first item is greater than or equal to the rest.
Example: (>= 10 10 5) ; true
Increment the given number.
Example:
(inc 2.2) ;; 3.3
(inc 1) ;; 2
Decrement the given number.
Example:
(dec 2.2) ;; 3.3
(dec 1) ;; 2
Raise a number to an exponent.
Example:
(pow 2 3) ;; 8
(pow 10 3) ;; 1000
Floor a number.
Example:
(floor 5.5) ;; 5.5
Create an integer from the input.
Example:
(int 3.2) ;; 3
Invert the bool. true becomes false and vice-versa.
logical or.
logical and.
logical xor.
Identity function. Returns what you give it.
Transforms the given input into a quote. Usually you will want to use the '(1 2 3) syntax.
Turn a string into a symbol
Make a string
Coerce a value to bool. In general if a collection is non-empty, it is true. The len method is called on Records
Print the given argument WITHOUT a newline.
Print the given argument WITH a newline.
Get user input from stdin
Split a string with some substring.
Example:
>>> (split "," "hello, world")
(tuple "hello" " world")
Replace a substring in a string with some other string.
Example:
>>> (replace "abc" "OwO" "abc def")
"OwO def"
Returns true if a given symbol exists in the interpeter
Eval an expression.
Example (in repl):
>>> '(+ 1 2)
(+ 1 2)
>>> (eval '(+ 1 2))
3
Parse an expression.
Example (in repl):
>>> (parse "(+ 1 2)")