From e08bfb24080b4607ea6a4515add30bf49133f27c Mon Sep 17 00:00:00 2001 From: Daniel Ledda Date: Sun, 13 Jul 2025 16:43:07 +0200 Subject: [PATCH] first commit --- .gitignore | 1 + .idea/.gitignore | 8 ++ .idea/misc.xml | 6 + .idea/modules.xml | 8 ++ .idea/uiDesigner.xml | 124 ++++++++++++++++ jlox.iml | 11 ++ src/de/djledda/lox/AstPrinter.java | 55 ++++++++ src/de/djledda/lox/Expression.java | 71 ++++++++++ src/de/djledda/lox/Interpreter.java | 134 ++++++++++++++++++ src/de/djledda/lox/Lox.java | 82 +++++++++++ src/de/djledda/lox/Parser.java | 186 ++++++++++++++++++++++++ src/de/djledda/lox/RuntimeError.java | 10 ++ src/de/djledda/lox/Scanner.java | 203 +++++++++++++++++++++++++++ src/de/djledda/lox/Statement.java | 37 +++++ src/de/djledda/lox/Token.java | 21 +++ src/de/djledda/lox/TokenType.java | 52 +++++++ src/de/djledda/tool/GenerateAst.java | 87 ++++++++++++ 17 files changed, 1096 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/uiDesigner.xml create mode 100644 jlox.iml create mode 100644 src/de/djledda/lox/AstPrinter.java create mode 100644 src/de/djledda/lox/Expression.java create mode 100644 src/de/djledda/lox/Interpreter.java create mode 100644 src/de/djledda/lox/Lox.java create mode 100644 src/de/djledda/lox/Parser.java create mode 100644 src/de/djledda/lox/RuntimeError.java create mode 100644 src/de/djledda/lox/Scanner.java create mode 100644 src/de/djledda/lox/Statement.java create mode 100644 src/de/djledda/lox/Token.java create mode 100644 src/de/djledda/lox/TokenType.java create mode 100644 src/de/djledda/tool/GenerateAst.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1fcb152 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +out diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..73f69e0 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..e0844bc --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..36c2d43 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..e96534f --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jlox.iml b/jlox.iml new file mode 100644 index 0000000..c90834f --- /dev/null +++ b/jlox.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/de/djledda/lox/AstPrinter.java b/src/de/djledda/lox/AstPrinter.java new file mode 100644 index 0000000..b3eae81 --- /dev/null +++ b/src/de/djledda/lox/AstPrinter.java @@ -0,0 +1,55 @@ +package de.djledda.lox; + +class AstPrinter implements Expression.Visitor { + public static void main(String[] args) { + Expression expression = new Expression.Binary( + new Expression.Unary( + new Token(TokenType.MINUS, "-", null, 1), + new Expression.Literal(123)), + new Token(TokenType.STAR, "*", null, 1), + new Expression.Grouping( + new Expression.Literal(45.67))); + + System.out.println(new AstPrinter().print(expression)); + } + + String print(Expression expression) { + return expression.accept(this); + } + + private String parenthesise(String name, Expression... expressions) { + StringBuilder builder = new StringBuilder(); + + builder.append("(").append(name); + for (Expression expression : expressions) { + builder.append(" "); + builder.append(expression.accept(this)); + } + builder.append(")"); + + return builder.toString(); + } + + @Override + public String visitBinaryExpression(Expression.Binary expression) { + return parenthesise(expression.operator.lexeme, expression.left, expression.right); + } + + @Override + public String visitGroupingExpression(Expression.Grouping expression) { + return parenthesise("group", expression.expression); + } + + @Override + public String visitLiteralExpression(Expression.Literal expression) { + if (expression.value == null) { + return "nil"; + } + return expression.value.toString(); + } + + @Override + public String visitUnaryExpression(Expression.Unary expression) { + return parenthesise(expression.operator.lexeme, expression.right); + } +} diff --git a/src/de/djledda/lox/Expression.java b/src/de/djledda/lox/Expression.java new file mode 100644 index 0000000..86d503c --- /dev/null +++ b/src/de/djledda/lox/Expression.java @@ -0,0 +1,71 @@ +package de.djledda.lox; + +abstract class Expression { + interface Visitor { + R visitBinaryExpression(Binary expression); + R visitGroupingExpression(Grouping expression); + R visitLiteralExpression(Literal expression); + R visitUnaryExpression(Unary expression); + } + + static class Binary extends Expression { + final Expression left; + final Token operator; + final Expression right; + + Binary(Expression left, Token operator, Expression right) { + this.left = left; + this.operator = operator; + this.right = right; + } + + @Override + R accept(Visitor visitor) { + return visitor.visitBinaryExpression(this); + } + } + + static class Grouping extends Expression { + final Expression expression; + + Grouping(Expression expression) { + this.expression = expression; + } + + @Override + R accept(Visitor visitor) { + return visitor.visitGroupingExpression(this); + } + } + + static class Literal extends Expression { + final Object value; + + Literal(Object value) { + this.value = value; + } + + @Override + R accept(Visitor visitor) { + return visitor.visitLiteralExpression(this); + } + } + + static class Unary extends Expression { + final Token operator; + final Expression right; + + Unary(Token operator, Expression right) { + this.operator = operator; + this.right = right; + } + + @Override + R accept(Visitor visitor) { + return visitor.visitUnaryExpression(this); + } + } + + + abstract R accept(Visitor visitor); +} diff --git a/src/de/djledda/lox/Interpreter.java b/src/de/djledda/lox/Interpreter.java new file mode 100644 index 0000000..37937a7 --- /dev/null +++ b/src/de/djledda/lox/Interpreter.java @@ -0,0 +1,134 @@ +package de.djledda.lox; + +class Interpreter implements Expression.Visitor, Statement.Visitor { + void interpret(Expression expression) { + try { + Object value = evaluate(expression); + System.out.println(stringify(value)); + } catch (RuntimeError error) { + Lox.runtimeError(error); + } + } + + 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(); + } + + private Object evaluate(Expression expression) { + return expression.accept(this); + } + + private Boolean isTruthy(Object object) { + if (object == null) { + return false; + } + if (object instanceof Boolean) { + return (boolean)object; + } + return true; + } + + private Boolean isEqual(Object left, Object right) { + if (left == null && right == null) { + return true; + } + if (left == null) { + return false; + } + return left.equals(right); + } + + 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 both be numbers."); + } + + @Override + public Object visitGroupingExpression(Expression.Grouping expression) { + return evaluate(expression.expression); + } + + @Override + public Object visitLiteralExpression(Expression.Literal expression) { + return expression.value; + } + + @Override + public Object visitUnaryExpression(Expression.Unary expression) { + Object right = evaluate(expression.right); + switch (expression.operator.type) { + case MINUS: + checkNumberOperand(expression.operator, right); + return -(double) right; + case BANG: + return !isTruthy(right); + } + return null; + } + + @Override + public Object visitBinaryExpression(Expression.Binary expression) { + Object left = evaluate(expression.left); + Object right = evaluate(expression.right); + + switch (expression.operator.type) { + case GREATER: + checkNumberOperands(expression.operator, left, right); + return (double)left > (double)right; + case GREATER_EQUAL: + checkNumberOperands(expression.operator, left, right); + return (double)left >= (double)right; + case LESS: + checkNumberOperands(expression.operator, left, right); + return (double)left < (double)right; + case LESS_EQUAL: + checkNumberOperands(expression.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(expression.operator, left, right); + return (double)left - (double)right; + case PLUS: + checkNumberOperands(expression.operator, left, right); + if (left instanceof Double && right instanceof Double) { + return left + right.toString(); + } + if (left instanceof String && right instanceof String) { + return left + right.toString(); + } + break; + case SLASH: + checkNumberOperands(expression.operator, left, right); + if ((double)right == 0) { + throw new RuntimeError(expression.operator, "Divide by zero error."); + } + return (double)left / (double)right; + case STAR: + checkNumberOperands(expression.operator, left, right); + return (double)left * (double)right; + } + throw new RuntimeError(expression.operator, "Operands must be two numbers or two strings."); + } +} diff --git a/src/de/djledda/lox/Lox.java b/src/de/djledda/lox/Lox.java new file mode 100644 index 0000000..1b7300c --- /dev/null +++ b/src/de/djledda/lox/Lox.java @@ -0,0 +1,82 @@ +package de.djledda.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(); + static boolean hadError = false; + 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())); + 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); + while (true) { + 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); + if (hadError) { + return; + } + } + + static void error(int line, String message) { + report(line, "", message); + } + + 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); + } + } + + static void runtimeError(RuntimeError error) { + System.err.println(error.getMessage() + "\n[line " + error.token.line + "]"); + hadRuntimeError = true; + } + + private static void report(int line, String where, String message) { + System.err.println("[line " + line + "] Error" + where + ": " + message); + hadError = true; + } +} diff --git a/src/de/djledda/lox/Parser.java b/src/de/djledda/lox/Parser.java new file mode 100644 index 0000000..31a9629 --- /dev/null +++ b/src/de/djledda/lox/Parser.java @@ -0,0 +1,186 @@ +package de.djledda.lox; + +import java.util.ArrayList; +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; + } + + List parse() { + List statements = new ArrayList<>(); + while (!isAtEnd()) { + statements.add(statement()); + } + return statements; + } + + private Statement statement() { + if (match(TokenType.PRINT)) { + return printStatement(); + } + return expressionStatement(); + } + + private Statement printStatement() { + Expression value = expression(); + consume(TokenType.SEMICOLON, "';' after value expected."); + return new Statement.Print(value); + } + + private Statement expressionStatement() { + Expression expression = expression(); + consume(TokenType.SEMICOLON, "';' after expression expected."); + return new Statement.Expression(expression); + } + + + private Expression expression() { + return equality(); + } + + private Expression equality() { + Expression expression = comparison(); + while (match(TokenType.BANG_EQUAL, TokenType.EQUAL_EQUAL)) { + Token operator = previous(); + Expression right = comparison(); + expression = new Expression.Binary(expression, operator, right); + } + return expression; + } + + private Expression comparison() { + Expression expression = term(); + while (match(TokenType.GREATER, TokenType.GREATER_EQUAL, TokenType.LESS, TokenType.LESS_EQUAL)) { + Token operator = previous(); + Expression right = term(); + expression = new Expression.Binary(expression, operator, right); + } + return expression; + } + + private Expression term() { + Expression expression = factor(); + while (match(TokenType.MINUS, TokenType.PLUS)) { + Token operator = previous(); + Expression right = factor(); + expression = new Expression.Binary(expression, operator, right); + } + return expression; + } + + private Expression factor() { + Expression expression = unary(); + while (match(TokenType.SLASH, TokenType.STAR)) { + Token operator = previous(); + Expression right = unary(); + expression = new Expression.Binary(expression, operator, right); + } + return expression; + } + + private Expression unary() { + if (match(TokenType.BANG, TokenType.MINUS)) { + Token operator = previous(); + Expression right = unary(); + return new Expression.Unary(operator, right); + } + return primary(); + } + + private Expression primary() { + if (match(TokenType.FALSE)) { + return new Expression.Literal(false); + } + if (match(TokenType.TRUE)) { + return new Expression.Literal(true); + } + if (match(TokenType.NIL)) { + return new Expression.Literal(null); + } + if (match(TokenType.NUMBER, TokenType.STRING)) { + return new Expression.Literal(previous().literal); + } + if (match(TokenType.LEFT_PAREN)) { + Expression expression = expression(); + consume(TokenType.RIGHT_PAREN, "')' after expression expected."); + return new Expression.Grouping(expression); + } + throw error(peek(), "Expression expected."); + } + + private boolean match(TokenType... types) { + for (TokenType type : types) { + if (check(type)) { + advance(); + return true; + } + } + return false; + } + + 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 == TokenType.EOF; + } + + private Token peek() { + return tokens.get(current); + } + + private Token previous() { + return tokens.get(current - 1); + } + + private Token consume(TokenType type, String message) { + if (check(type)) { + return advance(); + } + throw error(peek(), message); + } + + private ParseError error(Token token, String message) { + Lox.error(token, message); + return new ParseError(); + } + + private void synchronise() { + advance(); + while (!isAtEnd()) { + if (previous().type == TokenType.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/src/de/djledda/lox/RuntimeError.java b/src/de/djledda/lox/RuntimeError.java new file mode 100644 index 0000000..c5a8d47 --- /dev/null +++ b/src/de/djledda/lox/RuntimeError.java @@ -0,0 +1,10 @@ +package de.djledda.lox; + +class RuntimeError extends RuntimeException { + final Token token; + + RuntimeError(Token token, String message) { + super(message); + this.token = token; + } +} diff --git a/src/de/djledda/lox/Scanner.java b/src/de/djledda/lox/Scanner.java new file mode 100644 index 0000000..d666701 --- /dev/null +++ b/src/de/djledda/lox/Scanner.java @@ -0,0 +1,203 @@ +package de.djledda.lox; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public 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", TokenType.AND); + keywords.put("class", TokenType.CLASS); + keywords.put("else", TokenType.ELSE); + keywords.put("false", TokenType.FALSE); + keywords.put("for", TokenType.FOR); + keywords.put("fun", TokenType.FUN); + keywords.put("if", TokenType.IF); + keywords.put("nil", TokenType.NIL); + keywords.put("or", TokenType.OR); + keywords.put("print", TokenType.PRINT); + keywords.put("return", TokenType.RETURN); + keywords.put("super", TokenType.SUPER); + keywords.put("this", TokenType.THIS); + keywords.put("true", TokenType.TRUE); + keywords.put("var", TokenType.VAR); + keywords.put("while", TokenType.WHILE); + } + + Scanner(String source) { + this.source = source; + } + + List scanTokens() { + while (!isAtEnd()) { + start = current; + scanToken(); + } + + tokens.add(new Token(TokenType.EOF, "", null, line)); + return tokens; + } + + private boolean isAtEnd() { + return current >= source.length(); + } + + private void scanToken() { + char c = advance(); + switch(c) { + case '(': addToken(TokenType.LEFT_PAREN); break; + case ')': addToken(TokenType.RIGHT_PAREN); break; + case '{': addToken(TokenType.LEFT_BRACE); break; + case '}': addToken(TokenType.RIGHT_BRACE); break; + case ',': addToken(TokenType.COMMA); break; + case '.': addToken(TokenType.DOT); break; + case '-': addToken(TokenType.MINUS); break; + case '+': addToken(TokenType.PLUS); break; + case ';': addToken(TokenType.SEMICOLON); break; + case '*': addToken(TokenType.STAR); break; + case '!': + addToken(match('=') ? TokenType.BANG_EQUAL : TokenType.BANG); + break; + case '=': + addToken(match('=') ? TokenType.EQUAL_EQUAL : TokenType.EQUAL); + break; + case '<': + addToken(match('=') ? TokenType.LESS_EQUAL : TokenType.LESS); + break; + case '>': + addToken(match('=') ? TokenType.GREATER_EQUAL : TokenType.GREATER); + case '/': + if (match('/')) { + while (peek() != '\n' && !isAtEnd()) { + advance(); + } + } else { + addToken(TokenType.SLASH); + } + break; + case ' ': + case '\r': + case '\t': + break; + 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 = TokenType.IDENTIFIER; + } + addToken(type); + } + + private void number() { + while (isDigit(peek())) { + advance(); + } + if (peek() == '.' && isDigit(peekNext())) { + advance(); + while (isDigit(peek())) { + advance(); + } + } + addToken(TokenType.NUMBER, Double.parseDouble(source.substring(start, current))); + } + + private void string() { + while (peek() != '"') { + if (isAtEnd()) { + Lox.error(line, "Unterminated string."); + return; + } + if (peek() == '\n') { + line++; + } + advance(); + } + advance(); + + String value = source.substring(start + 1, current - 1); + addToken(TokenType.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 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/src/de/djledda/lox/Statement.java b/src/de/djledda/lox/Statement.java new file mode 100644 index 0000000..657bd58 --- /dev/null +++ b/src/de/djledda/lox/Statement.java @@ -0,0 +1,37 @@ +package de.djledda.lox; + +abstract class Statement { + interface Visitor { + R visitExpressionStatement(Expression statement); + R visitPrintStatement(Print statement); + } + + static class Expression extends Statement { + final Expression expression; + + Expression(Expression expression) { + this.expression = expression; + } + + @Override + R accept(Visitor visitor) { + return visitor.visitExpressionStatement(this); + } + } + + static class Print extends Statement { + final Expression expression; + + Print(Expression expression) { + this.expression = expression; + } + + @Override + R accept(Visitor visitor) { + return visitor.visitPrintStatement(this); + } + } + + + abstract R accept(Visitor visitor); +} diff --git a/src/de/djledda/lox/Token.java b/src/de/djledda/lox/Token.java new file mode 100644 index 0000000..bbeeb0f --- /dev/null +++ b/src/de/djledda/lox/Token.java @@ -0,0 +1,21 @@ +package de.djledda.lox; + +public 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/src/de/djledda/lox/TokenType.java b/src/de/djledda/lox/TokenType.java new file mode 100644 index 0000000..9a1509c --- /dev/null +++ b/src/de/djledda/lox/TokenType.java @@ -0,0 +1,52 @@ +package de.djledda.lox; + +public 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 + EOF +} diff --git a/src/de/djledda/tool/GenerateAst.java b/src/de/djledda/tool/GenerateAst.java new file mode 100644 index 0000000..ad8aab3 --- /dev/null +++ b/src/de/djledda/tool/GenerateAst.java @@ -0,0 +1,87 @@ +package de.djledda.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 "); + System.exit(64); + } + String outputDir = args[0]; + + defineAst(outputDir, "Expression", Arrays.asList( + "Binary : Expression left, Token operator, Expression right", + "Grouping : Expression expression", + "Literal : Object value", + "Unary : Token operator, Expression right" + )); + + defineAst(outputDir, "Statement", Arrays.asList( + "Expression : Expression expression", + "Print : Expression expression" + )); + } + + private static void defineAst(String outputDir, String baseName, List types) throws IOException { + String path = outputDir + "/" + baseName + ".java"; + PrintWriter writer = new PrintWriter(path, "UTF-8"); + + writer.println("package de.djledda.lox;"); + writer.println(); + writer.println("import java.util.List;"); + writer.println(); + writer.println("abstract class " + baseName + " {"); + + defineVisitor(writer, baseName, types); + writer.println(); + + for (String type : types) { + String className = type.split(":")[0].trim(); + String fields = type.split(":")[1].trim(); + defineType(writer, baseName, className, fields); + } + + writer.println(); + writer.println("\tabstract R accept(Visitor visitor);"); + + writer.println("}"); + writer.close(); + } + + private static void defineType(PrintWriter writer, String baseName, String className, String fieldList) { + writer.println("\tstatic class " + className + " extends " + baseName + " {"); + String[] fields = fieldList.split(", "); + for (String field : fields) { + writer.println("\t\tfinal " + field + ";"); + } + writer.println(); + writer.println("\t\t" + className + "(" + fieldList + ") {"); + for (String field : fields) { + String name = field.split(" ")[1]; + writer.println("\t\t\tthis." + name + " = " + name + ";"); + } + writer.println("\t\t}"); + writer.println(); + writer.println("\t\t@Override"); + writer.println("\t\t R accept(Visitor visitor) {"); + writer.println("\t\t\treturn visitor.visit" + className + baseName + "(this);"); + writer.println("\t\t}"); + writer.println("\t}"); + writer.println(); + } + + private static void defineVisitor(PrintWriter writer, String baseName, List types) { + writer.println("\tinterface Visitor {"); + + for (String type : types) { + String typeName = type.split(":")[0].trim(); + writer.println("\t\tR visit" + typeName + baseName + "(" + typeName + " " + baseName.toLowerCase() + ");"); + } + + writer.println("\t}"); + } +}