diff --git a/jlox/build.gradle.kts b/jlox/build.gradle.kts index e6ff126..b9fa29b 100644 --- a/jlox/build.gradle.kts +++ b/jlox/build.gradle.kts @@ -15,10 +15,20 @@ dependencies { } application { - mainClass.set("xyz.ctsk.jlox.Hello") + mainClass.set("xyz.ctsk.lox.Lox") +} + +tasks.jar { + manifest { + attributes(mapOf("Main-Class" to "xyz.ctsk.lox.Lox")) + } } tasks.getByName("test") { useJUnitPlatform() +} + +tasks.named("run") { + standardInput = System.`in` } \ No newline at end of file diff --git a/jlox/src/main/java/xyz/ctsk/jlox/Hello.java b/jlox/src/main/java/xyz/ctsk/jlox/Hello.java deleted file mode 100644 index 34b4f9c..0000000 --- a/jlox/src/main/java/xyz/ctsk/jlox/Hello.java +++ /dev/null @@ -1,7 +0,0 @@ -package xyz.ctsk.jlox; - -public class Hello { - public static void main(String[] arg) { - System.out.println("Hello World"); - } -} diff --git a/jlox/src/main/java/xyz/ctsk/lox/Lox.java b/jlox/src/main/java/xyz/ctsk/lox/Lox.java new file mode 100644 index 0000000..88a4a69 --- /dev/null +++ b/jlox/src/main/java/xyz/ctsk/lox/Lox.java @@ -0,0 +1,62 @@ +package xyz.ctsk.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; + +public class Lox { + private static boolean hadError = false; + private static void run(String source) { + var scanner = new Scanner(source); + scanner.scanTokens().forEach(System.out::println); + } + + static void error(int line, String message) { + report(line, "", message); + } + + private static void report(int line, String where, String message) { + System.err.printf("[line %d] Error %s: %s%n", line, where, message); + hadError = true; + } + + + private static void runPrompt() throws IOException { + var input = new InputStreamReader(System.in); + var reader = new BufferedReader(input); + + while (true) { + System.out.println("> "); + String line = reader.readLine(); + if (line == null) break; + run(line); + hadError = false; + } + }; + + 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); + } + } + + private static void printUsage() { + System.out.println("Usage: jlox [script]"); + } + + public static void main(String[] args) throws IOException { + if (args.length == 0) { + runPrompt(); + } else if (args.length == 1) { + runFile(args[0]); + } else { + printUsage(); + } + } +} diff --git a/jlox/src/main/java/xyz/ctsk/lox/Scanner.java b/jlox/src/main/java/xyz/ctsk/lox/Scanner.java new file mode 100644 index 0000000..12c0b5e --- /dev/null +++ b/jlox/src/main/java/xyz/ctsk/lox/Scanner.java @@ -0,0 +1,203 @@ +package xyz.ctsk.lox; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static java.util.Map.entry; +import static xyz.ctsk.lox.TokenType.*; + +public class Scanner { + private static final Map keywords = + Map.ofEntries( + entry("and", AND), + entry("class", CLASS), + entry("else", ELSE), + entry("false", FALSE), + entry("for", FOR), + entry("fun", FUN), + entry("if", IF), + entry("nil", NIL), + entry("or", OR), + entry("print", PRINT), + entry("return", RETURN), + entry("super", SUPER), + entry("this", THIS), + entry("true", TRUE), + entry("var", VAR), + entry("while", WHILE) + ); + + private final String source; + private final List tokens = new ArrayList<>(); + + private int start = 0; + private int current = 0; + private int line = 1; + + public Scanner(String source) { + this.source = source; + } + + private String currentMatch() { + return source.substring(start, current); + } + + private void addToken(TokenType type, Object literal) { + tokens.add(new Token(type, currentMatch(), literal, line)); + } + + private void addToken(TokenType type) { + addToken(type, null); + } + + + private boolean isAtEnd() { + return current >= source.length(); + } + private char advance() { + return source.charAt(current++); + } + private char peek() { + return isAtEnd() ? '\0' : source.charAt(current); + } + private boolean match(char expected) { + if (isAtEnd()) { + return false; + } + + if (source.charAt(current) != expected) { + return false; + } + + current++; + return true; + } + + private void string() { + while (peek() != '"' && !isAtEnd()) { + if (peek() == '\n') line++; + advance(); + } + + if (isAtEnd()) { + Lox.error(line, "Unterminated string."); + return; + } + + advance(); + + String value = source.substring(start + 1, current - 1); + addToken(STRING, value); + } + + private char peekNext() { + if (current + 1 >= source.length()) return '\0'; + return source.charAt(current + 1); + } + + private static boolean isDigit(char c) { + return '0' <= c && c <= '9'; + } + + private static boolean isAlpha(char c) { + return ('a' <= c && c <= 'z') || + ('A' <= c && c <= 'Z') || + c == '_'; + } + + private static boolean isAlphaNumeric(char c) { + return isDigit(c) || isAlpha(c); + } + + private static boolean isKeyword(String s) { + return keywords.containsKey(s); + } + + private void number() { + while (isDigit(peek())) advance(); + + // Look for a fractional part. + if (peek() == '.' && isDigit(peekNext())) { + // Consume the "." + advance(); + + while (isDigit(peek())) advance(); + } + + // Enhancement: Do not allow Letters in numbers + if (isAlpha(peek())) { + Lox.error(line, "Unexpected character in number."); + return; + } + + addToken(NUMBER, + Double.parseDouble(currentMatch())); + } + + + private void identifier() { + while (isAlphaNumeric(peek())) advance(); + + var text = currentMatch(); + if (isKeyword(text)) { + addToken(keywords.get(text)); + } else { + addToken(IDENTIFIER); + } + } + + private void scanToken() { + var c = advance(); + switch (c) { + case '(' -> addToken(LEFT_PAREN); + case ')' -> addToken(RIGHT_PAREN); + case '{' -> addToken(LEFT_BRACE); + case '}' -> addToken(RIGHT_BRACE); + case ',' -> addToken(COMMA); + case '.' -> addToken(DOT); + case '-' -> addToken(MINUS); + case '+' -> addToken(PLUS); + case ';' -> addToken(SEMICOLON); + case '*' -> addToken(STAR); + + case '!' -> addToken(match('=') ? BANG_EQUAL : BANG); + case '=' -> addToken(match('=') ? EQUAL_EQUAL : EQUAL); + case '<' -> addToken(match('=') ? LESS_EQUAL : LESS); + case '>' -> addToken(match('=') ? GREATER_EQUAL : GREATER); + + case '/' -> { + if (match('/')) { + while (peek() != '\n' && !isAtEnd()) advance(); + } else { + addToken(SLASH); + } + } + + case ' ', '\r', '\t' -> {} + case '\n' -> line++; + + case '"' -> string(); + + default -> { + if (isDigit(c)) { + number(); + } else if (isAlpha(c)) { + identifier(); + } else { + Lox.error(line, "Unexpected character."); + } + } + } + } + + List scanTokens() { + while (!isAtEnd()) { + start = current; + scanToken(); + } + + tokens.add(new Token(EOF, "", line)); + return tokens; + } +} diff --git a/jlox/src/main/java/xyz/ctsk/lox/Token.java b/jlox/src/main/java/xyz/ctsk/lox/Token.java new file mode 100644 index 0000000..5561fb1 --- /dev/null +++ b/jlox/src/main/java/xyz/ctsk/lox/Token.java @@ -0,0 +1,16 @@ +package xyz.ctsk.lox; + +public record Token(TokenType type, String lexeme, Object literal, int line) { + public Token(TokenType type, String lexeme, int line) { + this(type, lexeme, null, line); + } + + @Override + public String toString() { + if (literal == null) { + return "%s %s".formatted(type, lexeme); + } else { + return "%s %s %s".formatted(type, lexeme, literal); + } + } +}; diff --git a/jlox/src/main/java/xyz/ctsk/lox/TokenType.java b/jlox/src/main/java/xyz/ctsk/lox/TokenType.java new file mode 100644 index 0000000..8daedb1 --- /dev/null +++ b/jlox/src/main/java/xyz/ctsk/lox/TokenType.java @@ -0,0 +1,22 @@ +package xyz.ctsk.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 +}