├── gradle.properties ├── .gitignore ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── main │ ├── java │ │ └── org │ │ │ └── toylisp │ │ │ ├── IMacro.java │ │ │ ├── IFunc.java │ │ │ ├── Macro.java │ │ │ ├── Symbol.java │ │ │ ├── Env.java │ │ │ ├── Func.java │ │ │ ├── Cons.java │ │ │ ├── Main.java │ │ │ ├── Reader.java │ │ │ └── Runtime.java │ └── resources │ │ └── core.lisp └── test │ └── java │ └── org │ └── toylisp │ ├── TestUtil.java │ ├── ConsTests.java │ ├── RuntimeTests.java │ └── ReaderTests.java ├── examples └── map.lisp ├── README.md ├── gradlew.bat └── gradlew /gradle.properties: -------------------------------------------------------------------------------- 1 | version=0.1.0-SNAPSHOT -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .idea 3 | *.iml 4 | build 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerrypnz/toylisp4j/master/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/java/org/toylisp/IMacro.java: -------------------------------------------------------------------------------- 1 | package org.toylisp; 2 | 3 | /** 4 | * Marker interface for macros. 5 | * @author jerry created 18/02/15 6 | */ 7 | public interface IMacro {} 8 | -------------------------------------------------------------------------------- /src/main/java/org/toylisp/IFunc.java: -------------------------------------------------------------------------------- 1 | package org.toylisp; 2 | 3 | /** 4 | * @author jerry created 14/11/29 5 | */ 6 | public interface IFunc { 7 | 8 | Object invoke(Object... args); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Nov 30 10:45:12 CST 2014 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.1-bin.zip 7 | -------------------------------------------------------------------------------- /examples/map.lisp: -------------------------------------------------------------------------------- 1 | ; Example map function. 2 | (def map 3 | (lambda (f x) 4 | (cond 5 | x (cons (f (car x)) (map f (cdr x))) 6 | t nil))) 7 | 8 | ; Double 9 | (def double (lambda (n) (* n 2))) 10 | 11 | ; For testing comment 12 | (prn "Double: " (map double '(0 1 2 3 4 5 6))) 13 | -------------------------------------------------------------------------------- /src/main/java/org/toylisp/Macro.java: -------------------------------------------------------------------------------- 1 | package org.toylisp; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Macro is essentially a function with an IMacro marker
7 | * 8 | * @author jerry created 18/02/15 9 | */ 10 | public class Macro extends Func implements IMacro { 11 | 12 | public Macro(List argNames, Object body, Env env) { 13 | super(argNames, body, env); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/org/toylisp/TestUtil.java: -------------------------------------------------------------------------------- 1 | package org.toylisp; 2 | 3 | import java.util.Arrays; 4 | 5 | /** 6 | * TestUtil
7 | * 8 | * @author jerry created 14/11/29 9 | */ 10 | public class TestUtil { 11 | 12 | /** 13 | * Short method to easily construct cons cells 14 | */ 15 | public static Cons _(Object... args) { 16 | return Cons.fromList(Arrays.asList(args)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/org/toylisp/Symbol.java: -------------------------------------------------------------------------------- 1 | package org.toylisp; 2 | 3 | import java.util.concurrent.ConcurrentHashMap; 4 | import java.util.concurrent.ConcurrentMap; 5 | 6 | /** 7 | * Symbol
8 | * 9 | * @author jerry created 14/11/26 10 | */ 11 | public class Symbol { 12 | 13 | private static ConcurrentMap allSymbols = new ConcurrentHashMap<>(128); 14 | 15 | private final String name; 16 | 17 | private Symbol(String name) {this.name = name;} 18 | 19 | public static Symbol intern(String name) { 20 | Symbol sym = allSymbols.get(name); 21 | if (sym == null) { 22 | Symbol newSymbol = new Symbol(name); 23 | if ((sym = allSymbols.putIfAbsent(name, newSymbol)) == null) { 24 | sym = newSymbol; 25 | } 26 | } 27 | return sym; 28 | } 29 | 30 | @Override 31 | public String toString() { 32 | return name; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/resources/core.lisp: -------------------------------------------------------------------------------- 1 | (defmacro defun (name args & body) 2 | `(def ,name (lambda ,args ,@body))) 3 | 4 | (defmacro if (pred then else) 5 | `(cond 6 | (,pred ,then) 7 | (t ,else))) 8 | 9 | (defmacro when (pred & body) 10 | `(cond 11 | (,pred (do ,@body)))) 12 | 13 | (defun cadr (lst) (car (cdr lst))) 14 | (defun caar (lst) (car (car lst))) 15 | (defun cddr (lst) (cdr (cdr lst))) 16 | 17 | (defun caddr (lst) (car (cdr (cdr lst)))) 18 | (defun caadr (lst) (car (car (cdr lst)))) 19 | (defun cadar (lst) (car (cdr (car lst)))) 20 | 21 | (defun map (f x) 22 | (cond 23 | (x (cons (f (car x)) (map f (cdr x)))) 24 | (t nil))) 25 | 26 | (defmacro let (bindings & body) 27 | `((lambda ,(map car bindings) 28 | ,@body) 29 | ,@(map cadr bindings))) 30 | 31 | (defun -let-helper (bindings body) 32 | (if bindings 33 | `(((lambda (,(caar bindings)) 34 | ,@(-let-helper (cdr bindings) body)) 35 | ,(cadar bindings))) 36 | body)) 37 | 38 | (defmacro let* (bindings & body) 39 | (car (-let-helper bindings body))) 40 | -------------------------------------------------------------------------------- /src/main/java/org/toylisp/Env.java: -------------------------------------------------------------------------------- 1 | package org.toylisp; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | /** 7 | * Environment
8 | * 9 | * @author jerry created 14/11/26 10 | */ 11 | public class Env { 12 | 13 | private final Env parent; 14 | private final Map bindings = new HashMap<>(); 15 | 16 | private Env(Env parent) {this.parent = parent;} 17 | 18 | public static Env createRoot() { 19 | return new Env(null); 20 | } 21 | 22 | public Env push() { 23 | return new Env(this); 24 | } 25 | 26 | public Env pop() { 27 | return this.parent; 28 | } 29 | 30 | public Env set(Symbol name, Object val) { 31 | bindings.put(name, val); 32 | return this; 33 | } 34 | 35 | public Object get(Symbol name) { 36 | if (bindings.containsKey(name)) { 37 | return bindings.get(name); 38 | } else if (parent != null) { 39 | return parent.get(name); 40 | } 41 | throw new IllegalStateException("No value bound to symbol " + name); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/org/toylisp/ConsTests.java: -------------------------------------------------------------------------------- 1 | package org.toylisp; 2 | 3 | import java.util.Arrays; 4 | import org.junit.Test; 5 | 6 | import static org.junit.Assert.*; 7 | import static org.toylisp.Runtime.cons; 8 | import static org.toylisp.TestUtil._; 9 | 10 | public class ConsTests { 11 | 12 | @Test 13 | public void testConsToString() { 14 | Cons test = cons(cons("a", cons("b", null)), cons(1, cons(2, cons(3, null)))); 15 | assertEquals("((a b) 1 2 3)", test.toString()); 16 | } 17 | 18 | @Test 19 | public void testToList() { 20 | Cons cons = _("1", "2", "3", 4, 5, 6); 21 | assertEquals(Arrays.asList("1", "2", "3", 4, 5, 6), cons.toList()); 22 | cons = _("1", "2", null); 23 | assertEquals(Arrays.asList("1", "2", null), cons.toList()); 24 | } 25 | 26 | @Test 27 | public void testFromList() { 28 | Cons test = cons("a", cons(null, null)); 29 | assertEquals(test, Cons.fromList(Arrays.asList((Object)"a", null))); 30 | System.out.println(test); 31 | } 32 | 33 | @Test 34 | public void testConcat() { 35 | Cons expected = cons("a", cons("b", cons("c", cons("d", null)))); 36 | assertEquals(expected, Cons.concat(cons("a", cons("b", null)), 37 | cons("c", cons("d", null)))); 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # toylisp 2 | 3 | Very simple and basic Lisp interpreter written in Java 4 | 5 | ## Build 6 | 7 | You need JDK 1.7+ to build toylisp. 8 | 9 | Go to the project directory from a terminal, and type the following command 10 | to build: 11 | 12 | ``` 13 | ./gradlew build 14 | ``` 15 | 16 | If you are running Windows, use the following command instead: 17 | 18 | ``` 19 | gradlew.bat build 20 | ``` 21 | 22 | If everything goes well, you will find a jar under `build/libs` named `toylist-0.1.0-SNAPSHOT.jar`. 23 | 24 | ## Usage 25 | 26 | Run the main class to get a REPL and you are free to play with it: 27 | 28 | ``` 29 | java -cp build/libs/toylisp-0.1.0-SNAPSHOT.jar org.toylisp.Main 30 | ``` 31 | 32 | Example REPL session: 33 | 34 | ```lisp 35 | toylisp> (defun double (n) (* n 2)) 36 | org.toylisp.Func@33a17727 37 | toylisp> (map double '(0 1 2 3 4 5 6)) 38 | (0 2 4 6 8 10 12) 39 | ``` 40 | 41 | ## Data Types 42 | 43 | Currently only three: 44 | 45 | - Symbol 46 | - String 47 | - Number (implemented with Java `BigDecimal`) 48 | 49 | ## Operations Supported 50 | Currently only the following operators are supported: 51 | 52 | - Special forms: 53 | - `def` Define a global variable 54 | - `defun` Define a function 55 | - `let` local binding 56 | - `lambda` Define a function 57 | - `do` Execute forms in sequence and return the value of the last expression 58 | - `cond` Conditional expression. 59 | - `quote` 60 | - `if` 61 | - Functions: `cons`, `car`, `cdr`, `+`, `-`, `*`, `/`, `eq?` 62 | 63 | 64 | ## TODO 65 | 66 | Here is a list of features I'm planning to implement: 67 | 68 | - core library written in toylisp itself, including but not limited to the following operations: 69 | - `when`, `when-let`, `if-let`, `->`, `->>` (implemented with macros) 70 | - `mapc`, `mapcat`, `cat` and other list manipulation functions 71 | - a metacircular interpreter implemented with toylisp itself 72 | 73 | ## License 74 | 75 | Copyright © 2014 Jerry Peng 76 | 77 | Distributed under the Eclipse Public License, the same as Clojure. 78 | 79 | -------------------------------------------------------------------------------- /src/main/java/org/toylisp/Func.java: -------------------------------------------------------------------------------- 1 | package org.toylisp; 2 | 3 | import java.util.List; 4 | 5 | import static java.util.Arrays.asList; 6 | 7 | /** 8 | * Func
9 | * 10 | * @author jerry created 14/11/26 11 | */ 12 | public class Func implements IFunc { 13 | 14 | static final Symbol AND = Symbol.intern("&"); 15 | 16 | private final List argNames; 17 | private final Symbol restArgsName; 18 | private final Object body; 19 | private final Env closureEnv; 20 | 21 | public Func(List argNames, Object body, Env env) { 22 | Symbol restArgsName = null; 23 | int i = argNames.indexOf(AND); 24 | if (i >= 0) { 25 | // There can only be one rest arg 26 | // eg. (bindings & body) is valid, (bindings & body1 body2) is not. 27 | if (i != argNames.size() - 2) { 28 | throw new IllegalArgumentException("Invalid rest arg declaration"); 29 | } 30 | restArgsName = argNames.get(i + 1); 31 | argNames = argNames.subList(0, i); 32 | } 33 | this.argNames = argNames; 34 | this.restArgsName = restArgsName; 35 | this.body = body; 36 | this.closureEnv = env; 37 | } 38 | 39 | @Override 40 | public Object invoke(Object... args) { 41 | Env newEnv = closureEnv.push(); 42 | if (args.length < argNames.size()) { 43 | throw new IllegalArgumentException("Wrong arity: " + 44 | argNames.size() + 45 | " args expected, " + 46 | args.length + 47 | " given."); 48 | } 49 | 50 | for (int i = 0; i < argNames.size(); i++) { 51 | newEnv.set(argNames.get(i), args[i]); 52 | } 53 | 54 | if (restArgsName != null) { 55 | newEnv.set(restArgsName, Cons.fromList(asList(args).subList(argNames.size(), args.length))); 56 | } 57 | return Runtime.eval(body, newEnv); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /src/main/java/org/toylisp/Cons.java: -------------------------------------------------------------------------------- 1 | package org.toylisp; 2 | 3 | import java.util.LinkedList; 4 | import java.util.List; 5 | 6 | /** 7 | * Cons
8 | * 9 | * @author jerry created 14/11/26 10 | */ 11 | public class Cons { 12 | 13 | private final Object car; 14 | private final Object cdr; 15 | 16 | public Cons(Object car, Object cdr) { 17 | this.car = car; 18 | this.cdr = cdr; 19 | } 20 | 21 | public static Cons fromList(List list) { 22 | Cons tail = null; 23 | for (int i = list.size() - 1; i >= 0; i--) { 24 | tail = new Cons(list.get(i), tail); 25 | } 26 | return tail; 27 | } 28 | 29 | public static Cons concat(Object... args) { 30 | Cons tail = null; 31 | for (int i = args.length - 1; i >= 0; i--) { 32 | Object obj = args[i]; 33 | tail = concat((Cons) obj, tail); 34 | } 35 | return tail; 36 | } 37 | 38 | public static Cons concat(Cons head, Cons tail) { 39 | LinkedList objs = new LinkedList<>(); 40 | while (head != null) { 41 | objs.push(head.car); 42 | head = ((Cons) head.cdr); 43 | } 44 | while (!objs.isEmpty()) { 45 | tail = new Cons(objs.pop(), tail); 46 | } 47 | return tail; 48 | } 49 | 50 | public List toList() { 51 | Cons tail = this; 52 | List list = new LinkedList<>(); 53 | while (tail != null) { 54 | list.add(tail.car); 55 | tail = ((Cons) tail.cdr()); 56 | } 57 | return list; 58 | } 59 | 60 | public Object car() { 61 | return car; 62 | } 63 | 64 | public Object cdr() { 65 | return cdr; 66 | } 67 | 68 | @Override 69 | public boolean equals(Object o) { 70 | if (this == o) 71 | return true; 72 | if (o == null || getClass() != o.getClass()) 73 | return false; 74 | 75 | Cons cons = (Cons) o; 76 | 77 | return !(car != null ? !car.equals(cons.car) : cons.car != null) && 78 | !(cdr != null ? !cdr.equals(cons.cdr) : cons.cdr != null); 79 | 80 | } 81 | 82 | @Override 83 | public int hashCode() { 84 | int result = car != null ? car.hashCode() : 0; 85 | result = 31 * result + (cdr != null ? cdr.hashCode() : 0); 86 | return result; 87 | } 88 | 89 | private void appendToStr(StringBuilder buf) { 90 | buf.append(car.toString()); 91 | Object tail = cdr; 92 | while (tail != null) { 93 | buf.append(' '); 94 | if (tail instanceof Cons) { 95 | Cons cons = (Cons) tail; 96 | buf.append(cons.car()); 97 | tail = cons.cdr(); 98 | } else { 99 | buf.append(tail.toString()); 100 | tail = null; 101 | } 102 | } 103 | } 104 | 105 | @Override 106 | public String toString() { 107 | StringBuilder buf = new StringBuilder(32); 108 | buf.append('('); 109 | appendToStr(buf); 110 | buf.append(')'); 111 | return buf.toString(); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/org/toylisp/Main.java: -------------------------------------------------------------------------------- 1 | package org.toylisp; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.ByteArrayOutputStream; 5 | import java.io.FileInputStream; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.InputStreamReader; 9 | import java.nio.charset.Charset; 10 | import java.util.List; 11 | 12 | /** 13 | * Main
14 | * 15 | * @author jerry created 14/11/30 16 | */ 17 | public class Main { 18 | 19 | public static void runREPL() throws IOException { 20 | Env rootEnv = Runtime.getRootEnv(); 21 | BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); 22 | for (; ; ) { 23 | System.out.print("toylisp> "); 24 | System.out.flush(); 25 | String line = reader.readLine(); 26 | if (line == null) { 27 | return; 28 | } 29 | List forms; 30 | try { 31 | forms = Reader.read(line); 32 | } catch (Exception e) { 33 | e.printStackTrace(); 34 | continue; 35 | } 36 | 37 | for (Object form : forms) { 38 | Object ret = null; 39 | try { 40 | ret = Runtime.eval(form, rootEnv); 41 | } catch (Exception e) { 42 | e.printStackTrace(); 43 | } 44 | 45 | System.out.println(ret); 46 | } 47 | } 48 | } 49 | 50 | public static void runFile(String fileName, String encoding) throws IOException { 51 | String code; 52 | try (FileInputStream input = new FileInputStream(fileName)) { 53 | code = readCode(input, encoding); 54 | } 55 | runCode(code); 56 | } 57 | 58 | private static void runCode(String code) { 59 | List forms = Reader.read(code); 60 | for (Object form : forms) { 61 | Runtime.eval(form, Runtime.getRootEnv()); 62 | } 63 | } 64 | 65 | private static String readCode(InputStream input, String encoding) throws IOException {ByteArrayOutputStream codeBuf = new ByteArrayOutputStream(4096); 66 | byte[] buf = new byte[2048]; 67 | int len; 68 | while ((len = input.read(buf)) > 0) { 69 | codeBuf.write(buf, 0, len); 70 | } 71 | 72 | return codeBuf.toString(encoding); 73 | } 74 | 75 | private static void loadLib(String classpath) throws IOException { 76 | InputStream ins = Main.class.getClassLoader().getResourceAsStream(classpath); 77 | if (ins == null) { 78 | throw new IllegalStateException("Unable to load library from classpath: " + classpath); 79 | } 80 | 81 | String code; 82 | try { 83 | code = readCode(ins, "UTF-8"); 84 | } finally { 85 | ins.close(); 86 | } 87 | runCode(code); 88 | } 89 | 90 | public static void main(String[] args) throws IOException { 91 | loadLib("core.lisp"); 92 | 93 | if (args.length == 0) { 94 | runREPL(); 95 | } else { 96 | String fileName = args[0]; 97 | if (fileName == null || fileName.isEmpty()) { 98 | runREPL(); 99 | } else { 100 | runFile(fileName, Charset.defaultCharset().name()); 101 | } 102 | } 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/test/java/org/toylisp/RuntimeTests.java: -------------------------------------------------------------------------------- 1 | package org.toylisp; 2 | 3 | import java.util.Arrays; 4 | import java.util.Collections; 5 | import org.junit.Test; 6 | 7 | import static org.junit.Assert.*; 8 | import static org.toylisp.TestUtil._; 9 | 10 | public class RuntimeTests { 11 | 12 | @Test 13 | public void testBool() { 14 | assertTrue(Runtime.bool("foo")); 15 | assertTrue(Runtime.bool(new Object())); 16 | assertTrue(Runtime.bool(1)); 17 | assertTrue(Runtime.bool(0)); 18 | assertFalse(Runtime.bool(null)); 19 | assertFalse(Runtime.bool(false)); 20 | assertFalse(Runtime.bool(Boolean.FALSE)); 21 | } 22 | 23 | @Test 24 | public void testEvalSymbol() { 25 | Integer val = 409600; 26 | Symbol a = Symbol.intern("a"); 27 | Env env = Env.createRoot().set(a, val); 28 | assertEquals(val, Runtime.eval(a, env)); 29 | } 30 | 31 | @Test 32 | public void testEvalOtherAtoms() { 33 | Integer val = 409600; 34 | String foo = "foobar"; 35 | Env env = Env.createRoot(); 36 | assertEquals(val, Runtime.eval(val, env)); 37 | assertEquals(foo, Runtime.eval(foo, env)); 38 | } 39 | 40 | @Test 41 | public void testFunctionCall() { 42 | Symbol identity = Symbol.intern("identity"); 43 | Symbol arg = Symbol.intern("arg"); 44 | Env env = Env.createRoot(); 45 | Env funcEnv = env.push(); 46 | IFunc identityFunc = new Func(Arrays.asList(arg), arg, funcEnv); 47 | 48 | env.set(identity, identityFunc); 49 | 50 | assertEquals("foobar", Runtime.eval(_(identity, "foobar"), env)); 51 | assertEquals(42, Runtime.eval(_(identity, 42), env)); 52 | } 53 | 54 | @Test 55 | public void testClosure() { 56 | Symbol foo = Symbol.intern("foo"); 57 | Symbol v = Symbol.intern("v"); 58 | Env env = Env.createRoot(); 59 | Env funcEnv = env.push(); 60 | funcEnv.set(v, "hello world"); 61 | IFunc closure = new Func(Collections.emptyList(), v, funcEnv); 62 | 63 | env.set(foo, closure); 64 | 65 | assertEquals("hello world", Runtime.eval(_(foo), env)); 66 | } 67 | 68 | @Test 69 | public void testLambdaIdentity() { 70 | Symbol lambda = Symbol.intern("lambda"); 71 | Symbol arg = Symbol.intern("arg"); 72 | 73 | Env env = Runtime.getRootEnv(); 74 | Cons progn = _(_(lambda, 75 | _(arg), 76 | arg), 77 | "foobar"); 78 | assertEquals("foobar", Runtime.eval(progn, env)); 79 | } 80 | 81 | @Test 82 | public void testCarCdr() { 83 | Symbol car = Symbol.intern("car"); 84 | Symbol cdr = Symbol.intern("cdr"); 85 | Symbol quote = Symbol.intern("quote"); 86 | 87 | Cons data = _("foo", "bar"); 88 | 89 | Object expectedCar = data.car(); 90 | Object expectedCdr = data.cdr(); 91 | 92 | Env env = Runtime.getRootEnv(); 93 | 94 | assertEquals(expectedCar, Runtime.eval(_(car, _(quote, data)), env)); 95 | assertEquals(expectedCdr, Runtime.eval(_(cdr, _(quote, data)), env)); 96 | } 97 | 98 | 99 | @Test 100 | public void testLambdaCadr() { 101 | Symbol lambda = Symbol.intern("lambda"); 102 | Symbol arg = Symbol.intern("arg"); 103 | Symbol car = Symbol.intern("car"); 104 | Symbol cdr = Symbol.intern("cdr"); 105 | Symbol quote = Symbol.intern("quote"); 106 | 107 | Env env = Runtime.getRootEnv(); 108 | Cons code = _(_(lambda, _(arg), 109 | _(car, _(cdr, arg))), 110 | _(quote, _("foo", "bar"))); 111 | assertEquals("bar", Runtime.eval(code, env)); 112 | } 113 | 114 | @Test 115 | public void testCond() { 116 | Symbol lambda = Symbol.intern("lambda"); 117 | Symbol cond = Symbol.intern("cond"); 118 | Symbol eq = Symbol.intern("eq?"); 119 | Symbol quote = Symbol.intern("quote"); 120 | Symbol x = Symbol.intern("x"); 121 | Symbol t = Symbol.intern("t"); 122 | Symbol a = Symbol.intern("a"); 123 | Symbol b = Symbol.intern("b"); 124 | 125 | Env env = Runtime.getRootEnv(); 126 | 127 | Cons condFunc = _(lambda, _(x), 128 | _(cond, 129 | _(_(eq, x, _(quote, a)), "foo"), 130 | _(_(eq, x, _(quote, b)), "bar"), 131 | _(t, "oops"))); 132 | assertEquals("foo", Runtime.eval(_(condFunc, _(quote, a)), env)); 133 | assertEquals("bar", Runtime.eval(_(condFunc, _(quote, b)), env)); 134 | assertEquals("oops", Runtime.eval(_(condFunc, "nop"), env)); 135 | 136 | } 137 | 138 | @Test 139 | public void testCondWithNilValue() { 140 | Symbol cond = Symbol.intern("cond"); 141 | Symbol t = Symbol.intern("t"); 142 | Env env = Runtime.getRootEnv(); 143 | 144 | assertNull(Runtime.eval(_(cond, _(t, null)), env)); 145 | } 146 | 147 | } -------------------------------------------------------------------------------- /src/test/java/org/toylisp/ReaderTests.java: -------------------------------------------------------------------------------- 1 | package org.toylisp; 2 | 3 | import java.math.BigDecimal; 4 | import org.junit.Test; 5 | 6 | import static java.util.Arrays.asList; 7 | import static org.junit.Assert.assertEquals; 8 | import static org.toylisp.Reader.tokenize; 9 | import static org.toylisp.TestUtil._; 10 | 11 | public class ReaderTests { 12 | 13 | @Test 14 | public void testTokenize() throws Exception { 15 | // Basic 16 | assertEquals(asList("a"), tokenize("a")); 17 | assertEquals(asList("a", "b"), tokenize("a b")); 18 | assertEquals(asList("(", "a", ")"), tokenize("(a)")); 19 | assertEquals(asList("(", "a", "b", ")"), tokenize("(a b)")); 20 | 21 | // Strings 22 | assertEquals(asList("(", "a", "\"foo bar\"", ")"), tokenize("(a \"foo bar\")")); 23 | assertEquals(asList("(", "a", "\"foo bar \\\"hello\\\"\"", ")"), tokenize("(a \"foo bar \\\"hello\\\"\")")); 24 | 25 | // Quotes 26 | assertEquals(asList("'", "(", "a", "b", "c", ")"), tokenize("'(a b c)")); 27 | 28 | // Comments 29 | assertEquals(asList("(", "a", ")"), tokenize(";comment\r(a)")); 30 | assertEquals(asList("(", "a", ")"), tokenize(";comment\n(a)")); 31 | assertEquals(asList("(", "a", ")"), tokenize(";comment\r\n(a)")); 32 | assertEquals(asList("(", "a", "b", ")"), tokenize(";comment1\n;comment2\n(a b)")); 33 | 34 | // Backquote 35 | assertEquals(asList("`", "(", "a", "b", "c", ")"), tokenize("`(a b c)")); 36 | assertEquals(asList("`", "(", "a", ",", "b", "c", ")"), tokenize("`(a ,b c)")); 37 | assertEquals(asList("`", "(", "a", ",@", "b", "c", ")"), tokenize("`(a ,@b c)")); 38 | assertEquals(asList("`", "(", "a", ",@", "(", "b", "1", "2", ")", "c", ")"), tokenize("`(a ,@(b 1 2) c)")); 39 | } 40 | 41 | @Test 42 | public void testTokenToObject() throws Exception { 43 | assertEquals(new BigDecimal("14"), Reader.read("14", null, false)); 44 | assertEquals("14", Reader.read("\"14\"", null, false)); 45 | assertEquals(Symbol.intern("foo"), Reader.read("foo", null, false)); 46 | assertEquals("foo\r\ncol1\tcol2\t hello", Reader.read("\"foo\\r\\ncol1\\tcol2\\t hello\"", null, false)); 47 | } 48 | 49 | @Test 50 | public void testRead_NormalCases() throws Exception { 51 | Symbol foobar = Symbol.intern("foobar"); 52 | assertEquals(asList((Object)_(foobar, "1", "2")), Reader.read(asList("(", "foobar", "\"1\"", "\"2\"", ")"))); 53 | } 54 | 55 | @Test 56 | public void testRead_Quotes() throws Exception { 57 | Symbol quote = Symbol.intern("quote"); 58 | Symbol foobar = Symbol.intern("foobar"); 59 | assertEquals(asList((Object) _(quote, foobar)), Reader.read(asList("'", "foobar"))); 60 | } 61 | 62 | @Test(expected = IllegalArgumentException.class) 63 | public void testRead_UnmatchedParentheses1() throws Exception { 64 | Reader.read(asList("(")); 65 | } 66 | 67 | @Test(expected = IllegalArgumentException.class) 68 | public void testRead_UnmatchedParentheses2() throws Exception { 69 | Reader.read(asList(")")); 70 | } 71 | 72 | @Test(expected = IllegalArgumentException.class) 73 | public void testRead_UnmatchedParentheses3() throws Exception { 74 | Reader.read(asList("(", "foo", "bar")); 75 | } 76 | 77 | @Test(expected = IllegalArgumentException.class) 78 | public void testRead_UnmatchedParentheses4() throws Exception { 79 | Reader.read(asList("bar", "foo", ")")); 80 | } 81 | 82 | @Test 83 | public void testReadString() throws Exception { 84 | Symbol lambda = Symbol.intern("lambda"); 85 | Symbol quote = Symbol.intern("quote"); 86 | Symbol arg = Symbol.intern("arg"); 87 | Symbol car = Symbol.intern("car"); 88 | Symbol cdr = Symbol.intern("cdr"); 89 | Symbol a = Symbol.intern("a"); 90 | Symbol b = Symbol.intern("b"); 91 | Symbol c = Symbol.intern("c"); 92 | Symbol def = Symbol.intern("def"); 93 | 94 | assertEquals(asList((Object) //To avoid warnings 95 | _(_(lambda, _(arg), _(car, _(cdr, arg))), _(quote, _(a, b, c))), 96 | _(def, a, "foo bar"), 97 | _(def, b, new BigDecimal("12345")) 98 | ), 99 | Reader.read("((lambda (arg) (car (cdr arg))) '(a b c))\n" + 100 | "(def a \"foo bar\")\n" + 101 | "(def b 12345)")); 102 | 103 | // Null 104 | assertEquals(null, Reader.read("null").get(0)); 105 | assertEquals(_(a, b, null), Reader.read("(a b nil)").get(0)); 106 | } 107 | 108 | static final Symbol quote = Symbol.intern("quote"); 109 | static final Symbol concat = Symbol.intern("concat"); 110 | static final Symbol list = Symbol.intern("list"); 111 | 112 | @Test 113 | public void testBackquote() throws Exception { 114 | Symbol a = Symbol.intern("a"); 115 | Symbol b = Symbol.intern("b"); 116 | Symbol c = Symbol.intern("c"); 117 | Symbol e = Symbol.intern("e"); 118 | Symbol f = Symbol.intern("f"); 119 | assertEquals(_(concat, _(list, _(quote, a)), _(list, b), c), Reader.read("`(a ,b ,@c)").get(0)); 120 | assertEquals(_(concat, 121 | _(list, _(quote, e)), 122 | _(list, _(quote, f)), 123 | _(list, _(concat, _(list, _(quote, a)), _(list, b), c))), 124 | Reader.read("`(e f (a ,b ,@c))").get(0)); 125 | } 126 | 127 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /src/main/java/org/toylisp/Reader.java: -------------------------------------------------------------------------------- 1 | package org.toylisp; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.ArrayList; 5 | import java.util.Collections; 6 | import java.util.HashMap; 7 | import java.util.Iterator; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | /** 12 | * Lisp Reader
13 | * 14 | * @author jerry created 14/11/29 15 | */ 16 | public class Reader { 17 | 18 | static final Symbol QUOTE = Symbol.intern("quote"); 19 | static final Symbol CONCAT = Symbol.intern("concat"); 20 | static final Symbol LIST = Symbol.intern("list"); 21 | 22 | private static void finishToken(StringBuilder token, List tokens) { 23 | if (token.length() > 0) { 24 | tokens.add(token.toString()); 25 | token.delete(0, token.length()); 26 | } 27 | } 28 | 29 | public static List tokenize(String input) { 30 | StringBuilder currentToken = new StringBuilder(); 31 | List tokens = new ArrayList<>(256); 32 | 33 | boolean isInStr = false; 34 | boolean isInComment = false; 35 | char c = 0; 36 | char c0; 37 | for (int i = 0; i < input.length(); i++) { 38 | c0 = c; 39 | c = input.charAt(i); 40 | if (isInStr) { 41 | currentToken.append(c); 42 | if (c == '"' && c0 != '\\') { 43 | finishToken(currentToken, tokens); 44 | isInStr = false; 45 | } 46 | } else if (isInComment) { 47 | if (c == '\r' || c == '\n') { 48 | isInComment = false; 49 | } 50 | } else { 51 | switch (c) { 52 | case '"': 53 | finishToken(currentToken, tokens); 54 | currentToken.append(c); 55 | isInStr = true; 56 | break; 57 | 58 | case ';': 59 | isInComment = true; 60 | break; 61 | 62 | case ',': 63 | finishToken(currentToken, tokens); 64 | currentToken.append(c); 65 | // Look ahead to see if it is ",@". 66 | if (i < input.length() - 1 && input.charAt(i + 1) == '@') { 67 | currentToken.append('@'); 68 | i++; 69 | } 70 | finishToken(currentToken, tokens); 71 | break; 72 | 73 | case '\'': 74 | case '`': 75 | case '(': 76 | case ')': 77 | finishToken(currentToken, tokens); 78 | currentToken.append(c); 79 | finishToken(currentToken, tokens); 80 | break; 81 | 82 | case ' ': 83 | case '\t': 84 | case '\r': 85 | case '\n': 86 | finishToken(currentToken, tokens); 87 | break; 88 | 89 | default: 90 | currentToken.append(c); 91 | } 92 | 93 | } 94 | } 95 | 96 | finishToken(currentToken, tokens); 97 | return tokens; 98 | } 99 | 100 | public static List read(String input) { 101 | return read(tokenize(input)); 102 | } 103 | 104 | public static List read(List tokens) { 105 | List results = new ArrayList<>(); 106 | Iterator tokenIter = tokens.iterator(); 107 | while (tokenIter.hasNext()) { 108 | String currentToken = tokenIter.next(); 109 | results.add(read(currentToken, tokenIter, false)); 110 | } 111 | return results; 112 | } 113 | 114 | static Object read(String currentToken, Iterator tokenIterator, boolean isInBackQuote) { 115 | if (currentToken.equals(")")) { 116 | throw new IllegalArgumentException("Unmatched parentheses: unexpected )"); 117 | } 118 | Character firstChar = currentToken.charAt(0); 119 | ObjectReader reader = readers.get(firstChar); 120 | if (reader != null) { 121 | return reader.readObj(currentToken, tokenIterator, isInBackQuote); 122 | } else if (Character.isDigit(firstChar)) { 123 | return ObjectReader.NUMBER_READER.readObj(currentToken, tokenIterator, isInBackQuote); 124 | } else { 125 | return ObjectReader.SYMBOL_READER.readObj(currentToken, tokenIterator, isInBackQuote); 126 | } 127 | } 128 | 129 | private static final Map readers; 130 | 131 | static { 132 | Map readersMap = new HashMap<>(); 133 | readersMap.put('"', ObjectReader.STRING_READER); 134 | readersMap.put('(', ObjectReader.SEXP_READER); 135 | readersMap.put('\'', ObjectReader.QUOTE_READER); 136 | readersMap.put('`', ObjectReader.BACKQUOTE_READER); 137 | readersMap.put(',', ObjectReader.UNQUOTE_READER); 138 | readers = Collections.unmodifiableMap(readersMap); 139 | } 140 | 141 | // A special container that marks objects that need to be flattened (,@ inside backquote) 142 | static final class NeedFlatten { 143 | final Object coll; 144 | NeedFlatten(Object coll) {this.coll = coll;} 145 | } 146 | 147 | static enum ObjectReader { 148 | 149 | STRING_READER { 150 | @Override 151 | Object readObj(String currentToken, Iterator tokenIterator, boolean inBackQuote) { 152 | StringBuilder val = new StringBuilder(); 153 | boolean escaping = false; 154 | // Ignore quotes 155 | for (int i = 1; i < currentToken.length() - 1; i++) { 156 | char c = currentToken.charAt(i); 157 | if (escaping) { 158 | switch (c) { 159 | case 'n': 160 | val.append('\n'); 161 | break; 162 | 163 | case 'r': 164 | val.append('\r'); 165 | break; 166 | 167 | case 't': 168 | val.append('\t'); 169 | break; 170 | 171 | default: 172 | val.append(c); 173 | } 174 | escaping = false; 175 | } else { 176 | if (c == '\\') { 177 | escaping = true; 178 | } else { 179 | val.append(c); 180 | } 181 | } 182 | } 183 | 184 | return val.toString(); 185 | } 186 | }, 187 | 188 | NUMBER_READER { 189 | @Override 190 | Object readObj(String currentToken, Iterator tokenIterator, boolean inBackQuote) { 191 | return new BigDecimal(currentToken); 192 | } 193 | }, 194 | 195 | SYMBOL_READER { 196 | @Override 197 | Object readObj(String currentToken, Iterator tokenIterator, boolean inBackQuote) { 198 | if (currentToken.equals("nil") || currentToken.equals("null")) { 199 | return null; 200 | } else { 201 | Symbol symbol = Symbol.intern(currentToken); 202 | return inBackQuote ? new Cons(QUOTE, new Cons(symbol, null)) : symbol; 203 | } 204 | } 205 | }, 206 | 207 | SEXP_READER { 208 | @Override 209 | Object readObj(String currentToken, Iterator tokenIterator, boolean inBackQuote) { 210 | List objs = new ArrayList<>(); 211 | if (inBackQuote) { 212 | objs.add(CONCAT); 213 | } 214 | while (tokenIterator.hasNext()) { 215 | currentToken = tokenIterator.next(); 216 | if (")".equals(currentToken)) { 217 | return Cons.fromList(objs); 218 | } else { 219 | Object obj = read(currentToken, tokenIterator, inBackQuote); 220 | if (inBackQuote) { 221 | if (obj instanceof NeedFlatten) { 222 | obj = ((NeedFlatten) obj).coll; 223 | } else { 224 | obj = new Cons(LIST, new Cons(obj, null)); 225 | } 226 | } 227 | objs.add(obj); 228 | } 229 | } 230 | throw new IllegalArgumentException("Unmatched parentheses: need ) to match"); 231 | } 232 | }, 233 | 234 | BACKQUOTE_READER { 235 | @Override 236 | Object readObj(String currentToken, Iterator tokenIterator, boolean inBackQuote) { 237 | currentToken = tokenIterator.next(); 238 | return read(currentToken, tokenIterator, true); 239 | } 240 | }, 241 | 242 | UNQUOTE_READER { 243 | @Override 244 | Object readObj(String currentToken, Iterator tokenIterator, boolean inBackQuote) { 245 | if (!tokenIterator.hasNext()) { 246 | throw new IllegalArgumentException("No argument found for unquote"); 247 | } 248 | String nextToken = tokenIterator.next(); 249 | Object obj = read(nextToken, tokenIterator, false); 250 | if (currentToken.contains("@")) { 251 | return new NeedFlatten(obj); 252 | } else { 253 | return obj; 254 | } 255 | } 256 | }, 257 | 258 | QUOTE_READER { 259 | @Override 260 | Object readObj(String currentToken, Iterator tokenIterator, boolean inBackQuote) { 261 | if (!tokenIterator.hasNext()) { 262 | throw new IllegalArgumentException("No argument found for quote"); 263 | } 264 | return new Cons(QUOTE, new Cons(read(tokenIterator.next(), tokenIterator, inBackQuote), null)); 265 | } 266 | }; 267 | 268 | abstract Object readObj(String currentToken, Iterator tokenIterator, boolean inBackQuote); 269 | } 270 | 271 | 272 | } 273 | -------------------------------------------------------------------------------- /src/main/java/org/toylisp/Runtime.java: -------------------------------------------------------------------------------- 1 | package org.toylisp; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.ArrayList; 5 | import java.util.Arrays; 6 | import java.util.Collections; 7 | import java.util.IdentityHashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | /** 12 | * Runtime
13 | * 14 | * @author jerry created 14/11/26 15 | */ 16 | public class Runtime { 17 | 18 | static final Symbol DO = Symbol.intern("do"); 19 | static final Symbol COND = Symbol.intern("cond"); 20 | static final Symbol LAMBDA = Symbol.intern("lambda"); 21 | static final Symbol QUOTE = Symbol.intern("quote"); 22 | static final Symbol DEF = Symbol.intern("def"); 23 | static final Symbol DEFMACRO = Symbol.intern("defmacro"); 24 | 25 | public static Cons cons(Object car, Object cdr) { 26 | return new Cons(car, cdr); 27 | } 28 | 29 | public static boolean bool(Object obj) { 30 | return obj != null && !obj.equals(Boolean.FALSE); 31 | } 32 | 33 | public static Env getRootEnv() { 34 | return rootEnv; 35 | } 36 | 37 | private static void ensureArity(String name, int expected, Cons args) { 38 | int n = 0; 39 | while (args != null) { 40 | n++; 41 | args = (Cons) args.cdr(); 42 | } 43 | ensureArity(name, expected, n); 44 | } 45 | 46 | private static void ensureArity(String name, int expected, int actual) { 47 | if (expected != actual) { 48 | throw new IllegalArgumentException(name + ": expect " + expected + " args, " + actual + " given"); 49 | } 50 | } 51 | 52 | public static Object eval(Object form, Env env) { 53 | if (form instanceof Symbol) { 54 | return env.get((Symbol) form); 55 | } else if (form instanceof Cons) { 56 | Cons cons = (Cons) form; 57 | Object operator = cons.car(); 58 | Cons params = (Cons) cons.cdr(); 59 | SpecialForm specialForm; 60 | if (operator instanceof Symbol && 61 | (specialForm = SpecialForm.getSpecialForm((Symbol) operator)) != null) { 62 | return specialForm.run(params, env); 63 | } else { 64 | // function call or macro 65 | IFunc func = (IFunc) eval(operator, env); 66 | if (func instanceof IMacro) { 67 | return eval(macroExpand(func, params), env); 68 | } else { 69 | return callFunc(func, params, env); 70 | } 71 | } 72 | } else { 73 | // Everything else evaluates to itself. 74 | return form; 75 | } 76 | } 77 | 78 | private static Object callFunc(IFunc func, Cons params, Env env) {List args = new ArrayList<>(); 79 | while (params != null) { 80 | // eval arguments 81 | args.add(eval(params.car(), env)); 82 | params = (Cons) params.cdr(); 83 | } 84 | return func.invoke(args.toArray()); 85 | } 86 | 87 | private static Object macroExpand(IFunc macro, Cons params) { 88 | List args = new ArrayList<>(); 89 | while (params != null) { 90 | // eval arguments 91 | args.add(params.car()); 92 | params = (Cons) params.cdr(); 93 | } 94 | return macro.invoke(args.toArray()); 95 | } 96 | 97 | // Basic functions 98 | static final IFunc cons = new IFunc() { 99 | @Override 100 | public Object invoke(Object... args) { 101 | ensureArity("cons", 2, args.length); 102 | return cons(args[0], args[1]); 103 | } 104 | }; 105 | 106 | static final IFunc car = new IFunc() { 107 | @Override 108 | public Object invoke(Object... args) { 109 | ensureArity("car", 1, args.length); 110 | if (args[0] == null) { 111 | return null; 112 | } 113 | return ((Cons) args[0]).car(); 114 | } 115 | }; 116 | 117 | static final IFunc cdr = new IFunc() { 118 | @Override 119 | public Object invoke(Object... args) { 120 | ensureArity("cdr", 1, args.length); 121 | if (args[0] == null) { 122 | return null; 123 | } 124 | return ((Cons) args[0]).cdr(); 125 | } 126 | }; 127 | 128 | static final IFunc list = new IFunc() { 129 | @Override 130 | public Object invoke(Object... args) { 131 | if (args.length == 0) { 132 | return null; 133 | } 134 | return Cons.fromList(Arrays.asList(args)); 135 | } 136 | }; 137 | 138 | static final IFunc concat = new IFunc() { 139 | @Override 140 | public Object invoke(Object... args) { 141 | return Cons.concat(args); 142 | } 143 | }; 144 | 145 | static final IFunc eq = new IFunc() { 146 | @Override 147 | public Object invoke(Object... args) { 148 | ensureArity("eq", 2, args.length); 149 | return (args[0] instanceof Symbol) && 150 | (args[1] instanceof Symbol) && 151 | (args[0] == args[1]); 152 | } 153 | }; 154 | 155 | static final IFunc equal = new IFunc() { 156 | @Override 157 | public Object invoke(Object... args) { 158 | ensureArity("eq", 2, args.length); 159 | if (args[0] == null) { 160 | if (args[1] == null) { 161 | return true; 162 | } 163 | } else { 164 | return args[0].equals(args[1]); 165 | } 166 | return false; 167 | } 168 | }; 169 | 170 | static final IFunc prn = new IFunc() { 171 | @Override 172 | public Object invoke(Object... args) { 173 | StringBuilder msg = new StringBuilder(64); 174 | for (Object arg : args) { 175 | msg.append(arg); 176 | } 177 | System.out.println(msg.toString()); 178 | return null; 179 | } 180 | }; 181 | 182 | static final IFunc plus = new IFunc() { 183 | @Override 184 | public Object invoke(Object... args) { 185 | if (args.length == 0) { 186 | return new BigDecimal(0); 187 | } 188 | BigDecimal res = (BigDecimal) args[0]; 189 | for (int i = 1; i < args.length; i++) { 190 | res = res.add((BigDecimal) args[i]); 191 | } 192 | return res; 193 | } 194 | }; 195 | 196 | static final IFunc minus = new IFunc() { 197 | @Override 198 | public Object invoke(Object... args) { 199 | if (args.length == 0) { 200 | return new BigDecimal(0); 201 | } 202 | BigDecimal res = (BigDecimal) args[0]; 203 | for (int i = 1; i < args.length; i++) { 204 | res = res.subtract((BigDecimal) args[i]); 205 | } 206 | return res; 207 | } 208 | }; 209 | 210 | static final IFunc multiply = new IFunc() { 211 | @Override 212 | public Object invoke(Object... args) { 213 | if (args.length == 0) { 214 | return new BigDecimal(1); 215 | } 216 | BigDecimal res = (BigDecimal) args[0]; 217 | for (int i = 1; i < args.length; i++) { 218 | res = res.multiply((BigDecimal) args[i]); 219 | } 220 | return res; 221 | } 222 | }; 223 | 224 | static final IFunc divide = new IFunc() { 225 | @Override 226 | public Object invoke(Object... args) { 227 | if (args.length == 0) { 228 | return new BigDecimal(1); 229 | } 230 | BigDecimal res = (BigDecimal) args[0]; 231 | for (int i = 1; i < args.length; i++) { 232 | res = res.divideToIntegralValue((BigDecimal) args[i]); 233 | } 234 | return res; 235 | } 236 | }; 237 | 238 | static final IFunc macroexpand = new IFunc() { 239 | @Override 240 | public Object invoke(Object... args) { 241 | ensureArity("eq", 1, args.length); 242 | Object form = args[0]; 243 | if (!(form instanceof Cons)) { 244 | return form; 245 | } 246 | Cons cons = (Cons) form; 247 | Object operator = cons.car(); 248 | Cons params = (Cons) cons.cdr(); 249 | if (operator instanceof Symbol && 250 | (SpecialForm.getSpecialForm((Symbol) operator)) == null) { 251 | IFunc func = (IFunc) eval(operator, getRootEnv()); 252 | if (func instanceof IMacro) { 253 | return macroExpand(func, params); 254 | } 255 | } 256 | return form; 257 | } 258 | }; 259 | 260 | private static List getArgNames(Cons args) { 261 | List argNames = new ArrayList<>(); 262 | while (args != null) { 263 | argNames.add((Symbol) args.car()); 264 | args = (Cons) args.cdr(); 265 | } 266 | return argNames; 267 | } 268 | 269 | // Special Forms 270 | enum SpecialForm { 271 | 272 | _cond(COND) { 273 | @Override 274 | Object run(Cons args, Env env) { 275 | if (args == null) { 276 | throw new IllegalStateException("cond: no clause found"); 277 | } 278 | while (args != null) { 279 | Cons clause = (Cons) args.car(); 280 | Object pred = clause.car(); 281 | Cons expr = (Cons) clause.cdr(); 282 | if (clause == null || pred == null || expr == null || expr.cdr() != null) { 283 | throw new IllegalArgumentException("cond: invalid clause"); 284 | } 285 | if (bool(eval(pred, env))) { 286 | return eval(expr.car(), env); 287 | } 288 | args = (Cons) args.cdr(); 289 | } 290 | return null; 291 | } 292 | }, 293 | 294 | _def(DEF) { 295 | @Override 296 | Object run(Cons args, Env env) { 297 | ensureArity("def", 2, args); 298 | 299 | Symbol name = (Symbol) args.car(); 300 | Object form = ((Cons) args.cdr()).car(); 301 | 302 | Object obj = eval(form, env); 303 | getRootEnv().set(name, obj); 304 | return obj; 305 | } 306 | }, 307 | 308 | _quote(QUOTE) { 309 | @Override 310 | Object run(Cons args, Env env) { 311 | if (args.cdr() != null) { 312 | throw new IllegalArgumentException("Can only quote one argument"); 313 | } 314 | return args.car(); 315 | } 316 | }, 317 | 318 | _lambda(LAMBDA) { 319 | @Override 320 | Object run(Cons definition, Env env) { 321 | List argNames = getArgNames((Cons) definition.car()); 322 | Cons body = cons(DO, definition.cdr()); 323 | return new Func(Collections.unmodifiableList(argNames), body, env); 324 | } 325 | }, 326 | 327 | _defmacro(DEFMACRO) { 328 | @Override 329 | Object run(Cons definition, Env env) { 330 | Symbol name = (Symbol) definition.car(); 331 | Cons argsBody = (Cons) definition.cdr(); 332 | List argNames = getArgNames((Cons) argsBody.car()); 333 | Cons body = cons(DO, argsBody.cdr()); 334 | Macro macro = new Macro(Collections.unmodifiableList(argNames), body, env); 335 | getRootEnv().set(name, macro); 336 | return macro; 337 | } 338 | }, 339 | 340 | _do(DO) { 341 | @Override 342 | Object run(Cons args, Env env) { 343 | Object ret = null; 344 | while (args != null) { 345 | ret = Runtime.eval(args.car(), env); 346 | args = (Cons) args.cdr(); 347 | } 348 | return ret; 349 | } 350 | }; 351 | 352 | private static final Map specialForms = new IdentityHashMap<>(); 353 | 354 | static { 355 | for (SpecialForm form : SpecialForm.values()) { 356 | specialForms.put(form.operator, form); 357 | } 358 | } 359 | 360 | final Symbol operator; 361 | 362 | SpecialForm(Symbol operator) {this.operator = operator;} 363 | 364 | abstract Object run(Cons args, Env env); 365 | 366 | public static SpecialForm getSpecialForm(Symbol operator) { 367 | return specialForms.get(operator); 368 | } 369 | } 370 | 371 | static final Env rootEnv = Env.createRoot() 372 | .set(Symbol.intern("cons"), cons) 373 | .set(Symbol.intern("car"), car) 374 | .set(Symbol.intern("cdr"), cdr) 375 | .set(Symbol.intern("list"), list) 376 | .set(Symbol.intern("concat"), concat) 377 | .set(Symbol.intern("eq?"), eq) 378 | .set(Symbol.intern("="), equal) 379 | .set(Symbol.intern("prn"), prn) 380 | .set(Symbol.intern("+"), plus) 381 | .set(Symbol.intern("-"), minus) 382 | .set(Symbol.intern("*"), multiply) 383 | .set(Symbol.intern("/"), divide) 384 | .set(Symbol.intern("macroexpand"), macroexpand) 385 | .set(Symbol.intern("t"), Boolean.TRUE); 386 | 387 | } 388 | --------------------------------------------------------------------------------