TypeScript: step 1
authorvvakame <vvakame+dev@gmail.com>
Wed, 22 Feb 2017 18:05:01 +0000 (03:05 +0900)
committervvakame <vvakame+dev@gmail.com>
Wed, 22 Feb 2017 20:17:53 +0000 (05:17 +0900)
ts/package.json
ts/printer.ts
ts/reader.ts
ts/step1_read_print.ts [new file with mode: 0644]
ts/types.ts

index 52d091e..89d56fa 100644 (file)
@@ -4,9 +4,10 @@
   "version": "1.0.0",
   "description": "Make a Lisp (mal) language implemented in TypeScript",
   "scripts": {
-    "test": "npm run test:step0",
-    "test:step0": "cd .. && make 'test^ts^step0'"
     "build": "tsfmt -r && tsc -p ./",
+    "test": "npm run build && npm run test:step0 && npm run test:step1",
+    "test:step0": "cd .. && make 'test^ts^step0'",
+    "test:step1": "cd .. && make 'test^ts^step1'"
   },
   "dependencies": {
     "ffi": "^2.2.0"
index e69de29..e890180 100644 (file)
@@ -0,0 +1,38 @@
+import { MalType } from "./types";
+
+export function prStr(v: MalType, printReadably = true): string {
+    switch (v.type) {
+        case "list":
+            return `(${v.list.map(v => prStr(v)).join(" ")})`;
+        case "vector":
+            return `[${v.list.map(v => prStr(v)).join(" ")}]`;
+        case "hash-map":
+            let result = "{";
+            for (const [key, value] of v.map) {
+                if (result !== "{") {
+                    result += " ";
+                }
+                result += `${prStr(key)} ${prStr(value)}`;
+            }
+            result += "}";
+            return result;
+        case "number":
+        case "symbol":
+        case "boolean":
+            return `${v.v}`;
+        case "string":
+            if (printReadably) {
+                const str = v.v
+                    .replace(/\\/g, "\\\\")
+                    .replace(/"/g, '\\"')
+                    .replace(/\n/g, "\\n");
+                return `"${str}"`;
+            } else {
+                return v.v;
+            }
+        case "null":
+            return "nil";
+        case "keyword":
+            return `:${v.v.substr(1)}`;
+    }
+}
index e69de29..89db0f4 100644 (file)
@@ -0,0 +1,145 @@
+import { MalType, MalList, MalString, MalNumber, MalBoolean, MalNull, MalKeyword, MalSymbol, MalVector, MalHashMap } from "./types";
+
+class Reader {
+    position = 0;
+
+    constructor(private tokens: string[]) { }
+
+    next(): string {
+        const ret = this.peek();
+        this.position += 1;
+        return ret;
+    }
+
+    peek(): string {
+        return this.tokens[this.position];
+    }
+}
+
+export function readStr(input: string): MalType {
+    const tokens = tokenizer(input);
+    const reader = new Reader(tokens);
+    return readFrom(reader);
+}
+
+function tokenizer(input: string): string[] {
+    const regexp = /[\s,]*(~@|[\[\]{}()'`~^@]|"(?:\\.|[^\\"])*"|;.*|[^\s\[\]{}('"`,;)]*)/g;
+    const tokens: string[] = [];
+    while (true) {
+        const matches = regexp.exec(input);
+        if (!matches) {
+            break;
+        }
+        const match = matches[1];
+        if (match === "") {
+            break;
+        }
+        if (match[0] !== ";") {
+            tokens.push(match);
+        }
+    }
+
+    return tokens;
+}
+
+function readFrom(reader: Reader): MalType {
+    const token = reader.peek();
+    switch (token) {
+        case "(":
+            return readList(reader);
+        case "[":
+            return readVector(reader);
+        case "{":
+            return readHashMap(reader);
+        case "'":
+            return readSymbol("quote");
+        case "`":
+            return readSymbol("quasiquote");
+        case "~":
+            return readSymbol("unquote");
+        case "~@":
+            return readSymbol("splice-unquote");
+        case "@":
+            return readSymbol("deref");
+        case "^":
+            {
+                reader.next();
+                const sym = MalSymbol.get("with-meta");
+                const target = readFrom(reader);
+                return new MalList([sym, readFrom(reader), target]);
+            }
+        default:
+            return readAtom(reader);
+    }
+
+    function readSymbol(name: string) {
+        reader.next();
+        const sym = MalSymbol.get(name);
+        const target = readFrom(reader);
+        return new MalList([sym, target]);
+    }
+}
+
+function readList(reader: Reader): MalType {
+    return readParen(reader, MalList, "(", ")");
+}
+
+function readVector(reader: Reader): MalType {
+    return readParen(reader, MalVector, "[", "]");
+}
+
+function readHashMap(reader: Reader): MalType {
+    return readParen(reader, MalHashMap, "{", "}");
+}
+
+function readParen(reader: Reader, ctor: { new (list: MalType[]): MalType; }, open: string, close: string): MalType {
+    const token = reader.next(); // drop open paren
+    if (token !== open) {
+        throw new Error(`unexpected token ${token}, expected ${open}`);
+    }
+    const list: MalType[] = [];
+    while (true) {
+        const next = reader.peek();
+        if (next === close) {
+            break;
+        } else if (!next) {
+            throw new Error("unexpected EOF");
+        }
+        list.push(readFrom(reader));
+    }
+    reader.next(); // drop close paren
+
+    return new ctor(list);
+}
+
+function readAtom(reader: Reader): MalType {
+    const token = reader.next();
+    if (token.match(/^-?[0-9]+$/)) {
+        const v = parseInt(token, 10);
+        return new MalNumber(v);
+    }
+    if (token.match(/^-?[0-9]\.[0-9]+$/)) {
+        const v = parseFloat(token);
+        return new MalNumber(v);
+    }
+    if (token[0] === '"') {
+        const v = token.slice(1, token.length - 1)
+            .replace(/\\"/g, '"')
+            .replace(/\\n/g, "\n")
+            .replace(/\\\\/g, "\\");
+        return new MalString(v);
+    }
+    if (token[0] === ":") {
+        return new MalKeyword(token.substr(1));
+    }
+    switch (token) {
+        case "nil":
+            return new MalNull();
+        case "true":
+            return new MalBoolean(true);
+        case "false":
+            return new MalBoolean(false);
+    }
+
+    return MalSymbol.get(token);
+}
diff --git a/ts/step1_read_print.ts b/ts/step1_read_print.ts
new file mode 100644 (file)
index 0000000..05494a1
--- /dev/null
@@ -0,0 +1,38 @@
+import { readline } from "./node_readline";
+
+import { MalType } from "./types";
+import { readStr } from "./reader";
+import { prStr } from "./printer";
+
+function read(v: string): MalType {
+    return readStr(v);
+}
+
+function evalAST(v: any): any {
+    // TODO
+    return v;
+}
+
+function print(v: MalType): string {
+    return prStr(v);
+}
+
+function rep(v: string): string {
+    return print(evalAST(read(v)));
+}
+
+while (true) {
+    const line = readline("user> ");
+    if (line == null) {
+        break;
+    }
+    if (line === "") {
+        continue;
+    }
+    try {
+        console.log(rep(line));
+    } catch (e) {
+        const err: Error = e;
+        console.error(err.message);
+    }
+}
index e69de29..50283d8 100644 (file)
@@ -0,0 +1,78 @@
+export type MalType = MalList | MalNumber | MalString | MalNull | MalBoolean | MalSymbol | MalKeyword | MalVector | MalHashMap;
+
+export class MalList {
+    type: "list" = "list";
+
+    constructor(public list: MalType[]) {
+    }
+}
+
+export class MalNumber {
+    type: "number" = "number";
+    constructor(public v: number) {
+    }
+}
+
+export class MalString {
+    type: "string" = "string";
+    constructor(public v: string) {
+    }
+}
+
+export class MalNull {
+    type: "null" = "null";
+}
+
+export class MalBoolean {
+    type: "boolean" = "boolean";
+    constructor(public v: boolean) {
+    }
+}
+
+export class MalSymbol {
+    static map = new Map<symbol, MalSymbol>();
+
+    static get(name: string): MalSymbol {
+        const sym = Symbol.for(name);
+        let token = this.map.get(sym);
+        if (token) {
+            return token;
+        }
+        token = new MalSymbol(name);
+        this.map.set(sym, token);
+        return token;
+    }
+
+    type: "symbol" = "symbol";
+
+    private constructor(public v: string) {
+    }
+}
+
+export class MalKeyword {
+    type: "keyword" = "keyword";
+    constructor(public v: string) {
+        this.v = String.fromCodePoint(0x29E) + this.v;
+    }
+}
+
+export class MalVector {
+    type: "vector" = "vector";
+    constructor(public list: MalType[]) {
+    }
+}
+
+export class MalHashMap {
+    type: "hash-map" = "hash-map";
+    map = new Map<MalType, MalType>();
+    constructor(list: MalType[]) {
+        while (list.length !== 0) {
+            const key = list.shift() !;
+            const value = list.shift();
+            if (value == null) {
+                throw new Error("unexpected hash length");
+            }
+            this.map.set(key, value);
+        }
+    }
+}