Skip to content

Commit

Permalink
Merge pull request #157 from Mingun/stack
Browse files Browse the repository at this point in the history
Extract the variable stack to a separate file and test it
  • Loading branch information
hildjj authored Jun 2, 2021
2 parents cb7a18b + b11f0c0 commit 5108729
Show file tree
Hide file tree
Showing 3 changed files with 456 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
187 changes: 187 additions & 0 deletions lib/compiler/stack.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
"use strict";

/** Utility class that helps generating code for C-like languages. */
class Stack {
/**
* Constructs the helper for tracking variable slots of the stack virtual machine
*
* @param {string} ruleName The name of rule that will be used in error messages
* @param {string} varName The prefix for generated names of variables
* @param {string} type The type of the variables. For JavaScript there are `var` or `let`
*/
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 a stack underflow (there are more `pop`s than `push`es)
*/
name(i) {
if (i < 0) {
throw new RangeError(
`Rule '${this.ruleName}': The variable stack underflow: attempt to use a variable '${this.varName}<x>' at an 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 the stack.
*
* @param {number} [n=1] Quantity of variables, which need to be removed from the stack
* @return {string|string[]} Generated name(s). If `n > 1` than array has length of `n`
*
* @throws {RangeError} If the stack underflow (there are more `pop`s than `push`es)
*/
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 the 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 the stack size
*/
index(i) {
if (i < 0) {
throw new RangeError(
`Rule '${this.ruleName}': The variable stack overflow: attempt to get a variable at a 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 the stack is empty (there was no `push`es yet)
*/
result() {
if (this.maxSp < 0) {
throw new RangeError(
`Rule '${this.ruleName}': The variable 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 the type `this.type`.
* If the 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 the 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 the stack pointer (if it contains unbalanced `push`es 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 5108729

Please sign in to comment.