Skip to content

Commit

Permalink
Extract the variable stack to a separate file and test it
Browse files Browse the repository at this point in the history
Also this change add additional safety checks to the code generator and improves errors if they was violated
  • Loading branch information
Mingun committed May 27, 2021
1 parent d611345 commit 2c09f50
Show file tree
Hide file tree
Showing 3 changed files with 442 additions and 75 deletions.
98 changes: 23 additions & 75 deletions lib/compiler/passes/generate-js.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(); }
Expand Down Expand Up @@ -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;
Expand All @@ -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));
Expand All @@ -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));
Expand Down Expand Up @@ -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) + "\"",
Expand All @@ -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("}");
Expand Down
179 changes: 179 additions & 0 deletions lib/compiler/stack.js
Original file line number Diff line number Diff line change
@@ -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}<x>' 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;
Loading

0 comments on commit 2c09f50

Please sign in to comment.