https://github.com/codr7/sharpl
a custom Lisp
https://github.com/codr7/sharpl
Last synced: 2 months ago
JSON representation
a custom Lisp
- Host: GitHub
- URL: https://github.com/codr7/sharpl
- Owner: codr7
- License: mit
- Created: 2024-04-29T15:47:52.000Z (almost 2 years ago)
- Default Branch: main
- Last Pushed: 2024-09-16T23:10:31.000Z (over 1 year ago)
- Last Synced: 2024-09-17T11:31:01.054Z (over 1 year ago)
- Language: C#
- Homepage:
- Size: 711 KB
- Stars: 82
- Watchers: 1
- Forks: 4
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE.txt
Awesome Lists containing this project
README
# sharpl
```
$ dotnet run
sharpl v29
1 (say 'hello)
2
hello
```
## intro
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](https://github.com/codr7/sharpl/tree/main/tests) and expected to work as described.
## novelties
- [Pairs](https://github.com/codr7/sharpl/tree/main#pairs) (`a:b`) are used everywhere, all the time.
- [Range](https://github.com/codr7/sharpl?tab=readme-ov-file#ranges) support (`min..max:stride`).
- [Maps](https://github.com/codr7/sharpl/tree/main#maps) (`{k1:v1...kN:vN}`) are ordered.
- [Methods](https://github.com/codr7/sharpl/tree/main#methods) may be [composed](https://github.com/codr7/sharpl/tree/main#composition) using `&`.
- [Varargs](https://github.com/codr7/sharpl/tree/main#varargs) (`foo*`), similar to Python.
- [Declarative destructuring](https://github.com/codr7/sharpl/tree/main#destructuring) (`(let [_:y 1:2] y)`) of bindings.
- [Splatting](https://github.com/codr7/sharpl#splat) (`[1 2 3]*`), simlar to Python.
- [Deferred actions](https://github.com/codr7/sharpl#deferred-actions), similar to Go.
- Unified, deeply integrated [iterator](https://github.com/codr7/sharpl/tree/main#iterators) protocol.
- Default decimal type is [fixpoint](https://github.com/codr7/sharpl/tree/main#fixpoints).
- Nil is written `_`.
- Both `T` and `F` are defined.
- Zeros and empty strings, arrays, lists and maps are considered false.
- `=` compares values deeply, `is` may be used to only compare identity.
- [Lambdas](https://github.com/codr7/sharpl/tree/main#lambdas) look like anonymous methods.
- Compile time [eval](https://github.com/codr7/sharpl#evaluation) (`emit`), similar to Zig's comptime.
- Explicit [tail calls](https://github.com/codr7/sharpl/tree/main#tail-calls) using `return`.
- Parens are used for calls only.
- Many things are callable, simlar to Clojure.
- [Methods](https://github.com/codr7/sharpl/tree/main#methods ) have their own symbol (^).
- Line numbers are a thing.
## examples
The following examples may give you an idea what Sharpl is currently capable of:
- [Advent of Code 2023](https://github.com/codr7/sharpl/tree/main/examples/aoc23)
- [Fire](https://github.com/codr7/sharpl/tree/main/examples/fire)
- [Remote REPL](https://github.com/codr7/sharpl/tree/main/examples/remote-repl)
## bindings
Bindings come in two flavors, with lexical or dynamic scope.
### lexical scope
New lexically scoped bindings may be created using `let`.
```
(let [x 1
y (+ x 2)]
y)
```
`3`
### dynamic scope
New dynamically scoped bindings may be created using `var`.
```
(var foo 35)
(^bar []
foo)
(let [foo (+ foo 7)]
(bar))
```
`42`
### destructuring
Bindings support declarative destructuring of pairs.
```
(let [_:r:rr 1:2:3] r:rr)
```
`2:3`
### updates
`set` may be used to update a binding.
```
(let [foo 1 bar 2]
(set foo 3 bar 4)
foo:bar)
```
`3:4`
## branching
`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`
## methods
New methods may be defined using `^`.
```
(^foo [x]
x)
(foo 42)
```
`42`
### lambdas
Leaving out the name creates a lambda.
```
(let [f (^[x] x)]
(f 42))
```
`42`
### closures
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`
### tail calls
`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.
```
### varargs
`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
```
### composition
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`
## looping
### loop
`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
`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}`
## quoting
Expressions may be quoted by prefixing with `'`.
### unquoting
`,` may be used to temporarily decrease quoting depth while evaluating the next form.
```
(let [bar 42]
'[foo ,bar baz])
```
#### splat
Unquoting may be combined with `*` to expand in place.
```
(let [bar 42
baz "abc"]
'[foo ,[bar baz]* qux])
```
`['foo 42 "abc" 'qux]`
## value and identity
`=` 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`
## types
### nil
The `Nil` type has one value (`_`), which represents the absence of a value.
### bits
The `Bit` type has two values, `T` and `F`.
### symbols
Symbols may be created by quoting identifiers or using the type constructor.
```
(= (Sym 'foo "bar") 'foobar)
```
`T`
#### unique
`gensym` may be used to create unique symbols.
```
(gensym 'foo)
'7foo
```
### characters
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
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`
### fixpoints
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]`
## composite types
### pairs
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
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]`
### strings
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"`
### lengths
`#` or `length` may be used to get the length of any composite value.
```
(= #[1 2 3] (length "foo"))
```
`T`
### stacks
Pairs, arrays, lists and strings all implement the stack trait.
#### push
`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
`peek` may be used to get the top item in a stack.
```
(peek "abc")
```
`\a`
#### pop
`pop` may be used to remove the top item from a stack.
```
(let [s [1 2 3]]
(pop s):s)
```
`3:[1 2]`
### maps
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}`
### slicing
Composite types may be sliced by indexing using a pair.
```
('{a:1 b:2 c:3 d:4} 'b:'c)
```
`{'b:2 'c:3}`
## iterators
### map
`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
`filter` returns an iterator for items matching the specified predicate.
```
[(filter (^[x] (> x 2)) [1 2 3 4 5])*]
```
`[3 4 5]`
### reduce
`reduce` may be used to transform any iterable into a single value.
```
(reduce - [2 3] 0)
```
`1`
### sum
`sum` is provided as a shortcut.
```
(sum 1 2 3)
```
`10`
### zip
`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
`enumerate` may be used to zip any iterable with indexes.
```
[(enumerate 42 '[foo bar])*]
```
`[42:'foo 43:'bar]`
### find
`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`
## user defined types
### traits
`trait` may be used to define abstract types.
Supers are required to be traits.
```
(trait Foo)
(trait Bar Foo)
(> Bar Foo)
```
`T`
### data
`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
```
## errors
`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
```
### handling
`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
```
### location
`LOC` may be used to get the error source location inside handlers.
```
(try [Any:(^[e] LOC)]
(fail _))
```
`repl@2:4`
### custom errors
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)`
### restarts
```
(+ '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`
## deferred actions
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
```
## libraries
`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)`
## evaluation
### dynamic
`eval` may be used to dynamically evaluate code at runtime.
```
(eval '(+ 1 2))
```
`3`
### static
`emit` may be used to evaluate code once as it is emitted.
```
(emit '(+ 1 2))
```
`3`
## time
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`
### durations
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`
## ranges
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
`MONTHS` maps indexes to month names.
```
time/MONTHS
```
`[_ 'jan 'feb 'mar 'apr 'may 'jun 'jul 'aug 'sep 'oct 'nov 'dec]`
### weekdays
`WEEKDAYS` maps indexes to week day names.
```
time/WEEKDAYS
```
`['su 'mo 'tu 'we 'th 'fr 'sa]`
### time zones
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`
## communication
### pipes
Pipes are unbounded, thread safe communication channels. Pipes may be called without arguments to read and with to write.
### ports
Ports are bidirectional communication channels. Like pipes, ports may be called without arguments to read and with to write.
### polling
`poll` returns the first argument that's ready for reading.
```
(let [p1 (Pipe) p2 (Pipe)]
(p2 42)
((poll [p1 p2])))
```
`42`
## threads
`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
`json/encode` and `json/decode` may be used to convert values to/from [JSON](https://www.json.org/json-en.html).
```
(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`
## terminal control
The terminal control library is intented as a portable foundation that provides all the bits and pieces needed to build a full TUI.
### keys
The following standard keys are defined as constants:
- `DOWN`
- `LEFT`
- `ENTER`
- `ÈSC`
- `RIGHT`
- `SPACE`
- `UP`
`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"`
## tests
`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](https://github.com/codr7/sharpl/blob/main/tests/all-tests.sl) for examples.
## benchmarks
`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
```
## building
`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/
```
## debugging
`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
```
## contributing
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.