JMurmel is a lightweight Lisp-1-ish (mostly based on a small subset of Common Lisp with a side of Scheme) interpreter/ compiler compatible with Java8..23 that can be used standalone as well as embedded.
Currently weighing in at ~460kB (size of the compiled jmurmel.jar file containing the interpreter + compiler + runtime + REPL), or one single Java source file.
Murmel is the name of the programming language (which is a Lisp dialect), JMurmel is the name of the interpreter/ compiler that implements Murmel. For more details on the language see murmel-langref.md, and mlib.md for Murmel's default library.
The interpreter, REPL, compiler as well as Jars with compiled Murmel all run on top of the JVM. Compilation is done as Murmel to Java source which is then compiled to .class files using the JDK compiler. JMurmel as well as compiled Murmel programs should run on all platforms that support Java8+ (including CheerpJ).
Key features
JMurmel features an interpreter and a compiler, a REPL with a trace facility (trace and untrace function calls), full tail call optimization, lexical environments, a macro facility, backquote expansion including nested backquotes, JSR223 support, support for JFR (Java Flight Recorder)/ JMC (Java Mission Control), turtle- and bitmap graphics and garbage collection c/o JVM.
Goals, Priorities
Murmel is inspired by Common Lisp, i.e. when in doubt try to do it the Common Lisp way. It should be relatively easy to port a program from Murmel to Common Lisp, and Murmel knowledge should transfer to Common Lisp.
See Murmel vs Common Lisp for an overview of differences and similarities.
Status
At this time Murmel is a personal project, don't expect it to be an industrial strength Lisp in the near future.
Both the language Murmel as well as the interpreter/ compiler JMurmel currently are work in progress. There may be some incompatible language changes in the future. See also "Known Issues" in murmel-langref.md.
Copyright
Murmel and JMurmel are Copyright (C) 2020-2024 Robert Mayer.
This work is licensed under the terms of the MIT license. For a copy, see LICENSE.
Online REPL
Use the Murmel Online REPL for a first glimpse.
Quickstart for Java 11+ users
Instead of cloning the repo and building the jarfile Java 11+ users can download LambdaJ.java and run
C:\> java Lambdaj.java
for a first peek at JMurmel (compiling Murmel won't work, though, only the interpreter works that way).
Optional: building from source
Pre-requisites: JDK8 or higher (8..23 is being tested), optionally: maven
Clone the repo and build using maven (this will use the maven wrapper included in the repo, a local maven installation is not required):
C:\> git clone https://github.com/mayerrobert/jmurmel.git
...
C:\> cd jmurmel
C:\jmurmel> mvnw package
The resulting jar will end up in lambda\target\jmurmel.jar
Quickstart using a precompiled version
Make sure you have Java 8+ installed (Java 8 is minimum, more modern versions are preferred).
java -version
will tell (GNU/linux or Unix or Mac users please adjust as appropriate):
C:\> java -version
openjdk version "1.8.0_252"
OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_252-b09)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.252-b09, mixed mode)
With jmurmel.jar
(and preferably mlib.lisp
) in the current directory start JMurmel with the following command
C:\> java -jar jmurmel.jar
At the REPL prompt enter e.g. (write "Hello, World!")
and hit ENTER
,
your screen should look like this:
D:\jmurmel\lambda\target>java -jar jmurmel.jar
Enter a Murmel form or :command (or enter :h for command help or :q to exit):
JMurmel> (write "Hello, World!")
"Hello, World!"
==> "Hello, World!"
JMurmel>
You just wrote and ran a Murmel program!
Now take it from there,
e.g. read murmel-langref.md and mlib.md.
Or take a look at the example code in samples.murmel/
and samples.murmel-mlib/
Or type :h
at the REPL prompt.
Or run java -jar jmurmel.jar --help
.
$ java -jar jmurmel.jar
Enter a Murmel form or :command (or enter :h for command help or :q to exit):
JMurmel>
The command above will wait for you to enter an S-expression, interpret it and print it's result. Try e.g.
$ java -jar jmurmel.jar
Enter a Murmel form or :command (or enter :h for command help or :q to exit):
JMurmel> (+ 1 2)
==> 3.0
JMurmel>
JMurmel also can read program text from stdin:
Windows:
C:\> echo (write (quote Hello\,\ World!))| java -jar jmurmel.jar
|Hello, World!|
==> |Hello, World!|
C:\>
or Unix:
$ echo '(write (quote Hello\,\ World!))' | java -jar jmurmel.jar
|Hello, World!|
==> |Hello, World!|
$
The commands above will read an S-expression from stdin and interpret it.
Command line parameters in standalone mode:
--result
... print the result to stdout, this is the default when reading an S-expression from the console--help
..... show all the available commandline parameters and quit
Installation
Currently there is no setup.exe or .msi or .rpm or .deb file. Just copy jmurmel.jar
and (optional but recommended) mlib.lisp
somewhere convenient,
and maybe create a batchfile along these lines (Windows .cmd-style shown here):
jm.cmd:
--- snip ---
@echo off
setlocal
set JAVA_HOME=C:\Apps\Java\X64\jdk8u252-b09
set JMURMEL=C:\jmurmel\lambda\target\jmurmel.jar
%JAVA_HOME%\bin\java -jar %JMURMEL% %*
endlocal
--- snip ---
or setup an alias (again Windows style shown here):
set JAVA_HOME=c:\Program files\jdk-17.0.2
set JAVA_OPTS=-Xms200M -Xmx1G -Xss2m -XX:+UseZGC
doskey jm="%JAVA_HOME%\bin\java" %JAVA_OPTS% -jar C:\jmurmel\lambda\target\jmurmel.jar --libdir C:\jmurmel\samples.murmel-mlib $*
That way to run e.g. the file hanoi.lisp
you can use the following command:
C:\> jm hanoi.lisp
or if you want to interpret a file and then go into REPL:
C:\> jm --repl hanoi.lisp
Or use JMurmel from within Emacs: add (setq inferior-lisp-program "jm --tty")
to your .emacs
file and do M-x (run-lisp)
and use C-M-x
to eval Murmel S-expressions.
Or see HACKING for how to use rlwrap to get a command history and TAB-completion at the JMurmel REPL.
JMurmel can also be used embedded in another Java program. JMurmel uses Java8 only, but should run on higher versions as well (Github CI builds and tests 8, 17, 20 and 21-ea). It comes as one self contained jar, no further dependencies needed.
Minimal "Hello, World!" example:
@Test
public void testMinimal() {
Object result = new LambdaJ()
.interpretExpression(new StringReader("(cons 'Hello\\,\\ World! nil)")::read, s -> {});
assertEquals("(Hello, World!)", result.toString());
}
Slightly more advanced example:
@Test
public void testCons() {
// run a Lisp program
LambdaJ interpreter = new LambdaJ();
StringBuffer program = new StringReader("(cons 'a 'b)")
StringBuffer output = new StringBuffer();
Object result = interpreter.interpretExpression(program::read, output::append);
// done, that was it!
// check results
assertEquals("(a . b)", result.toString());
assertEquals(0, out.length());
assertTrue(result instanceof LambdaJ.ConsCell); // type of result depends on the Lisp program
// could be String, Double or ConsCell (i.e. list)
LambdaJ.ConsCell list = (LambdaJ.ConsCell)result;
String s = "";
for (Object car: list) { // the iterator will return subsequent car
// and - for dotted lists - the cdr of the last cons cell
s += car.toString();
}
assertEquals("ab", s);
}
Java calls JMurmel function:
@Test
public void testCallMurmelFromJava() {
LambdaJ interp = new LambdaJ();
interp.interpretExpression(new StringReader("(defun f (p1 p2) (* p1 p2))")::read, s -> {});
MurmelFunction add = interp.getFunction("f");
Object result = add.apply(2, 3);
assertEquals(6.0, result);
}
See EmbeddedTest.java
or FFITest.java
for more embedded use examples
including an example of how to hook up your own Lisp primitives written in Java.
Or see JSR223Test.java
for an example on how to use JMurmel through the
Java Scripting API
(setting/ accessing Java objects via JSR223 from Murmel code is not supported yet.)
Or use jrunscript
to use JMurmel via JSR223:
C:\robert\jmurmel>jrunscript -cp lambda\target\jmurmel.jar -l Murmel -f samples.murmel/fizzbuzz.lisp
1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, Fizz Buzz, 16, 17, Fizz, 19, Buzz, Fizz,
22, 23, Fizz, Buzz, 26, Fizz, 28, 29, Fizz Buzz, 31, 32, Fizz, 34, Buzz, Fizz, 37, 38, Fizz, Buzz, 41,
Fizz, 43, 44, Fizz Buzz, 46, 47, Fizz, 49, Buzz, Fizz, 52, 53, Fizz, Buzz, 56, Fizz, 58, 59, Fizz Buzz,
61, 62, Fizz, 64, Buzz, Fizz, 67, 68, Fizz, Buzz, 71, Fizz, 73, 74, Fizz Buzz, 76, 77, Fizz, 79, Buzz,
Fizz, 82, 83, Fizz, Buzz, 86, Fizz, 88, 89, Fizz Buzz, 91, 92, Fizz, 94, Buzz, Fizz, 97, 98, Fizz, Buzz
Or use jshell
to interactively explore JMurmel's API,
i.e. invoke JMurmel's Java API from Java's "REPL".
A sample jshell
session creating an interpreter,
defining Murmel functions,
calling Murmel functions from Java
and using Java to load
Murmel source files into the interpreter
is given below:
C:\jmurmel>jshell --class-path lambda\target\jmurmel.jar
| Welcome to JShell -- Version 17.0.2
| For an introduction type: /help intro
jshell> import io.github.jmurmel.*
jshell> LambdaJ intp = new LambdaJ()
intp ==> io.github.jmurmel.LambdaJ@67b6d4ae
jshell> LambdaJ.ReadSupplier in = () -> -1;
in ==> $Lambda$122/0x0000000800c3c000@436e852b
jshell> LambdaJ.WriteConsumer out = System.out::print
out ==> $Lambda$123/0x0000000800c3c418@3e57cd70
jshell> String lisp = "(defun add (n m) (+ n m)) (add 2 3)"
lisp ==> "(defun add (n m) (+ n m)) (add 2 3)"
jshell> Object result = intp.interpretExpressions(new StringReader(lisp)::read, in, out)
result ==> 5.0
jshell> LambdaJ.MurmelFunction add = intp.getFunction("add")
add ==> io.github.jmurmel.LambdaJ$CallLambda@77ec78b9
jshell> result = add.apply(1, 2)
result ==> 3.0
jshell> lisp = "(load \"samples.murmel/fibonacci.lisp\")"
lisp ==> "(load \"samples.murmel/fibonacci.lisp\")"
jshell> Object result = intp.interpretExpressions(new StringReader(lisp)::read, in, out)
recursive-fibonacci 25: 75025.0, iterative-fibonacci 25: 75025.0, matrix-fibonacci 25: 75025.0
result ==> null
jshell> LambdaJ.MurmelFunction fib = intp.getFunction("matrix-fibonacci")
fib ==> io.github.jmurmel.LambdaJ$CallLambda@1786f9d5
jshell> fib.apply(25)
$28 ==> 75025.0
jshell> LambdaJ.main(new String[] { "--tty"})
Enter a Murmel form or :command (or enter :h for command help or :q to exit):
JMurmel> (* 1 2 3)
==> 6.0
JMurmel> :q
bye.
jshell>
Or use jshell
to interactively explore the Java Scripting API (defined by JSR 223)
with JMurmel as a scripting engine (sample session shown using Debian running in WSL2):
robert@ROBERT-DELL:/mnt/c/robert/jmurmel/lambda/target$ jshell --module-path jmurmel.jar --add-modules io.github.jmurmel
| Welcome to JShell -- Version 11.0.12
| For an introduction type: /help intro
jshell> import javax.script.*;
jshell> ScriptEngineManager manager = new ScriptEngineManager();
...> ScriptEngine engine = manager.getEngineByName("jmurmel");
manager ==> javax.script.ScriptEngineManager@234bef66
engine ==> io.github.jmurmel.jsr223.JMurmelScriptEngine@5056dfcb
jshell> engine.eval("(writeln \"Hello, World!\" nil)")
Hello, World!
$4 ==> "Hello, World!"
jshell> engine.eval(new java.io.FileReader("../../samples.murmel/fizzbuzz.lisp"));
1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, Fizz Buzz, 16, 17, Fizz, 19, Buzz, Fizz,
22, 23, Fizz, Buzz, 26, Fizz, 28, 29, Fizz Buzz, 31, 32, Fizz, 34, Buzz, Fizz, 37, 38, Fizz, Buzz, 41,
Fizz, 43, 44, Fizz Buzz, 46, 47, Fizz, 49, Buzz, Fizz, 52, 53, Fizz, Buzz, 56, Fizz, 58, 59, Fizz Buzz,
61, 62, Fizz, 64, Buzz, Fizz, 67, 68, Fizz, Buzz, 71, Fizz, 73, 74, Fizz Buzz, 76, 77, Fizz, 79, Buzz,
Fizz, 82, 83, Fizz, Buzz, 86, Fizz, 88, 89, Fizz Buzz, 91, 92, Fizz, 94, Buzz, Fizz, 97, 98, Fizz, Buzz$5 ==> null
jshell>
A Quine
JMurmel> ((lambda (x) (list x (list (quote quote) x))) (quote (lambda (x) (list x (list (quote quote) x)))))
==> ((lambda (x) (list x (list (quote quote) x))) (quote (lambda (x) (list x (list (quote quote) x)))))
JMurmel>
write, jformat
JMurmel> (write (jformat nil "%s, World!%n" "Hello") nil)
Hello, World!
==> "Hello, World!
"
Tail recursion, locale dependent number formatting
JMurmel> (labels ((factTR (n a)
(if (= n 0)
a
(factTR (- n 1) (* n a)))))
(write (jformat-locale nil "en-US" "Factorial of 50 is %g" (factTR 50 1))))
"Factorial of 50 is 3.04141e+64"
==> t
Graphical "Hello, World!" with Java FFI
JMurmel> ((jmethod "javax.swing.JOptionPane" "showMessageDialog" "java.awt.Component" "Object") nil "Hello, World!")
==> nil
See samples.murmel/ and samples.murmel-mlib/ for more Murmel example code including usage of Murmel's turtle graphics.
JMurmel supports the special forms:
-
quote, cond, if
-
lambda
... create a lexical closure -
labels
... define local functions -
define
... inserts a (symbol value) pair into the top level environment, returnsvalue
. E.g.(define *answer* 42)
- or
(define print-answer (lambda () (write (jformat nil "%2.2g" *answer*))))
-
defun
...(<symbol> (<params>*) <bodyform>*)
, e.g.(defun addone (n) (+ n 1))
-
let, let <symbol>
(i.e. named let), let*, letrec, progn
-
setq
-
defmacro
,macrolet
... define global and local macros -
throw, catch, unwind-protect
-
multiple-value-call, multiple-value-bind
-
declaim, load, require, provide
The environment contains the symbols nil
, t
, pi
, *command-line-argument-list*
,
*condition-handler*
, *features*
, *random-state*
and the functions
-
cons, car, cdr, rplaca, rplacd
-
apply
... works similar to Scheme, e.g.(apply + '(1 2 3))
or(apply + (cons 2 (cons 3 nil)))
-
eval, eq, eql, null, atom
-
consp, listp, functionp, floatp, integerp, numberp, characterp, stringp, symbolp, vectorp, random-state-p
-
assq, assoc, append, list, list*
-
vector, svref, svset, svlength, make-array, simple-vector->list, list->simple-vector
-
read, write, writeln
-
=, <, <=, >, >=, /=, +, -, *, /, 1+, 1-
-
sqrt, log, log10, exp, expt, signum, mod, rem
-
ceiling, floor, round, truncate, fceiling, ffloor, fround, ftruncate
-
char-code, code-char, string=, string->list, list->string
-
values, gensym, macroexpand-1, trace, untrace, random, make-random-state
-
...
For more functions (including bitvectors, hashtables, turtle graphics and functions to call Java) and more details on the language supported see murmel-langref.md.
Tail calls including tail recursive calls are optimized.
Lambdas can be dynamic or lexical, this can be selected via a keyword parameter to lambda
.
Variables and functions share one namespace.
Math operators (except 1+, 1-
) are implemented using Java operators for double.
This is probably different to Common Lisp
esp. around +/-NaN, +/-Infinity, division by zero and so on.
E.g. the S-expression (/ 1 -0)
is valid and will yield -Infinity
.
The data types currently supported are symbols, pairs (i.e. lists), numbers (represented as Java Double or Long), characters, vectors (character vectors aka strings, bitvectors and vectors that can contain any data) and hashtables. String literals are 2000 chars max length.
Most of these features can be disabled using commandline arguments.
You can disable pretty much everything except S-expressions, symbols, cons cells and lambda.
If you want to experiment with a bare-bones-Lisp use --help-features
for details.
E.g. use
java -jar jmurmel.jar --XX-dyn --XX-oldlambda --min+
to get a REPL with a Lisp that is pretty much the same as John McCarthy's original LISP as described in paper Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I, except: McCarthy's paper describes code in M-expressions while Murmel read S-expressions instead of M-expressions, as did the first implementation of LISP by Steve Russell on an IBM 704 computer using punched cards.
Example session with pure LISP:
C:\jmurmel\>java -jar jmurmel.jar --XX-dyn --min+ --XX-oldlambda
Enter a Murmel form or :command (or enter :h for command help or :q to exit):
JMurmel> ;; FIND FIRST ATOM IN TREE
;; CORRECT RESULT OF EXPRESSION IS `A`
;; RECURSIVE CONDITIONAL FUNCTION BINDING
((LAMBDA (FF X) (FF X))
(QUOTE (LAMBDA (X)
(COND ((ATOM X) X)
((QUOTE T) (FF (CAR X))))))
(QUOTE ((A) B C)))
==> A
JMurmel> ;; LISP IMPLEMENTED IN LISP
;; WITHOUT ANY SUBJECTIVE SYNTACTIC SUGAR
;; RUNS "FIND FIRST ATOM IN TREE" PROGRAM
;; CORRECT RESULT OF EXPRESSION IS STILL `A`
;; REQUIRES CONS CAR CDR QUOTE ATOM EQ LAMBDA COND
;; SIMPLIFIED BUG FIXED VERSION OF JOHN MCCARTHY PAPER
;; NOTE: ((EQ (CAR E) ()) (QUOTE *UNDEFINED)) CAN HELP
;; NOTE: ((EQ (CAR E) (QUOTE LAMBDA)) E) IS NICE
((LAMBDA (ASSOC EVCON PAIRLIS EVLIS APPLY EVAL)
(EVAL (QUOTE ((LAMBDA (FF X) (FF X))
(QUOTE (LAMBDA (X)
(COND ((ATOM X) X)
((QUOTE T) (FF (CAR X))))))
(QUOTE ((A) B C))))
()))
(QUOTE (LAMBDA (X Y)
(COND ((EQ Y ()) ())
((EQ X (CAR (CAR Y)))
(CDR (CAR Y)))
((QUOTE T)
(ASSOC X (CDR Y))))))
(QUOTE (LAMBDA (C A)
(COND ((EVAL (CAR (CAR C)) A)
(EVAL (CAR (CDR (CAR C))) A))
((QUOTE T) (EVCON (CDR C) A)))))
(QUOTE (LAMBDA (X Y A)
(COND ((EQ X ()) A)
((QUOTE T) (CONS (CONS (CAR X) (CAR Y))
(PAIRLIS (CDR X) (CDR Y) A))))))
(QUOTE (LAMBDA (M A)
(COND ((EQ M ()) ())
((QUOTE T) (CONS (EVAL (CAR M) A)
(EVLIS (CDR M) A))))))
(QUOTE (LAMBDA (FN X A)
(COND
((ATOM FN)
(COND ((EQ FN (QUOTE CAR)) (CAR (CAR X)))
((EQ FN (QUOTE CDR)) (CDR (CAR X)))
((EQ FN (QUOTE ATOM)) (ATOM (CAR X)))
((EQ FN (QUOTE CONS)) (CONS (CAR X) (CAR (CDR X))))
((EQ FN (QUOTE EQ)) (EQ (CAR X) (CAR (CDR X))))
((QUOTE T) (APPLY (EVAL FN A) X A))))
((EQ (CAR FN) (QUOTE LAMBDA))
(EVAL (CAR (CDR (CDR FN)))
(PAIRLIS (CAR (CDR FN)) X A))))))
(QUOTE (LAMBDA (E A)
(COND
((ATOM E) (ASSOC E A))
((ATOM (CAR E))
(COND ((EQ (CAR E) (QUOTE QUOTE)) (CAR (CDR E)))
((EQ (CAR E) (QUOTE COND)) (EVCON (CDR E) A))
((QUOTE T) (APPLY (CAR E) (EVLIS (CDR E) A) A))))
((QUOTE T) (APPLY (CAR E) (EVLIS (CDR E) A) A))))))
==> A
JMurmel>
(The code above was stolen from sectorlisp's Lisp challenge.)
JMurmel comes with:
- a parser that reads S-expressions composed of symbols, lists, doubles, longs, characters, vectors, hashtables and strings.
- math support for Longs and Doubles
- simple console and file I/O
- some support for strings, vectors and hashtables
However:
You could substitute the default parser for reading data and/ or programs by your own parsers that reads e.g. XML or JSON instead of S-expressions, and/ or support additional data types.
See SerializeTest
: this uses Java's serialization to deserialize Lisp objects as forms from a stream
and feeds then to the interpreter instead of parsing S-expressions.
You could extend LambdaJ's environment with your own builtin functions and/ or data types e.g. for supporting BigDecimals or reading/ writing from/ to a JDBC Datasource.
See EmbeddedTest#testCustomEnv()
for an example.
Additional datatypes can be supported in your custom Parser, in your custom primitives or in both.
JMurmel is based on values of the Java type Object. It will see your custom datatypes as atoms and should handle them without any need for change.