$ dotnet run
sharpl v29
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 7 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*), similar to Python. - Declarative destructuring (
(let [_:y 1:2] y)) of bindings. - Splatting (
[1 2 3]*), simlar to Python. - Deferred actions, similar to Go.
- Unified, deeply integrated iterator protocol.
- Default decimal type is fixpoint.
- Nil is written
_. - Both
TandFare defined. - Zeros and empty strings, arrays, lists and maps are considered false.
=compares values deeply,ismay be used to only compare identity.- Lambdas look like anonymous methods.
- Compile time eval (
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.
The following examples may give you an idea what Sharpl is currently capable of:
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}
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
trait may be used to define abstract types.
Supers are required to be traits.
(trait Foo)
(trait Bar Foo)
(> Bar Foo)
T
type may be used to define new types.
A default constructor is automatically generated if not specified.
(data Foo Int)
(let [x (Foo 2)]
(say x)
(say (+ x 3))
(Foo 2)
3
Specifying a constructor allows customizing the instantiation.
(type Bar Map
(^[x y z] {x:1 y:2 z:3}))
(let [bar (Bar 'a 'b 'c)]
(say bar)
(say (bar 'b)))
(Bar {'a:1 'b:2 'c:3})
2
fail may be used to signal an error.
The first argument is the error type (default Error); remaining arguments are passed to the constructor, which by default concatenates a message.
(fail _ 'bummer)
Sharpl.UserError: repl@1:2 bummer
at Sharpl.Libs.Core.<>c.<.ctor>b__27_22(VM vm, List`1 stack, Method target, Int32 arity, Loc loc) in /home/codr7/Code/sharpl/src/Sharpl/Libs/Core.cs:line 433
at Sharpl.Method.Call(VM vm, List`1 stack, Int32 arity, Loc loc) in /home/codr7/Code/sharpl/src/Sharpl/Method.cs:line 24
at Sharpl.VM.Eval(Int32 startPC, List`1 stack) in /home/codr7/Code/sharpl/src/Sharpl/VM.cs:line 271
try may be used to register error handlers for a block of code, handlers are checked in specified order when an error occurs.
(try [Any:(^[_] (say 'inside-error-handler))]
(say 'before)
(fail _ 'bummer)
(say 'after))
(say 'done)
before
inside-error-handler
done
LOC may be used to get the error source location inside handlers.
(try [Any:(^[e] LOC)]
(fail _))
repl@2:4
Any type may be used as an error.
(trait Foo)
(data Bar [Pair Foo]
(^[x y z] x:y:z))
(try [Foo:(^[e] e)]
(fail Bar 3 2 1))
(Bar 1:2:3)
(+ 'foo 1)
Sharpl.NonNumericError: repl@1:2 Expected numeric value: 'foo
1 use-value
2 stop
Pick an alternative (1-2) and press ⏎: 1
Enter new value: 42
43
Actions may be registered to run unconditionally at scope exit using defer.
Deferred actions are evaluated last in first out.
(do
(say 'before)
(defer (^[] (say 'defer)))
(say 'after))
before
after
defer
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)
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
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))
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
The terminal control library is intented as a portable foundation that provides all the bits and pieces needed to build a full TUI.
The following standard keys are defined as constants:
DOWNLEFTENTERÈSCRIGHTSPACEUP
poll-key may be used to check if a key is available.
(term/poll-key)
F
read-key may be used to read the next key press, echoing is disabled by default but may be turned on by passing T.
(term/read-key T)
(term/Key Enter)
ask may be used to read a line of input with optional prompt and/or echo (enabled by default).
(ask "Enter password" \*)
Enter password: ***
"abc"
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!
Have 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
Contributions are very welcome, feel free to submit pull requests.
Or even better, register a GitHub issue describing the change and allow us to make sure we agree it's a good idea first.
- Fork the project.
- Create a feature branch (
git checkout -b issue-x). - Commit your changes (
git commit -m '...'). - Push to the branch (
git push origin issue-x). - Open a pull request.