From 68a2ebd34fc94488e89ffb82b359ec6e7e152ae9 Mon Sep 17 00:00:00 2001 From: Tom Willemse Date: Thu, 8 Jul 2021 00:14:31 -0700 Subject: Restructure project to make room for clox --- jlox/src/com/craftinginterpreters/lox/.gitignore | 1 + .../lox/.settings/org.eclipse.jdt.core.prefs | 2 + .../com/craftinginterpreters/lox/AstPrinter.java | 147 +++++++ .../com/craftinginterpreters/lox/CMakeLists.txt | 28 ++ .../com/craftinginterpreters/lox/Environment.java | 63 +++ .../com/craftinginterpreters/lox/Interpreter.java | 392 ++++++++++++++++++ jlox/src/com/craftinginterpreters/lox/Lox.java | 93 +++++ .../com/craftinginterpreters/lox/LoxCallable.java | 9 + .../src/com/craftinginterpreters/lox/LoxClass.java | 51 +++ .../com/craftinginterpreters/lox/LoxFunction.java | 52 +++ .../com/craftinginterpreters/lox/LoxInstance.java | 34 ++ jlox/src/com/craftinginterpreters/lox/Parser.java | 457 +++++++++++++++++++++ .../src/com/craftinginterpreters/lox/Resolver.java | 299 ++++++++++++++ jlox/src/com/craftinginterpreters/lox/Return.java | 10 + .../com/craftinginterpreters/lox/RuntimeError.java | 10 + jlox/src/com/craftinginterpreters/lox/Scanner.java | 207 ++++++++++ jlox/src/com/craftinginterpreters/lox/Token.java | 19 + .../com/craftinginterpreters/lox/TokenType.java | 20 + 18 files changed, 1894 insertions(+) create mode 100644 jlox/src/com/craftinginterpreters/lox/.gitignore create mode 100644 jlox/src/com/craftinginterpreters/lox/.settings/org.eclipse.jdt.core.prefs create mode 100644 jlox/src/com/craftinginterpreters/lox/AstPrinter.java create mode 100644 jlox/src/com/craftinginterpreters/lox/CMakeLists.txt create mode 100644 jlox/src/com/craftinginterpreters/lox/Environment.java create mode 100644 jlox/src/com/craftinginterpreters/lox/Interpreter.java create mode 100644 jlox/src/com/craftinginterpreters/lox/Lox.java create mode 100644 jlox/src/com/craftinginterpreters/lox/LoxCallable.java create mode 100644 jlox/src/com/craftinginterpreters/lox/LoxClass.java create mode 100644 jlox/src/com/craftinginterpreters/lox/LoxFunction.java create mode 100644 jlox/src/com/craftinginterpreters/lox/LoxInstance.java create mode 100644 jlox/src/com/craftinginterpreters/lox/Parser.java create mode 100644 jlox/src/com/craftinginterpreters/lox/Resolver.java create mode 100644 jlox/src/com/craftinginterpreters/lox/Return.java create mode 100644 jlox/src/com/craftinginterpreters/lox/RuntimeError.java create mode 100644 jlox/src/com/craftinginterpreters/lox/Scanner.java create mode 100644 jlox/src/com/craftinginterpreters/lox/Token.java create mode 100644 jlox/src/com/craftinginterpreters/lox/TokenType.java (limited to 'jlox/src/com/craftinginterpreters/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, Stmt.Visitor { + 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 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 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 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, Stmt.Visitor { + final Environment globals = new Environment(); + private Environment environment = globals; + private final Map locals = new HashMap<>(); + + Interpreter() { + globals.define("clock", new LoxCallable() { + @Override + public int arity() { + return 0; + } + + @Override + public Object call(Interpreter interpreter, List arguments) { + return (double) System.currentTimeMillis() / 1000.0; + } + + @Override + public String toString() { + return ""; + } + }); + } + + @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 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 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 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 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 tokens = scanner.scanTokens(); + Parser parser = new Parser(tokens); + List 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 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 methods; + + LoxClass(String name, LoxClass superclass, Map 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 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 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 ""; + } +} 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 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 tokens; + private int current = 0; + + Parser(List tokens) { + this.tokens = tokens; + } + + public List parse() { + List 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 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 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 body = block(); + return new Stmt.Function(name, parameters, body); + } + + private List block() { + List 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 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, Stmt.Visitor { + private final Interpreter interpreter; + private final Stack> 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 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()); + } + + private void endScope() { + scopes.pop(); + + } + + private void declare(Token name) { + if (scopes.isEmpty()) + return; + + Map 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 tokens = new ArrayList<>(); + private int start = 0; + private int current = 0; + private int line = 1; + private static final Map 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 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 +} -- cgit v1.2.3-54-g00ecf