DotLisp - A Lisp dialect for .Net

©2003 Rich Hickey, All Rights Reserved

News

Objectives

To provide an interactive, Lisp-like language for .Net scripting and development

To provide a framework for language experimentation

Deep .Net integration, sharing type system, GC and other runtime services etc., with transparent access to .Net w/o a FFI or wrappers

To have fun

Non-Objectives

Compatibility w/Scheme or Common Lisp, e.g. strings are mutable in Scheme and CL, but not in .Net, so not in DotLisp.

Speed (although it is quite useable)

Caveats

I'm not a Lisp expert. Helpful suggestions are always welcome.

Features

Command-line interpreter with Read-Eval-Print Loop

Embeddable Interpreter object in a DLL assembly

Lexically scoped, Lisp-1

&key, &opt and &rest parameters

CL-style Macros

Generic functions (single and binary dispatch)

.Net type system (strings, ints, floats, bools, chars, arrays etc are the .Net System types, no conversions/mapping)

Transparent access to the .Net framework - simple syntax for constructor and method invocation and property access

Implementation Notes

Started with Silk (now JScheme), a Scheme for Java

Ported from Java to C#, JVM to .Net CLR

Moved away from Scheme compatibility

No Silk-based code left

Documentation Overview

This documentation presumes that you know Scheme or Common Lisp, or are willing to learn the fundamentals of Lisp from some other source (i.e. Scheme books or Common Lisp books). It also presumes you have at least the .Net Framework SDK (if not Visual Studio .Net), and will learn about .Net and its framework from the help included with same.

Obtaining DotLisp

DotLisp is now open source, released under the BSD license, and hosted as a project on SourceForge.

Philosophy

The idea behind DotLisp was to build a Lisp for .Net that yielded to the CLR those things provided by the CLR that languages normally have to provide themselves: a type system, a memory management system, a library, while at the same time retaining the essence of Lisp as a language.

Table of Contents

At a command prompt:

DotLispREPL boot.lisp [other lisp files to load]

to exit:

Ctrl-Z, Enter

A DotLisp program is a sequence of expressions. An expression is a literal, or the parenthesized list indicating the application of a primitive or other expression to zero or more arguments. The first argument of a parenthesized expression is evaluated like any other before being 'applied' to the arguments. Many things can be applied to arguments in DotLisp including functions, members (both functions and properties), types (as constructors) and anything else (as an index to an object with a default indexer).

DotLisp is case sensitive.

; denotes a to-end-of-line comment

true, false

Booleans (System.Boolean)

nil

()

Nil (equivalent to .Net null reference, and the empty list), matches any type

1, 2, -3

Integers (System.Int32)

1.2, 3.e10

Floating point (System.Double)

"a string"

Strings (System.String). Strings can span multiple lines, whitespace is preserved.

There is no character literal yet, suggestions?

Characters

'fred

Symbols (DotLisp.Symbol)

'*varname

Dynamic vars must have prefix *

:a-keyword-symbol

Keywords symbols are prefixed with : and evaluate to themselves

'(a b c)

Lists (DotLisp.Cons)

[1 2 3] ;Int32[]

['a 'b 12] ;Object[]

Arrays (if homogeneous args, will be array of common type, else Object[])

note: arrays are not true literals, merely shorthand for (vector ...)

Int32.

Hashtable.

Types (the framework name followed by a dot). Funcallable, act as constructors:

(Hashtable. 1000) ;yields a new Hashtable with initial capacity of 1000

.foo ;instance member - can be field, property or function

(set (.foo x) 5) or (.foo x 5) ;==> x.foo = 5 in C#

Console:WriteLine ;static member - type:member - field, property or function

(Console:WriteLine "Hello World") ;invocation

Instance members (the member name preceded by a dot). Funcallable. Fields, properties and instance member function are all generalized to be functions on the target object. If the member function takes arguments, they follow the target object:

(.foo x 1 2 3)

There is some syntactic sugar for instance member access - x.foo is transformed (at read time) to (.foo x) in all cases except when in first position of form, where it is translated to .foo x (no parens). This lets you do all of the expected things with no more parens than C# (just in different places):

(prn x.y) ;=> (prn (.y x)) - access a member, no parens required

x.foo ;=> (.foo x) - no arg function call - no parens!

(x.foo 1 2 3) ;=> (.foo x 1 2 3) - function call with args

In addition, explicit qualification of members is supported when needed using .type:member -

(.IEnumerable:GetEnumerator obj)

This is occasionally required when a type provides only an explicit implementation of an inherited interface function.

_ ;underscore

Shorthand for System.Reflection.Missing.Value

$, $$, $$$

The last, next-to-last, and next-next-to-last values evaluated

!

The last exception thrown (only the message is reported at the interactive prompt when the exception is thrown, other aspects of the error can be determined by evaluating ! or its members).

interpreter

The current interpreter. (DotLisp.Interpreter)

(eql? x y)

Function: Equality - returns true if x and y are the same object, or are .Net value types and x.Equals(y), else false

(eqv? x y)

Function: Equivalence - returns x.Equals(y)

(if testexpr thenexpr [elseexpr])

Primitive: if testexpr is true, yields thenexpr and does not evaluate elseexpr, else returns elseepxr. If elseexpr is omitted it defaults to nil. Note: DotLisp has a generalized notion of truth where nil and false are conditional false and everything else is conditional true.

(when testexpr thenexpr)

Macro: same as (if textexpr thenexpr)

(not x)

Function: returns false if x is conditional true, else true

(when-not testexpr thenexpr)

Macro: same as (when (not textexpr) thenexpr)

(cond testexpr1 thenexpr1

...

testexprN thenexprN

[:else elseexpr])

Macro: evaluates the testexprs in order and returns the corresponding thenexpr as soon as one is conditional true. If no testexpr is true and else clause is present, returns elseexpr, otherwise returns nil. Note: fewer parens than Common Lisp because thenexprs are not in implicit progns (blocks).

(case testexpr

(keya1 ... keyaN) thenexpra

...

(keyn1 ... keynN) thenexprN

[:else elseexpr])

Macro: compares the testexpr to unevaluated keys with eql? and returns the corresponding thenexpr as soon as one is eql?. If no key is eql? and else clause is present, returns elseexpr, otherwise returns nil. Note: fewer parens than Common Lisp because thenexprs are not in implicit progns (blocks)

(and arg1 ... argN)

Macro: evaluates args in order, returning nil as soon as one is conditional false, else returns argN.

(or arg1 ... argN)

Primitive: evaluates args in order, returning first that is conditional true, else returns nil.

(nand x y)

(xor x y)

Macros that do what you expect

(nil? x)

Function: returns true if x is nil, else false

(to-bool x)

Function: converts DotLisp conditional truth values to Boolean true or false, i.e. to-bool nil or false -> false, to-bool anything else -> true

(missing? x)

Function: return true is x is System.Reflection.Missing.Value (_)

(quote x)

'x

Primitive: returns x un-evaluated

(type-of x)

Function: returns corresponding Type object

(is? x atype)

Function: returns true if x is instance of atype

(def x expr)

Macro: creates a top-level variable bound to x with initial value of expr

(block exprs)

Primitive: evaluates exprs in order and returns the value of the last

(fn ([params]) exprs)

Macro: creates an anonymous function object that when invoked, evaluates the exprs in an implicit block with the params bound to the actual arguments

Params can contain optional parameters (&opt), keyword parameters (&key) and rest parameters (&rest). Pretty much like Common Lisp lambda except no supplied-p-parameters. &opt and &key params without defaults get assigned System.Reflection.Missing.Value (_, testable with missing?) . &opt and &key parameters with defaults will get the defaults when no args is supplied, as well as when _ (Missing.Value) is supplied:

>(def (foo &opt (x true)) x) >(foo) true >(foo _) true >(foo false) false >(def (bar &opt x) (foo x)) >(bar) true >(bar false) false

(def (f [params]) exprs)

Macro: creates a top-level variable f bound to a function. Same as

(def f (fn (params) exprs))

(let (var1 initexpr1 ... varN initexprN)

exprs)

Macro: evaluates exprs in an implicit block, with each var i bound to the corresponding initexpr i , returning the value of the last. Same as

((fn (vars) exprs) initexprs)

Note fewer parens than Common Lisp since no single vars default bound to nil

(lets (var1 initexpr1 ... varN initexprN)

exprs)

Macro: Let sequential - Like let, but vars are updated sequentially

(letfn ((f1 [params]) body1 ... (fN [params]) bodyN)

exprs)

Macro: Let function(s) - Like let, but with each f i bound to a fn with body i . Body definitions can be recursive, referring to themselves or each other:

(letfn ( (foo x)

(when (> x 0) (prn x) (bar (- x 1)))

(bar x)

(foo x))

(foo 2))

(dynamic-let (*var1 initexpr1 ... *varN initexprN)

exprs)

Macro: Dynamic-let, like let but for dynamic variables, original values are restored after exprs block completes

(eval astring)

Function: reads the string and evaluates it

(apply f arg1 ... seq)

Function: The last arg must support get-enum. Applies f to arg1 through argN-1 followed by the set yielded by enumerating seq.

(load afilename)

Function: loads the file and evaluates the expressions contained therein as if entered at top-level.

(load-assembly name)

Function: loads the specified assembly and makes names and types accessible to the program.

(load-assembly-from filename)

Function: loads the specified assembly file and makes names and types accessible to the program.

(set place1 val1 ... placeN valN)

Macro: sets each place to its corresponding val in sequence, returning valN

(parallel-set place1 val1 ... placeN valN)

Macro: sets each place to its corresponding val in parallel, returning valN

(shift-set place1 ... placeN)

Macro: each place takes on the value of the subsequent place, and placeN keeps its value

(rotate-set place1 ... placeN)

Macro: each place takes on the value of the subsequent place, and placeN gets the value of place1

(def-setter placefn setfn)

Macro: associates setfn as the setter of placefn, such that calls of the form

(set (placefn args) val) are transformed into (setfn args val)

Allowing you to say (set (first x) val) instead of (set-first x val)

(cons x y)

Function: creates a new Cons object with a first of x and a rest of y. Note: Cons objects in DotLisp are not arbitrary pairs, i.e. the rest must be another Cons object or nil. Therefore all lists are 'proper'. Conses implement IEnumerable.

cons?, atom?, list?, first, rest, second, third, fourth, nth, len

Functions: all take a list and do what you would expect.

(set-first lst val)

(set-rest lst cons-or-nil)

(nth-rest lst n)

(reverse lst)

Functions: Do what you would expect

(reverse! lst)

Function: destructively reverses lst, minimizing consing

(append list1 list2)

Function: returns a new list of the items in list1 followed by the items in list2. May share structure with list2

(concat! list1 list2)

Function: destructively concatenates list1 and list2, minimizing consing

(last lst &opt (n 1))

Function: returns tail containing last n elements of lst

(butlast lst &opt (n 1))

Function: returns list containing all but last n elements

(mapcat! f &rest lists)

Function: Similar to CL mapcan

(member obj lst &key (test eql?))

(member-if pred lst)

Functions: Return tail beginning with found element or nil if not found

(push! val cons-place)

Macro: sets cons-place to (cons val cons-place)

(pop! cons-place)

(next! cons-place)

Macros: set cons-place to (rest cons-place)

(vector arg1 ... argN)

[arg1 ... argN]

Function: makes a one-dimensional array with args as initial elements. If all args are of same type, than array is of that type, otherwise an array of Object.

(vector-of type arg1 ... argN)

Function: makes a one-dimensional array of type

Note these are just sugar, you can do these same things and more through the Array type.

(n anarray)

Returns the nth element in the array.

(n array val)

Sets the nth element of the array to val and returns val

Note the above 2 array operations are just instances of the general indexing capability. Any non-function, non-type, non-member in the first position in an expression will be 'applied' to the first argument if it supports a default indexer, and if a second argument is supplied it will be treated as a set operation. So:

(1 "fred") -> r

("fred" ahashtable "ethel") ;set fred key to ethel value

("fred" ahashtable) -> "ethel" ;access it

+, -, *, /, min, max

Functions: multi-argument arithmetic.

+=, -=, *=, /=, ++, --

(+= x n) -> (set x (add x n))

Macros: calc and set, ++ and -- add/subtract 1

<, <=, >, >=, ==, !=

Functions: comparisons

(add x y), (subtract x y), (multiply x y), (divide x y), (compare x y)

Generic BinOps: upon which arithmetic ops are built. Extend by defining new BinOp methods. compare returns an Int32 with the same semantics as IComparer.Compare.

(bit-and x y), (bit-or x y), (bit-xor x y)

Generic BinOps: bitwise ops. Methods defined for integer types and enums.

(bit-not x)

Generic function: bitwise not. Methods defined for integer types and enums.

even?, odd?, zero?, positive?, negative?

Functions: test what they imply

(while test exprs)

Primitive: While test is true, evaluates exprs, returns nil

(until test exprs)

Macro: While test is non-true, evaluates exprs, returns nil

(for inits test update &rest body)

Macro: roughly, (lets inits (while test (block body update))), returns nil

(dolist var lst &rest body)

Macro: evaluates body with var bound to successive elements of lst

(dotails var lst &rest body)

Macro: evaluates body with var bound to successive tails of lst

(for-each var seq &rest body)

Macro: seq must support get-enum. Evaluates body with var bound to successive elements of seq

DotLisp supports a generalized notion of sequence and several functions that take and yield sequences. A sequence is any object for whom the get-enum generic function is defined.

(get-enum x)

Generic function: Returns an IEnumerator over x. Methods are predefined for IEnumerator and IEnumerable, so all of the sequence functions work with .Net collections.

(make-enum inits get &rest move)

Macro: creates a lazy IEnumerator object based upon supplied code. inits are bound as by lets, get is evaluated to define IEnumerator.Current(), and the expressions of move are in an implicit block, the last of which must yield a boolean which will be the return value of IEnumerator.MoveNext() For example, range is defined as follows:

(def (range start end &opt (step 1)) (make-enum (x start curr start) curr (set curr x) (+= x step) (< curr end)))

filter, map, concat and others are defined using make-enum. N.B. no Reset() support or off-the-ends protection.

(map->list &rest seqs)

Like CL mapcar except args can be sequences and not just lists. Differs from most of the following sequence functions in returning a list rather than another (lazy) sequence.

(map1 f seq)

Function: yields a lazy sequence that is the result of calling f for each element in seq.

(map f &rest seqs)

Function: f must be a function that takes as many args as there are seqs. Yields a lazy sequence that is the result of calling f with the first element from each seq, then the second etc. Stops when the end of the shortest seq is reached.

(filter pred seq)

Function: returns a lazy sequence that containing those values in seq for which pred returns true

(find val seq &key (test eqv?))

Function: returns a lazy sequence of those items in seq for which (test item val) returns true.

(concat &rest seqs)

Function: returns a lazy sequence which is a concatenation of the items in seqs.

(reduce f seq &key init)

Function: If seq is empty, returns init if supplied, else the result of calling f with no args. For some non-empty seq comprised of a b c, if init is supplied the result is equivalent to:

(f (f (f init a) b) c)

if init is missing, result is equivalent to:

(f (f a b) c)

(any pred &rest seqs)

Function: pred must take as many args as there are seqs. Calls pred with the first element from each seq, then the second etc and returns the first true value returned, else stops when the end of the shortest seq is reached and returns false .

(every pred &rest seqs)

Function: pred must take as many args as there are seqs. Calls pred with the first element from each seq, then the second etc and returns the first false value returned, else stops when the end of the shortest seq is reached and returns the last value returned by pred.

(into coll seq)

Generic function: Dumps seq into the collection coll by means equivalent to append (i.e. the collection will contain the seq in order if the collection has a notion of order). Methods are predefined for IList, Cons and nil, the last of which will cons up a new list:

(into nil [1 2 3]) -> (1 2 3)

The general idiom being the use of sequence functions to dump into your choice of existing or new collection:

(into (ArrayList.) (map .ToString [1 2 3])) ;puts "1", "2", "3" into new ArrayList

Note that you can map any funcallable thing, including member functions, properties, types (constructors) and indexes.

backquote (`), unquote (~) and unquote-splicing (~@) operators are all supported. Note: use of tilde rather than comma for unquote because comma will be needed for multi-dimensional array type names (which are not yet implemented)

(def-macro (m [params-pattern]) exprs)

Macro: Defines a macro named m. Destructuring is supported for the params pattern as long as it does not contain &key or %opt args, i.e. params patterns can contain nested list structure including &rest params, or &key and &opt params, but not both.

(macroexpand-1 pattern)

Function: returns the result of expansion on the pattern

(gensym)

Function: generates a new, unique symbol

DotLisp supports generic functions of arbitrary arglists that dispatch on the type or value of the first arg, and generic binary operators of exactly 2 args that dispatch on both argument types (but not on values).

(def-method (gfname (p1 dispatch-type-or-value) &rest params) &rest body)

Macro: defines a new method for the generic function gfname that will be operable for calls where the first argument is of the (best-matching) type dispatch-type-or-value if it is a Type, or its value otherwise. &opt, &key and &rest params are supported in the argument list. No effort is made to ensure that different methods for the same generic function have conforming signatures.

Within the body, the function (call-base-method) is available (for type-dispatching methods only), and when called with no args will invoke the method that would match the base class/interface of p1. If there are methods defined on more than one base, the one chosen is undefined.

(def-binop (gbname (p1 dispatch1) (p2 dispatch2)) &rest body)

Macro: defines a new method for the generic binop gbname. Generic binops match only on types, not values. Binops do a best-match on p1 followed by a best match on p2.

(str x)

Generic Function: returns a str representation of x for printing. Methods are defined for Object (using .ToString), nil, true, false, String, ICollection, Cons etc. No effort is made for round-tripping values via print and read.

(pr &rest xs)

Function: for each x in xs, prints each (str x) to *pr-writer using .Write, with the separator *pr-sep

(prn &rest xs)

Function: same as (pr xs) followed by a newline

(prs &rest xs)

Function: for each x in xs, prints each x to *pr-writer using .Write, with the separator *pr-sep

(prns &rest xs)

Function: same as (prs xs) followed by a newline

(def-record type &rest fields)

Macro: type can be a single symbol or (NewType BaseType.), where BaseType is also a record type. Note that the new type symbol is not followed by the dot(.) because the type dpes not yet exist. After calling defrecord you can refer to NewType. with the dot like any other type. If no base is supplied, the base is DotLisp.Record. Fields can be single symbols or parenthesized symbol/default-value pairs as per &keys args. The type is created along with code to support make-record (which must be used rather than the typical constructor call in order to create an initialized record object).

(make-record type &rest args)

Function: creates an instance of the type, which must have been dreated with def-record, and a set of key/value pairs corresponding to some or all of the members of type or its bases class(es). Any members for which no values are supplied will be initialized to their defaults (if supplied to def-record) or else _ (.Missing.Value).

> (def-record NewRec (x 1) (y 2) z)

NewRec.

> (def rec (make-record NewRec. :x 5))

{NewRec. {"x" 5} {"y" 2} {"z" _}}

Fields of records can be accessed using the member syntax:

> (.x rec) ;access x member, rec.x ok too

5

> (.x rec 10) ;set x member

> rec

{NewRec. {"x" 10} {"y" 2} {"z" _}}

In addition, record types are expando - i.e. new members can be introduced via set:

> (set rec.w 10) ;set non-existent w member, ok

> rec

{NewRec. {"x" 10} {"y" 2} {"z" _} {"w" 10}}

However it is an error to access a non-existent member that has not been set:

>rec.b

!Exception: Record does not contain member: b

Derived records add fields to their base:

> (def-record (DerivedRec NewRec.) (ethel "fred") (ricky "lucy"))

DerivedRec.

> (make-record DerivedRec.)

{DerivedRec. {"x" 1} {"y" 2} {"z" _} {"ethel" "fred"} {"ricky" "lucy"}}

However only fields declared in the class can be initialized in make-record. Base class members will get their defaults (i.e. z below cannot be initialized via make-record DerivedRec.):

> (make-record DerivedRec. :z 9 :ethel "merman")

{DerivedRec. {"x" 1} {"y" 2} {"z" _} {"ethel" "merman"} {"ricky" "lucy"}}

To allow base members to be initialized or define new defaults, they can be redeclared in the derived record:

> (def-record (DerivedRec NewRec.) (ethel "fred") (ricky "lucy") (z 9)) ;redeclare z

DerivedRec.

> (make-record DerivedRec.)

{DerivedRec. {"x" 1} {"y" 2} {"z" 9} {"ethel" "fred"} {"ricky" "lucy"}}

> (make-record DerivedRec. :z 11 :ethel "merman")

{DerivedRec. {"x" 1} {"y" 2} {"z" 11} {"ethel" "merman"} {"ricky" "lucy"}}

Record types can be redefined without restarting. To .Net, the "members" do not exist as fields or properties, rather, the type has a default String indexer through which the member values can be obtained.

(try body &key catch finally (catch-name 'ex))

Macro: Will execute body in a try block. If an exception is thrown and catch is provided, will execute catch expression with the catch-name symbol bound to the thrown exception. In all cases, will execute the finally expression if provided.

(with-dispose inits &rest body)

Macro: will initialize inits via lets, then evaluate body in a try block whose finally clause will call IDisposable:Dispose on any non-null vars from inits.

(throw ex)

Function: throws the Exception ex

(error msg)

Function: throws an Exception with the message text msg

(trace &rest fnames)

Macro: will write to System.Diagnostic.Trace whenever functions in fnames are called

(untrace &rest fnames)

Macro: cancels tracing for funcs in fnames. If fnames is nil, cancels all traces

DotLisp supports dynamic creation of delegates bound to closures.

(make-delegate DelegateType. (&rest delegate-args) &rest body)

Macro: makes an instance of DelegateType bound to the closure body. The number of args and return type (if any) must match the delegate:

>(set e (make-delegate EventHandler. (sender e)

(prn (list sender e))))

System.EventHandler

>(e.Invoke "x" EventArgs:Empty)

("x" System.EventArgs)

DotLisp is now packaged as a DLL assembly. Inside the assembly the Interpreter class is public. Creating an instance of this class gives you a fully independent interpreter, with the following interface:

Interpreter()

Constructor. Builds an interpreter instance with built-ins and primitives loaded into its environment.

Boolean Eof(Object o)

Returns true if o is the end-of-file object, else false. Use this to test the value returned by Read().

Object Eval(Object expr)

Evaluate expr, where expr is an expression returned from Read(). For now, consider the type and structure of expr to be opaque, i.e. don't hand something to Eval() that you didn't get from Read().

void Intern(String name, Object val)

Set the global value of the symbol named name to be val. Use this to expose your application-level objects to DotLisp code.

void InternType(Type t)

Expose the type t to DotLisp code so it can be referenced via type literals (i.e. with Typename. )

void InternTypesFrom(Assembly a)

Interns all the types from a.

Object Load(TextReader t)

Read and Eval the code from t. Return value TBD.

Object LoadFile(String filepath)

Loads the code from filepath. Return value TBD.

Object Read(TextReader t)

Reads the next expression from t. Returns the expression, suitable for evaluation by Eval(), or an object for which Eof() will return true, indicating end-of-file was reached.

String Str(Object o)

Returns the String representation of o that would be returned by the generic function str in the DotLisp environment.

void Trace(DotLisp.Symbol sym)

Turn on tracing of calls to sym.

void UnTrace(DotLisp.Symbol sym)

Turn off tracing of calls to sym.

void UnTraceAll()

Turn off tracing of all symbols

ICollection TraceList{get;}

The set of all symbols currently being traced.

Given this interface, a basic REPL loop looks like this:

DotLisp.Interpreter interpreter = new DotLisp.Interpreter();

for(;;) { try{ Console.Write("> "); Object r = interpreter.Read(Console.In); if(interpreter.Eof(r)) return; Object x = interpreter.Eval(r); Console.WriteLine(interpreter.Str(x)); } catch(Exception e) { Console.WriteLine("!Exception: " + e.GetBaseException().Message); } }

DotLisp exposes the delegate type Function and the interface IFunction to allow functions written in other languages can be incorporated into DotLisp. Any instance of Function or instance of a class implementing IFunction with be funcallable. Just implement in the language of your choice and then Intern them with the name you desire. The signatures are:

public delegate Object Function(params Object[] args);

public interface IFunction { Object Invoke(params Object[] args); }

In addition, closures written in DotLisp implement IFunction and can be invoked via that interface.