summaryrefslogtreecommitdiffstats
path: root/src/com/craftinginterpreters/lox/Scanner.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/craftinginterpreters/lox/Scanner.java')
-rw-r--r--src/com/craftinginterpreters/lox/Scanner.java207
1 files changed, 207 insertions, 0 deletions
diff --git a/src/com/craftinginterpreters/lox/Scanner.java b/src/com/craftinginterpreters/lox/Scanner.java
new file mode 100644
index 0000000..e21f701
--- /dev/null
+++ b/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<Token> tokens = new ArrayList<>();
+ private int start = 0;
+ private int current = 0;
+ private int line = 1;
+ private static final Map<String, TokenType> 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<Token> 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));
+ }
+}