366 lines
10 KiB
JavaScript
366 lines
10 KiB
JavaScript
|
|
// @ts-check
|
||
|
|
"use strict";
|
||
|
|
|
||
|
|
const { SourceNode } = require("source-map-generator");
|
||
|
|
const GrammarLocation = require("../grammar-location.js");
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @typedef {(string|SourceNode)[]} SourceArray
|
||
|
|
*/
|
||
|
|
|
||
|
|
/** 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`
|
||
|
|
* @param {number[]} bytecode Bytecode for error messages
|
||
|
|
*/
|
||
|
|
constructor(ruleName, varName, type, bytecode) {
|
||
|
|
/** Last used variable in the stack. */
|
||
|
|
this.sp = -1;
|
||
|
|
/** Maximum stack size. */
|
||
|
|
this.maxSp = -1;
|
||
|
|
this.varName = varName;
|
||
|
|
this.ruleName = ruleName;
|
||
|
|
this.type = type;
|
||
|
|
this.bytecode = bytecode;
|
||
|
|
/**
|
||
|
|
* Map from stack index, to label targetting that index
|
||
|
|
* @type {Record<number,{label:string,location:import("../peg.js").LocationRange}>}
|
||
|
|
*/
|
||
|
|
this.labels = {};
|
||
|
|
/**
|
||
|
|
* Stack of in-flight source mappings
|
||
|
|
* @type {[SourceArray, number, PEG.LocationRange][]}
|
||
|
|
*/
|
||
|
|
this.sourceMapStack = [];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 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}.\nBytecode: ${this.bytecode}`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return this.varName + i;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
*
|
||
|
|
* @param {PEG.LocationRange} location
|
||
|
|
* @param {SourceArray} chunks
|
||
|
|
* @param {string} [name]
|
||
|
|
* @returns
|
||
|
|
*/
|
||
|
|
static sourceNode(location, chunks, name) {
|
||
|
|
const start = GrammarLocation.offsetStart(location);
|
||
|
|
return new SourceNode(
|
||
|
|
start.line,
|
||
|
|
start.column ? start.column - 1 : null,
|
||
|
|
String(location.source),
|
||
|
|
chunks,
|
||
|
|
name
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 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|SourceNode} Assignment code
|
||
|
|
*/
|
||
|
|
push(exprCode) {
|
||
|
|
if (++this.sp > this.maxSp) { this.maxSp = this.sp; }
|
||
|
|
|
||
|
|
const label = this.labels[this.sp];
|
||
|
|
const code = [this.name(this.sp), " = ", exprCode, ";"];
|
||
|
|
if (label) {
|
||
|
|
if (this.sourceMapStack.length) {
|
||
|
|
const sourceNode = Stack.sourceNode(
|
||
|
|
label.location,
|
||
|
|
code.splice(0, 2),
|
||
|
|
label.label
|
||
|
|
);
|
||
|
|
const { parts, location } = this.sourceMapPopInternal();
|
||
|
|
const newLoc = (location.start.offset < label.location.end.offset)
|
||
|
|
? {
|
||
|
|
start: label.location.end,
|
||
|
|
end: location.end,
|
||
|
|
source: location.source,
|
||
|
|
}
|
||
|
|
: location;
|
||
|
|
|
||
|
|
const outerNode = Stack.sourceNode(
|
||
|
|
newLoc,
|
||
|
|
code.concat("\n")
|
||
|
|
);
|
||
|
|
this.sourceMapStack.push([parts, parts.length + 1, location]);
|
||
|
|
return new SourceNode(
|
||
|
|
null,
|
||
|
|
null,
|
||
|
|
label.location.source,
|
||
|
|
[sourceNode, outerNode]
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
return Stack.sourceNode(
|
||
|
|
label.location,
|
||
|
|
code.concat("\n")
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return code.join("");
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @overload
|
||
|
|
* @param {undefined} [n]
|
||
|
|
* @return {string}
|
||
|
|
*/
|
||
|
|
/**
|
||
|
|
* @overload
|
||
|
|
* @param {number} n
|
||
|
|
* @return {string[]}
|
||
|
|
*/
|
||
|
|
/**
|
||
|
|
* Returns name or `n` names of the variable(s) from the top of the stack.
|
||
|
|
*
|
||
|
|
* @param {number} [n] Quantity of variables, which need to be removed from the stack
|
||
|
|
* @returns {string[]|string} Generated name(s). If n is defined then it returns an
|
||
|
|
* array of length `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}.\nBytecode: ${this.bytecode}`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
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.\nBytecode: ${this.bytecode}`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
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.
|
||
|
|
*
|
||
|
|
* @template T
|
||
|
|
* @param {number} pos Opcode number for error messages
|
||
|
|
* @param {() => T} generateIf First function that works with this stack
|
||
|
|
* @param {(() => T)|null} [generateElse] Second function that works with this stack
|
||
|
|
* @return {T[]}
|
||
|
|
*
|
||
|
|
* @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;
|
||
|
|
|
||
|
|
const ifResult = generateIf();
|
||
|
|
|
||
|
|
if (!generateElse) {
|
||
|
|
return [ifResult];
|
||
|
|
}
|
||
|
|
const thenSp = this.sp;
|
||
|
|
|
||
|
|
this.sp = baseSp;
|
||
|
|
const elseResult = 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 + "). "
|
||
|
|
+ "Bytecode: " + this.bytecode
|
||
|
|
);
|
||
|
|
}
|
||
|
|
return [ifResult, elseResult];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Checks that code in the `generateBody` do not move stack pointer.
|
||
|
|
*
|
||
|
|
* @template T
|
||
|
|
* @param {number} pos Opcode number for error messages
|
||
|
|
* @param {() => T} generateBody Function that works with this stack
|
||
|
|
* @return {T}
|
||
|
|
*
|
||
|
|
* @throws {Error} If `generateBody` move the stack pointer (if it contains unbalanced `push`es and `pop`s)
|
||
|
|
*/
|
||
|
|
checkedLoop(pos, generateBody) {
|
||
|
|
const baseSp = this.sp;
|
||
|
|
|
||
|
|
const result = 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 + "). "
|
||
|
|
+ "Bytecode: " + this.bytecode
|
||
|
|
);
|
||
|
|
}
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
*
|
||
|
|
* @param {SourceArray} parts
|
||
|
|
* @param {PEG.LocationRange} location
|
||
|
|
*/
|
||
|
|
sourceMapPush(parts, location) {
|
||
|
|
if (this.sourceMapStack.length) {
|
||
|
|
const top = this.sourceMapStack[this.sourceMapStack.length - 1];
|
||
|
|
// If the current top of stack starts at the same location as
|
||
|
|
// the about to be pushed item, we should update its start location to
|
||
|
|
// be past the new one. Otherwise any code it generates will
|
||
|
|
// get allocated to the inner node.
|
||
|
|
if (top[2].start.offset === location.start.offset
|
||
|
|
&& top[2].end.offset > location.end.offset) {
|
||
|
|
top[2] = {
|
||
|
|
start: location.end,
|
||
|
|
end: top[2].end,
|
||
|
|
source: top[2].source,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
this.sourceMapStack.push([
|
||
|
|
parts,
|
||
|
|
parts.length,
|
||
|
|
location,
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @returns {{parts:SourceArray,location:PEG.LocationRange}}
|
||
|
|
*/
|
||
|
|
sourceMapPopInternal() {
|
||
|
|
const elt = this.sourceMapStack.pop();
|
||
|
|
if (!elt) {
|
||
|
|
throw new RangeError(
|
||
|
|
`Rule '${this.ruleName}': Attempting to pop an empty source map stack.\nBytecode: ${this.bytecode}`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
const [
|
||
|
|
parts,
|
||
|
|
index,
|
||
|
|
location,
|
||
|
|
] = elt;
|
||
|
|
const chunks = parts.splice(index).map(
|
||
|
|
chunk => (chunk instanceof SourceNode
|
||
|
|
? chunk
|
||
|
|
: chunk + "\n"
|
||
|
|
)
|
||
|
|
);
|
||
|
|
if (chunks.length) {
|
||
|
|
const start = GrammarLocation.offsetStart(location);
|
||
|
|
parts.push(new SourceNode(
|
||
|
|
start.line,
|
||
|
|
start.column - 1,
|
||
|
|
String(location.source),
|
||
|
|
chunks
|
||
|
|
));
|
||
|
|
}
|
||
|
|
return { parts, location };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {number} [offset]
|
||
|
|
* @returns {[SourceArray, number, PEG.LocationRange]|undefined}
|
||
|
|
*/
|
||
|
|
sourceMapPop(offset) {
|
||
|
|
const { location } = this.sourceMapPopInternal();
|
||
|
|
if (this.sourceMapStack.length
|
||
|
|
&& location.end.offset
|
||
|
|
< this.sourceMapStack[this.sourceMapStack.length - 1][2].end.offset) {
|
||
|
|
const { parts, location: outer } = this.sourceMapPopInternal();
|
||
|
|
const newLoc = (outer.start.offset < location.end.offset)
|
||
|
|
? {
|
||
|
|
start: location.end,
|
||
|
|
end: outer.end,
|
||
|
|
source: outer.source,
|
||
|
|
}
|
||
|
|
: outer;
|
||
|
|
|
||
|
|
this.sourceMapStack.push([
|
||
|
|
parts,
|
||
|
|
parts.length + (offset || 0),
|
||
|
|
newLoc,
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
return undefined;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = Stack;
|