aboutsummaryrefslogtreecommitdiffstats
path: root/jlox
diff options
context:
space:
mode:
Diffstat (limited to 'jlox')
-rwxr-xr-xjlox3
-rw-r--r--jlox/.gitignore1
-rw-r--r--jlox/CMakeLists.txt8
-rw-r--r--jlox/CMakePresets.json17
-rwxr-xr-xjlox/jlox3
-rw-r--r--jlox/src/com/craftinginterpreters/CMakeLists.txt2
-rw-r--r--jlox/src/com/craftinginterpreters/lox/.gitignore1
-rw-r--r--jlox/src/com/craftinginterpreters/lox/.settings/org.eclipse.jdt.core.prefs2
-rw-r--r--jlox/src/com/craftinginterpreters/lox/AstPrinter.java147
-rw-r--r--jlox/src/com/craftinginterpreters/lox/CMakeLists.txt28
-rw-r--r--jlox/src/com/craftinginterpreters/lox/Environment.java63
-rw-r--r--jlox/src/com/craftinginterpreters/lox/Interpreter.java392
-rw-r--r--jlox/src/com/craftinginterpreters/lox/Lox.java93
-rw-r--r--jlox/src/com/craftinginterpreters/lox/LoxCallable.java9
-rw-r--r--jlox/src/com/craftinginterpreters/lox/LoxClass.java51
-rw-r--r--jlox/src/com/craftinginterpreters/lox/LoxFunction.java52
-rw-r--r--jlox/src/com/craftinginterpreters/lox/LoxInstance.java34
-rw-r--r--jlox/src/com/craftinginterpreters/lox/Parser.java457
-rw-r--r--jlox/src/com/craftinginterpreters/lox/Resolver.java299
-rw-r--r--jlox/src/com/craftinginterpreters/lox/Return.java10
-rw-r--r--jlox/src/com/craftinginterpreters/lox/RuntimeError.java10
-rw-r--r--jlox/src/com/craftinginterpreters/lox/Scanner.java207
-rw-r--r--jlox/src/com/craftinginterpreters/lox/Token.java19
-rw-r--r--jlox/src/com/craftinginterpreters/lox/TokenType.java20
-rw-r--r--jlox/src/com/craftinginterpreters/tool/CMakeLists.txt3
-rw-r--r--jlox/src/com/craftinginterpreters/tool/GenerateAst.java100
26 files changed, 2028 insertions, 3 deletions
diff --git a/jlox b/jlox
deleted file mode 100755
index c6be2ea..0000000
--- a/jlox
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/usr/bin/env sh
-
-/usr/bin/java -jar src/_build/com/craftinginterpreters/lox/Lox.jar "$@"
diff --git a/jlox/.gitignore b/jlox/.gitignore
new file mode 100644
index 0000000..e35d885
--- /dev/null
+++ b/jlox/.gitignore
@@ -0,0 +1 @@
+_build
diff --git a/jlox/CMakeLists.txt b/jlox/CMakeLists.txt
new file mode 100644
index 0000000..da162c1
--- /dev/null
+++ b/jlox/CMakeLists.txt
@@ -0,0 +1,8 @@
+cmake_minimum_required(VERSION 2.19)
+
+find_package(Java REQUIRED)
+include(UseJava)
+
+project(Lox NONE)
+
+add_subdirectory(src/com/craftinginterpreters)
diff --git a/jlox/CMakePresets.json b/jlox/CMakePresets.json
new file mode 100644
index 0000000..728cdb0
--- /dev/null
+++ b/jlox/CMakePresets.json
@@ -0,0 +1,17 @@
+{
+ "version": 1,
+ "cmakeMinimumRequired": {
+ "major": 3,
+ "minor": 19,
+ "patch": 0
+ },
+ "configurePresets": [
+ {
+ "name": "default",
+ "displayName": "Default Config",
+ "description": "Default build using Makefile generator",
+ "generator": "Unix Makefiles",
+ "binaryDir": "${sourceDir}/_build"
+ }
+ ]
+}
diff --git a/jlox/jlox b/jlox/jlox
new file mode 100755
index 0000000..6798309
--- /dev/null
+++ b/jlox/jlox
@@ -0,0 +1,3 @@
+#!/usr/bin/env sh
+
+java -jar "$(dirname "$0")/_build/src/com/craftinginterpreters/lox/Lox.jar" "$@"
diff --git a/jlox/src/com/craftinginterpreters/CMakeLists.txt b/jlox/src/com/craftinginterpreters/CMakeLists.txt
new file mode 100644
index 0000000..959eedd
--- /dev/null
+++ b/jlox/src/com/craftinginterpreters/CMakeLists.txt
@@ -0,0 +1,2 @@
+add_subdirectory(tool)
+add_subdirectory(lox)
diff --git a/jlox/src/com/craftinginterpreters/lox/.gitignore b/jlox/src/com/craftinginterpreters/lox/.gitignore
new file mode 100644
index 0000000..0260b11
--- /dev/null
+++ b/jlox/src/com/craftinginterpreters/lox/.gitignore
@@ -0,0 +1 @@
+Expr.java
diff --git a/jlox/src/com/craftinginterpreters/lox/.settings/org.eclipse.jdt.core.prefs b/jlox/src/com/craftinginterpreters/lox/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..479bf4d
--- /dev/null
+++ b/jlox/src/com/craftinginterpreters/lox/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.formatter.lineSplit=80
diff --git a/jlox/src/com/craftinginterpreters/lox/AstPrinter.java b/jlox/src/com/craftinginterpreters/lox/AstPrinter.java
new file mode 100644
index 0000000..4c120d3
--- /dev/null
+++ b/jlox/src/com/craftinginterpreters/lox/AstPrinter.java
@@ -0,0 +1,147 @@
+package com.craftinginterpreters.lox;
+
+import java.util.List;
+
+/**
+ * AstPrinter
+ */
+class AstPrinter implements Expr.Visitor<String>, Stmt.Visitor<String> {
+ String print(Expr expr) {
+ return expr.accept(this);
+ }
+
+ @Override
+ public String visitCallExpr(Expr.Call expr) {
+ StringBuilder builder = new StringBuilder();
+
+ builder.append(print(expr.callee));
+ builder.append(parenthesizeExpr("", expr.arguments));
+
+ return builder.toString();
+ }
+
+ @Override
+ public String visitBinaryExpr(Expr.Binary expr) {
+ return parenthesizeExpr(expr.operator.lexeme, expr.left, expr.right);
+ }
+
+ @Override
+ public String visitGroupingExpr(Expr.Grouping expr) {
+ return parenthesizeExpr("group", expr.expression);
+ }
+
+ @Override
+ public String visitLiteralExpr(Expr.Literal expr) {
+ if (expr.value == null)
+ return "nil";
+ return expr.value.toString();
+ }
+
+ @Override
+ public String visitLogicalExpr(Expr.Logical expr) {
+ return parenthesizeExpr(expr.operator.type.toString(), expr.left, expr.right);
+ }
+
+ @Override
+ public String visitUnaryExpr(Expr.Unary expr) {
+ return parenthesizeExpr(expr.operator.lexeme, expr.right);
+ }
+
+ @Override
+ public String visitVariableExpr(Expr.Variable expr) {
+ return expr.name.lexeme;
+ }
+
+ @Override
+ public String visitAssignExpr(Expr.Assign expression) {
+ return parenthesizeExpr(expression.name.lexeme + " = ", expression.value);
+ }
+
+ @Override
+ public String visitVarStmt(Stmt.Var statement) {
+ return parenthesizeExpr("define " + statement.name.lexeme + " = ", statement.initializer);
+ }
+
+ @Override
+ public String visitWhileStmt(Stmt.While statement) {
+ StringBuilder builder = new StringBuilder();
+
+ builder.append('(');
+ builder.append(parenthesizeExpr("while", statement.condition));
+ builder.append(parenthesizeStmt("do", statement.body));
+ builder.append(')');
+
+ return builder.toString();
+ }
+
+ @Override
+ public String visitPrintStmt(Stmt.Print statement) {
+ return parenthesizeExpr("print", statement.expression);
+ }
+
+ @Override
+ public String visitExpressionStmt(Stmt.Expression expression) {
+ return expression.expression.accept(this);
+ }
+
+ @Override
+ public String visitIfStmt(Stmt.If stmt) {
+ StringBuilder builder = new StringBuilder();
+
+ builder.append("(");
+ builder.append(parenthesizeExpr("if", stmt.condition));
+ builder.append(parenthesizeStmt("then", stmt.thenBranch));
+
+ if (stmt.elseBranch != null) {
+ builder.append(parenthesizeStmt("else", stmt.elseBranch));
+ }
+ return builder.toString();
+ }
+
+ @Override
+ public String visitBlockStmt(Stmt.Block block) {
+ return parenthesizeStmt("", block.statements);
+ }
+
+ private String parenthesizeExpr(String name, Expr... exprs) {
+ return parenthesizeExpr(name, exprs);
+ }
+
+ private String parenthesizeExpr(String name, List<Expr> exprs) {
+ StringBuilder builder = new StringBuilder();
+
+ builder.append("(").append(name);
+ for (Expr expr : exprs) {
+ builder.append(" ");
+ builder.append(expr.accept(this));
+ }
+ builder.append(")");
+
+ return builder.toString();
+ }
+
+ private String parenthesizeStmt(String name, List<Stmt> statements) {
+ StringBuilder builder = new StringBuilder();
+
+ builder.append("(").append(name);
+ for (Stmt statement : statements) {
+ builder.append(" ");
+ builder.append(statement.accept(this));
+ }
+ builder.append(")");
+
+ return builder.toString();
+ }
+
+ private String parenthesizeStmt(String name, Stmt... statements) {
+ return parenthesizeStmt(name, statements);
+ }
+
+ public static void main(String[] args) {
+ Expr expression = new Expr.Binary(
+ new Expr.Unary(new Token(TokenType.MINUS, "-", null, 1), new Expr.Literal(123)),
+ new Token(TokenType.STAR, "*", null, 1), new Expr.Grouping(new Expr.Literal(45.67)));
+
+ System.out.println(new AstPrinter().print(expression));
+ }
+}
diff --git a/jlox/src/com/craftinginterpreters/lox/CMakeLists.txt b/jlox/src/com/craftinginterpreters/lox/CMakeLists.txt
new file mode 100644
index 0000000..aeab240
--- /dev/null
+++ b/jlox/src/com/craftinginterpreters/lox/CMakeLists.txt
@@ -0,0 +1,28 @@
+get_target_property(GENERATE_AST_JAR GenerateAst JAR_FILE)
+
+set(GENERATED_JAVA_FILENAMES
+ ${CMAKE_CURRENT_BINARY_DIR}/Expr.java
+ ${CMAKE_CURRENT_BINARY_DIR}/Stmt.java
+ CACHE INTERNAL "")
+
+add_custom_command(OUTPUT ${GENERATED_JAVA_FILENAMES}
+ COMMAND java -jar ${GENERATE_AST_JAR} ${CMAKE_CURRENT_BINARY_DIR}
+ DEPENDS GenerateAst ${GENERATE_AST_JAR})
+
+add_jar(Lox
+ Lox.java
+ TokenType.java
+ Token.java
+ Scanner.java
+ ${GENERATED_JAVA_FILENAMES}
+ Parser.java
+ Interpreter.java
+ RuntimeError.java
+ Environment.java
+ LoxCallable.java
+ LoxFunction.java
+ Return.java
+ Resolver.java
+ LoxClass.java
+ LoxInstance.java
+ ENTRY_POINT com/craftinginterpreters/lox/Lox)
diff --git a/jlox/src/com/craftinginterpreters/lox/Environment.java b/jlox/src/com/craftinginterpreters/lox/Environment.java
new file mode 100644
index 0000000..407b9b3
--- /dev/null
+++ b/jlox/src/com/craftinginterpreters/lox/Environment.java
@@ -0,0 +1,63 @@
+package com.craftinginterpreters.lox;
+
+import java.util.HashMap;
+import java.util.Map;
+
+class Environment {
+ final Environment enclosing;
+ private final Map<String, Object> values = new HashMap<>();
+
+ Environment() {
+ enclosing = null;
+ }
+
+ Environment(Environment enclosing) {
+ this.enclosing = enclosing;
+ }
+
+ Object get(Token name) {
+ if (values.containsKey(name.lexeme)) {
+ return values.get(name.lexeme);
+ }
+
+ if (enclosing != null)
+ return enclosing.get(name);
+
+ throw new RuntimeError(name, "Undefined variable '" + name.lexeme + "'.");
+ }
+
+ void assign(Token name, Object value) {
+ if (values.containsKey(name.lexeme)) {
+ values.put(name.lexeme, value);
+ return;
+ }
+
+ if (enclosing != null) {
+ enclosing.assign(name, value);
+ return;
+ }
+
+ throw new RuntimeError(name, "Undefined variable '" + name.lexeme + "'.");
+ }
+
+ void define(String name, Object value) {
+ values.put(name, value);
+ }
+
+ Environment ancestor(int distance) {
+ Environment environment = this;
+ for (int i = 0; i < distance; i++) {
+ environment = environment.enclosing;
+ }
+
+ return environment;
+ }
+
+ Object getAt(int distance, String name) {
+ return ancestor(distance).values.get(name);
+ }
+
+ void assignAt(int distance, Token name, Object value) {
+ ancestor(distance).values.put(name.lexeme, value);
+ }
+}
diff --git a/jlox/src/com/craftinginterpreters/lox/Interpreter.java b/jlox/src/com/craftinginterpreters/lox/Interpreter.java
new file mode 100644
index 0000000..3f4d385
--- /dev/null
+++ b/jlox/src/com/craftinginterpreters/lox/Interpreter.java
@@ -0,0 +1,392 @@
+package com.craftinginterpreters.lox;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+class Interpreter implements Expr.Visitor<Object>, Stmt.Visitor<Void> {
+ final Environment globals = new Environment();
+ private Environment environment = globals;
+ private final Map<Expr, Integer> locals = new HashMap<>();
+
+ Interpreter() {
+ globals.define("clock", new LoxCallable() {
+ @Override
+ public int arity() {
+ return 0;
+ }
+
+ @Override
+ public Object call(Interpreter interpreter, List<Object> arguments) {
+ return (double) System.currentTimeMillis() / 1000.0;
+ }
+
+ @Override
+ public String toString() {
+ return "<native fn>";
+ }
+ });
+ }
+
+ @Override
+ public Object visitLiteralExpr(Expr.Literal expr) {
+ return expr.value;
+ }
+
+ @Override
+ public Object visitLogicalExpr(Expr.Logical expr) {
+ Object left = evaluate(expr.left);
+
+ if (expr.operator.type == TokenType.OR) {
+ if (isTruthy(left))
+ return left;
+ } else {
+ if (!isTruthy(left))
+ return left;
+ }
+
+ return evaluate(expr.right);
+ }
+
+ @Override
+ public Object visitSetExpr(Expr.Set expr) {
+ Object object = evaluate(expr.object);
+
+ if (!(object instanceof LoxInstance)) {
+ throw new RuntimeError(expr.name, "Only instances have fields.");
+ }
+
+ Object value = evaluate(expr.value);
+ ((LoxInstance) object).set(expr.name, value);
+ return value;
+ }
+
+ @Override
+ public Object visitSuperExpr(Expr.Super expr) {
+ int distance = locals.get(expr);
+ LoxClass superclass = (LoxClass) environment.getAt(distance, "super");
+
+ LoxInstance object = (LoxInstance) environment.getAt(distance - 1, "this");
+
+ LoxFunction method = superclass.findMethod(expr.method.lexeme);
+
+ if (method == null) {
+ throw new RuntimeError(expr.method, "Undefined property '" + expr.method.lexeme + "'.");
+ }
+
+ return method.bind(object);
+ }
+
+ @Override
+ public Object visitThisExpr(Expr.This expr) {
+ return lookUpVariable(expr.keyword, expr);
+ }
+
+ @Override
+ public Object visitUnaryExpr(Expr.Unary expr) {
+ Object right = evaluate(expr.right);
+
+ switch (expr.operator.type) {
+ case BANG:
+ return !isTruthy(right);
+ case MINUS:
+ checkNumberOperand(expr.operator, right);
+ return -(double) right;
+ }
+
+ // Unreachable.
+ return null;
+ }
+
+ @Override
+ public Object visitVariableExpr(Expr.Variable expr) {
+ return lookUpVariable(expr.name, expr);
+ }
+
+ private Object lookUpVariable(Token name, Expr expr) {
+ Integer distance = locals.get(expr);
+ if (distance != null) {
+ return environment.getAt(distance, name.lexeme);
+ } else {
+ return globals.get(name);
+ }
+ }
+
+ private void checkNumberOperand(Token operator, Object operand) {
+ if (operand instanceof Double)
+ return;
+ throw new RuntimeError(operator, "Operand must be a number.");
+ }
+
+ private void checkNumberOperands(Token operator, Object left, Object right) {
+ if (left instanceof Double && right instanceof Double)
+ return;
+
+ throw new RuntimeError(operator, "Operands must be numbers.");
+ }
+
+ private boolean isTruthy(Object object) {
+ if (object == null)
+ return false;
+ if (object instanceof Boolean)
+ return (boolean) object;
+ return true;
+ }
+
+ private boolean isEqual(Object a, Object b) {
+ if (a == null && b == null)
+ return true;
+ if (a == null)
+ return false;
+
+ return a.equals(b);
+ }
+
+ private String stringify(Object object) {
+ if (object == null)
+ return "nil";
+
+ if (object instanceof Double) {
+ String text = object.toString();
+ if (text.endsWith(".0")) {
+ text = text.substring(0, text.length() - 2);
+ }
+ return text;
+ }
+
+ return object.toString();
+ }
+
+ @Override
+ public Object visitGroupingExpr(Expr.Grouping expr) {
+ return evaluate(expr.expression);
+ }
+
+ private Object evaluate(Expr expr) {
+ return expr.accept(this);
+ }
+
+ private void execute(Stmt stmt) {
+ stmt.accept(this);
+ }
+
+ public void resolve(Expr expr, int depth) {
+ locals.put(expr, depth);
+ }
+
+ public void executeBlock(List<Stmt> statements, Environment environment) {
+ Environment previous = this.environment;
+
+ try {
+ this.environment = environment;
+
+ for (Stmt statement : statements) {
+ execute(statement);
+ }
+ } finally {
+ this.environment = previous;
+ }
+ }
+
+ @Override
+ public Void visitBlockStmt(Stmt.Block stmt) {
+ executeBlock(stmt.statements, new Environment(environment));
+ return null;
+ }
+
+ @Override
+ public Void visitClassStmt(Stmt.Class stmt) {
+ Object superclass = null;
+ if (stmt.superclass != null) {
+ superclass = evaluate(stmt.superclass);
+ if (!(superclass instanceof LoxClass)) {
+ throw new RuntimeError(stmt.superclass.name, "Superclass must be a class.");
+ }
+ }
+ environment.define(stmt.name.lexeme, null);
+
+ if (stmt.superclass != null) {
+ environment = new Environment(environment);
+ environment.define("super", superclass);
+ }
+
+ Map<String, LoxFunction> methods = new HashMap<>();
+ for (Stmt.Function method : stmt.methods) {
+ LoxFunction function = new LoxFunction(method, environment, method.name.lexeme.equals("init"));
+ methods.put(method.name.lexeme, function);
+ }
+
+ LoxClass klass = new LoxClass(stmt.name.lexeme, (LoxClass) superclass, methods);
+
+ if (superclass != null) {
+ environment = environment.enclosing;
+ }
+
+ environment.assign(stmt.name, klass);
+ return null;
+ }
+
+ @Override
+ public Void visitExpressionStmt(Stmt.Expression stmt) {
+ evaluate(stmt.expression);
+ return null;
+ }
+
+ @Override
+ public Void visitFunctionStmt(Stmt.Function stmt) {
+ LoxFunction function = new LoxFunction(stmt, environment, false);
+ environment.define(stmt.name.lexeme, function);
+ return null;
+ }
+
+ @Override
+ public Void visitIfStmt(Stmt.If stmt) {
+ if (isTruthy(evaluate(stmt.condition))) {
+ execute(stmt.thenBranch);
+ } else if (stmt.elseBranch != null) {
+ execute(stmt.elseBranch);
+ }
+
+ return null;
+ }
+
+ @Override
+ public Void visitPrintStmt(Stmt.Print stmt) {
+ Object value = evaluate(stmt.expression);
+ System.out.println(stringify(value));
+ return null;
+ }
+
+ @Override
+ public Void visitReturnStmt(Stmt.Return stmt) {
+ Object value = null;
+ if (stmt.value != null)
+ value = evaluate(stmt.value);
+
+ throw new Return(value);
+ }
+
+ @Override
+ public Void visitVarStmt(Stmt.Var stmt) {
+ Object value = null;
+ if (stmt.initializer != null) {
+ value = evaluate(stmt.initializer);
+ }
+
+ environment.define(stmt.name.lexeme, value);
+ return null;
+ }
+
+ @Override
+ public Void visitWhileStmt(Stmt.While stmt) {
+ while (isTruthy(evaluate(stmt.condition))) {
+ execute(stmt.body);
+ }
+
+ return null;
+ }
+
+ @Override
+ public Object visitAssignExpr(Expr.Assign expr) {
+ Object value = evaluate(expr.value);
+
+ Integer distance = locals.get(expr);
+ if (distance != null) {
+ environment.assignAt(distance, expr.name, value);
+ } else {
+ globals.assign(expr.name, value);
+ }
+
+ return value;
+ }
+
+ @Override
+ public Object visitBinaryExpr(Expr.Binary expr) {
+ Object left = evaluate(expr.left);
+ Object right = evaluate(expr.right);
+
+ switch (expr.operator.type) {
+ case GREATER:
+ checkNumberOperands(expr.operator, left, right);
+ return (double) left > (double) right;
+ case GREATER_EQUAL:
+ checkNumberOperands(expr.operator, left, right);
+ return (double) left >= (double) right;
+ case LESS:
+ checkNumberOperands(expr.operator, left, right);
+ return (double) left < (double) right;
+ case LESS_EQUAL:
+ checkNumberOperands(expr.operator, left, right);
+ return (double) left <= (double) right;
+ case BANG_EQUAL:
+ return !isEqual(left, right);
+ case EQUAL_EQUAL:
+ return isEqual(left, right);
+ case MINUS:
+ checkNumberOperands(expr.operator, left, right);
+ return (double) left - (double) right;
+ case PLUS:
+ if (left instanceof Double && right instanceof Double) {
+ return (double) left + (double) right;
+ }
+
+ if (left instanceof String && right instanceof String) {
+ return (String) left + (String) right;
+ }
+
+ throw new RuntimeError(expr.operator, "Operands must be two numbers or two strings.");
+ case SLASH:
+ checkNumberOperands(expr.operator, left, right);
+ return (double) left / (double) right;
+ case STAR:
+ checkNumberOperands(expr.operator, left, right);
+ return (double) left * (double) right;
+ }
+
+ // Unreachable.
+ return null;
+ }
+
+ @Override
+ public Object visitCallExpr(Expr.Call expr) {
+ Object callee = evaluate(expr.callee);
+
+ List<Object> arguments = new ArrayList<>();
+ for (Expr argument : expr.arguments) {
+ arguments.add(evaluate(argument));
+ }
+
+ if (!(callee instanceof LoxCallable)) {
+ throw new RuntimeError(expr.paren, "Can only call functions and classes.");
+ }
+
+ LoxCallable function = (LoxCallable) callee;
+ if (arguments.size() != function.arity()) {
+ throw new RuntimeError(expr.paren,
+ "Expected " + function.arity() + " arguments but got " + arguments.size() + ".");
+ }
+
+ return function.call(this, arguments);
+ }
+
+ @Override
+ public Object visitGetExpr(Expr.Get expr) {
+ Object object = evaluate(expr.object);
+ if (object instanceof LoxInstance) {
+ return ((LoxInstance) object).get(expr.name);
+ }
+
+ throw new RuntimeError(expr.name, "Only instances have properties.");
+ }
+
+ public void interpret(List<Stmt> statements) {
+ try {
+ for (Stmt statement : statements) {
+ execute(statement);
+ }
+ } catch (RuntimeError error) {
+ Lox.runtimeError(error);
+ }
+ }
+}
diff --git a/jlox/src/com/craftinginterpreters/lox/Lox.java b/jlox/src/com/craftinginterpreters/lox/Lox.java
new file mode 100644
index 0000000..6115f8f
--- /dev/null
+++ b/jlox/src/com/craftinginterpreters/lox/Lox.java
@@ -0,0 +1,93 @@
+package com.craftinginterpreters.lox;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.List;
+
+public class Lox {
+ private static final Interpreter interpreter = new Interpreter();
+ public static boolean hadError = false;
+ public static boolean hadRuntimeError = false;
+
+ public static void main(String[] args) throws IOException {
+ if (args.length > 1) {
+ System.out.println("Usage: jlox [script]");
+ System.exit(64);
+ } else if (args.length == 1) {
+ runFile(args[0]);
+ } else {
+ runPrompt();
+ }
+ }
+
+ private static void runFile(String path) throws IOException {
+ byte[] bytes = Files.readAllBytes(Paths.get(path));
+ run(new String(bytes, Charset.defaultCharset()));
+
+ // Indicate an error in the exit code.
+ if (hadError)
+ System.exit(65);
+ if (hadRuntimeError)
+ System.exit(70);
+ }
+
+ private static void runPrompt() throws IOException {
+ InputStreamReader input = new InputStreamReader(System.in);
+ BufferedReader reader = new BufferedReader(input);
+
+ for (;;) {
+ System.out.print("> ");
+ String line = reader.readLine();
+ if (line == null)
+ break;
+ run(line);
+ hadError = false;
+ }
+ }
+
+ private static void run(String source) {
+ Scanner scanner = new Scanner(source);
+ List<Token> tokens = scanner.scanTokens();
+ Parser parser = new Parser(tokens);
+ List<Stmt> statements = parser.parse();
+
+ // Stop if there was a syntax error
+ if (hadError)
+ return;
+
+ Resolver resolver = new Resolver(interpreter);
+ resolver.resolve(statements);
+
+ // Stop if there was a resolution error
+ if (hadError)
+ return;
+
+ interpreter.interpret(statements);
+ }
+
+ public static void error(int line, String message) {
+ report(line, "", message);
+ }
+
+ private static void report(int line, String where, String message) {
+ System.err.println("[line " + line + "] Error" + where + ": " + message);
+ hadError = true;
+ }
+
+ public static void error(Token token, String message) {
+ if (token.type == TokenType.EOF) {
+ report(token.line, " at end", message);
+ } else {
+ report(token.line, " at '" + token.lexeme + "'", message);
+ }
+ }
+
+ public static void runtimeError(RuntimeError error) {
+ System.err.println(error.getMessage() + "\n[line " + error.token.line + "]");
+ hadRuntimeError = true;
+ }
+}
diff --git a/jlox/src/com/craftinginterpreters/lox/LoxCallable.java b/jlox/src/com/craftinginterpreters/lox/LoxCallable.java
new file mode 100644
index 0000000..4204c20
--- /dev/null
+++ b/jlox/src/com/craftinginterpreters/lox/LoxCallable.java
@@ -0,0 +1,9 @@
+package com.craftinginterpreters.lox;
+
+import java.util.List;
+
+interface LoxCallable {
+ int arity();
+
+ Object call(Interpreter interpreter, List<Object> arguments);
+}
diff --git a/jlox/src/com/craftinginterpreters/lox/LoxClass.java b/jlox/src/com/craftinginterpreters/lox/LoxClass.java
new file mode 100644
index 0000000..5d1c136
--- /dev/null
+++ b/jlox/src/com/craftinginterpreters/lox/LoxClass.java
@@ -0,0 +1,51 @@
+package com.craftinginterpreters.lox;
+
+import java.util.List;
+import java.util.Map;
+
+class LoxClass implements LoxCallable {
+ final String name;
+ final LoxClass superclass;
+ private final Map<String, LoxFunction> methods;
+
+ LoxClass(String name, LoxClass superclass, Map<String, LoxFunction> methods) {
+ this.superclass = superclass;
+ this.name = name;
+ this.methods = methods;
+ }
+
+ LoxFunction findMethod(String name) {
+ if (methods.containsKey(name)) {
+ return methods.get(name);
+ }
+
+ if (superclass != null) {
+ return superclass.findMethod(name);
+ }
+
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ @Override
+ public Object call(Interpreter interpreter, List<Object> arguments) {
+ LoxInstance instance = new LoxInstance(this);
+ LoxFunction initializer = findMethod("init");
+ if (initializer != null) {
+ initializer.bind(instance).call(interpreter, arguments);
+ }
+ return instance;
+ }
+
+
+ @Override
+ public int arity() {
+ LoxFunction initializer = findMethod("init");
+ if (initializer == null) return 0;
+ return initializer.arity();
+ }
+}
diff --git a/jlox/src/com/craftinginterpreters/lox/LoxFunction.java b/jlox/src/com/craftinginterpreters/lox/LoxFunction.java
new file mode 100644
index 0000000..07dd727
--- /dev/null
+++ b/jlox/src/com/craftinginterpreters/lox/LoxFunction.java
@@ -0,0 +1,52 @@
+package com.craftinginterpreters.lox;
+
+import java.util.List;
+
+class LoxFunction implements LoxCallable {
+ private final Stmt.Function declaration;
+ private final Environment closure;
+ private final boolean isInitializer;
+
+ LoxFunction(Stmt.Function declaration, Environment closure, boolean isInitializer) {
+ this.isInitializer = isInitializer;
+ this.closure = closure;
+ this.declaration = declaration;
+ }
+
+ LoxFunction bind(LoxInstance instance) {
+ Environment environment = new Environment(closure);
+ environment.define("this", instance);
+ return new LoxFunction(declaration, environment, isInitializer);
+ }
+
+ @Override
+ public int arity() {
+ return declaration.params.size();
+ }
+
+ @Override
+ public Object call(Interpreter interpreter, List<Object> arguments) {
+ Environment environment = new Environment(closure);
+
+ for (int i = 0; i < declaration.params.size(); i++) {
+ environment.define(declaration.params.get(i).lexeme, arguments.get(i));
+ }
+
+ try {
+ interpreter.executeBlock(declaration.body, environment);
+ } catch (Return returnValue) {
+ if (isInitializer)
+ return closure.getAt(0, "this");
+ return returnValue.value;
+ }
+
+ if (isInitializer)
+ return closure.getAt(0, "this");
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return "<fn " + declaration.name.lexeme + ">";
+ }
+}
diff --git a/jlox/src/com/craftinginterpreters/lox/LoxInstance.java b/jlox/src/com/craftinginterpreters/lox/LoxInstance.java
new file mode 100644
index 0000000..64989e0
--- /dev/null
+++ b/jlox/src/com/craftinginterpreters/lox/LoxInstance.java
@@ -0,0 +1,34 @@
+package com.craftinginterpreters.lox;
+
+import java.util.HashMap;
+import java.util.Map;
+
+class LoxInstance {
+ private LoxClass klass;
+ private final Map<String, Object> fields = new HashMap<>();
+
+ LoxInstance(LoxClass klass) {
+ this.klass = klass;
+ }
+
+ Object get(Token name) {
+ if (fields.containsKey(name.lexeme)) {
+ return fields.get(name.lexeme);
+ }
+
+ LoxFunction method = klass.findMethod(name.lexeme);
+ if (method != null)
+ return method.bind(this);
+
+ throw new RuntimeError(name, "Undefined proprety '" + name.lexeme + "'.");
+ }
+
+ void set(Token name, Object value) {
+ fields.put(name.lexeme, value);
+ }
+
+ @Override
+ public String toString() {
+ return klass.name + " instance";
+ }
+}
diff --git a/jlox/src/com/craftinginterpreters/lox/Parser.java b/jlox/src/com/craftinginterpreters/lox/Parser.java
new file mode 100644
index 0000000..ab4baaa
--- /dev/null
+++ b/jlox/src/com/craftinginterpreters/lox/Parser.java
@@ -0,0 +1,457 @@
+package com.craftinginterpreters.lox;
+
+import static com.craftinginterpreters.lox.TokenType.*;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+class Parser {
+ private static class ParseError extends RuntimeException {
+ }
+
+ private final List<Token> tokens;
+ private int current = 0;
+
+ Parser(List<Token> tokens) {
+ this.tokens = tokens;
+ }
+
+ public List<Stmt> parse() {
+ List<Stmt> statements = new ArrayList<>();
+
+ while (!isAtEnd()) {
+ statements.add(declaration());
+ }
+
+ return statements;
+ }
+
+ private Expr expression() {
+ return assignment();
+ }
+
+ private Stmt declaration() {
+ try {
+ if (match(CLASS))
+ return classDeclaration();
+ if (match(FUN))
+ return function("function");
+ if (match(VAR))
+ return varDeclaration();
+
+ return statement();
+ } catch (ParseError error) {
+ synchronize();
+ return null;
+ }
+ }
+
+ private Stmt classDeclaration() {
+ Token name = consume(IDENTIFIER, "Expect class name.");
+
+ Expr.Variable superclass = null;
+ if (match(LESS)) {
+ consume(IDENTIFIER, "Expect superclass name.");
+ superclass = new Expr.Variable(previous());
+ }
+
+ consume(LEFT_BRACE, "Expect '{' before class body.");
+
+ List<Stmt.Function> methods = new ArrayList<>();
+ while (!check(RIGHT_BRACE) && !isAtEnd()) {
+ methods.add(function("method"));
+ }
+
+ consume(RIGHT_BRACE, "Expect '}' after class body.");
+
+ return new Stmt.Class(name, superclass, methods);
+ }
+
+ private Stmt statement() {
+ if (match(FOR))
+ return forStatement();
+ if (match(IF))
+ return ifStatement();
+ if (match(PRINT))
+ return printStatement();
+ if (match(RETURN))
+ return returnStatement();
+ if (match(WHILE))
+ return whileStatement();
+ if (match(LEFT_BRACE))
+ return new Stmt.Block(block());
+
+ return expressionStatement();
+ }
+
+ private Stmt forStatement() {
+ consume(LEFT_PAREN, "Expect '(' after 'for'.");
+
+ Stmt initializer;
+ if (match(SEMICOLON)) {
+ initializer = null;
+ } else if (match(VAR)) {
+ initializer = varDeclaration();
+ } else {
+ initializer = expressionStatement();
+ }
+
+ Expr condition = null;
+ if (!check(SEMICOLON)) {
+ condition = expression();
+ }
+ consume(SEMICOLON, "Expect ';' after loop condition.");
+
+ Expr increment = null;
+ if (!check(RIGHT_PAREN)) {
+ increment = expression();
+ }
+ consume(RIGHT_PAREN, "Expect ')' after for clauses.");
+ Stmt body = statement();
+
+ if (increment != null) {
+ body = new Stmt.Block(Arrays.asList(body, new Stmt.Expression(increment)));
+ }
+
+ if (condition == null)
+ condition = new Expr.Literal(true);
+ body = new Stmt.While(condition, body);
+
+ if (initializer != null) {
+ body = new Stmt.Block(Arrays.asList(initializer, body));
+ }
+
+ return body;
+ }
+
+ private Stmt ifStatement() {
+ consume(LEFT_PAREN, "Expect '(' after 'if'.");
+ Expr condition = expression();
+ consume(RIGHT_PAREN, "Expect ')' after if condition.");
+
+ Stmt thenBranch = statement();
+ Stmt elseBranch = null;
+ if (match(ELSE)) {
+ elseBranch = statement();
+ }
+
+ return new Stmt.If(condition, thenBranch, elseBranch);
+ }
+
+ private Stmt printStatement() {
+ Expr value = expression();
+ consume(SEMICOLON, "Expect ';' after value.");
+ return new Stmt.Print(value);
+ }
+
+ private Stmt returnStatement() {
+ Token keyword = previous();
+ Expr value = null;
+ if (!check(SEMICOLON)) {
+ value = expression();
+ }
+
+ consume(SEMICOLON, "Expect ';' after return value.");
+ return new Stmt.Return(keyword, value);
+ }
+
+ private Stmt varDeclaration() {
+ Token name = consume(IDENTIFIER, "Expect variable name.");
+
+ Expr initializer = null;
+ if (match(EQUAL)) {
+ initializer = expression();
+ }
+
+ consume(SEMICOLON, "Expect ';' after variable declaration.");
+ return new Stmt.Var(name, initializer);
+ }
+
+ private Stmt whileStatement() {
+ consume(LEFT_PAREN, "Expect '(' after 'while'.");
+ Expr condition = expression();
+ consume(RIGHT_PAREN, "Expect ')' after condition.");
+ Stmt body = statement();
+
+ return new Stmt.While(condition, body);
+ }
+
+ private Stmt expressionStatement() {
+ Expr expr = expression();
+ consume(SEMICOLON, "Expect ';' after expression.");
+ return new Stmt.Expression(expr);
+ }
+
+ private Stmt.Function function(String kind) {
+ Token name = consume(IDENTIFIER, "Expect " + kind + " name.");
+ consume(LEFT_PAREN, "Expect '(' after " + kind + " name.");
+ List<Token> parameters = new ArrayList();
+ if (!check(RIGHT_PAREN)) {
+ do {
+ if (parameters.size() >= 255) {
+ error(peek(), "Can't have more than 255 parameters.");
+ }
+
+ parameters.add(consume(IDENTIFIER, "Expect parameter name."));
+ } while (match(COMMA));
+ }
+ consume(RIGHT_PAREN, "Expect ')' after parameters.");
+
+ consume(LEFT_BRACE, "Expect '{' before " + kind + " body.");
+ List<Stmt> body = block();
+ return new Stmt.Function(name, parameters, body);
+ }
+
+ private List<Stmt> block() {
+ List<Stmt> statements = new ArrayList<>();
+
+ while (!check(RIGHT_BRACE) && !isAtEnd()) {
+ statements.add(declaration());
+ }
+
+ consume(RIGHT_BRACE, "Expect '}' after block.");
+ return statements;
+ }
+
+ private Expr assignment() {
+ Expr expr = or();
+
+ if (match(EQUAL)) {
+ Token equals = previous();
+ Expr value = assignment();
+
+ if (expr instanceof Expr.Variable) {
+ Token name = ((Expr.Variable) expr).name;
+ return new Expr.Assign(name, value);
+ } else if (expr instanceof Expr.Get) {
+ Expr.Get get = (Expr.Get) expr;
+ return new Expr.Set(get.object, get.name, value);
+ }
+
+ error(equals, "Invalid assignment target.");
+ }
+
+ return expr;
+ }
+
+ private Expr or() {
+ Expr expr = and();
+
+ while (match(OR)) {
+ Token operator = previous();
+ Expr right = and();
+ expr = new Expr.Logical(expr, operator, right);
+ }
+
+ return expr;
+ }
+
+ private Expr and() {
+ Expr expr = equality();
+
+ while (match(AND)) {
+ Token operator = previous();
+ Expr right = equality();
+ expr = new Expr.Logical(expr, operator, right);
+ }
+
+ return expr;
+ }
+
+ private Expr equality() {
+ Expr expr = comparison();
+
+ while (match(BANG_EQUAL, EQUAL_EQUAL)) {
+ Token operator = previous();
+ Expr right = comparison();
+ expr = new Expr.Binary(expr, operator, right);
+ }
+
+ return expr;
+ }
+
+ private Expr comparison() {
+ Expr expr = term();
+
+ while (match(GREATER, GREATER_EQUAL, LESS, LESS_EQUAL)) {
+ Token operator = previous();
+ Expr right = term();
+ expr = new Expr.Binary(expr, operator, right);
+ }
+
+ return expr;
+ }
+
+ private Expr term() {
+ Expr expr = factor();
+
+ while (match(MINUS, PLUS)) {
+ Token operator = previous();
+ Expr right = factor();
+ expr = new Expr.Binary(expr, operator, right);
+ }
+
+ return expr;
+ }
+
+ private Expr factor() {
+ Expr expr = unary();
+
+ while (match(SLASH, STAR)) {
+ Token operator = previous();
+ Expr right = unary();
+ expr = new Expr.Binary(expr, operator, right);
+ }
+
+ return expr;
+ }
+
+ private Expr unary() {
+ if (match(BANG, MINUS)) {
+ Token operator = previous();
+ Expr right = unary();
+ return new Expr.Unary(operator, right);
+ }
+
+ return call();
+ }
+
+ private Expr finishCall(Expr callee) {
+ List<Expr> arguments = new ArrayList<>();
+ if (!check(RIGHT_PAREN)) {
+ do {
+ if (arguments.size() >= 255) {
+ error(peek(), "Can't have more than 255 arguments.");
+ }
+ arguments.add(expression());
+ } while (match(COMMA));
+ }
+
+ Token paren = consume(RIGHT_PAREN, "Expect ')' after arguments.");
+
+ return new Expr.Call(callee, paren, arguments);
+ }
+
+ private Expr call() {
+ Expr expr = primary();
+
+ while (true) {
+ if (match(LEFT_PAREN)) {
+ expr = finishCall(expr);
+ } else if (match(DOT)) {
+ Token name = consume(IDENTIFIER, "Expect property name after '.'");
+ expr = new Expr.Get(expr, name);
+ } else {
+ break;
+ }
+ }
+
+ return expr;
+ }
+
+ private Expr primary() {
+ if (match(FALSE))
+ return new Expr.Literal(false);
+ if (match(TRUE))
+ return new Expr.Literal(true);
+ if (match(NIL))
+ return new Expr.Literal(null);
+
+ if (match(NUMBER, STRING)) {
+ return new Expr.Literal(previous().literal);
+ }
+
+ if (match(SUPER)) {
+ Token keyword = previous();
+ consume(DOT, "Expect '.' after 'super'.");
+ Token method = consume(IDENTIFIER, "Expect superclass method name.");
+ return new Expr.Super(keyword, method);
+ }
+
+ if (match(THIS))
+ return new Expr.This(previous());
+
+ if (match(IDENTIFIER)) {
+ return new Expr.Variable(previous());
+ }
+
+ if (match(LEFT_PAREN)) {
+ Expr expr = expression();
+ consume(RIGHT_PAREN, "Expect ')' after expression.");
+ return new Expr.Grouping(expr);
+ }
+
+ throw error(peek(), "Expect expression.");
+ }
+
+ private boolean match(TokenType... types) {
+ for (TokenType type : types) {
+ if (check(type)) {
+ advance();
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private Token consume(TokenType type, String message) {
+ if (check(type))
+ return advance();
+
+ throw error(peek(), message);
+ }
+
+ private boolean check(TokenType type) {
+ if (isAtEnd())
+ return false;
+ return peek().type == type;
+ }
+
+ private Token advance() {
+ if (!isAtEnd())
+ current++;
+ return previous();
+ }
+
+ private boolean isAtEnd() {
+ return peek().type == EOF;
+ }
+
+ private Token peek() {
+ return tokens.get(current);
+ }
+
+ private Token previous() {
+ return tokens.get(current - 1);
+ }
+
+ private ParseError error(Token token, String message) {
+ Lox.error(token, message);
+ return new ParseError();
+ }
+
+ private void synchronize() {
+ advance();
+
+ while (!isAtEnd()) {
+ if (previous().type == SEMICOLON)
+ return;
+
+ switch (peek().type) {
+ case CLASS:
+ case FUN:
+ case VAR:
+ case FOR:
+ case IF:
+ case WHILE:
+ case PRINT:
+ case RETURN:
+ return;
+ }
+
+ advance();
+ }
+ }
+}
diff --git a/jlox/src/com/craftinginterpreters/lox/Resolver.java b/jlox/src/com/craftinginterpreters/lox/Resolver.java
new file mode 100644
index 0000000..fe3a641
--- /dev/null
+++ b/jlox/src/com/craftinginterpreters/lox/Resolver.java
@@ -0,0 +1,299 @@
+package com.craftinginterpreters.lox;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Stack;
+
+class Resolver implements Expr.Visitor<Void>, Stmt.Visitor<Void> {
+ private final Interpreter interpreter;
+ private final Stack<Map<String, Boolean>> scopes = new Stack<>();
+ private FunctionType currentFunction = FunctionType.NONE;
+
+ Resolver(Interpreter interpreter) {
+ this.interpreter = interpreter;
+ }
+
+ private enum FunctionType {
+ NONE, FUNCTION, INITIALIZER, METHOD
+ }
+
+ private enum ClassType {
+ NONE, CLASS, SUBCLASS
+ }
+
+ private ClassType currentClass = ClassType.NONE;
+
+ public void resolve(List<Stmt> statements) {
+ for (Stmt statement : statements) {
+ resolve(statement);
+ }
+ }
+
+ @Override
+ public Void visitBlockStmt(Stmt.Block stmt) {
+ beginScope();
+ resolve(stmt.statements);
+ endScope();
+ return null;
+ }
+
+ @Override
+ public Void visitClassStmt(Stmt.Class stmt) {
+ ClassType enclosingClass = currentClass;
+ currentClass = ClassType.CLASS;
+
+ declare(stmt.name);
+ define(stmt.name);
+
+ if (stmt.superclass != null && stmt.name.lexeme.equals(stmt.superclass.name.lexeme)) {
+ Lox.error(stmt.superclass.name, "A class can't inherit from itself.");
+ }
+
+ if (stmt.superclass != null) {
+ currentClass = ClassType.SUBCLASS;
+
+ resolve(stmt.superclass);
+ }
+
+ if (stmt.superclass != null) {
+ beginScope();
+ scopes.peek().put("super", true);
+ }
+
+ beginScope();
+ scopes.peek().put("this", true);
+
+ for (Stmt.Function method : stmt.methods) {
+ FunctionType declaration = FunctionType.METHOD;
+ if (method.name.lexeme.equals("init")) {
+ declaration = FunctionType.INITIALIZER;
+ }
+ resolveFunction(method, declaration);
+ }
+
+ endScope();
+
+ if (stmt.superclass != null) {
+ endScope();
+ }
+
+ currentClass = enclosingClass;
+ return null;
+ }
+
+ @Override
+ public Void visitExpressionStmt(Stmt.Expression stmt) {
+ resolve(stmt.expression);
+ return null;
+ }
+
+ @Override
+ public Void visitFunctionStmt(Stmt.Function stmt) {
+ declare(stmt.name);
+ define(stmt.name);
+
+ resolveFunction(stmt, FunctionType.FUNCTION);
+ return null;
+ }
+
+ @Override
+ public Void visitIfStmt(Stmt.If stmt) {
+ resolve(stmt.condition);
+ resolve(stmt.thenBranch);
+ if (stmt.elseBranch != null)
+ resolve(stmt.elseBranch);
+ return null;
+ }
+
+ @Override
+ public Void visitPrintStmt(Stmt.Print stmt) {
+ resolve(stmt.expression);
+ return null;
+ }
+
+ @Override
+ public Void visitReturnStmt(Stmt.Return stmt) {
+ if (currentFunction == FunctionType.NONE) {
+ Lox.error(stmt.keyword, "Can't return from top-level code.");
+ }
+
+ if (stmt.value != null) {
+ if (currentFunction == FunctionType.INITIALIZER) {
+ Lox.error(stmt.keyword, "Can't return a value from an initializer.");
+ }
+ resolve(stmt.value);
+ }
+
+ return null;
+ }
+
+ @Override
+ public Void visitVarStmt(Stmt.Var stmt) {
+ declare(stmt.name);
+ if (stmt.initializer != null) {
+ resolve(stmt.initializer);
+ }
+ define(stmt.name);
+ return null;
+ }
+
+ @Override
+ public Void visitWhileStmt(Stmt.While stmt) {
+ resolve(stmt.condition);
+ resolve(stmt.body);
+ return null;
+ }
+
+ @Override
+ public Void visitAssignExpr(Expr.Assign expr) {
+ resolve(expr.value);
+ resolveLocal(expr, expr.name);
+ return null;
+ }
+
+ @Override
+ public Void visitBinaryExpr(Expr.Binary expr) {
+ resolve(expr.left);
+ resolve(expr.right);
+ return null;
+ }
+
+ @Override
+ public Void visitCallExpr(Expr.Call expr) {
+ resolve(expr.callee);
+
+ for (Expr argument : expr.arguments) {
+ resolve(argument);
+ }
+
+ return null;
+ }
+
+ @Override
+ public Void visitGetExpr(Expr.Get expr) {
+ resolve(expr.object);
+ return null;
+ }
+
+ @Override
+ public Void visitGroupingExpr(Expr.Grouping expr) {
+ resolve(expr.expression);
+ return null;
+ }
+
+ @Override
+ public Void visitLiteralExpr(Expr.Literal expr) {
+ return null;
+ }
+
+ @Override
+ public Void visitSetExpr(Expr.Set expr) {
+ resolve(expr.value);
+ resolve(expr.object);
+ return null;
+ }
+
+ @Override
+ public Void visitSuperExpr(Expr.Super expr) {
+ if (currentClass == ClassType.NONE) {
+ Lox.error(expr.keyword, "Can't use 'super' outside of a class.");
+ } else if (currentClass != ClassType.SUBCLASS) {
+ Lox.error(expr.keyword, "Can't use 'super' in a class with no superclass.");
+ }
+
+ resolveLocal(expr, expr.keyword);
+ return null;
+ }
+
+ @Override
+ public Void visitThisExpr(Expr.This expr) {
+ if (currentClass == ClassType.NONE) {
+ Lox.error(expr.keyword, "Can't use 'this' outside of a class.");
+ return null;
+ }
+
+ resolveLocal(expr, expr.keyword);
+ return null;
+ }
+
+ @Override
+ public Void visitLogicalExpr(Expr.Logical expr) {
+ resolve(expr.left);
+ resolve(expr.right);
+ return null;
+ }
+
+ @Override
+ public Void visitUnaryExpr(Expr.Unary expr) {
+ resolve(expr.right);
+ return null;
+ }
+
+ @Override
+ public Void visitVariableExpr(Expr.Variable expr) {
+ if (!scopes.isEmpty() && scopes.peek().get(expr.name.lexeme) == Boolean.FALSE) {
+ Lox.error(expr.name, "Can't read local variable in its own initializer.");
+ }
+
+ resolveLocal(expr, expr.name);
+ return null;
+ }
+
+ private void resolve(Stmt stmt) {
+ stmt.accept(this);
+ }
+
+ private void resolve(Expr expr) {
+ expr.accept(this);
+ }
+
+ private void resolveFunction(Stmt.Function function, FunctionType type) {
+ FunctionType enclosingFunction = currentFunction;
+ currentFunction = type;
+
+ beginScope();
+ for (Token param : function.params) {
+ declare(param);
+ define(param);
+ }
+ resolve(function.body);
+ endScope();
+ currentFunction = enclosingFunction;
+ }
+
+ private void beginScope() {
+ scopes.push(new HashMap<String, Boolean>());
+ }
+
+ private void endScope() {
+ scopes.pop();
+
+ }
+
+ private void declare(Token name) {
+ if (scopes.isEmpty())
+ return;
+
+ Map<String, Boolean> scope = scopes.peek();
+ if (scope.containsKey(name.lexeme)) {
+ Lox.error(name, "Already variable with this name in this scope.");
+ }
+ scope.put(name.lexeme, false);
+ }
+
+ private void define(Token name) {
+ if (scopes.isEmpty())
+ return;
+ scopes.peek().put(name.lexeme, true);
+ }
+
+ private void resolveLocal(Expr expr, Token name) {
+ for (int i = scopes.size() - 1; i >= 0; i--) {
+ if (scopes.get(i).containsKey(name.lexeme)) {
+ interpreter.resolve(expr, scopes.size() - 1 - i);
+ return;
+ }
+ }
+ }
+}
diff --git a/jlox/src/com/craftinginterpreters/lox/Return.java b/jlox/src/com/craftinginterpreters/lox/Return.java
new file mode 100644
index 0000000..5bd62d0
--- /dev/null
+++ b/jlox/src/com/craftinginterpreters/lox/Return.java
@@ -0,0 +1,10 @@
+package com.craftinginterpreters.lox;
+
+class Return extends RuntimeException {
+ final Object value;
+
+ Return(Object value) {
+ super(null, null, false, false);
+ this.value = value;
+ }
+}
diff --git a/jlox/src/com/craftinginterpreters/lox/RuntimeError.java b/jlox/src/com/craftinginterpreters/lox/RuntimeError.java
new file mode 100644
index 0000000..017865b
--- /dev/null
+++ b/jlox/src/com/craftinginterpreters/lox/RuntimeError.java
@@ -0,0 +1,10 @@
+package com.craftinginterpreters.lox;
+
+class RuntimeError extends RuntimeException {
+ final Token token;
+
+ RuntimeError(Token token, String message) {
+ super(message);
+ this.token = token;
+ }
+}
diff --git a/jlox/src/com/craftinginterpreters/lox/Scanner.java b/jlox/src/com/craftinginterpreters/lox/Scanner.java
new file mode 100644
index 0000000..e21f701
--- /dev/null
+++ b/jlox/src/com/craftinginterpreters/lox/Scanner.java
@@ -0,0 +1,207 @@
+package com.craftinginterpreters.lox;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static com.craftinginterpreters.lox.TokenType.*;
+
+class Scanner {
+ private final String source;
+ private final List<Token> tokens = new ArrayList<>();
+ private int start = 0;
+ private int current = 0;
+ private int line = 1;
+ private static final Map<String, TokenType> keywords;
+
+ static {
+ keywords = new HashMap<>();
+ keywords.put("and", AND);
+ keywords.put("class", CLASS);
+ keywords.put("else", ELSE);
+ keywords.put("false", FALSE);
+ keywords.put("for", FOR);
+ keywords.put("fun", FUN);
+ keywords.put("if", IF);
+ keywords.put("nil", NIL);
+ keywords.put("or", OR);
+ keywords.put("print", PRINT);
+ keywords.put("return", RETURN);
+ keywords.put("super", SUPER);
+ keywords.put("this", THIS);
+ keywords.put("true", TRUE);
+ keywords.put("var", VAR);
+ keywords.put("while", WHILE);
+ }
+
+ Scanner(String source) {
+ this.source = source;
+ }
+
+ List<Token> scanTokens() {
+ while (!isAtEnd()) {
+ // We are at the beginning of the next lexeme.
+ start = current;
+ scanToken();
+ }
+
+ tokens.add(new Token(EOF, "", null, line));
+ return tokens;
+ }
+
+ private void scanToken() {
+ char c = advance();
+
+ switch (c) {
+ case '(': addToken(LEFT_PAREN); break;
+ case ')': addToken(RIGHT_PAREN); break;
+ case '{': addToken(LEFT_BRACE); break;
+ case '}': addToken(RIGHT_BRACE); break;
+ case ',': addToken(COMMA); break;
+ case '.': addToken(DOT); break;
+ case '-': addToken(MINUS); break;
+ case '+': addToken(PLUS); break;
+ case ';': addToken(SEMICOLON); break;
+ case '*': addToken(STAR); break;
+ case '!':
+ addToken(match('=') ? BANG_EQUAL : BANG);
+ break;
+ case '=':
+ addToken(match('=') ? EQUAL_EQUAL : EQUAL);
+ break;
+ case '<':
+ addToken(match('=') ? LESS_EQUAL : LESS);
+ break;
+ case '>':
+ addToken(match('=') ? GREATER_EQUAL : GREATER);
+ break;
+ case '/':
+ if (match('/')) {
+ // A comment goes until the end of the line.
+ while (peek() != '\n' && !isAtEnd()) advance();
+ } else {
+ addToken(SLASH);
+ }
+ break;
+
+ case ' ':
+ case '\r':
+ case '\t':
+ // Ignore whitespace.
+ break;
+
+ // I guess this code isn't meant to run on Mac OS 9 and before.
+ case '\n':
+ line++;
+ break;
+
+ case '"': string(); break;
+
+ default:
+ if (isDigit(c)) {
+ number();
+ } else if (isAlpha(c)) {
+ identifier();
+ } else {
+ Lox.error(line, "Unexpected character.");
+ }
+ break;
+ }
+ }
+
+ private void identifier() {
+ while (isAlphaNumeric(peek())) advance();
+
+ String text = source.substring(start, current);
+ TokenType type = keywords.get(text);
+ if (type == null) type = IDENTIFIER;
+ addToken(type);
+ }
+
+ private void number() {
+ while (isDigit(peek())) advance();
+
+ // Look for a fractional part.
+ if (peek() == '.' && isDigit(peekNext())) {
+ // Consume the "."
+ advance();
+
+ while (isDigit(peek())) advance();
+ }
+
+ addToken(NUMBER,
+ Double.parseDouble(source.substring(start, current)));
+ }
+
+ // I guess we won't be able to include escaped characters in the string? At
+ // least not escaped " characters.
+ private void string() {
+ while (peek() != '"' && !isAtEnd()) {
+ if (peek() == '\n') line++;
+ advance();
+ }
+
+ if (isAtEnd()) {
+ Lox.error(line, "Unterminated string.");
+ return;
+ }
+
+ // The closing ".
+ advance();
+
+ // Trim the surrounding quotes.
+ String value = source.substring(start + 1, current - 1);
+ addToken(STRING, value);
+ }
+
+ private boolean match(char expected) {
+ if (isAtEnd()) return false;
+ if (source.charAt(current) != expected) return false;
+
+ current++;
+ return true;
+ }
+
+ private char peek() {
+ if (isAtEnd()) return '\0';
+ return source.charAt(current);
+ }
+
+ private char peekNext() {
+ if (current + 1 >= source.length()) return '\0';
+ return source.charAt(current + 1);
+ }
+
+ private boolean isAlpha(char c) {
+ return (c >= 'a' && c <= 'z') ||
+ (c >= 'A' && c <= 'Z') ||
+ c == '_';
+ }
+
+ private boolean isAlphaNumeric(char c) {
+ return isAlpha(c) || isDigit(c);
+ }
+
+ private boolean isDigit(char c) {
+ return c >= '0' && c <= '9';
+ }
+
+ private boolean isAtEnd() {
+ return current >= source.length();
+ }
+
+ private char advance() {
+ current++;
+ return source.charAt(current - 1);
+ }
+
+ private void addToken(TokenType type) {
+ addToken(type, null);
+ }
+
+ private void addToken(TokenType type, Object literal) {
+ String text = source.substring(start, current);
+ tokens.add(new Token(type, text, literal, line));
+ }
+}
diff --git a/jlox/src/com/craftinginterpreters/lox/Token.java b/jlox/src/com/craftinginterpreters/lox/Token.java
new file mode 100644
index 0000000..84762a4
--- /dev/null
+++ b/jlox/src/com/craftinginterpreters/lox/Token.java
@@ -0,0 +1,19 @@
+package com.craftinginterpreters.lox;
+
+class Token {
+ final TokenType type;
+ final String lexeme;
+ final Object literal;
+ final int line;
+
+ Token(TokenType type, String lexeme, Object literal, int line) {
+ this.type = type;
+ this.lexeme = lexeme;
+ this.literal = literal;
+ this.line = line;
+ }
+
+ public String toString() {
+ return type + " " + lexeme + " " + literal;
+ }
+}
diff --git a/jlox/src/com/craftinginterpreters/lox/TokenType.java b/jlox/src/com/craftinginterpreters/lox/TokenType.java
new file mode 100644
index 0000000..9ab2a8b
--- /dev/null
+++ b/jlox/src/com/craftinginterpreters/lox/TokenType.java
@@ -0,0 +1,20 @@
+package com.craftinginterpreters.lox;
+
+enum TokenType {
+ // Single-character tokens.
+ LEFT_PAREN, RIGHT_PAREN, LEFT_BRACE, RIGHT_BRACE, COMMA, DOT, MINUS, PLUS,
+ SEMICOLON, SLASH, STAR,
+
+ // One or two character tokens.
+ BANG, BANG_EQUAL, EQUAL, EQUAL_EQUAL, GREATER, GREATER_EQUAL, LESS,
+ LESS_EQUAL,
+
+ // Literals.
+ IDENTIFIER, STRING, NUMBER,
+
+ // Keywords.
+ AND, CLASS, ELSE, FALSE, FUN, FOR, IF, NIL, OR, PRINT, RETURN, SUPER, THIS,
+ TRUE, VAR, WHILE,
+
+ EOF
+}
diff --git a/jlox/src/com/craftinginterpreters/tool/CMakeLists.txt b/jlox/src/com/craftinginterpreters/tool/CMakeLists.txt
new file mode 100644
index 0000000..b98d2eb
--- /dev/null
+++ b/jlox/src/com/craftinginterpreters/tool/CMakeLists.txt
@@ -0,0 +1,3 @@
+add_jar(GenerateAst
+ GenerateAst.java
+ ENTRY_POINT com/craftinginterpreters/tool/GenerateAst)
diff --git a/jlox/src/com/craftinginterpreters/tool/GenerateAst.java b/jlox/src/com/craftinginterpreters/tool/GenerateAst.java
new file mode 100644
index 0000000..04b4500
--- /dev/null
+++ b/jlox/src/com/craftinginterpreters/tool/GenerateAst.java
@@ -0,0 +1,100 @@
+package com.craftinginterpreters.tool;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.List;
+
+public class GenerateAst {
+ public static void main(String[] args) throws IOException {
+ if (args.length != 1) {
+ System.err.println("Usage: generate_ast <output directory>");
+ System.exit(64);
+ }
+
+ String outputDir = args[0];
+
+ defineAst(outputDir, "Expr",
+ Arrays.asList("Assign : Token name, Expr value", "Binary : Expr left, Token operator, Expr right",
+ "Call : Expr callee, Token paren, List<Expr> arguments", "Get : Expr object, Token name",
+ "Grouping : Expr expression", "Literal : Object value",
+ "Logical : Expr left, Token operator, Expr right", "Set : Expr object, Token name, Expr value",
+ "Super : Token keyword, Token method", "This : Token keyword",
+ "Unary : Token operator, Expr right", "Variable : Token name"));
+ defineAst(outputDir, "Stmt",
+ Arrays.asList("Block : List<Stmt> statements",
+ "Class : Token name, Expr.Variable superclass, List<Stmt.Function> methods",
+ "Expression : Expr expression", "Function : Token name, List<Token> params, List<Stmt> body",
+ "If : Expr condition, Stmt thenBranch, Stmt elseBranch", "Print : Expr expression",
+ "Return : Token keyword, Expr value", "Var : Token name, Expr initializer",
+ "While : Expr condition, Stmt body"));
+ }
+
+ private static void defineAst(String outputDir, String baseName, List<String> types) throws IOException {
+ String path = outputDir + "/" + baseName + ".java";
+ PrintWriter writer = new PrintWriter(path);
+
+ writer.println("package com.craftinginterpreters.lox;");
+ writer.println();
+ writer.println("import java.util.List;");
+ writer.println();
+ writer.println("abstract class " + baseName + " {");
+
+ defineVisitor(writer, baseName, types);
+
+ for (String type : types) {
+ String className = type.split(":")[0].trim();
+ String fields = type.split(":")[1].trim();
+ defineType(writer, baseName, className, fields);
+ }
+
+ // The base accept() method
+ writer.println();
+ writer.println(" abstract <R> R accept(Visitor<R> visitor);");
+
+ writer.println("}");
+ writer.close();
+ }
+
+ private static void defineVisitor(PrintWriter writer, String baseName, List<String> types) {
+ writer.println(" interface Visitor<R> {");
+
+ for (String type : types) {
+ String typeName = type.split(":")[0].trim();
+ writer.println(" R visit" + typeName + baseName + "(" + typeName + " " + baseName.toLowerCase() + ");");
+ }
+
+ writer.println(" }");
+ }
+
+ private static void defineType(PrintWriter writer, String baseName, String className, String fieldList) {
+ writer.println(" static class " + className + " extends " + baseName + " {");
+
+ // Constructor
+ writer.println(" " + className + "(" + fieldList + ") {");
+
+ // Store parameters in fields.
+ String[] fields = fieldList.split(", ");
+ for (String field : fields) {
+ String name = field.split(" ")[1];
+ writer.println(" this." + name + " = " + name + ";");
+ }
+
+ writer.println(" }");
+
+ // Visitor pattern.
+ writer.println();
+ writer.println(" @Override");
+ writer.println(" <R> R accept(Visitor<R> visitor) {");
+ writer.println(" return visitor.visit" + className + baseName + "(this);");
+ writer.println(" }");
+
+ // Fields
+ writer.println();
+ for (String field : fields) {
+ writer.println(" final " + field + ";");
+ }
+
+ writer.println(" }");
+ }
+}