From 3a6e4e7833c27f7d5d4289071f78d145eb779437 Mon Sep 17 00:00:00 2001 From: Michael Thomson Date: Wed, 9 Apr 2025 20:17:32 -0400 Subject: [PATCH] Chapter 4: Scanning --- .../com/craftinginterpreters/lox/Lox.java | 4 +- .../com/craftinginterpreters/lox/Scanner.java | 190 +++++++++++++++++- 2 files changed, 181 insertions(+), 13 deletions(-) diff --git a/jlox/src/main/java/com/craftinginterpreters/lox/Lox.java b/jlox/src/main/java/com/craftinginterpreters/lox/Lox.java index f7505be..edc6089 100644 --- a/jlox/src/main/java/com/craftinginterpreters/lox/Lox.java +++ b/jlox/src/main/java/com/craftinginterpreters/lox/Lox.java @@ -36,7 +36,7 @@ public class Lox { BufferedReader reader = new BufferedReader(input); for (;;) { - System.out.println("> "); + System.out.print("> "); String line = reader.readLine(); if (line == null) break; run(line); @@ -58,7 +58,7 @@ public class Lox { } private static void report(int line, String where, String message) { - System.err.println("[line " + line + "] Error" + where + ": " + message);); + System.err.println("[line " + line + "] Error" + where + ": " + message); hadError = true; } } diff --git a/jlox/src/main/java/com/craftinginterpreters/lox/Scanner.java b/jlox/src/main/java/com/craftinginterpreters/lox/Scanner.java index 33cae28..64e3d9c 100644 --- a/jlox/src/main/java/com/craftinginterpreters/lox/Scanner.java +++ b/jlox/src/main/java/com/craftinginterpreters/lox/Scanner.java @@ -1,7 +1,9 @@ package com.craftinginterpreters.lox; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; class Scanner { private final String source; @@ -10,6 +12,28 @@ class Scanner { 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); + } + public Scanner(String source) { this.source = source; } @@ -27,19 +51,163 @@ class Scanner { 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(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); + break; + 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(); + + // Look for a fractional part + if (peek() == '.' && isDigit(peekNext())) { + advance(); + + while (isDigit(peek())) + advance(); + } + + addToken(TokenType.NUMBER, Double.parseDouble(source.substring(start, current))); + } + + private void string() { + while (peek() != '"' && !isAtEnd()) { + if (peek() == '\n') + line++; + advance(); + } + + if (isAtEnd()) { + Lox.error(line, "Unterminated string."); + return; + } + + // the closing " + advance(); + + // trim quotes + 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 boolean isAtEnd() { return current >= source.length(); } @@ -53,7 +221,7 @@ class Scanner { } private void addToken(TokenType type, Object literal) { - String text = source.substring(start, current); + String text = source.substring(start, current); tokens.add(new Token(type, text, literal, line)); } }