$ dotnet run
sharpl v28
1 (say 'hello)
2
hello
Sharpl is a custom Lisp interpreter implemented in C#.
It's trivial to embed and comes with a simple REPL.
The code base currently hovers around 5 kloc and has no external dependencies.
All features described in this document are part of the test suite and expected to work as described.
- Pairs (
a:b
) are used everywhere, all the time. - Range support (
min..max:stride
). - Maps (
{k1:v1...kN:vN}
) are ordered. - Methods may be composed using
&
. - Varargs (
foo*
) are similar to Python. - Splatting (
[1 2 3]*
) is simlar to Python. - Unified, deeply integrated iterator protocol.
- Default decimal type is fixpoint.
- Nil is written
_
. - Both
T
andF
are defined. - Zeros and empty strings, arrays, lists and maps are considered false.
=
is generic and compares values deeply,is
may be used to compare identity.- Lambdas look like anonymous methods.
- Compile time eval (called
emit
), similar to Zig's comptime. - Explicit tail calls using
return
. - Parens are used for calls only.
- Many things are callable, simlar to Clojure.
- Methods have their own symbol (^).
- Line numbers are a thing.
Bindings come in two flavors, with lexical or dynamic scope.
New lexically scoped bindings may be created using let
.
(let [x 1
y (+ x 2)]
y)
3
New dynamically scoped bindings may be created using var
.
(var foo 35)
(^bar []
foo)
(let [foo (+ foo 7)]
(bar))
42
Bindings support declarative destructuring of pairs.
(let [_:r:rr 1:2:3] r:rr)
2:3
set
may be used to update a binding.
(let [foo 1 bar 2]
(set foo 3 bar 4)
foo:bar)
3:4
if
may be used to conditionally evaluate a block of code.
(if T 'is-true)
'is-true
else
may be used to specify an else-clause.
(else F 1 2)
2
New methods may be defined using ^
.
(^foo [x]
x)
(foo 42)
42
Leaving out the name creates a lambda.
(let [f (^[x] x)]
(f 42))
42
External bindings are captured at method definition time.
(^make-countdown [max]
(let [n max]
(^[] (dec n))))
(var foo (make-countdown 42))
(foo)
41
(foo)
40
return
may be used to convert any call into a tail call.
This example will keep going forever without consuming more space:
(^foo []
(return (foo)))
(foo)
Without return
it quickly runs out of space:
(^foo []
(foo))
(foo)
System.IndexOutOfRangeException: Index was outside the bounds of the array.
methods
may be defined as taking a variable number of arguments by suffixing the last parameter with *
.
The name is bound to an array containing all trailing arguments when the method is called.
(^foo [bar*]
(say bar*))
(foo 1 2 3)
123
Methods may be composed using &
.
(^foo [x]
(+ x 1))
(^bar [x]
(* x 2))
(let [f foo & bar]
(say f)
(f 20))
(foo & bar [values*])
42
loop
does exactly what it says, and nothing more.
(^enumerate [n]
(let [result (List) i 0]
(loop
(push result i)
(if (is (inc i) n) (return result)))))
(enumerate 3)
[0 1 2]
for
may be used to iterate any number of iterables.
(let [result {}]
(for [i [1 2 3]
j [4 5 6 7]]
(result i j))
result)
{1:4 2:5 3:6}
Expressions may be quoted by prefixing with '
.
,
may be used to temporarily decrease quoting depth while evaluating the next form.
(let [bar 42]
'[foo ,bar baz])
Unquoting may be combined with *
to expand in place.
(let [bar 42
baz "abc"]
'[foo ,[bar baz]* qux])
['foo 42 "abc" 'qux]
=
may be used to compare values deeply, while is
compares identity.
For some types they return the same result; integers, strings, pairs, methods etc.
For others; like arrays and maps; two values may well be equal despite having different identities.
(= [1 2 3] [1 2 3])
T
(is [1 2 3] [1 2 3])
F
The Nil
type has one value (_
), which represents the absence of a value.
The Bit
type has two values, T
and F
.
Symbols may be created by quoting identifiers or using the type constructor.
(= (Sym 'foo "bar") 'foobar)
T
gensym
may be used to create unique symbols.
(gensym 'foo)
'7foo
Character literals may be defined by prefixing with \
.
(char/is-digit \7)
T
Special characters require one more escape.
\\n
\\n
Characters support ranges.
[\a..\z:2*]
[\a \c \e \g \i \k \m \o \q \s \u \w \y]
Integers support the regular arithmetic operations.
(- (+ 1 4 5) 3 2)
5
Negative integers lack syntax, and must be created by way of subtraction.
(- 42)
-42
Integers support ranges.
[1..10:2*]
[1 3 5 7 9]
parse-int
may be used to parse integers from strings, it returns the parsed value and the next index in the string.
(parse-int "42foo")
42:2
Decimal expressions are read as fixpoint values with specified number of decimals.
Like integers, fixpoints support the regular arithmetic operations.
1.234
1.234
Leading zero is optional.
(= 0.123 .123)
T
Also like integers; negative fixpoints lack syntax, and must be created by way of subtraction.
(- 1.234)
-1.234
Fixpoints support ranges.
[1.1..1.4:.1*]
[1.1 1.2 1.3]
Pairs may be formed by putting a colon between two values.
1:2:3
1:2:3
Or by calling the constructor explicitly.
(Pair [1 2 3]*)
1:2:3
Arrays are fixed size sequences of values.
New arrays may be created by enclosing a sequence of values in brackets.
[1 2 3]
Or by calling the constructor explicitly.
(Array 1:2:3*)
[1 2 3]
String literals may be defined using double quotes.
up
and down
may be used to change case.
(string/up "Foo")
"FOO"
split
may be used to split a string on a pattern.
(string/split "foo bar" " ")
["foo" "bar"]
reverse
may be used to reverse a string.
(string/reverse "foo")
"oof"
replace
may be used to replace all occurrences of a pattern.
(string/replace "foo bar baz" " ba" "")
"foorz"
#
or length
may be used to get the length of any composite value.
(= #[1 2 3] (length "foo"))
T
Pairs, arrays, lists and strings all implement the stack trait.
push
may be used to push a new item to a stack; for pairs it's added to the front, for others to the back.
(let [s 2:3]
(push s 1)
s)
1:2:3
peek
may be used to get the top item in a stack.
(peek "abc")
\a
pop
may be used to remove the top item from a stack.
(let [s [1 2 3]]
(pop s):s)
3:[1 2]
Maps are ordered mappings from keys to values.
New maps may be created by enclosing a sequence of pairs in curly braces.
'{foo:1 bar:2 baz:3}
Or by calling the constructor explicitly.
(Map '[foo:1 bar:2 baz:3]*)
{'bar:2 'baz:3 'foo:1}
All composite types may be sliced by indexing using a pair.
('{a:1 b:2 c:3 d:4} 'b:'c)
{'b:2 'c:3}
map
may be used to map a method over one or more iterables, it returns an iterator.
[(map Pair '[foo bar baz] [1 2 3 4])*]
['foo:1 'bar:2 'baz:3]
filter
returns an iterator for items matching the specified predicate.
[(filter (^[x] (> x 2)) [1 2 3 4 5])*]
[3 4 5]
reduce
may be used to transform any iterable into a single value.
(reduce - [2 3] 0)
1
sum
is provided as a shortcut.
(sum 1 2 3)
10
zip
may be used to braid any number of iterables.
[(zip '[foo bar] '[1 2 3] [T F])*]
['foo:1:T 'bar:2:F]
enumerate
may be used to zip any iterable with indexes.
[(enumerate 42 '[foo bar])*]
[42:'foo 43:'bar]
find-first
may be used to find the first value in an iterable matching the specified predicate, along with its index; or _
if not found.
(find-first (^[x] (> x 3)) [1 3 5 7 9])
5:2
The time library uses established naming conventions when referring to fields: Y
for years, M
for months, D
for days, h
for hours, m
for minutes, s
for seconds, ms
for milliseconds and us
for microseconds.
time/today
and time/now
may be used to get the current date/time.
(is (time/trunc (time/now)) (time/today))
T
The Timestamp
constructor may be used to create new timestamps manually, pass _
for default.
(Timestamp 2024 _ _ 21 22)
2024-01-01 21:22:00
Subtracting timestamps results in a duration.
(- (time/now) (time/today))
16:36:27.2404435
Suffixes may be used as constructors.
(is (time/m 120) (time/h))
T
When applied to timestamps or durations they return the value for the specified field.
(time/D (time/W 2))
14
Durations may be added/subtracted to/from timestamps.
(+ (time/today) (time/m 90))
2024-09-15 01:30:00
Timestamps support ranges.
The following example generates timestamps between 2024-1-1
and the next day, separated by six hours.
(let [t (Timestamp 2024 1 1)]
[t..(+ t (time/D 1)):(time/h 6)*])
[2024-01-01 00:00:00 2024-01-01 06:00:00 2024-01-01 12:00:00 2024-01-01 18:00:00]
MONTHS
maps indexes to month names.
time/MONTHS
[_ 'jan 'feb 'mar 'apr 'may 'jun 'jul 'aug 'sep 'oct 'nov 'dec]
WEEKDAYS
maps indexes to week day names.
time/WEEKDAYS
['su 'mo 'tu 'we 'th 'fr 'sa]
By default all timestamps are local, time/to-utc
and time/from-utc
may be used to convert back and forth.
(let [t (time/now)]
(is (time/to-local (time/to-utc t)) t))
T
Pipes are unbounded, thread safe communication channels. Pipes may be called without arguments to read and with to write.
Ports are bidirectional communication channels. Like pipes, ports may be called without arguments to read and with to write.
poll
returns the first argument that's ready for reading.
(let [p1 (Pipe) p2 (Pipe)]
(p2 42)
((poll p1 p2)))
42
spawn
may be used to start new threads, each thread runs in a separate VM. A port is created for communication, one side passed as argument and the other returned.
(let [p (spawn [p] (p 42))]
(p))
lib
may be used to define/extend libraries.
(lib foo
(var bar 42))
foo/bar
42
When called with one argument, it specifies the current library for the entire file.
(lib foo)
(var bar 42)
And when called without arguments, it returns the current library.
(lib)
(Lib user)
fail
may be used to signal an error.
Since there are no facilities for handling errors yet, this means evaluation will stop unconditionally.
(fail 'bummer)
Sharpl.EvalError: repl@1:2 bummer
eval
may be used to dynamically evaluate code at runtime.
(eval '(+ 1 2))
3
emit
may be used to evaluate code once as it is emitted.
(emit '(+ 1 2))
3
json/encode
and json/decode
may be used to convert values to/from JSON.
(let [dv {'foo:42 'bar:.5 'baz:"abc" 'qux:[T F _]}
ev (json/encode dv)]
ev:(= (json/decode ev) dv))
"{\"bar\":0.5,\"baz\":\"abc\",\"foo\":42,\"qux\":[true,false,null]}":T
check
fails with an error if the result of evaluating its body isn't equal to the specified value.
(check 5
(+ 2 2))
Sharpl.EvalError: repl@1:2 Check failed: expected 5, actual 4!
When called with a single argument, it simply checks if it's true.
(check 0)
Sharpl.EvalError: repl@1:2 Check failed: expected T, actual 0!
Take a look at the test suite for examples.
bench
may be used to measure the number of milliseconds it takes to repeat a block of code N times with warmup.
dotnet run -c=release benchmarks/fib.sl
dotnet publish
may be used to build a standalone executable.
$ dotnet publish
MSBuild version 17.9.8+b34f75857 for .NET
Determining projects to restore...
Restored ~/sharpl/sharpl.csproj (in 324 ms).
sharpl -> ~/sharpl/bin/Release/net8.0/linux-x64/sharpl.dll
Generating native code
sharpl -> ~/sharpl/bin/Release/net8.0/linux-x64/publish/
dmit
may be used to display the VM operations emitted for an expression.
(dmit '(+ 1 2))
9 Push 1
10 Push 2
11 CallMethod (Method + []) 2 False