diff --git a/lib/compiler/passes/generate-js.js b/lib/compiler/passes/generate-js.js index d2cb28b4..fe0c268e 100644 --- a/lib/compiler/passes/generate-js.js +++ b/lib/compiler/passes/generate-js.js @@ -2,6 +2,7 @@ const asts = require("../asts"); const op = require("../opcodes"); +const Stack = require("../stack"); const VERSION = require("../../version"); function hex(ch) { return ch.charCodeAt(0).toString(16).toUpperCase(); } @@ -223,46 +224,7 @@ function generateJS(ast, options) { function generateRuleFunction(rule) { const parts = []; - const stackVars = []; - - function s(i) { return "s" + i; } // |stack[i]| of the abstract machine - - const stack = { - sp: -1, - maxSp: -1, - - push(exprCode) { - const code = s(++this.sp) + " = " + exprCode + ";"; - - if (this.sp > this.maxSp) { this.maxSp = this.sp; } - - return code; - }, - - pop(n) { - if (n === undefined) { - return s(this.sp--); - } else { - const values = Array(n); - - for (let i = 0; i < n; i++) { - values[i] = s(this.sp - n + 1 + i); - } - - this.sp -= n; - - return values; - } - }, - - top() { - return s(this.sp); - }, - - index(i) { - return s(this.sp - i); - } - }; + const stack = new Stack(rule.name, "s", "var"); function compile(bc) { let ip = 0; @@ -274,26 +236,19 @@ function generateJS(ast, options) { const baseLength = argCount + 3; const thenLength = bc[ip + baseLength - 2]; const elseLength = bc[ip + baseLength - 1]; - const baseSp = stack.sp; - let elseCode, elseSp; - - ip += baseLength; - const thenCode = compile(bc.slice(ip, ip + thenLength)); - const thenSp = stack.sp; - ip += thenLength; - - if (elseLength > 0) { - stack.sp = baseSp; - elseCode = compile(bc.slice(ip, ip + elseLength)); - elseSp = stack.sp; - ip += elseLength; - - if (thenSp !== elseSp) { - throw new Error( - "Branches of a condition must move the stack pointer in the same way." - ); - } - } + let thenCode, elseCode; + + stack.checkedIf(ip, + () => { + ip += baseLength; + thenCode = compile(bc.slice(ip, ip + thenLength)); + ip += thenLength; + }, + elseLength > 0 ? () => { + elseCode = compile(bc.slice(ip, ip + elseLength)); + ip += elseLength; + } : null + ); parts.push("if (" + cond + ") {"); parts.push(indent2(thenCode)); @@ -307,16 +262,13 @@ function generateJS(ast, options) { function compileLoop(cond) { const baseLength = 2; const bodyLength = bc[ip + baseLength - 1]; - const baseSp = stack.sp; + let bodyCode; - ip += baseLength; - const bodyCode = compile(bc.slice(ip, ip + bodyLength)); - const bodySp = stack.sp; - ip += bodyLength; - - if (bodySp !== baseSp) { - throw new Error("Body of a loop can't move the stack pointer."); - } + stack.checkedLoop(ip, () => { + ip += baseLength; + bodyCode = compile(bc.slice(ip, ip + bodyLength)); + ip += bodyLength; + }); parts.push("while (" + cond + ") {"); parts.push(indent2(bodyCode)); @@ -553,11 +505,7 @@ function generateJS(ast, options) { parts.push(" var startPos = peg$currPos;"); } - for (let i = 0; i <= stack.maxSp; i++) { - stackVars[i] = s(i); - } - - parts.push(" var " + stackVars.join(", ") + ";"); + parts.push(indent2(stack.defines())); parts.push(indent2(generateRuleHeader( "\"" + stringEscape(rule.name) + "\"", @@ -566,7 +514,7 @@ function generateJS(ast, options) { parts.push(indent2(code)); parts.push(indent2(generateRuleFooter( "\"" + stringEscape(rule.name) + "\"", - s(0) + stack.result() ))); parts.push("}"); diff --git a/lib/compiler/stack.js b/lib/compiler/stack.js new file mode 100644 index 00000000..0ca313f6 --- /dev/null +++ b/lib/compiler/stack.js @@ -0,0 +1,179 @@ +"use strict"; + +class Stack { + constructor(ruleName, varName, type) { + /** Last used variable in the stack. */ + this.sp = -1; + /** Maximum stack size. */ + this.maxSp = -1; + this.varName = varName; + this.ruleName = ruleName; + this.type = type; + } + + /** + * Returns name of the variable at the index `i`. + * + * @param {number} i Index for which name must be generated + * @return {string} Generated name + * + * @throws {RangeError} If `i < 0` which means stack underflow (there a more `pop`'s than `push`'s) + */ + name(i) { + if (i < 0) { + throw new RangeError( + `Rule '${this.ruleName}': Var stack underflow: attempt to use a variable '${this.varName}' at index ${i}` + ); + } + + return this.varName + i; + } + + /** + * Assigns `exprCode` to the new variable in the stack, returns generated code. + * As the result, the size of a stack increases on 1. + * + * @param {string} exprCode Any expression code that must be assigned to the new variable in the stack + * @return {string} Assignment code + */ + push(exprCode) { + const code = this.name(++this.sp) + " = " + exprCode + ";"; + + if (this.sp > this.maxSp) { this.maxSp = this.sp; } + + return code; + } + + /** + * Returns name or `n` names of the variable(s) from the top of stack. + * + * @param {number} [n=1] Quantity of variables which need to be removed from a stack + * @return {string|string[]} Generated name(s). If `n > 1` than array has length of `n` + * + * @throws {RangeError} If stack underflow (there a more single `pop`'s than `push`'s) + */ + pop(n) { + if (n !== undefined) { + this.sp -= n; + + return Array.from({ length: n }, (v, i) => this.name(this.sp + 1 + i)); + } + + return this.name(this.sp--); + } + + /** + * Returns name of the first free variable. The same as `index(0)`. + * + * @return {string} Generated name + * + * @throws {RangeError} If stack is empty (there was no `push`'s yet) + */ + top() { return this.name(this.sp); } + + /** + * Returns name of the variable at index `i`. + * + * @param {number} [i] Index of the variable from top of the stack + * @return {string} Generated name + * + * @throws {RangeError} If `i < 0` or more than stack size + */ + index(i) { + if (i < 0) { + throw new RangeError( + `Rule '${this.ruleName}': Var stack overflow: attempt to get a variable at negative index ${i}` + ); + } + + return this.name(this.sp - i); + } + + /** + * Returns variable name that contains result (bottom of the stack). + * + * @return {string} Generated name + * + * @throws {RangeError} If stack is empty (there was no `push`'s yet) + */ + result() { + if (this.maxSp < 0) { + throw new RangeError( + `Rule '${this.ruleName}': Var stack is empty, can't get the result'` + ); + } + + return this.name(0); + } + + /** + * Returns defines of all used variables. + * + * @return {string} Generated define variable expression with type `this.type`. + * If stack is empty, returns empty string + */ + defines() { + if (this.maxSp < 0) { + return ""; + } + + return this.type + " " + Array.from({ length: this.maxSp + 1 }, (v, i) => this.name(i)).join(", ") + ";"; + } + + /** + * Checks that code in the `generateIf` and `generateElse` move the stack pointer in the same way. + * + * @param {number} pos Opcode number for error messages + * @param {function()} generateIf First function that works with this stack + * @param {function()} [generateElse] Second function that works with this stack + * @return {undefined} + * + * @throws {Error} If `generateElse` is defined and stack pointer moved differently in the + * `generateIf` and `generateElse` + */ + checkedIf(pos, generateIf, generateElse) { + const baseSp = this.sp; + + generateIf(); + + if (generateElse) { + const thenSp = this.sp; + + this.sp = baseSp; + generateElse(); + + if (thenSp !== this.sp) { + throw new Error( + "Rule '" + this.ruleName + "', position " + pos + ": " + + "Branches of a condition can't move the stack pointer differently " + + "(before: " + baseSp + ", after then: " + thenSp + ", after else: " + this.sp + ")." + ); + } + } + } + + /** + * Checks that code in the `generateBody` do not move stack pointer. + * + * @param {number} pos Opcode number for error messages + * @param {function()} generateBody Function that works with this stack + * @return {undefined} + * + * @throws {Error} If `generateBody` move stack pointer (contains unbalanced `push` and `pop`'s) + */ + checkedLoop(pos, generateBody) { + const baseSp = this.sp; + + generateBody(); + + if (baseSp !== this.sp) { + throw new Error( + "Rule '" + this.ruleName + "', position " + pos + ": " + + "Body of a loop can't move the stack pointer " + + "(before: " + baseSp + ", after: " + this.sp + ")." + ); + } + } +} + +module.exports = Stack; diff --git a/test/unit/compiler/stack.spec.js b/test/unit/compiler/stack.spec.js new file mode 100644 index 00000000..db680b02 --- /dev/null +++ b/test/unit/compiler/stack.spec.js @@ -0,0 +1,240 @@ +"use strict"; + +const chai = require("chai"); +const Stack = require("../../../lib/compiler/stack"); + +const expect = chai.expect; + +describe("utility class Stack", function() { + describe("for empty stack", function() { + let stack; + + beforeEach(() => { stack = new Stack("rule", "v", "let"); }); + + it("throws error when attempt `pop`", function() { + expect(() => stack.pop()).to.throw(RangeError, + "Rule 'rule': Var stack underflow: attempt to use a variable 'v' at index -1" + ); + }); + + it("throws error when attempt `top`", function() { + expect(() => stack.top()).to.throw(RangeError, + "Rule 'rule': Var stack underflow: attempt to use a variable 'v' at index -1" + ); + }); + + it("throws error when attempt `result`", function() { + expect(() => stack.result()).to.throw(RangeError, + "Rule 'rule': Var stack is empty, can't get the result" + ); + }); + + it("throws error when attempt `index`", function() { + expect(() => stack.index(-2)).to.throw(RangeError, + "Rule 'rule': Var stack overflow: attempt to get a variable at negative index -2" + ); + expect(() => stack.index(0)).to.throw(RangeError, + "Rule 'rule': Var stack underflow: attempt to use a variable 'v' at index -1" + ); + expect(() => stack.index(2)).to.throw(RangeError, + "Rule 'rule': Var stack underflow: attempt to use a variable 'v' at index -3" + ); + }); + + it("`defines` returns empty string", function() { + expect(stack.defines()).to.equal(""); + }); + }); + + it("throws error when attempt `pop` more than `push`", function() { + const stack = new Stack("rule", "v", "let"); + + stack.push("1"); + + expect(() => stack.pop(3)).to.throw(RangeError, + "Rule 'rule': Var stack underflow: attempt to use a variable 'v' at index -2" + ); + }); + + it("returns variable with index 0 for `result`", function() { + const stack = new Stack("rule", "v", "let"); + + stack.push("1"); + + expect(stack.result()).to.equal("v0"); + }); + + it("`defines` returns define expression for all used variables", function() { + const stack = new Stack("rule", "v", "let"); + + stack.push("1"); + stack.push("2"); + stack.pop(); + stack.push("3"); + + expect(stack.defines()).to.equal("let v0, v1;"); + }); + + describe("`checkedIf` method", function() { + let stack; + + beforeEach(function() { + stack = new Stack("rule", "v", "let"); + stack.push("1"); + }); + + it("without else brach do not throws error", function() { + expect(() => stack.checkedIf(0, () => {/* lint */})).to.not.throw(); + expect(() => stack.checkedIf(0, () => stack.pop())).to.not.throw(); + expect(() => stack.checkedIf(0, () => stack.push("2"))).to.not.throw(); + }); + + it("do not throws error when stack pointer doesn't move in both arms", function() { + function fn1() {/* lint */} + function fn2() { + stack.push("1"); + stack.pop(); + } + function fn3() { + stack.push("1"); + stack.push("2"); + stack.pop(2); + } + function fn4() { + stack.push("1"); + stack.pop(); + stack.push("2"); + stack.pop(); + } + + expect(() => stack.checkedIf(0, fn1, fn1)).to.not.throw(); + expect(() => stack.checkedIf(0, fn2, fn2)).to.not.throw(); + expect(() => stack.checkedIf(0, fn3, fn3)).to.not.throw(); + expect(() => stack.checkedIf(0, fn4, fn4)).to.not.throw(); + }); + + it("do not throws error when stack pointer increases on the same value in both arms", function() { + expect(() => stack.checkedIf(0, + () => stack.push("1"), + () => stack.push("2") + )).to.not.throw(); + }); + + it("do not throws error when stack pointer decreases on the same value in both arms", function() { + stack.push("2"); + + expect(() => stack.checkedIf(0, + () => stack.pop(2), + () => { stack.pop(); stack.pop(); } + )).to.not.throw(); + }); + + describe("throws error when stack pointer", function() { + it("do not move in `if` and decreases in `then`", function() { + expect(() => { + stack.checkedIf(0, () => {/* lint */}, () => stack.pop()); + }).to.throw(Error, + "Rule 'rule', position 0: " + + "Branches of a condition can't move the stack pointer differently " + + "(before: 0, after then: 0, after else: -1)." + ); + }); + it("decreases in `if` and do not move in `then`", function() { + expect(() => { + stack.checkedIf(0, () => stack.pop(), () => {/* lint */}); + }).to.throw(Error, + "Rule 'rule', position 0: " + + "Branches of a condition can't move the stack pointer differently " + + "(before: 0, after then: -1, after else: 0)." + ); + }); + + it("do not move in `if` and increases in `then`", function() { + expect(() => { + stack.checkedIf(0, () => {/* lint */}, () => stack.push("2")); + }).to.throw(Error, + "Rule 'rule', position 0: " + + "Branches of a condition can't move the stack pointer differently " + + "(before: 0, after then: 0, after else: 1)." + ); + }); + it("increases in `if` and do not move in `then`", function() { + expect(() => { + stack.checkedIf(0, () => stack.push("2"), () => {/* lint */}); + }).to.throw(Error, + "Rule 'rule', position 0: " + + "Branches of a condition can't move the stack pointer differently " + + "(before: 0, after then: 1, after else: 0)." + ); + }); + + it("decreases in `if` and increases in `then`", function() { + expect(() => { + stack.checkedIf(0, () => stack.pop(), () => stack.push("2")); + }).to.throw(Error, + "Rule 'rule', position 0: " + + "Branches of a condition can't move the stack pointer differently " + + "(before: 0, after then: -1, after else: 1)." + ); + }); + it("increases in `if` and decreases in `then`", function() { + expect(() => { + stack.checkedIf(0, () => stack.push("2"), () => stack.pop()); + }).to.throw(Error, + "Rule 'rule', position 0: " + + "Branches of a condition can't move the stack pointer differently " + + "(before: 0, after then: 1, after else: -1)." + ); + }); + }); + }); + + describe("`checkedLoop` method", function() { + let stack; + + beforeEach(function() { + stack = new Stack("rule", "v", "let"); + stack.push("1"); + }); + + it("do not throws error when stack pointer not moves", function() { + function fn1() {/* lint */} + function fn2() { + stack.push("1"); + stack.pop(); + } + function fn3() { + stack.push("1"); + stack.push("2"); + stack.pop(2); + } + function fn4() { + stack.push("1"); + stack.pop(); + stack.push("2"); + stack.pop(); + } + + expect(() => stack.checkedLoop(0, fn1)).to.not.throw(); + expect(() => stack.checkedLoop(0, fn2)).to.not.throw(); + expect(() => stack.checkedLoop(0, fn3)).to.not.throw(); + expect(() => stack.checkedLoop(0, fn4)).to.not.throw(); + }); + + it("throws error when stack pointer increases", function() { + expect(() => stack.checkedLoop(0, () => stack.push("1"))).to.throw(Error, + "Rule 'rule', position 0: " + + "Body of a loop can't move the stack pointer " + + "(before: 0, after: 1)." + ); + }); + + it("throws error when stack pointer decreases", function() { + expect(() => stack.checkedLoop(0, () => stack.pop())).to.throw(Error, + "Rule 'rule', position 0: " + + "Body of a loop can't move the stack pointer " + + "(before: 0, after: -1)." + ); + }); + }); +});