commit:ab79b14f69d5cffc6c2c3eb466c3b502e14856ac
author:Chip Black
committer:Chip Black
date:Sun Oct 21 00:09:23 2018 -0500
parents:4f5e88b40b9373196cd8328961275be8272994c5
Use nearley parser for scripting
diff --git a/package-lock.json b/package-lock.json
line changes: +67/-4
index 6560a2d..78a5d3f
--- a/package-lock.json
+++ b/package-lock.json
@@ -4,6 +4,16 @@
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
+    "@types/moo": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/@types/moo/-/moo-0.4.1.tgz",
+      "integrity": "sha512-09pPEdpXBwg5nqyuXLKeW8nNxKgwtjVp9IsRFiMj5qoqU3wU+4VyjaQk5W6sJ6g5GxpFm/Oep1vLfdg9kVPJ7Q=="
+    },
+    "@types/nearley": {
+      "version": "2.11.0",
+      "resolved": "https://registry.npmjs.org/@types/nearley/-/nearley-2.11.0.tgz",
+      "integrity": "sha512-sW7qB59pz+AJCpiT3ivsMMsr/5stuCUf4IC22QfhYAMItBy3XSjyzCa/oRG+oTTzYnx5VuPtPNHN4fZ0QRnKDg=="
+    },
     "@webassemblyjs/ast": {
       "version": "1.7.8",
       "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.8.tgz",
@@ -758,6 +768,11 @@
       "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
       "dev": true
     },
+    "colors": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/colors/-/colors-0.5.1.tgz",
+      "integrity": "sha1-fQAj6usVTo7p/Oddy5I9DtFmd3Q="
+    },
     "commander": {
       "version": "2.13.0",
       "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz",
@@ -1020,6 +1035,11 @@
         "randombytes": "2.0.6"
       }
     },
+    "discontinuous-range": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz",
+      "integrity": "sha1-44Mx8IRLukm5qctxx3FYWqsbxlo="
+    },
     "domain-browser": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
@@ -2646,6 +2666,11 @@
         "minimist": "0.0.8"
       }
     },
+    "moo": {
+      "version": "0.4.3",
+      "resolved": "https://registry.npmjs.org/moo/-/moo-0.4.3.tgz",
+      "integrity": "sha512-gFD2xGCl8YFgGHsqJ9NKRVdwlioeW3mI1iqfLNYQOv0+6JRwG58Zk9DIGQgyIaffSYaO1xsKnMaYzzNr1KyIAw=="
+    },
     "move-concurrently": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@@ -2692,6 +2717,18 @@
         "to-regex": "3.0.2"
       }
     },
+    "nearley": {
+      "version": "2.15.1",
+      "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.15.1.tgz",
+      "integrity": "sha512-8IUY/rUrKz2mIynUGh8k+tul1awMKEjeHHC5G3FHvvyAW6oq4mQfNp2c0BMea+sYZJvYcrrM6GmZVIle/GRXGw==",
+      "requires": {
+        "moo": "0.4.3",
+        "nomnom": "1.6.2",
+        "railroad-diagrams": "1.0.0",
+        "randexp": "0.4.6",
+        "semver": "5.5.1"
+      }
+    },
     "neo-async": {
       "version": "2.5.2",
       "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.5.2.tgz",
@@ -2749,6 +2786,15 @@
         }
       }
     },
+    "nomnom": {
+      "version": "1.6.2",
+      "resolved": "https://registry.npmjs.org/nomnom/-/nomnom-1.6.2.tgz",
+      "integrity": "sha1-hKZqJgF0QI/Ft3oY+IjszET7aXE=",
+      "requires": {
+        "colors": "0.5.1",
+        "underscore": "1.4.4"
+      }
+    },
     "normalize-path": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
@@ -3091,6 +3137,20 @@
       "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=",
       "dev": true
     },
+    "railroad-diagrams": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz",
+      "integrity": "sha1-635iZ1SN3t+4mcG5Dlc3RVnN234="
+    },
+    "randexp": {
+      "version": "0.4.6",
+      "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz",
+      "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==",
+      "requires": {
+        "discontinuous-range": "1.0.0",
+        "ret": "0.1.15"
+      }
+    },
     "randombytes": {
       "version": "2.0.6",
       "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz",
@@ -3200,8 +3260,7 @@
     "ret": {
       "version": "0.1.15",
       "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
-      "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==",
-      "dev": true
+      "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="
     },
     "rimraf": {
       "version": "2.6.2",
@@ -3259,8 +3318,7 @@
     "semver": {
       "version": "5.5.1",
       "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz",
-      "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==",
-      "dev": true
+      "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw=="
     },
     "serialize-javascript": {
       "version": "1.5.0",
@@ -3760,6 +3818,11 @@
         }
       }
     },
+    "underscore": {
+      "version": "1.4.4",
+      "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz",
+      "integrity": "sha1-YaajIBBiKvoHljvzJSA88SI51gQ="
+    },
     "union-value": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",

diff --git a/package.json b/package.json
line changes: +8/-2
index 9f49b55..7cb08c0
--- a/package.json
+++ b/package.json
@@ -3,7 +3,12 @@
   "version": "1.0.0",
   "description": "",
   "main": "main.js",
-  "dependencies": {},
+  "dependencies": {
+    "@types/moo": "^0.4.1",
+    "@types/nearley": "^2.11.0",
+    "moo": "^0.4.3",
+    "nearley": "^2.15.1"
+  },
   "devDependencies": {
     "awesome-typescript-loader": "^5.2.1",
     "source-map-loader": "^0.2.4",
@@ -13,7 +18,8 @@
   },
   "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1",
-    "watch": "webpack --mode=development --watch"
+    "watch": "webpack --mode=development --watch",
+    "rebuild-parser": "nearleyc src/script/parser.ne --out src/script/parser.ts"
   },
   "author": "",
   "license": "MIT"

diff --git a/src/main.ts b/src/main.ts
line changes: +1/-0
index c6d83a1..1d64efe
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,4 +1,5 @@
 import gs from './gamestate';
+import { Script } from './script';
 
 function keydown(e: KeyboardEvent) {
     switch(e.code) {

diff --git a/src/script/clear.ts b/src/script/clear.ts
line changes: +9/-0
index 0000000..fc5b80f
--- /dev/null
+++ b/src/script/clear.ts
@@ -0,0 +1,9 @@
+import { ScriptInstruction } from './';
+import gs from '../gamestate';
+
+export class ClearScreenInstruction implements ScriptInstruction {
+    execute(): Promise<void> {
+        gs.textView.clear();
+        return Promise.resolve();
+    }
+}

diff --git a/src/script/index.ts b/src/script/index.ts
line changes: +13/-174
index 8b455a4..8400989
--- a/src/script/index.ts
+++ b/src/script/index.ts
@@ -1,82 +1,15 @@
-import gs from './gamestate';
+import gs from '../gamestate';
+import * as nearley from 'nearley';
+import * as parserGrammar from './parser';
 
-interface ScriptInstruction {
+export interface ScriptInstruction {
     execute(): Promise<void>
 }
 
-class SayInstruction implements ScriptInstruction {
-    s: string
-
-    constructor(s: string) {
-        this.s = s;
-    }
-
-    execute(): Promise<void> {
-        gs.textView.print(this.s);
-        return Promise.resolve();
-    }
-}
-
-interface QueryOption {
-    label: string, instruction: ScriptInstruction
-}
-
-class QueryInstruction implements ScriptInstruction {
-    options: QueryOption[]
-
-    constructor(options: QueryOption[]) {
-        this.options = options;
-    }
-
-    execute(): Promise<void> {
-        return new Promise( (resolve, reject) => {
-            gs.actions.clear();
-            for (let o of this.options) {
-                gs.actions.addAction(o.label, () => {
-                    gs.actions.clear();
-                    resolve(o.instruction.execute());
-                });
-            }
-        });
-    }
-}
-
-class SleepInstruction implements ScriptInstruction {
-    duration: number
-
-    constructor(duration: number) {
-        this.duration = duration;
-    }
-
-    execute(): Promise<void> {
-        return new Promise( (resolve, reject) => {
-            setTimeout(resolve, this.duration * 1000);
-        });
-    }
-}
-
-class PauseInstruction implements ScriptInstruction {
-    text: string
-
-    constructor(text?: string) {
-        this.text = text || 'Continue';
-    }
-
-    execute(): Promise<void> {
-        return new Promise( (resolve, reject) => {
-            gs.actions.clear();
-            gs.actions.addAction(this.text, () => {
-                gs.actions.clear();
-                resolve();
-            });
-        });
-    }
-}
-
-class ClearScreenInstruction implements ScriptInstruction {
-    execute(): Promise<void> {
-        gs.textView.clear();
-        return Promise.resolve();
+// TODO: integrate this into script execution state?
+export async function executeInstructions(l: ScriptInstruction[]) {
+    for (const i of l) {
+        await i.execute()
     }
 }
 
@@ -92,105 +25,11 @@ export class Script {
     }
 
     parse(text: string) {
-        const lines = text.split(/[\r\n]+/);
-
-        let n = 0;
-        var parseQuery = () => {
-            const options: QueryOption[] = [];
-            let tokens;
-            do {
-                n++;
-                tokens = this.parseLine(lines[n]);
-                const arr = tokens.indexOf('=>');
-                if (arr == -1)
-                    continue;
-                const label = tokens.slice(0, arr).join(' ');
-                const instruction = parseTokens(tokens.slice(arr + 1));
-                options.push({ label, instruction });
-            } while (tokens[0] != 'endquery');
-            return new QueryInstruction(options);
-        }
-
-        var parseTokens = (tokens: string[]) => {
-            switch (tokens[0]) {
-                case 'say':
-                    return new SayInstruction(tokens.slice(1).join(' '));
-                case 'query':
-                    return parseQuery();
-                case 'sleep':
-                    return new SleepInstruction(parseFloat(tokens[1]));
-                case 'pause':
-                    return new PauseInstruction(tokens.slice(1).join(' '));
-                case 'clear':
-                    return new ClearScreenInstruction();
-                case undefined:
-                    // empty line
-                    break;
-                default:
-                    this.dump();
-                    throw Error('Unrecognized instruction on line ' + n + ': ' + tokens[0]);
-            }
-        }
-
-        while (n < lines.length) {
-            const tokens = this.parseLine(lines[n]);
-            const instruction = parseTokens(tokens);
-            if (instruction) {
-                this.instructions.push(instruction);
-            }
-            n++;
-        }
-    }
-
-    parseLine(line: string): string[] {
-        const tokens: string[] = [];
-        let b = 0;
-        let n = 0;
-        const scan = (test: (c: string) => boolean) => {
-            while (n < line.length && test(line[n]))
-                n++;
-        };
-
-        const pushToken = () => {
-            if (b < n)
-                tokens.push(line.slice(b, n));
-        };
-
-        // fun function fuckery I'll forget in a fortnight
-        const testFor        = (r: RegExp) => (c: string) => r.test(c);
-        const testForNot     = (r: RegExp) => (c: string) => !r.test(c);
-        const testQuote      = testFor(/['"]/);
-        const testNotQuote   = testForNot(/['"]/);
-        const testWhitespace = testFor(/\s/);
-
-        scan(testWhitespace);
-        b = n;
-
-        while (n < line.length) {
-            if (testQuote(line[n])) {
-                const q = line[n];
-                n++;
-                b = n;
-                // scan until next matching quote
-                const re = new RegExp(q);
-                scan(testForNot(re));
-                pushToken();
-                n++;
-                //scan(testWhitespace);
-                b = n;
-            } else if (testWhitespace(line[n])) {
-                pushToken();
-                scan(testWhitespace);
-                b = n;
-            } else if (n + 1 == line.length) {
-                n++;
-                pushToken();
-            } else {
-                n++;
-            }
-        }
-
-        return tokens;
+        const parser = new nearley.Parser(nearley.Grammar.fromCompiled(parserGrammar));
+        parser.feed(text);
+        // Feed in an extra space because our grammar requires whitespace at the end of a statement
+        parser.feed(' ');
+        this.instructions = parser.results[0];
     }
 
     step(): Promise<void> {

diff --git a/src/script/menu.ts b/src/script/menu.ts
line changes: +84/-0
index 0000000..1fb4ae5
--- /dev/null
+++ b/src/script/menu.ts
@@ -0,0 +1,84 @@
+import { ScriptInstruction, executeInstructions } from './';
+import gs from '../gamestate';
+
+enum MenuOptionType {
+    Item,
+    Buy,
+    Exit,
+}
+
+interface MenuOptionCommon {
+    kind: MenuOptionType
+    label: string
+}
+
+interface MenuItem extends MenuOptionCommon {
+    kind: MenuOptionType.Item
+    instructions: ScriptInstruction[]
+}
+
+interface MenuBuy extends MenuOptionCommon {
+    kind: MenuOptionType.Buy
+    object: string
+    value: number
+}
+
+interface MenuExit extends MenuOptionCommon {
+    kind: MenuOptionType.Exit
+}
+
+type MenuOption = MenuItem | MenuBuy | MenuExit
+
+export class MenuInstruction implements ScriptInstruction {
+    options: MenuOption[]
+
+    constructor(options: any[]) {
+        this.options = options.map(o => {
+            switch(o[1].value) {
+                case 'item':
+                    return <MenuItem>{
+                        kind: MenuOptionType.Item,
+                        label: o[3],
+                        instructions: o[5],
+                    };
+                case 'buy':
+                    return <MenuBuy>{
+                        kind: MenuOptionType.Buy,
+                        label: o[3][1].value, // TODO: fetch object name from object database
+                        object: o[3][1].value,
+                        value: o[5].value,
+                    };
+                case 'exit':
+                    return <MenuExit>{
+                        kind: MenuOptionType.Exit,
+                        label: o[3],
+                    };
+                default:
+                    throw new Error('Unknown menu item type: ' + o[1].value);
+            }
+        });
+    }
+
+    execute(): Promise<void> {
+        return new Promise( (resolve, reject) => {
+            gs.actions.clear();
+            for (let o of this.options) {
+                gs.actions.addAction(o.label, () => {
+                    gs.actions.clear();
+                    switch(o.kind) {
+                        case MenuOptionType.Item:
+                            resolve(executeInstructions(o.instructions));
+                            break;
+                        case MenuOptionType.Buy:
+                            console.log('Buy', o.object, 'for', o.value);
+                            resolve();
+                            break;
+                        case MenuOptionType.Exit:
+                            resolve();
+                            break;
+                    }
+                });
+            }
+        });
+    }
+}

diff --git a/src/script/parser.ne b/src/script/parser.ne
line changes: +65/-0
index 0000000..f102fe9
--- /dev/null
+++ b/src/script/parser.ne
@@ -0,0 +1,65 @@
+@preprocessor typescript
+@{%
+import * as moo from 'moo';
+import { SayInstruction } from './say';
+import { MenuInstruction } from './menu';
+import { ClearScreenInstruction } from './clear';
+import { SleepInstruction } from './sleep';
+import { PauseInstruction } from './pause';
+
+const lexer = moo.compile({
+  ws:     { match: /[ \t\r\n]+/, lineBreaks: true },
+  number: /[0-9]+(?:\.[0-9]+)?/,
+  keyword: ['say', 'menu', 'item', 'end', 'exit', 'goto', 'if',
+  'finish', 'clear', 'buy', 'transact', 'setflag'],
+  label:  /[a-zA-Z][a-zA-Z0-9]*:/,
+  word:   /[a-zA-Z][a-zA-Z0-9]*/,
+  dqstring: /"(?:\\["\\]|[^\n"\\])*"/,
+  sqstring: /'(?:\\['\\]|[^\n'\\])*'/,
+  mlstring_start: '[[[',
+  mlstring_end:   ']]]',
+  bang:      '!',
+  flag_mark: '$',
+  obj_mark:  '@',
+  mlcontent: /.+(?!\]\]\])/,
+});
+
+function cn(l: any[]) {
+	return l.filter(x => x);
+}
+%}
+
+@lexer lexer
+
+program -> __:? statement:* {% data => data[1] || data[0] %}
+
+statement -> "say" __ string __             {% data => new SayInstruction(data[2]) %}
+statement -> "menu" __ menuitem:* "end" __  {% data => new MenuInstruction(data[2]) %}
+statement -> %label __
+statement -> "goto" __ %word __
+statement -> "finish" __
+statement -> "clear" __                     {% data => new ClearScreenInstruction() %}
+statement -> "sleep" __ %number __          {% data => new SleepInstruction(data[2]) %}
+statement -> "pause" __ string __           {% data => new PauseInstruction(data[2]) %}
+
+menuitem -> if:? "item" __ string __ program "end" __
+menuitem -> if:? "buy" __ obj_identifier __ %number __
+menuitem -> if:? "exit" __ string __
+
+if -> "if" __ %bang:? flag_identifier __
+if -> "if" __ %bang:? obj_identifier %word __
+
+flag_identifier -> %flag_mark %word
+obj_identifier -> %obj_mark %word
+
+number -> %number {% data => parseInt(data[0]) %}
+
+string -> %dqstring {% (s: any[]) => s[0].value.slice(1, -1) %}
+string -> %sqstring {% (s: any[]) => s[0].value.slice(1, -1) %}
+string -> mlstring {% id %}
+mlstring -> %mlstring_start %mlstring_end
+mlstring -> %mlstring_start %word %mlstring_end
+mlstring -> %mlstring_start %mlcontent %mlstring_end
+
+__ -> %ws {% (x: any) => null %}
+_ -> null | %ws {% (x: any) => null %}

diff --git a/src/script/pause.ts b/src/script/pause.ts
line changes: +20/-0
index 0000000..a3cf1b9
--- /dev/null
+++ b/src/script/pause.ts
@@ -0,0 +1,20 @@
+import { ScriptInstruction } from './';
+import gs from '../gamestate';
+
+export class PauseInstruction implements ScriptInstruction {
+    text: string
+
+    constructor(text?: string) {
+        this.text = text || 'Continue';
+    }
+
+    execute(): Promise<void> {
+        return new Promise( (resolve, reject) => {
+            gs.actions.clear();
+            gs.actions.addAction(this.text, () => {
+                gs.actions.clear();
+                resolve();
+            });
+        });
+    }
+}

diff --git a/src/script/say.ts b/src/script/say.ts
line changes: +16/-0
index 0000000..3b58ee0
--- /dev/null
+++ b/src/script/say.ts
@@ -0,0 +1,16 @@
+import { ScriptInstruction } from './';
+import gs from '../gamestate';
+
+export class SayInstruction implements ScriptInstruction {
+    s: string
+
+    constructor(s: string) {
+        this.s = s;
+    }
+
+    execute(): Promise<void> {
+        gs.textView.print(this.s);
+        return Promise.resolve();
+    }
+}
+

diff --git a/src/script/sleep.ts b/src/script/sleep.ts
line changes: +15/-0
index 0000000..5d02ef8
--- /dev/null
+++ b/src/script/sleep.ts
@@ -0,0 +1,15 @@
+import { ScriptInstruction } from './';
+
+export class SleepInstruction implements ScriptInstruction {
+    duration: number
+
+    constructor(duration: number) {
+        this.duration = duration;
+    }
+
+    execute(): Promise<void> {
+        return new Promise( (resolve, reject) => {
+            setTimeout(resolve, this.duration);
+        });
+    }
+}