diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fe235c4..73e10d5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -34,7 +34,7 @@ java { application { // Define the main class for the application. - mainClass = "org.example.App" + mainClass = "org.tuxlanginterpreter.App" } tasks.named("test") { diff --git a/app/src/main/java/org/example/App.java b/app/src/main/java/org/example/App.java deleted file mode 100644 index e7e1af9..0000000 --- a/app/src/main/java/org/example/App.java +++ /dev/null @@ -1,14 +0,0 @@ -/* - * This source file was generated by the Gradle 'init' task - */ -package org.example; - -public class App { - public String getGreeting() { - return "Hello World!"; - } - - public static void main(String[] args) { - System.out.println(new App().getGreeting()); - } -} diff --git a/app/src/main/java/org/tuxlanginterpreter/App.java b/app/src/main/java/org/tuxlanginterpreter/App.java new file mode 100644 index 0000000..4ed1458 --- /dev/null +++ b/app/src/main/java/org/tuxlanginterpreter/App.java @@ -0,0 +1,64 @@ +package org.tuxlanginterpreter; + +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 App { + static boolean hadError = false; + + public static void main(String[] args) throws IOException { + if (args.length > 1) { + System.out.println("Usage: txli [script]"); + System.exit(64); + } else if (args.length == 1) { + run_file(args[0]); + } else { + run_prompt(); + } + } + + private static void run_file(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); + } + + private static void run_prompt() 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.scan_tokens(); + + // For now, just print the tokens. + for (Token token : tokens) { + System.out.println(token); + } + } + + 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; + } +} diff --git a/app/src/main/java/org/tuxlanginterpreter/Scanner.java b/app/src/main/java/org/tuxlanginterpreter/Scanner.java new file mode 100644 index 0000000..52bb0b1 --- /dev/null +++ b/app/src/main/java/org/tuxlanginterpreter/Scanner.java @@ -0,0 +1,154 @@ +package org.tuxlanginterpreter; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +class Scanner { + private final String source; + private final List tokens = new ArrayList<>(); + private int start = 0; + private int current = 0; + private int line = 1; + + Scanner(String source) { + this.source = source; + } + + private boolean is_at_end() { + return current >= source.length(); + } + + List scan_tokens() { + while (!is_at_end()) { + // We are at the beginning of the next lexeme. + start = current; + scan_token(); + } + + tokens.add(new Token(TokenType.EOF, "", null, line)); + return tokens; + } + + private void scan_token() { + char c = advance(); + switch (c) { + case '(': add_token(TokenType.LEFT_PAREN); break; + case ')': add_token(TokenType.RIGHT_PAREN); break; + case '{': add_token(TokenType.LEFT_BRACE); break; + case '}': add_token(TokenType.RIGHT_BRACE); break; + case ',': add_token(TokenType.COMMA); break; + case '.': add_token(TokenType.DOT); break; + case '-': add_token(TokenType.MINUS); break; + case '+': add_token(TokenType.PLUS); break; + case ';': add_token(TokenType.SEMICOLON); break; + case '*': add_token(TokenType.STAR); break; + case '!': + add_token(match('=') ? TokenType.BANG_EQUAL : TokenType.BANG); + break; + case '=': + add_token(match('=') ? TokenType.EQUAL_EQUAL : TokenType.EQUAL); + break; + case '<': + add_token(match('=') ? TokenType.LESS_EQUAL : TokenType.LESS); + break; + case '>': + add_token(match('=') ? TokenType.GREATER_EQUAL : TokenType.GREATER); + break; + case '/': + if (match('/')) { + // A comment goes until the end of the line. + while (peek() != '\n' && !is_at_end()) advance(); + } else { + add_token(TokenType.SLASH); + } + break; + case ' ': + case '\r': + case '\t': + // Ignore whitespace. + break; + case '\n': + line++; + break; + case '"': string(); break; + + default: + if (is_digit(c)) { + number(); + } else { + App.error(line, "Unexpected character."); + } + break; + } + } + + private boolean is_digit(char c) { + return c >= '0' && c <= '9'; + } + + private void number() { + while (is_digit(peek())) advance(); + + // Look for a fractional part. + if (peek() == '.' && is_digit(peek_next())) { + // Consume the "." + advance(); + + while (is_digit(peek())) advance(); + } + + add_token(TokenType.NUMBER, Double.parseDouble(source.substring(start, current))); + } + + private void string() { + while (peek() != '"' && !is_at_end()) { + if (peek() == '\n') line++; + advance(); + } + + if (is_at_end()) { + App.error(line, "Unterminated string."); + return; + } + + // The closing ". + advance(); + + // Trim the surrounding quotes. + String value = source.substring(start + 1, current - 1); + add_token(TokenType.STRING, value); + } + + private char advance() { + return source.charAt(current++); + } + + private char peek() { + if (is_at_end()) return '\0'; + return source.charAt(current); + } + + private char peek_next() { + if (current + 1 >= source.length()) return '\0'; + return source.charAt(current + 1); + } + + private boolean match(char expected) { + if (is_at_end()) return false; + if (source.charAt(current) != expected) return false; + + current++; + return true; + } + + private void add_token(TokenType type) { + add_token(type, null); + } + + private void add_token(TokenType type, Object literal) { + String text = source.substring(start, current); + tokens.add(new Token(type, text, literal, line)); + } +} diff --git a/app/src/main/java/org/tuxlanginterpreter/Token.java b/app/src/main/java/org/tuxlanginterpreter/Token.java new file mode 100644 index 0000000..c97b435 --- /dev/null +++ b/app/src/main/java/org/tuxlanginterpreter/Token.java @@ -0,0 +1,40 @@ +package org.tuxlanginterpreter; + +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 +} + +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/app/src/test/java/org/example/AppTest.java b/app/src/test/java/org/tuxlanginterpreter/AppTest.java similarity index 75% rename from app/src/test/java/org/example/AppTest.java rename to app/src/test/java/org/tuxlanginterpreter/AppTest.java index f5ce33d..eb9754d 100644 --- a/app/src/test/java/org/example/AppTest.java +++ b/app/src/test/java/org/tuxlanginterpreter/AppTest.java @@ -1,14 +1,13 @@ -/* - * This source file was generated by the Gradle 'init' task - */ -package org.example; +package org.tuxlanginterpreter; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class AppTest { + /* @Test void appHasAGreeting() { App classUnderTest = new App(); assertNotNull(classUnderTest.getGreeting(), "app should have a greeting"); } + */ }