Skip to content

codr7/sharpl

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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 and expected to work as described.

novelties

  • 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 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 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.

examples

The following examples may give you an idea what Sharpl is currently capable of:

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.

(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 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.

About

a custom Lisp

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published