commit 413070b52ff8ec387b559d48d4e3b1d2b2c4f0c4
Author: Szymon Mikulicz <szymon.mikulicz@posteo.net>
Date: Fri, 9 Sep 2022 16:20:44 +0200
Inital commit
Diffstat:
9 files changed, 357 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,16 @@
+.dub
+docs.json
+__dummy.html
+docs/
+/lox
+lox.so
+lox.dylib
+lox.dll
+lox.a
+lox.lib
+lox-test-*
+*.exe
+*.o
+*.obj
+*.lst
+*.dlox_history
diff --git a/dlox b/dlox
Binary files differ.
diff --git a/dub.json b/dub.json
@@ -0,0 +1,12 @@
+{
+ "authors": [
+ "Ashymad"
+ ],
+ "copyright": "Copyright © 2022, Ashymad",
+ "dependencies": {
+ "linenoise": "~>1.1.0"
+ },
+ "description": "lox interpreter in D",
+ "license": "MIT",
+ "name": "dlox"
+}
+\ No newline at end of file
diff --git a/dub.selections.json b/dub.selections.json
@@ -0,0 +1,6 @@
+{
+ "fileVersion": 1,
+ "versions": {
+ "linenoise": "1.1.0+1.0.0"
+ }
+}
diff --git a/source/app.d b/source/app.d
@@ -0,0 +1,80 @@
+import std.stdio;
+import std.file;
+import std.container;
+import token;
+import scaner;
+import deimos.linenoise;
+import std.string;
+import std.conv;
+import keywords;
+
+int main(string[] args) {
+ Lox lox = new Lox;
+ if (args.length > 2) {
+ writeln("Usage: dlox [script]");
+ return 64;
+ } else if (args.length == 2) {
+ lox.runFile(args[1]);
+ } else {
+ lox.runPrompt();
+ }
+ if (lox.hadError) return 65;
+ return 0;
+}
+
+class Lox {
+ static bool hadError = false;
+
+ extern(C) static void completion(const char *buf, linenoiseCompletions *lc) {
+ auto bufs = fromStringz(buf);
+ auto lastsp = lastIndexOfAny(bufs, [' ', '\t']) + 1;
+ auto klen = bufs.length - lastsp;
+ if (bufs.length <= 0) return;
+
+ foreach (key; keywords.keywords.keys) {
+ if (klen <= key.length && bufs[lastsp..bufs.length] == key[0..klen]) {
+ linenoiseAddCompletion(lc, toStringz(bufs[0..lastsp] ~ key));
+ }
+ }
+ }
+
+ void runFile(string file) {
+ run(to!(char[])(readText(file)));
+ }
+
+ void runPrompt() {
+ char* line;
+ const char* history = ".dlox_history";
+
+ linenoiseSetCompletionCallback(&completion);
+ linenoiseHistoryLoad(history);
+
+ while((line = linenoise("lox> ")) !is null) {
+ if (line[0] != '\0' && line[0] != '/') {
+ linenoiseHistoryAdd(line);
+ linenoiseHistorySave(history);
+ }
+ run(fromStringz(line));
+ hadError = false;
+ }
+ }
+
+ void run(char[] source) {
+ Scanner scanner = new Scanner(source);
+
+ auto tokens = scanner.scanTokens();
+ foreach (token; tokens) {
+ writeln(token);
+ }
+ }
+
+ static void error(int line, string msg) {
+ report(line, "", msg);
+ }
+
+ static void report(int line, string where, string msg) {
+ writefln("[line %s] Error%s: %s", line, where, msg);
+ hadError = true;
+ }
+}
+
diff --git a/source/keywords.d b/source/keywords.d
@@ -0,0 +1,26 @@
+import tokentype;
+
+static immutable TokenType[const(char)[]] keywords;
+
+shared static this() {
+ with (TokenType) {
+ keywords = [
+ "and": AND,
+ "class": CLASS,
+ "else": ELSE,
+ "false": FALSE,
+ "for": FOR,
+ "fun": FUN,
+ "if": IF,
+ "nil": NIL,
+ "or": OR,
+ "print": PRINT,
+ "return": RETURN,
+ "super": SUPER,
+ "this": THIS,
+ "true": TRUE,
+ "var": VAR,
+ "while": WHILE,
+ ];
+ }
+}
diff --git a/source/scaner.d b/source/scaner.d
@@ -0,0 +1,171 @@
+import token;
+import tokentype;
+import std.container;
+import std.format;
+import std.conv;
+import app;
+
+class Scanner {
+ private string source;
+ private int start = 0;
+ private int current = 0;
+ private int line = 0;
+
+ private Array!Token tokens;
+
+ this(string source) {
+ this.source = source;
+ this.tokens = Array!Token();
+ }
+
+ this(char[] source) {
+ this.source = to!string(source);
+ this.tokens = Array!Token();
+ }
+
+ Array!Token scanTokens() {
+ while (!isAtEnd()) {
+ start = current;
+ scanToken();
+ }
+ tokens.insert(new Token(TokenType.EOF, "", null, line));
+ return tokens;
+ }
+
+ void scanToken() {
+ char c = advance();
+ with (TokenType) {
+ 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 if (match('*')) {
+ int nest = 1;
+ while (nest > 0) {
+ while ((peek() != '*' || peekNext() != '/') && !isAtEnd()) {
+ if (peek() == '\n') line++;
+ if (peek() == '/' && peekNext() == '*') nest++;
+ advance();
+ }
+ advance();
+ advance();
+ nest--;
+ }
+ } else {
+ addToken(SLASH);
+ }
+ break;
+ case '"': stringToken(); break;
+ case ' ':
+ case '\r':
+ case '\t':
+ break;
+ case '\n':
+ line++;
+ break;
+ default:
+ if (isDigit(c)) {
+ numberToken();
+ } else {
+ Lox.error(line, format("Unexpected character: %c", c));
+ }
+ break;
+ }
+ }
+ }
+
+ private char peekNext() {
+ if (current + 1 >= source.length) return '\0';
+ return source[current + 1];
+ }
+
+ private void numberToken() {
+ while (isDigit(peek())) advance();
+
+ // Look for a fractional part.
+ if (peek() == '.' && isDigit(peekNext())) {
+ // Consume the "."
+ advance();
+
+ while (isDigit(peek())) advance();
+ }
+
+ addToken(TokenType.NUMBER,
+ to!double(source[start..current]));
+ }
+
+ private bool isDigit(char c) {
+ return c >= '0' && c <= '9';
+ }
+
+ private void stringToken() {
+ 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[start + 1..current - 1];
+ addToken(TokenType.STRING, value);
+ }
+
+ private char peek() {
+ if (isAtEnd()) return '\0';
+ return source[current];
+ }
+
+ private bool match(char expected) {
+ if (isAtEnd()) return false;
+ if (source[current] != expected) return false;
+
+ current++;
+ return true;
+ }
+
+ private char advance() {
+ return source[current++];
+ }
+
+ private void addToken(TokenType type) {
+ addToken(type, null);
+ }
+
+ private void addToken(T)(TokenType type, T literal) {
+ string text = source[start..current];
+ tokens.insert(new Token(type, text, literal, line));
+ }
+ private bool isAtEnd() {
+ return current >= source.length;
+ }
+}
diff --git a/source/token.d b/source/token.d
@@ -0,0 +1,25 @@
+import tokentype;
+import std.conv;
+import std.variant;
+
+class Token {
+ immutable TokenType type;
+ immutable string lexeme;
+ const Variant literal;
+ immutable int line;
+
+ this(T)(TokenType type, string lexeme, T literal, int line) {
+ this.type = type;
+ this.lexeme = lexeme;
+ this.literal = literal;
+ this.line = line;
+ }
+
+ void toString(scope void delegate(const(char)[]) sink) const {
+ sink(text(type));
+ sink(" ");
+ sink(lexeme);
+ sink(" ");
+ sink(text(line));
+ }
+}
diff --git a/source/tokentype.d b/source/tokentype.d
@@ -0,0 +1,20 @@
+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
+}