├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── utils.js ├── package.json ├── index.js ├── LICENSE ├── tests ├── examples.test.js └── operations.test.js ├── parser ├── grammar.jison ├── utils.js └── grammar.js ├── generator ├── serialize.js └── build.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4 3 | } 4 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | function ParseError(msg) { 2 | throw `FSM parse error: ${msg}`; 3 | } 4 | 5 | function BuildError(msg) { 6 | throw `FSM build error: ${msg}`; 7 | } 8 | 9 | ParseError.prototype = Error.prototype; 10 | BuildError.prototype = Error.prototype; 11 | 12 | function isMetaProperty(property) { 13 | return property.startsWith("@@"); 14 | } 15 | 16 | module.exports = { BuildError, ParseError, isMetaProperty }; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fsm", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "fsm.js", 6 | "scripts": { 7 | "parse": "jison ./parser/grammar.jison -o ./parser/grammar.js", 8 | "prettier": "prettier --write .", 9 | "test": "jest" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "jest": "^29.1.1", 16 | "jison": "^0.4.18", 17 | "prettier": "2.7.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { build, inline } = require("./generator/build.js"); 2 | const fs = require("fs"); 3 | 4 | function main() { 5 | const args = process.argv.slice(2); 6 | 7 | if (args.length > 0) { 8 | const inFile = args[0]; 9 | const outArg = args[1]; 10 | const emitFile = outArg.endsWith(".js") ? outArg : `${outArg}.js`; 11 | 12 | let src; 13 | try { 14 | src = fs.readFileSync(inFile, "utf-8"); 15 | } catch (e) { 16 | throw `An error occurred while reading source file: ${e}`; 17 | } 18 | build(src, { emitFile }); 19 | } 20 | } 21 | 22 | // main(); 23 | 24 | module.exports = { build, inline }; 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jake Reid Browning 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/examples.test.js: -------------------------------------------------------------------------------- 1 | const { build } = require("../index.js"); 2 | 3 | const accepted = (machine, input) => 4 | machine.consume(input, { reset: true }).inAcceptState(); 5 | 6 | test("Builds a NFSA that accepts odd binary numbers", () => { 7 | const { machine } = build(`.s0 -0> s0 -1> s0 -1> (s1);`); 8 | expect(accepted(machine, "0011")).toBe(true); 9 | expect(accepted(machine, "0000110")).toBe(false); 10 | }); 11 | 12 | test("Builds a NPDA that accepts odd-length palindromes", () => { 13 | const { machine } = build(` 14 | .s0 -[_:$]> s1; 15 | s1 -a[_:a]> -b[_:b]> s1; 16 | s1 -a> -b> s2; 17 | s2 -a[a]> -b[b]> s2; 18 | s2 -[$]> (s3); 19 | `); 20 | expect(accepted(machine, "aabbbaa")).toBe(true); 21 | expect(accepted(machine, "a")).toBe(true); 22 | expect(accepted(machine, "abbbaa")).toBe(false); 23 | }); 24 | 25 | test("Builds a NPDA that accepts a^(n)b^(n)", () => { 26 | const { machine } = build(` 27 | .s0 -a[_:a]> s0; 28 | s0 -_> (s1); 29 | s1 -b[a]> s1; 30 | `); 31 | expect(accepted(machine, "aaabbb")).toBe(true); 32 | expect(accepted(machine, "")).toBe(true); 33 | expect(accepted(machine, "aaabbbb")).toBe(false); 34 | expect(accepted(machine, "ba")).toBe(false); 35 | }); 36 | 37 | test("Builds a self-driving robot", () => { 38 | const { machine } = build(` 39 | .off forward backward -push> off; 40 | `); 41 | expect(machine.consume(["push", "collide"]).state).toBe("backward"); 42 | }); 43 | 44 | test("Builds a NPDA that accepts a^(n)b^(n)c^(n)", () => { 45 | const { machine } = build(` 46 | .s0 -a[:a]> s0 -_> s1 -b[a, :b]> s1 -_> s2 -c[_, b]> (s2); 47 | `); 48 | expect(accepted(machine, "aaabbbccc")).toBe(true); 49 | expect(accepted(machine, "abc")).toBe(true); 50 | expect(accepted(machine, "aabbccc")).toBe(false); 51 | expect(accepted(machine, "aabcc")).toBe(false); 52 | }); 53 | -------------------------------------------------------------------------------- /tests/operations.test.js: -------------------------------------------------------------------------------- 1 | const { build } = require("../index.js"); 2 | 3 | test("Left transition", () => { 4 | const { machine } = build(`s1 { 9 | const { machine } = build(`s1 .s0;`); 10 | expect(machine.consume("ff").state).toBe("s0"); 11 | }); 12 | 13 | test("Double transition", () => { 14 | const { machine } = build(`s1 .s0;`); 15 | expect(machine.consume("ff").state).toBe("s0"); 16 | machine.reset(); 17 | expect(machine.consume("f").state).toBe("s1"); 18 | expect(machine.consume("g").state).toBe("s1"); 19 | }); 20 | 21 | test("Transitions to the success stata via epsilons", () => { 22 | const { machine } = build( 23 | `.s0 -_> s1 -_[_]> s2 -[_:_]> s3 -_[_, _]> s4 -_[_:_, _:_]> (s5);` 24 | ); 25 | expect(machine.consume("").state).toBe("s5"); 26 | }); 27 | 28 | test("Adds $ to stack given input a", () => { 29 | const { machine } = build(`.s0 -[:$]> s1 -a[$:b]> (s2);`); 30 | machine.consume("a"); 31 | expect(machine.state).toBe("s2"); 32 | expect(machine.stacks[0][0]).toBe("b"); 33 | expect(machine.input.length).toBe(0); 34 | }); 35 | 36 | test("Attempts to use an unknown transition", () => { 37 | const { machine } = build(`.s0 -f> s1;`); 38 | expect(machine.consume("g").state).toBe("s0"); 39 | }); 40 | 41 | test("No starting state", () => { 42 | expect(() => build(`s0 -f> s1;`)).toThrow(); 43 | }); 44 | 45 | test("Halts after exhausting possible paths", () => { 46 | const { machine } = build(` 47 | .q0 -[:Z]> q1 -a[:a]> q1 -b[:b]> q1 -a> q2; 48 | q1 -b> q2 -a[a]> q2 -b[b]> q2 -[Z]> (q3); 49 | `); 50 | machine.consume("ab"); 51 | expect(machine.state).toBe("q3"); 52 | expect(machine.input.length).toBe(1); 53 | expect(machine.input[0]).toBe("b"); 54 | }); 55 | 56 | test("Regex", () => { 57 | const { machine } = build(` 58 | .s0 -e> s1 -f> s2; 59 | s2 -g> t1 -g> (t2); 60 | /^s[0-9]/ -foo> bar; 61 | `); 62 | expect(machine.state).toBe("s0"); 63 | machine.consume(["e", "foo"]); 64 | expect(machine.state).toBe("bar"); 65 | 66 | machine.reset(); 67 | expect(machine.state).toBe("s0"); 68 | machine.consume(["e", "f", "foo"]); 69 | expect(machine.state).toBe("bar"); 70 | }); 71 | -------------------------------------------------------------------------------- /parser/grammar.jison: -------------------------------------------------------------------------------- 1 | %lex 2 | 3 | %% 4 | 5 | \s*\#[^\n\r]* /* skip line comments */ 6 | \s+ /* skip whitespace */ 7 | 8 | ";" return "LINE_END"; 9 | "(" return "("; 10 | ")" return ")"; 11 | ">" return ">"; 12 | "<" return "<"; 13 | "-" return "-"; 14 | "*" return "*"; 15 | "[" return "["; 16 | "]" return "]"; 17 | "," return ","; 18 | "," return ","; 19 | "." return "."; 20 | ":" return ":"; 21 | \/[^\/]*\/ return "REGEX"; 22 | 23 | [A-Za-z0-9_$]+ return "IDENT"; 24 | 25 | /lex 26 | 27 | %{ 28 | 29 | const utils = require("./utils"); 30 | 31 | %} 32 | 33 | %% 34 | start 35 | : rules { return utils.mergeRules($1); } 36 | ; 37 | rules 38 | : rules rule -> [...$1, utils.unpackRuleStmt($2)] 39 | | rule -> [utils.unpackRuleStmt($1)] 40 | ; 41 | rule 42 | : LINE_END -> [] 43 | | rule_transitions state LINE_END -> [...$1, $2] 44 | ; 45 | rule_transitions 46 | : rule_transitions rule_transition -> [...$1, ...$2] 47 | | rule_transition 48 | ; 49 | rule_transition 50 | : state transitions -> [$1, $2] 51 | ; 52 | state 53 | : "(" IDENT ")" -> { type: "state", name: $2, accept: true } 54 | | "(" "." IDENT ")" -> { type: "state", name: $2, initial: true, accept: true } 55 | | "." IDENT -> { type: "state", name: $2, initial: true } 56 | | IDENT -> { type: "state", name: $1 } 57 | | "*" -> { type: "state", name: "@@regexp:.*" } 58 | | REGEX -> { type: "state", name: `@@regexp:${$1.slice(1, -1)}` } 59 | ; 60 | transitions 61 | : transitions transition -> [$1, $2] 62 | | transition 63 | ; 64 | transition 65 | : "<" IDENT ">" -> { type: "transition", direction: "lr", input: $2 } 66 | | "<" IDENT "-" -> { type: "transition", direction: "l", input: $2 } 67 | | "-" IDENT ">" -> { type: "transition", direction: "r", input: $2 } 68 | | "<" pda_definition ">" -> { type: "transition", direction: "lr", ...$2 } 69 | | "<" pda_definition "-" -> { type: "transition", direction: "l", ...$2 } 70 | | "-" pda_definition ">" -> { type: "transition", direction: "r", ...$2 } 71 | ; 72 | pda_definition 73 | : IDENT "[" stack_pairs "]" -> { input: $1, stacks: $3 } 74 | | "[" stack_pairs "]" -> { input: "_", stacks: $2 } 75 | ; 76 | stack_pairs 77 | : stack_pairs "," stack_pair -> [...$1, $3] 78 | | stack_pair -> [$1] 79 | ; 80 | stack_pair 81 | : IDENT -> { read: $1, write: null } 82 | | IDENT ":" IDENT -> { read: $1, write: $3 === "_" ? null : $3 } 83 | | ":" IDENT -> { read: "_", write: $2 } 84 | ; 85 | -------------------------------------------------------------------------------- /generator/serialize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Logic and source code for converting machine javascript object 3 | * to a string that can be written and loaded from a JS file 4 | */ 5 | 6 | function makeTransitionSrc(transition, nextState) { 7 | const transitionAction = transition.input; 8 | 9 | let fnStr = `this.state = '${nextState}';\n`; 10 | 11 | if (transitionAction !== "_") { 12 | fnStr += "this.input.shift();\n"; 13 | } 14 | 15 | for (let i = 0; i < transition.stacks?.length || 0; i++) { 16 | if (transition.stacks[i].read !== "_") { 17 | fnStr += `this.stacks[${i}].pop();\n`; 18 | } 19 | const stackVal = transition.stacks[i].write; 20 | if (stackVal) { 21 | fnStr += `this.stacks[${i}].push("${stackVal}");`; 22 | } 23 | } 24 | return fnStr; 25 | } 26 | 27 | function makeTransitionFunction(transitionName, nextState, stackVal) { 28 | const fnStr = makeTransitionSrc(transitionName, nextState, stackVal); 29 | return { callable: new Function(fnStr), src: fnStr }; 30 | } 31 | 32 | function serialize( 33 | initial, 34 | transitions, 35 | transitionsFound, 36 | acceptStates, 37 | { target, name } = { target: "node" } 38 | ) { 39 | if (target === "browser" && !name) { 40 | throw new Error( 41 | "`name` property must be defined when `target` property is 'browser'" 42 | ); 43 | } 44 | let serialized = ` 45 | const initial = "${initial}"; 46 | const fsm = { 47 | stacks: [[], []], 48 | input: [], 49 | acceptStates: ${JSON.stringify(acceptStates)}, 50 | state: "${initial}", 51 | consume: ${this.consume.toString()}, 52 | reset: ${this.reset.toString()}, 53 | inAcceptState: ${this.inAcceptState.toString()}, 54 | _mostExhausted: ${this._mostExhausted.toString()}, 55 | _evaluateSnapshot: ${this._evaluateSnapshot.toString()}, 56 | _getPossibleTransitions: ${this._getPossibleTransitions.toString()}, 57 | _stackMatch: ${this._stackMatch.toString()}, 58 | _clone: ${this._clone.toString()}, 59 | _totalStacksLength: ${this._totalStacksLength.toString()}, 60 | transitions: { 61 | `; 62 | 63 | for (const [name, stateTransitions] of Object.entries(transitions)) { 64 | serialized += ` "${name}": [\n`; 65 | const transitionSrc = []; 66 | for (const stateTransition of stateTransitions) { 67 | transitionSrc.push( 68 | `{ filter: ${JSON.stringify( 69 | stateTransition.filter 70 | )}, fn: { callable: function() {\n${ 71 | stateTransition.fn.src 72 | } } } }`.replace(/\n/g, `\n `) 73 | ); 74 | } 75 | serialized += `${transitionSrc.join(", ")}],\n`; 76 | } 77 | serialized += " },\n};\n"; 78 | 79 | serialized += 80 | target === "node" 81 | ? "\nmodule.exports = fsm;\n" 82 | : `\nwindow.${name} = fsm;\n`; 83 | 84 | return serialized; 85 | } 86 | module.exports = { serialize, makeTransitionFunction }; 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # automata-golf 2 | 3 | A domain-specific language (DSL) for parsing regular, context-free and recursively enumerable languages. 4 | 5 | In `automata-golf`, a machine is defined by a series of path statements. 6 | There's no need to explicitly define states or transitions. 7 | 8 | The example below shows a machine with an initial state `s0`, which transitions 9 | via `f` to and from the accepted state `s1` . 10 | 11 | Screenshot 2022-10-05 at 13 28 29 12 | 13 | ``` 14 | # 1,2, and 3 are all equivalent: 15 | 16 | # 1 17 | .s0 -f> (s1) -f> s0; 18 | 19 | # 2 20 | .s0 (s1); 21 | 22 | # 3 23 | s1 -f> .s0; 24 | s0 t1; 35 | ``` 36 | 37 | ## Stacking transitions 38 | 39 | Multiple transitions from the same state can be stacked up: 40 | 41 | ``` 42 | .s0 -f> -g> -h> s1; 43 | ``` 44 | 45 | ## Pushdown automata 46 | 47 | `automata-golf` supports pushdown automota, i.e. a finite-state machine with 48 | a stack and transitions that push/pop the stack. 49 | 50 | The following transitions to `s1` on input `f` when `a` is top of the stack. 51 | Upon the transition, it pushes `b` to the stack. 52 | 53 | ``` 54 | .s0 -f[a:b]> s1; 55 | ``` 56 | 57 | Screenshot 2022-10-05 at 13 35 39 58 | 59 | ### Multiple stacks 60 | 61 | 2-stack PDAs are supported, making them equivalent to Turing machines. See the 62 | example and corresponding automaton diagram below. 63 | 64 | ``` 65 | .s0 -f[a:b, _:c]> s1; 66 | ``` 67 | 68 | 69 | 70 | ### Epsilon transitions 71 | 72 | Epsilon is represented by `_`. For example the following transitions to `s1` 73 | and pushes `$` to the second stack without consuming any input or popping either stack. 74 | 75 | ``` 76 | .s0 -_[_:_, _:$]> (s1); 77 | 78 | # or equivalently: 79 | 80 | .s0 -[_, :$]> (s1); 81 | ``` 82 | 83 | # Examples 84 | 85 | ## Regular languages 86 | 87 | Regular languages can be captured using finite-state machines. 88 | 89 | ### Odd binary numbers 90 | 91 | The following program accepts all binary numbers ending in `1` 92 | 93 | ```js 94 | const { build } = require("./automata-golf/index.js"); 95 | 96 | const { machine } = build(` 97 | .s0 -0> -1> s0 -1> (s1); 98 | `); 99 | 100 | machine.consume("10110").inAcceptState(); // false 101 | machine.consume("1011").inAcceptState(); // true 102 | ``` 103 | 104 | Screenshot 2022-10-05 at 13 25 48 105 | 106 | ### Self-driving robot 107 | 108 | The following finite-state machine creates a robot that can be turned on and off, 109 | and switches direction when it collides. 110 | 111 | ```js 112 | const { build } = require("./automata-golf/index.js"); 113 | 114 | const { machine } = build(` 115 | .off forward backward -push> off; 116 | `); 117 | 118 | machine.consume(["push", "collide"]).state; // backward 119 | ``` 120 | 121 | ## Context-free languages 122 | 123 | Pushdown automota are required to parse context-free languages. 124 | 125 | ### Odd-length palindromes 126 | 127 | The following accepts all odd-length palindromes in the language `{a, b}` 128 | 129 | ```js 130 | const { build } = require("automata-golf/index.js"); 131 | 132 | const { machine } = build(` 133 | .s0 -[:$]> s1; 134 | s1 -a[:a]> -b[:b]> s1; 135 | s1 -a> -b> s2; 136 | s2 -a[a]> -b[b]> s2; 137 | s2 -[$]> (s3); 138 | `); 139 | 140 | machine.consume("abbba").inAcceptState(); // true 141 | machine.consume("abb").inAcceptState(); // false 142 | ``` 143 | 144 | Note the program can be condensed to 145 | 146 | ``` 147 | .s0 -[:$]> s1 -a[_:a]> -b[:b]> s1 -a> -b> s2 -a[a]> -b[b]> s2 -[$]> (s3); 148 | ``` 149 | 150 | Screenshot 2022-10-05 at 13 27 33 151 | 152 | ## Recursively enumerable languages 153 | 154 | Recursively enumerable languages can be parsed by using a pushdown automaton with 2 stacks, equivalent to a Turing machine. 155 | 156 | ### anbncn 157 | 158 | The following accepts input of the format anbncn: 159 | 160 | ```js 161 | const { build } = require("automata-golf/index.js"); 162 | 163 | const machine = build(` 164 | .s0 -a[:a]> s0 -_> s1 -b[a, :b]> s1 -_> s2 -c[_, b]> (s2); 165 | `); 166 | 167 | machine.consume("aabbcc").inAcceptState(); // true 168 | machine.consume("abbc").inAcceptState(); // false 169 | ``` 170 | 171 | 172 | 173 | ## Build to JS 174 | 175 | The machine can be written to a JS file 176 | 177 | ```js 178 | // A.js 179 | const { build } = require("automata-golf/index.js"); 180 | build(".s0 -f> (s1)", { emitFile: "./machine.js" }); 181 | 182 | // B.js 183 | const machine = require("./machine.js"); 184 | machine.consume("f"); 185 | ``` 186 | 187 | ### Target 188 | 189 | Set `target` to `'browser'` to generate a machine that can be run in a browser 190 | environment: 191 | 192 | ```js 193 | build(".s0 -f> (s1)", { emitFile: "./machine.js", target: "browser" }); 194 | ``` 195 | -------------------------------------------------------------------------------- /parser/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility functions for parsing. Transforms the Jison rule output from a stream 3 | * of tokens to a grouped list of transitions. 4 | * 5 | * Below shows the parsing process starting from the Jison token stream for the following program: 6 | * 7 | * .s0 -f> s1; 8 | * s0 -g> (s2); 9 | * 10 | * *************** Initial Jison output *************** 11 | * 12 | * Line 1: 13 | * [ 14 | * { type: 'state', name: 's0', initial: true }, 15 | * { type: 'transition', direction: 'r', input: 'f' }, 16 | * { type: 'state', name: 's1' }, 17 | * ] 18 | * 19 | * Line 2: 20 | * [ 21 | * { type: 'state', name: 's0' }, 22 | * { type: 'transition', direction: 'r', input: 'g' }, 23 | * { type: 'state', name: 's2', accept: true }, 24 | * ] 25 | * 26 | * *************** Transition objects generated by `unpackRuleStmt` *************** 27 | * 28 | * Line 1: 29 | * { 30 | * initial: 's0', 31 | * transitions: [ s0: [ [Object] ] ], 32 | * statesFound: [ 's0', 's1' ], 33 | * transitionsFound: [ 'f' ], 34 | * acceptStates: [], 35 | * } 36 | * 37 | * Line 2: 38 | * { 39 | * initial: false, 40 | * transitions: [ s0: [ [Object] ] ], 41 | * statesFound: [ 's0', 's2' ], 42 | * transitionsFound: [ 'g' ], 43 | * acceptStates: [ 's2' ], 44 | * } 45 | * 46 | * *************** Transformed output after `utils.mergeRules` *************** 47 | * 48 | * { 49 | * initial: 's0', 50 | * transitions: { s0: [ [Object], [Object] ] }, 51 | * transitionsFound: [ 'f', 'g' ], 52 | * acceptStates: [ 's2' ], 53 | * } 54 | */ 55 | 56 | const { ParseError, isMetaProperty } = require("../utils.js"); 57 | const { makeTransitionFunction } = require("../generator/serialize.js"); 58 | 59 | function mergeTransitions(...rules) { 60 | /** 61 | * Merges array of transition rules for multiple path statements into a single object. 62 | */ 63 | const target = {}; 64 | 65 | for (const rule of rules) { 66 | for (const state in rule) { 67 | if (!target[state]) { 68 | target[state] = []; 69 | } 70 | target[state].push(...rule[state]); 71 | } 72 | } 73 | 74 | return target; 75 | } 76 | 77 | function TransitionBuilder() { 78 | /** 79 | * Builds an object of executable transition functions, pushing a new transition 80 | * for each `(state, transition, nextState)` tuple given to the `addTransition` method. 81 | */ 82 | this.transitions = []; 83 | 84 | this.addTransition = function (state, transition, nextState) { 85 | const fn = makeTransitionFunction(transition, nextState.name); 86 | 87 | if (!this.transitions[state.name]) { 88 | this.transitions[state.name] = []; 89 | } 90 | 91 | this.transitions[state.name].push({ 92 | fn, 93 | filter: { 94 | state: state.name, 95 | input: transition.input, 96 | stackValues: transition.stacks?.map((s) => s.read) || [], 97 | }, 98 | }); 99 | }; 100 | } 101 | 102 | function unpackRuleStmt(ruleArr) { 103 | /** 104 | * Converts rule statement into an array of transitions. 105 | */ 106 | const builder = new TransitionBuilder(); 107 | 108 | // Use statesFound instead of checking `Object.keys(transitions)` as some states 109 | // might only be defined as destination nodes (e.g. `source -f> destination`) 110 | const statesFound = []; 111 | const transitionsFound = []; 112 | const acceptStates = []; 113 | 114 | const pushFound = (arr, name) => { 115 | if (!arr.includes(name) && !isMetaProperty(name)) { 116 | arr.push(name); 117 | } 118 | }; 119 | 120 | ruleArr.forEach((state, i) => { 121 | if (state.type === "state" && i !== ruleArr.length - 1) { 122 | // TODO - handle 1-item array in jison parser; 123 | // `transitionItem` may be a kvp object or array 124 | const transitionItem = ruleArr[i + 1]; 125 | const transitions = Array.isArray(transitionItem) 126 | ? transitionItem 127 | : [transitionItem]; 128 | const nextState = ruleArr[i + 2]; 129 | 130 | pushFound(statesFound, state.name); 131 | pushFound(statesFound, nextState.name); 132 | 133 | state.accept && pushFound(acceptStates, state.name); 134 | nextState.accept && pushFound(acceptStates, nextState.name); 135 | 136 | for (const transition of transitions) { 137 | pushFound(transitionsFound, transition.input); 138 | 139 | switch (transition.direction) { 140 | case "r": 141 | builder.addTransition(state, transition, nextState); 142 | break; 143 | case "l": 144 | builder.addTransition(nextState, transition, state); 145 | break; 146 | case "lr": 147 | builder.addTransition(state, transition, nextState); 148 | builder.addTransition(nextState, transition, state); 149 | break; 150 | default: 151 | break; 152 | } 153 | } 154 | } 155 | }); 156 | 157 | const initialState = ruleArr.find((r) => r.initial); 158 | 159 | return { 160 | initial: initialState?.name || false, 161 | transitions: builder.transitions, 162 | statesFound, 163 | transitionsFound, 164 | acceptStates, 165 | }; 166 | } 167 | 168 | function addRegexToTransitions( 169 | transitions, 170 | statesFound, 171 | regexp, 172 | regexTransitions 173 | ) { 174 | /** 175 | * Search for states matching given regex pattern, and add the transitions for that regex state 176 | * if the state matches. 177 | */ 178 | for (const state of statesFound) { 179 | if (state.match(regexp)) { 180 | if (!transitions[state]) { 181 | transitions[state] = []; 182 | } 183 | transitions[state].push( 184 | ...regexTransitions.map((t) => ({ 185 | ...t, 186 | filter: { ...t.filter, state }, 187 | })) 188 | ); 189 | } 190 | } 191 | } 192 | 193 | function applyRegex(transitions, statesFound) { 194 | /** 195 | * Adds new transitions matching given regex. For example 196 | * 197 | * /s[0-2]/ -foo> bar; 198 | * 199 | * Adds the following transitions: 200 | * s0 -foo> bar; 201 | * s1 -foo> bar; 202 | * s2 -foo> bar; 203 | */ 204 | for (const state in transitions) { 205 | const [, regex] = state.split("@@regexp:"); 206 | if (!regex) { 207 | continue; 208 | } 209 | const regexTransitions = transitions[state]; 210 | const regexp = new RegExp(regex); 211 | // We don't want `/regexp/` to be treated as a literal state 212 | delete transitions[state]; 213 | addRegexToTransitions( 214 | transitions, 215 | statesFound, 216 | regexp, 217 | regexTransitions 218 | ); 219 | } 220 | } 221 | 222 | function mergeRules(rules) { 223 | /** 224 | * Merges array of processed rule objects into a single object 225 | */ 226 | const flatUnique = (arr, prop) => [ 227 | ...new Set(arr.map((r) => r[prop]).flat()), 228 | ]; 229 | const transitions = mergeTransitions(...rules.map((r) => r.transitions)); 230 | const statesFound = flatUnique(rules, "statesFound"); 231 | const acceptStates = flatUnique(rules, "acceptStates"); 232 | const transitionsFound = flatUnique(rules, "transitionsFound"); 233 | 234 | applyRegex(transitions, statesFound); 235 | 236 | const initial = rules.find((r) => r.initial)?.initial; 237 | if (!initial) { 238 | throw new ParseError( 239 | "Initial state not found; set initial state by prefixing with `.`, e.g. `.s0 -f> s1`" 240 | ); 241 | } 242 | 243 | return { 244 | initial, 245 | transitions, 246 | transitionsFound, 247 | acceptStates, 248 | }; 249 | } 250 | 251 | module.exports = { unpackRuleStmt, mergeRules }; 252 | -------------------------------------------------------------------------------- /generator/build.js: -------------------------------------------------------------------------------- 1 | const { parser } = require("../parser/grammar.js"); 2 | const { serialize } = require("../generator/serialize.js"); 3 | const { BuildError } = require("../utils.js"); 4 | const { isMetaProperty } = require("../utils"); 5 | const fs = require("fs"); 6 | 7 | function filterMetaProperties(machine) { 8 | /** 9 | * Remove object properties related to source code generation, that are only used 10 | * during build and shouldn't be on the public machine object. 11 | */ 12 | delete machine._serialize; 13 | for (const state in machine.transitions) { 14 | for (const transitionName in machine.transitions[state]) { 15 | if (isMetaProperty(transitionName)) { 16 | delete machine.transitions[state][transitionName]; 17 | } 18 | } 19 | } 20 | return machine; 21 | } 22 | 23 | function build(src, { emitFile, target, name } = {}) { 24 | /** 25 | * Parses the given source and generates a machine. 26 | * 27 | * emitFile: 28 | * If `emitFile` is left out, returns the machine object as `{ machine }`. 29 | * If `emitFile` is `true`, returns the source code too as `{ machine, src }`. 30 | * If `emitFile` is a file path string, writes the source to that path and returns `{ machine, src }`. 31 | * 32 | * target: 33 | * Target environment for source code emission: either 'node' or 'browser'. 34 | * If target is 'browser', then `name` should also be defined as this determines the name of the window object. 35 | * 36 | * name: 37 | * The name of the window object when target is 'browser'. 38 | * 39 | * NOTE: All code used in methods on the `machine` object should originate from the object, so that the 40 | * object can be serialized correctly. E.g. `require` or using a resource defined elsewhere in this file 41 | */ 42 | 43 | if (!target) { 44 | target = "node"; 45 | } 46 | 47 | const unpackedRules = parser.parse(src); 48 | 49 | const { initial, transitions, transitionsFound, acceptStates } = 50 | unpackedRules; 51 | const ret = {}; 52 | 53 | const machine = { 54 | stacks: [[], []], // TODO - support n stacks 55 | state: initial, 56 | input: [], 57 | transitions, 58 | acceptStates, 59 | 60 | consume: function (input, { reset } = { reset: true }) { 61 | /** 62 | * Iterate through every possible path in the machine until either an accepted state is found with an empty input, 63 | * or all possible paths are exhausted. 64 | */ 65 | 66 | if (reset) { 67 | this.reset(); 68 | } 69 | 70 | if (typeof input === "string") { 71 | input = input.split(""); 72 | } 73 | this.input = input; 74 | 75 | const snapshotStack = [this._clone()]; 76 | let exhaustedSnapshot = snapshotStack[0]; 77 | 78 | while (snapshotStack.length) { 79 | const snapshot = snapshotStack.pop(); 80 | const possibleTransitions = snapshot._getPossibleTransitions(); 81 | 82 | if (possibleTransitions.length === 1) { 83 | if ( 84 | this._evaluateSnapshot(snapshot, possibleTransitions[0]) 85 | ) 86 | return this; 87 | exhaustedSnapshot = this._mostExhausted( 88 | snapshot, 89 | exhaustedSnapshot 90 | ); 91 | snapshotStack.push(snapshot._clone()); 92 | } else if (possibleTransitions.length > 1) { 93 | // If there are multiple possible paths from the current state, 94 | // evaluate the snapshot at each path and merge into current state if one is accepted. 95 | // otherwise push the transitioned snapshot onto the stack. 96 | for (const possibleTransition of possibleTransitions) { 97 | const snapshotCopy = snapshot._clone(); 98 | if ( 99 | this._evaluateSnapshot( 100 | snapshotCopy, 101 | possibleTransition 102 | ) 103 | ) 104 | return this; 105 | exhaustedSnapshot = this._mostExhausted( 106 | snapshotCopy, 107 | exhaustedSnapshot 108 | ); 109 | snapshotStack.push(snapshotCopy); 110 | } 111 | } 112 | } 113 | // In the case that no accepted state is found with an empty input, 114 | // halt on the snapshot with the lowest total stack + input value 115 | Object.assign(this, exhaustedSnapshot); 116 | return this; 117 | }, 118 | 119 | reset: function () { 120 | this.stacks = [[], []]; 121 | this.state = initial; 122 | return this; 123 | }, 124 | 125 | _mostExhausted: function (s1, s2) { 126 | return s1._totalStacksLength() + s1.input.length <= 127 | s2._totalStacksLength() + s2.input.length 128 | ? s1 129 | : s2; 130 | }, 131 | 132 | _evaluateSnapshot: function (snapshot, transition) { 133 | /** 134 | * Perform the transition on given snapshot and merge 135 | * into the current object if it's in an accept state 136 | */ 137 | transition.fn.callable.call(snapshot); 138 | 139 | if (snapshot.inAcceptState()) { 140 | Object.assign(this, snapshot); 141 | return true; 142 | } 143 | return false; 144 | }, 145 | 146 | _getPossibleTransitions: function () { 147 | /** 148 | * Get all possible transitions (including epsilon transitions) 149 | * given the current state, stack and input 150 | */ 151 | const stackValues = this.stacks.map((s) => s[s.length - 1]); 152 | const inputValue = this.input[0]; 153 | const stateTransitions = this.transitions[this.state]; 154 | const possibleTransitions = []; 155 | 156 | if (!stateTransitions) { 157 | return []; 158 | } 159 | 160 | for (const t of stateTransitions) { 161 | const { filter } = t; 162 | if ( 163 | (filter.input === inputValue || filter.input === "_") && 164 | filter.stackValues.every( 165 | (sv, idx) => sv === stackValues[idx] || sv === "_" 166 | ) 167 | ) { 168 | possibleTransitions.push(t); 169 | } 170 | } 171 | return possibleTransitions; 172 | }, 173 | 174 | _serialize: function (f, { target, name } = { target: "node" }) { 175 | /** 176 | * Converts the object instance to a string for writing to file. 177 | * Note this property is removed in the public `machine` object. 178 | */ 179 | const serialized = serialize.call( 180 | this, 181 | initial, 182 | transitions, 183 | transitionsFound, 184 | acceptStates, 185 | { target, name } 186 | ); 187 | if (f) { 188 | fs.writeFile(f, serialized, (err) => { 189 | if (err) { 190 | throw BuildError(`Could not write to file: ${err}`); 191 | } 192 | }); 193 | } 194 | return serialized; 195 | }, 196 | 197 | _stackMatch: function (t, stackValue) { 198 | /** 199 | * If transition stack value is epsilon then return true 200 | * otherwise perform equality check. 201 | */ 202 | return t === "_" || t === stackValue; 203 | }, 204 | 205 | inAcceptState: function () { 206 | const { input, state } = this; 207 | return ( 208 | !input.length && 209 | !this._totalStacksLength() && 210 | this.acceptStates.includes(state) 211 | ); 212 | }, 213 | 214 | _totalStacksLength: function () { 215 | return this.stacks.reduce((acc, el) => acc + el.length, 0); 216 | }, 217 | 218 | _clone: function () { 219 | return { 220 | ...this, 221 | stacks: [...this.stacks.map((s) => [...s])], 222 | input: [...this.input], 223 | state: this.state, 224 | }; 225 | }, 226 | }; 227 | 228 | if (emitFile && typeof emitFile === "string") { 229 | ret.src = machine._serialize(emitFile, { target, name }); 230 | } else if (emitFile) { 231 | // Don't write src to file 232 | ret.src = machine._serialize(null, { target, name }); 233 | } 234 | ret.machine = filterMetaProperties(machine); 235 | return ret; 236 | } 237 | 238 | function inline(strings, ...values) { 239 | return build(String.raw({ raw: strings }, ...values)).machine; 240 | } 241 | 242 | module.exports = { build, inline }; 243 | -------------------------------------------------------------------------------- /parser/grammar.js: -------------------------------------------------------------------------------- 1 | /* parser generated by jison 0.4.18 */ 2 | /* 3 | Returns a Parser object of the following structure: 4 | 5 | Parser: { 6 | yy: {} 7 | } 8 | 9 | Parser.prototype: { 10 | yy: {}, 11 | trace: function(), 12 | symbols_: {associative list: name ==> number}, 13 | terminals_: {associative list: number ==> name}, 14 | productions_: [...], 15 | performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate, $$, _$), 16 | table: [...], 17 | defaultActions: {...}, 18 | parseError: function(str, hash), 19 | parse: function(input), 20 | 21 | lexer: { 22 | EOF: 1, 23 | parseError: function(str, hash), 24 | setInput: function(input), 25 | input: function(), 26 | unput: function(str), 27 | more: function(), 28 | less: function(n), 29 | pastInput: function(), 30 | upcomingInput: function(), 31 | showPosition: function(), 32 | test_match: function(regex_match_array, rule_index), 33 | next: function(), 34 | lex: function(), 35 | begin: function(condition), 36 | popState: function(), 37 | _currentRules: function(), 38 | topState: function(), 39 | pushState: function(condition), 40 | 41 | options: { 42 | ranges: boolean (optional: true ==> token location info will include a .range[] member) 43 | flex: boolean (optional: true ==> flex-like lexing behaviour where the rules are tested exhaustively to find the longest match) 44 | backtrack_lexer: boolean (optional: true ==> lexer regexes are tested in order and for each matching regex the action code is invoked; the lexer terminates the scan when a token is returned by the action code) 45 | }, 46 | 47 | performAction: function(yy, yy_, $avoiding_name_collisions, YY_START), 48 | rules: [...], 49 | conditions: {associative list: name ==> set}, 50 | } 51 | } 52 | 53 | 54 | token location info (@$, _$, etc.): { 55 | first_line: n, 56 | last_line: n, 57 | first_column: n, 58 | last_column: n, 59 | range: [start_number, end_number] (where the numbers are indexes into the input string, regular zero-based) 60 | } 61 | 62 | 63 | the parseError function receives a 'hash' object with these members for lexer and parser errors: { 64 | text: (matched text) 65 | token: (the produced terminal token, if any) 66 | line: (yylineno) 67 | } 68 | while parser (grammar) errors will also provide these members, i.e. parser errors deliver a superset of attributes: { 69 | loc: (yylloc) 70 | expected: (string describing the set of expected tokens) 71 | recoverable: (boolean: TRUE when the parser has a error recovery rule available for this particular error) 72 | } 73 | */ 74 | var grammar = (function () { 75 | var o = function (k, v, o, l) { 76 | for (o = o || {}, l = k.length; l--; o[k[l]] = v); 77 | return o; 78 | }, 79 | $V0 = [1, 4], 80 | $V1 = [1, 8], 81 | $V2 = [1, 10], 82 | $V3 = [1, 9], 83 | $V4 = [1, 11], 84 | $V5 = [1, 12], 85 | $V6 = [1, 6, 11, 12, 14, 15, 16], 86 | $V7 = [11, 12, 14, 15, 16], 87 | $V8 = [1, 18], 88 | $V9 = [1, 19], 89 | $Va = [6, 18, 20], 90 | $Vb = [11, 12, 14, 15, 16, 18, 20], 91 | $Vc = [1, 27], 92 | $Vd = [1, 34], 93 | $Ve = [1, 39], 94 | $Vf = [1, 40], 95 | $Vg = [1, 46], 96 | $Vh = [24, 25], 97 | $Vi = [19, 20]; 98 | var parser = { 99 | trace: function trace() {}, 100 | yy: {}, 101 | symbols_: { 102 | error: 2, 103 | start: 3, 104 | rules: 4, 105 | rule: 5, 106 | LINE_END: 6, 107 | rule_transitions: 7, 108 | state: 8, 109 | rule_transition: 9, 110 | transitions: 10, 111 | "(": 11, 112 | IDENT: 12, 113 | ")": 13, 114 | ".": 14, 115 | "*": 15, 116 | REGEX: 16, 117 | transition: 17, 118 | "<": 18, 119 | ">": 19, 120 | "-": 20, 121 | pda_definition: 21, 122 | "[": 22, 123 | stack_pairs: 23, 124 | "]": 24, 125 | ",": 25, 126 | stack_pair: 26, 127 | ":": 27, 128 | $accept: 0, 129 | $end: 1, 130 | }, 131 | terminals_: { 132 | 2: "error", 133 | 6: "LINE_END", 134 | 11: "(", 135 | 12: "IDENT", 136 | 13: ")", 137 | 14: ".", 138 | 15: "*", 139 | 16: "REGEX", 140 | 18: "<", 141 | 19: ">", 142 | 20: "-", 143 | 22: "[", 144 | 24: "]", 145 | 25: ",", 146 | 27: ":", 147 | }, 148 | productions_: [ 149 | 0, 150 | [3, 1], 151 | [4, 2], 152 | [4, 1], 153 | [5, 1], 154 | [5, 3], 155 | [7, 2], 156 | [7, 1], 157 | [9, 2], 158 | [8, 3], 159 | [8, 4], 160 | [8, 2], 161 | [8, 1], 162 | [8, 1], 163 | [8, 1], 164 | [10, 2], 165 | [10, 1], 166 | [17, 3], 167 | [17, 3], 168 | [17, 3], 169 | [17, 3], 170 | [17, 3], 171 | [17, 3], 172 | [21, 4], 173 | [21, 3], 174 | [23, 3], 175 | [23, 1], 176 | [26, 1], 177 | [26, 3], 178 | [26, 2], 179 | ], 180 | performAction: function anonymous( 181 | yytext, 182 | yyleng, 183 | yylineno, 184 | yy, 185 | yystate /* action[1] */, 186 | $$ /* vstack */, 187 | _$ /* lstack */ 188 | ) { 189 | /* this == yyval */ 190 | 191 | var $0 = $$.length - 1; 192 | switch (yystate) { 193 | case 1: 194 | return utils.mergeRules($$[$0]); 195 | break; 196 | case 2: 197 | this.$ = [...$$[$0 - 1], utils.unpackRuleStmt($$[$0])]; 198 | break; 199 | case 3: 200 | this.$ = [utils.unpackRuleStmt($$[$0])]; 201 | break; 202 | case 4: 203 | this.$ = []; 204 | break; 205 | case 5: 206 | this.$ = [...$$[$0 - 2], $$[$0 - 1]]; 207 | break; 208 | case 6: 209 | this.$ = [...$$[$0 - 1], ...$$[$0]]; 210 | break; 211 | case 8: 212 | case 15: 213 | this.$ = [$$[$0 - 1], $$[$0]]; 214 | break; 215 | case 9: 216 | this.$ = { type: "state", name: $$[$0 - 1], accept: true }; 217 | break; 218 | case 10: 219 | this.$ = { 220 | type: "state", 221 | name: $$[$0 - 2], 222 | initial: true, 223 | accept: true, 224 | }; 225 | break; 226 | case 11: 227 | this.$ = { type: "state", name: $$[$0], initial: true }; 228 | break; 229 | case 12: 230 | this.$ = { type: "state", name: $$[$0] }; 231 | break; 232 | case 13: 233 | this.$ = { type: "state", name: "@@regexp:.*" }; 234 | break; 235 | case 14: 236 | this.$ = { 237 | type: "state", 238 | name: `@@regexp:${$$[$0].slice(1, -1)}`, 239 | }; 240 | break; 241 | case 17: 242 | this.$ = { 243 | type: "transition", 244 | direction: "lr", 245 | input: $$[$0 - 1], 246 | }; 247 | break; 248 | case 18: 249 | this.$ = { 250 | type: "transition", 251 | direction: "l", 252 | input: $$[$0 - 1], 253 | }; 254 | break; 255 | case 19: 256 | this.$ = { 257 | type: "transition", 258 | direction: "r", 259 | input: $$[$0 - 1], 260 | }; 261 | break; 262 | case 20: 263 | this.$ = { 264 | type: "transition", 265 | direction: "lr", 266 | ...$$[$0 - 1], 267 | }; 268 | break; 269 | case 21: 270 | this.$ = { 271 | type: "transition", 272 | direction: "l", 273 | ...$$[$0 - 1], 274 | }; 275 | break; 276 | case 22: 277 | this.$ = { 278 | type: "transition", 279 | direction: "r", 280 | ...$$[$0 - 1], 281 | }; 282 | break; 283 | case 23: 284 | this.$ = { input: $$[$0 - 3], stacks: $$[$0 - 1] }; 285 | break; 286 | case 24: 287 | this.$ = { input: "_", stacks: $$[$0 - 1] }; 288 | break; 289 | case 25: 290 | this.$ = [...$$[$0 - 2], $$[$0]]; 291 | break; 292 | case 26: 293 | this.$ = [$$[$0]]; 294 | break; 295 | case 27: 296 | this.$ = { read: $$[$0], write: null }; 297 | break; 298 | case 28: 299 | this.$ = { 300 | read: $$[$0 - 2], 301 | write: $$[$0] === "_" ? null : $$[$0], 302 | }; 303 | break; 304 | case 29: 305 | this.$ = { read: "_", write: $$[$0] }; 306 | break; 307 | } 308 | }, 309 | table: [ 310 | { 311 | 3: 1, 312 | 4: 2, 313 | 5: 3, 314 | 6: $V0, 315 | 7: 5, 316 | 8: 7, 317 | 9: 6, 318 | 11: $V1, 319 | 12: $V2, 320 | 14: $V3, 321 | 15: $V4, 322 | 16: $V5, 323 | }, 324 | { 1: [3] }, 325 | { 326 | 1: [2, 1], 327 | 5: 13, 328 | 6: $V0, 329 | 7: 5, 330 | 8: 7, 331 | 9: 6, 332 | 11: $V1, 333 | 12: $V2, 334 | 14: $V3, 335 | 15: $V4, 336 | 16: $V5, 337 | }, 338 | o($V6, [2, 3]), 339 | o($V6, [2, 4]), 340 | { 8: 14, 9: 15, 11: $V1, 12: $V2, 14: $V3, 15: $V4, 16: $V5 }, 341 | o($V7, [2, 7]), 342 | { 10: 16, 17: 17, 18: $V8, 20: $V9 }, 343 | { 12: [1, 20], 14: [1, 21] }, 344 | { 12: [1, 22] }, 345 | o($Va, [2, 12]), 346 | o($Va, [2, 13]), 347 | o($Va, [2, 14]), 348 | o($V6, [2, 2]), 349 | { 6: [1, 23], 10: 16, 17: 17, 18: $V8, 20: $V9 }, 350 | o($V7, [2, 6]), 351 | o($V7, [2, 8], { 17: 24, 18: $V8, 20: $V9 }), 352 | o($Vb, [2, 16]), 353 | { 12: [1, 25], 21: 26, 22: $Vc }, 354 | { 12: [1, 28], 21: 29, 22: $Vc }, 355 | { 13: [1, 30] }, 356 | { 12: [1, 31] }, 357 | o($Va, [2, 11]), 358 | o($V6, [2, 5]), 359 | o($Vb, [2, 15]), 360 | { 19: [1, 32], 20: [1, 33], 22: $Vd }, 361 | { 19: [1, 35], 20: [1, 36] }, 362 | { 12: $Ve, 23: 37, 26: 38, 27: $Vf }, 363 | { 19: [1, 41], 22: $Vd }, 364 | { 19: [1, 42] }, 365 | o($Va, [2, 9]), 366 | { 13: [1, 43] }, 367 | o($Vb, [2, 17]), 368 | o($Vb, [2, 18]), 369 | { 12: $Ve, 23: 44, 26: 38, 27: $Vf }, 370 | o($Vb, [2, 20]), 371 | o($Vb, [2, 21]), 372 | { 24: [1, 45], 25: $Vg }, 373 | o($Vh, [2, 26]), 374 | o($Vh, [2, 27], { 27: [1, 47] }), 375 | { 12: [1, 48] }, 376 | o($Vb, [2, 19]), 377 | o($Vb, [2, 22]), 378 | o($Va, [2, 10]), 379 | { 24: [1, 49], 25: $Vg }, 380 | o($Vi, [2, 24]), 381 | { 12: $Ve, 26: 50, 27: $Vf }, 382 | { 12: [1, 51] }, 383 | o($Vh, [2, 29]), 384 | o($Vi, [2, 23]), 385 | o($Vh, [2, 25]), 386 | o($Vh, [2, 28]), 387 | ], 388 | defaultActions: {}, 389 | parseError: function parseError(str, hash) { 390 | if (hash.recoverable) { 391 | this.trace(str); 392 | } else { 393 | var error = new Error(str); 394 | error.hash = hash; 395 | throw error; 396 | } 397 | }, 398 | parse: function parse(input) { 399 | var self = this, 400 | stack = [0], 401 | tstack = [], 402 | vstack = [null], 403 | lstack = [], 404 | table = this.table, 405 | yytext = "", 406 | yylineno = 0, 407 | yyleng = 0, 408 | recovering = 0, 409 | TERROR = 2, 410 | EOF = 1; 411 | var args = lstack.slice.call(arguments, 1); 412 | var lexer = Object.create(this.lexer); 413 | var sharedState = { yy: {} }; 414 | for (var k in this.yy) { 415 | if (Object.prototype.hasOwnProperty.call(this.yy, k)) { 416 | sharedState.yy[k] = this.yy[k]; 417 | } 418 | } 419 | lexer.setInput(input, sharedState.yy); 420 | sharedState.yy.lexer = lexer; 421 | sharedState.yy.parser = this; 422 | if (typeof lexer.yylloc == "undefined") { 423 | lexer.yylloc = {}; 424 | } 425 | var yyloc = lexer.yylloc; 426 | lstack.push(yyloc); 427 | var ranges = lexer.options && lexer.options.ranges; 428 | if (typeof sharedState.yy.parseError === "function") { 429 | this.parseError = sharedState.yy.parseError; 430 | } else { 431 | this.parseError = Object.getPrototypeOf(this).parseError; 432 | } 433 | function popStack(n) { 434 | stack.length = stack.length - 2 * n; 435 | vstack.length = vstack.length - n; 436 | lstack.length = lstack.length - n; 437 | } 438 | _token_stack: var lex = function () { 439 | var token; 440 | token = lexer.lex() || EOF; 441 | if (typeof token !== "number") { 442 | token = self.symbols_[token] || token; 443 | } 444 | return token; 445 | }; 446 | var symbol, 447 | preErrorSymbol, 448 | state, 449 | action, 450 | a, 451 | r, 452 | yyval = {}, 453 | p, 454 | len, 455 | newState, 456 | expected; 457 | while (true) { 458 | state = stack[stack.length - 1]; 459 | if (this.defaultActions[state]) { 460 | action = this.defaultActions[state]; 461 | } else { 462 | if (symbol === null || typeof symbol == "undefined") { 463 | symbol = lex(); 464 | } 465 | action = table[state] && table[state][symbol]; 466 | } 467 | if ( 468 | typeof action === "undefined" || 469 | !action.length || 470 | !action[0] 471 | ) { 472 | var errStr = ""; 473 | expected = []; 474 | for (p in table[state]) { 475 | if (this.terminals_[p] && p > TERROR) { 476 | expected.push("'" + this.terminals_[p] + "'"); 477 | } 478 | } 479 | if (lexer.showPosition) { 480 | errStr = 481 | "Parse error on line " + 482 | (yylineno + 1) + 483 | ":\n" + 484 | lexer.showPosition() + 485 | "\nExpecting " + 486 | expected.join(", ") + 487 | ", got '" + 488 | (this.terminals_[symbol] || symbol) + 489 | "'"; 490 | } else { 491 | errStr = 492 | "Parse error on line " + 493 | (yylineno + 1) + 494 | ": Unexpected " + 495 | (symbol == EOF 496 | ? "end of input" 497 | : "'" + 498 | (this.terminals_[symbol] || symbol) + 499 | "'"); 500 | } 501 | this.parseError(errStr, { 502 | text: lexer.match, 503 | token: this.terminals_[symbol] || symbol, 504 | line: lexer.yylineno, 505 | loc: yyloc, 506 | expected: expected, 507 | }); 508 | } 509 | if (action[0] instanceof Array && action.length > 1) { 510 | throw new Error( 511 | "Parse Error: multiple actions possible at state: " + 512 | state + 513 | ", token: " + 514 | symbol 515 | ); 516 | } 517 | switch (action[0]) { 518 | case 1: 519 | stack.push(symbol); 520 | vstack.push(lexer.yytext); 521 | lstack.push(lexer.yylloc); 522 | stack.push(action[1]); 523 | symbol = null; 524 | if (!preErrorSymbol) { 525 | yyleng = lexer.yyleng; 526 | yytext = lexer.yytext; 527 | yylineno = lexer.yylineno; 528 | yyloc = lexer.yylloc; 529 | if (recovering > 0) { 530 | recovering--; 531 | } 532 | } else { 533 | symbol = preErrorSymbol; 534 | preErrorSymbol = null; 535 | } 536 | break; 537 | case 2: 538 | len = this.productions_[action[1]][1]; 539 | yyval.$ = vstack[vstack.length - len]; 540 | yyval._$ = { 541 | first_line: 542 | lstack[lstack.length - (len || 1)].first_line, 543 | last_line: lstack[lstack.length - 1].last_line, 544 | first_column: 545 | lstack[lstack.length - (len || 1)].first_column, 546 | last_column: lstack[lstack.length - 1].last_column, 547 | }; 548 | if (ranges) { 549 | yyval._$.range = [ 550 | lstack[lstack.length - (len || 1)].range[0], 551 | lstack[lstack.length - 1].range[1], 552 | ]; 553 | } 554 | r = this.performAction.apply( 555 | yyval, 556 | [ 557 | yytext, 558 | yyleng, 559 | yylineno, 560 | sharedState.yy, 561 | action[1], 562 | vstack, 563 | lstack, 564 | ].concat(args) 565 | ); 566 | if (typeof r !== "undefined") { 567 | return r; 568 | } 569 | if (len) { 570 | stack = stack.slice(0, -1 * len * 2); 571 | vstack = vstack.slice(0, -1 * len); 572 | lstack = lstack.slice(0, -1 * len); 573 | } 574 | stack.push(this.productions_[action[1]][0]); 575 | vstack.push(yyval.$); 576 | lstack.push(yyval._$); 577 | newState = 578 | table[stack[stack.length - 2]][ 579 | stack[stack.length - 1] 580 | ]; 581 | stack.push(newState); 582 | break; 583 | case 3: 584 | return true; 585 | } 586 | } 587 | return true; 588 | }, 589 | }; 590 | 591 | const utils = require("./utils"); 592 | 593 | /* generated by jison-lex 0.3.4 */ 594 | var lexer = (function () { 595 | var lexer = { 596 | EOF: 1, 597 | 598 | parseError: function parseError(str, hash) { 599 | if (this.yy.parser) { 600 | this.yy.parser.parseError(str, hash); 601 | } else { 602 | throw new Error(str); 603 | } 604 | }, 605 | 606 | // resets the lexer, sets new input 607 | setInput: function (input, yy) { 608 | this.yy = yy || this.yy || {}; 609 | this._input = input; 610 | this._more = this._backtrack = this.done = false; 611 | this.yylineno = this.yyleng = 0; 612 | this.yytext = this.matched = this.match = ""; 613 | this.conditionStack = ["INITIAL"]; 614 | this.yylloc = { 615 | first_line: 1, 616 | first_column: 0, 617 | last_line: 1, 618 | last_column: 0, 619 | }; 620 | if (this.options.ranges) { 621 | this.yylloc.range = [0, 0]; 622 | } 623 | this.offset = 0; 624 | return this; 625 | }, 626 | 627 | // consumes and returns one char from the input 628 | input: function () { 629 | var ch = this._input[0]; 630 | this.yytext += ch; 631 | this.yyleng++; 632 | this.offset++; 633 | this.match += ch; 634 | this.matched += ch; 635 | var lines = ch.match(/(?:\r\n?|\n).*/g); 636 | if (lines) { 637 | this.yylineno++; 638 | this.yylloc.last_line++; 639 | } else { 640 | this.yylloc.last_column++; 641 | } 642 | if (this.options.ranges) { 643 | this.yylloc.range[1]++; 644 | } 645 | 646 | this._input = this._input.slice(1); 647 | return ch; 648 | }, 649 | 650 | // unshifts one char (or a string) into the input 651 | unput: function (ch) { 652 | var len = ch.length; 653 | var lines = ch.split(/(?:\r\n?|\n)/g); 654 | 655 | this._input = ch + this._input; 656 | this.yytext = this.yytext.substr(0, this.yytext.length - len); 657 | //this.yyleng -= len; 658 | this.offset -= len; 659 | var oldLines = this.match.split(/(?:\r\n?|\n)/g); 660 | this.match = this.match.substr(0, this.match.length - 1); 661 | this.matched = this.matched.substr(0, this.matched.length - 1); 662 | 663 | if (lines.length - 1) { 664 | this.yylineno -= lines.length - 1; 665 | } 666 | var r = this.yylloc.range; 667 | 668 | this.yylloc = { 669 | first_line: this.yylloc.first_line, 670 | last_line: this.yylineno + 1, 671 | first_column: this.yylloc.first_column, 672 | last_column: lines 673 | ? (lines.length === oldLines.length 674 | ? this.yylloc.first_column 675 | : 0) + 676 | oldLines[oldLines.length - lines.length].length - 677 | lines[0].length 678 | : this.yylloc.first_column - len, 679 | }; 680 | 681 | if (this.options.ranges) { 682 | this.yylloc.range = [r[0], r[0] + this.yyleng - len]; 683 | } 684 | this.yyleng = this.yytext.length; 685 | return this; 686 | }, 687 | 688 | // When called from action, caches matched text and appends it on next action 689 | more: function () { 690 | this._more = true; 691 | return this; 692 | }, 693 | 694 | // When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead. 695 | reject: function () { 696 | if (this.options.backtrack_lexer) { 697 | this._backtrack = true; 698 | } else { 699 | return this.parseError( 700 | "Lexical error on line " + 701 | (this.yylineno + 1) + 702 | ". You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n" + 703 | this.showPosition(), 704 | { 705 | text: "", 706 | token: null, 707 | line: this.yylineno, 708 | } 709 | ); 710 | } 711 | return this; 712 | }, 713 | 714 | // retain first n characters of the match 715 | less: function (n) { 716 | this.unput(this.match.slice(n)); 717 | }, 718 | 719 | // displays already matched input, i.e. for error messages 720 | pastInput: function () { 721 | var past = this.matched.substr( 722 | 0, 723 | this.matched.length - this.match.length 724 | ); 725 | return ( 726 | (past.length > 20 ? "..." : "") + 727 | past.substr(-20).replace(/\n/g, "") 728 | ); 729 | }, 730 | 731 | // displays upcoming input, i.e. for error messages 732 | upcomingInput: function () { 733 | var next = this.match; 734 | if (next.length < 20) { 735 | next += this._input.substr(0, 20 - next.length); 736 | } 737 | return ( 738 | next.substr(0, 20) + (next.length > 20 ? "..." : "") 739 | ).replace(/\n/g, ""); 740 | }, 741 | 742 | // displays the character position where the lexing error occurred, i.e. for error messages 743 | showPosition: function () { 744 | var pre = this.pastInput(); 745 | var c = new Array(pre.length + 1).join("-"); 746 | return pre + this.upcomingInput() + "\n" + c + "^"; 747 | }, 748 | 749 | // test the lexed token: return FALSE when not a match, otherwise return token 750 | test_match: function (match, indexed_rule) { 751 | var token, lines, backup; 752 | 753 | if (this.options.backtrack_lexer) { 754 | // save context 755 | backup = { 756 | yylineno: this.yylineno, 757 | yylloc: { 758 | first_line: this.yylloc.first_line, 759 | last_line: this.last_line, 760 | first_column: this.yylloc.first_column, 761 | last_column: this.yylloc.last_column, 762 | }, 763 | yytext: this.yytext, 764 | match: this.match, 765 | matches: this.matches, 766 | matched: this.matched, 767 | yyleng: this.yyleng, 768 | offset: this.offset, 769 | _more: this._more, 770 | _input: this._input, 771 | yy: this.yy, 772 | conditionStack: this.conditionStack.slice(0), 773 | done: this.done, 774 | }; 775 | if (this.options.ranges) { 776 | backup.yylloc.range = this.yylloc.range.slice(0); 777 | } 778 | } 779 | 780 | lines = match[0].match(/(?:\r\n?|\n).*/g); 781 | if (lines) { 782 | this.yylineno += lines.length; 783 | } 784 | this.yylloc = { 785 | first_line: this.yylloc.last_line, 786 | last_line: this.yylineno + 1, 787 | first_column: this.yylloc.last_column, 788 | last_column: lines 789 | ? lines[lines.length - 1].length - 790 | lines[lines.length - 1].match(/\r?\n?/)[0].length 791 | : this.yylloc.last_column + match[0].length, 792 | }; 793 | this.yytext += match[0]; 794 | this.match += match[0]; 795 | this.matches = match; 796 | this.yyleng = this.yytext.length; 797 | if (this.options.ranges) { 798 | this.yylloc.range = [ 799 | this.offset, 800 | (this.offset += this.yyleng), 801 | ]; 802 | } 803 | this._more = false; 804 | this._backtrack = false; 805 | this._input = this._input.slice(match[0].length); 806 | this.matched += match[0]; 807 | token = this.performAction.call( 808 | this, 809 | this.yy, 810 | this, 811 | indexed_rule, 812 | this.conditionStack[this.conditionStack.length - 1] 813 | ); 814 | if (this.done && this._input) { 815 | this.done = false; 816 | } 817 | if (token) { 818 | return token; 819 | } else if (this._backtrack) { 820 | // recover context 821 | for (var k in backup) { 822 | this[k] = backup[k]; 823 | } 824 | return false; // rule action called reject() implying the next rule should be tested instead. 825 | } 826 | return false; 827 | }, 828 | 829 | // return next match in input 830 | next: function () { 831 | if (this.done) { 832 | return this.EOF; 833 | } 834 | if (!this._input) { 835 | this.done = true; 836 | } 837 | 838 | var token, match, tempMatch, index; 839 | if (!this._more) { 840 | this.yytext = ""; 841 | this.match = ""; 842 | } 843 | var rules = this._currentRules(); 844 | for (var i = 0; i < rules.length; i++) { 845 | tempMatch = this._input.match(this.rules[rules[i]]); 846 | if ( 847 | tempMatch && 848 | (!match || tempMatch[0].length > match[0].length) 849 | ) { 850 | match = tempMatch; 851 | index = i; 852 | if (this.options.backtrack_lexer) { 853 | token = this.test_match(tempMatch, rules[i]); 854 | if (token !== false) { 855 | return token; 856 | } else if (this._backtrack) { 857 | match = false; 858 | continue; // rule action called reject() implying a rule MISmatch. 859 | } else { 860 | // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) 861 | return false; 862 | } 863 | } else if (!this.options.flex) { 864 | break; 865 | } 866 | } 867 | } 868 | if (match) { 869 | token = this.test_match(match, rules[index]); 870 | if (token !== false) { 871 | return token; 872 | } 873 | // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) 874 | return false; 875 | } 876 | if (this._input === "") { 877 | return this.EOF; 878 | } else { 879 | return this.parseError( 880 | "Lexical error on line " + 881 | (this.yylineno + 1) + 882 | ". Unrecognized text.\n" + 883 | this.showPosition(), 884 | { 885 | text: "", 886 | token: null, 887 | line: this.yylineno, 888 | } 889 | ); 890 | } 891 | }, 892 | 893 | // return next match that has a token 894 | lex: function lex() { 895 | var r = this.next(); 896 | if (r) { 897 | return r; 898 | } else { 899 | return this.lex(); 900 | } 901 | }, 902 | 903 | // activates a new lexer condition state (pushes the new lexer condition state onto the condition stack) 904 | begin: function begin(condition) { 905 | this.conditionStack.push(condition); 906 | }, 907 | 908 | // pop the previously active lexer condition state off the condition stack 909 | popState: function popState() { 910 | var n = this.conditionStack.length - 1; 911 | if (n > 0) { 912 | return this.conditionStack.pop(); 913 | } else { 914 | return this.conditionStack[0]; 915 | } 916 | }, 917 | 918 | // produce the lexer rule set which is active for the currently active lexer condition state 919 | _currentRules: function _currentRules() { 920 | if ( 921 | this.conditionStack.length && 922 | this.conditionStack[this.conditionStack.length - 1] 923 | ) { 924 | return this.conditions[ 925 | this.conditionStack[this.conditionStack.length - 1] 926 | ].rules; 927 | } else { 928 | return this.conditions["INITIAL"].rules; 929 | } 930 | }, 931 | 932 | // return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available 933 | topState: function topState(n) { 934 | n = this.conditionStack.length - 1 - Math.abs(n || 0); 935 | if (n >= 0) { 936 | return this.conditionStack[n]; 937 | } else { 938 | return "INITIAL"; 939 | } 940 | }, 941 | 942 | // alias for begin(condition) 943 | pushState: function pushState(condition) { 944 | this.begin(condition); 945 | }, 946 | 947 | // return the number of states currently on the stack 948 | stateStackSize: function stateStackSize() { 949 | return this.conditionStack.length; 950 | }, 951 | options: {}, 952 | performAction: function anonymous( 953 | yy, 954 | yy_, 955 | $avoiding_name_collisions, 956 | YY_START 957 | ) { 958 | var YYSTATE = YY_START; 959 | switch ($avoiding_name_collisions) { 960 | case 0 /* skip line comments */: 961 | break; 962 | case 1 /* skip whitespace */: 963 | break; 964 | case 2: 965 | return "LINE_END"; 966 | break; 967 | case 3: 968 | return "("; 969 | break; 970 | case 4: 971 | return ")"; 972 | break; 973 | case 5: 974 | return ">"; 975 | break; 976 | case 6: 977 | return "<"; 978 | break; 979 | case 7: 980 | return "-"; 981 | break; 982 | case 8: 983 | return "*"; 984 | break; 985 | case 9: 986 | return "["; 987 | break; 988 | case 10: 989 | return "]"; 990 | break; 991 | case 11: 992 | return ","; 993 | break; 994 | case 12: 995 | return ","; 996 | break; 997 | case 13: 998 | return "."; 999 | break; 1000 | case 14: 1001 | return ":"; 1002 | break; 1003 | case 15: 1004 | return "REGEX"; 1005 | break; 1006 | case 16: 1007 | return "IDENT"; 1008 | break; 1009 | } 1010 | }, 1011 | rules: [ 1012 | /^(?:\s*#[^\n\r]*)/, 1013 | /^(?:\s+)/, 1014 | /^(?:;)/, 1015 | /^(?:\()/, 1016 | /^(?:\))/, 1017 | /^(?:>)/, 1018 | /^(?:<)/, 1019 | /^(?:-)/, 1020 | /^(?:\*)/, 1021 | /^(?:\[)/, 1022 | /^(?:\])/, 1023 | /^(?:,)/, 1024 | /^(?:,)/, 1025 | /^(?:\.)/, 1026 | /^(?::)/, 1027 | /^(?:\/[^\/]*\/)/, 1028 | /^(?:[A-Za-z0-9_$]+)/, 1029 | ], 1030 | conditions: { 1031 | INITIAL: { 1032 | rules: [ 1033 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 1034 | 16, 1035 | ], 1036 | inclusive: true, 1037 | }, 1038 | }, 1039 | }; 1040 | return lexer; 1041 | })(); 1042 | parser.lexer = lexer; 1043 | function Parser() { 1044 | this.yy = {}; 1045 | } 1046 | Parser.prototype = parser; 1047 | parser.Parser = Parser; 1048 | return new Parser(); 1049 | })(); 1050 | 1051 | if (typeof require !== "undefined" && typeof exports !== "undefined") { 1052 | exports.parser = grammar; 1053 | exports.Parser = grammar.Parser; 1054 | exports.parse = function () { 1055 | return grammar.parse.apply(grammar, arguments); 1056 | }; 1057 | exports.main = function commonjsMain(args) { 1058 | if (!args[1]) { 1059 | console.log("Usage: " + args[0] + " FILE"); 1060 | process.exit(1); 1061 | } 1062 | var source = require("fs").readFileSync( 1063 | require("path").normalize(args[1]), 1064 | "utf8" 1065 | ); 1066 | return exports.parser.parse(source); 1067 | }; 1068 | if (typeof module !== "undefined" && require.main === module) { 1069 | exports.main(process.argv.slice(1)); 1070 | } 1071 | } 1072 | --------------------------------------------------------------------------------