antlr4-runtime
Version:
JavaScript runtime for ANTLR4
443 lines (405 loc) • 14.8 kB
JavaScript
import Token from "./Token.js";
import Interval from "./misc/Interval.js";
/**
* @typedef {import("./CommonTokenStream").default} CommonTokenStream
* @typedef {Array<RewriteOperation | undefined>} Rewrites
* @typedef {unknown} Text
*/
export default class TokenStreamRewriter {
// eslint-disable-next-line no-undef
static DEFAULT_PROGRAM_NAME = "default";
/**
* @param {CommonTokenStream} tokens The token stream to modify
*/
constructor(tokens) {
this.tokens = tokens;
/** @type {Map<string, Rewrites>} */
this.programs = new Map();
}
/**
* @returns {CommonTokenStream}
*/
getTokenStream() {
return this.tokens;
}
/**
* Insert the supplied text after the specified token (or token index)
* @param {Token | number} tokenOrIndex
* @param {Text} text
* @param {string} [programName]
*/
insertAfter(tokenOrIndex, text, programName = TokenStreamRewriter.DEFAULT_PROGRAM_NAME) {
/** @type {number} */
let index;
if (typeof tokenOrIndex === "number") {
index = tokenOrIndex;
} else {
index = tokenOrIndex.tokenIndex;
}
// to insert after, just insert before next index (even if past end)
let rewrites = this.getProgram(programName);
let op = new InsertAfterOp(this.tokens, index, rewrites.length, text);
rewrites.push(op);
}
/**
* Insert the supplied text before the specified token (or token index)
* @param {Token | number} tokenOrIndex
* @param {Text} text
* @param {string} [programName]
*/
insertBefore(tokenOrIndex, text, programName = TokenStreamRewriter.DEFAULT_PROGRAM_NAME) {
/** @type {number} */
let index;
if (typeof tokenOrIndex === "number") {
index = tokenOrIndex;
} else {
index = tokenOrIndex.tokenIndex;
}
const rewrites = this.getProgram(programName);
const op = new InsertBeforeOp(this.tokens, index, rewrites.length, text);
rewrites.push(op);
}
/**
* Replace the specified token with the supplied text
* @param {Token | number} tokenOrIndex
* @param {Text} text
* @param {string} [programName]
*/
replaceSingle(tokenOrIndex, text, programName = TokenStreamRewriter.DEFAULT_PROGRAM_NAME) {
this.replace(tokenOrIndex, tokenOrIndex, text, programName);
}
/**
* Replace the specified range of tokens with the supplied text
* @param {Token | number} from
* @param {Token | number} to
* @param {Text} text
* @param {string} [programName]
*/
replace(from, to, text, programName = TokenStreamRewriter.DEFAULT_PROGRAM_NAME) {
if (typeof from !== "number") {
from = from.tokenIndex;
}
if (typeof to !== "number") {
to = to.tokenIndex;
}
if (from > to || from < 0 || to < 0 || to >= this.tokens.size) {
throw new RangeError(`replace: range invalid: ${from}..${to}(size=${this.tokens.size})`);
}
let rewrites = this.getProgram(programName);
let op = new ReplaceOp(this.tokens, from, to, rewrites.length, text);
rewrites.push(op);
}
/**
* Delete the specified range of tokens
* @param {number | Token} from
* @param {number | Token} to
* @param {string} [programName]
*/
delete(from, to, programName = TokenStreamRewriter.DEFAULT_PROGRAM_NAME) {
if (typeof to === "undefined") {
to = from;
}
this.replace(from, to, null, programName);
}
/**
* @param {string} name
* @returns {Rewrites}
*/
getProgram(name) {
let is = this.programs.get(name);
if (is == null) {
is = this.initializeProgram(name);
}
return is;
}
/**
* @param {string} name
* @returns {Rewrites}
*/
initializeProgram(name) {
const is = [];
this.programs.set(name, is);
return is;
}
/**
* Return the text from the original tokens altered per the instructions given to this rewriter
* @param {Interval | string} [intervalOrProgram]
* @param {string} [programName]
* @returns {string}
*/
getText(intervalOrProgram, programName = TokenStreamRewriter.DEFAULT_PROGRAM_NAME) {
let interval;
if (intervalOrProgram instanceof Interval) {
interval = intervalOrProgram;
} else {
interval = new Interval(0, this.tokens.size - 1);
}
if (typeof intervalOrProgram === "string") {
programName = intervalOrProgram;
}
const rewrites = this.programs.get(programName);
let start = interval.start;
let stop = interval.stop;
// ensure start/end are in range
if (stop > this.tokens.size - 1) {
stop = this.tokens.size - 1;
}
if (start < 0) {
start = 0;
}
if (rewrites == null || rewrites.length === 0) {
return this.tokens.getText(new Interval(start, stop)); // no instructions to execute
}
let buf = [];
// First, optimize instruction stream
let indexToOp = this.reduceToSingleOperationPerIndex(rewrites);
// Walk buffer, executing instructions and emitting tokens
let i = start;
while (i <= stop && i < this.tokens.size) {
let op = indexToOp.get(i);
indexToOp.delete(i); // remove so any left have index size-1
let t = this.tokens.get(i);
if (op == null) {
// no operation at that index, just dump token
if (t.type !== Token.EOF) {
buf.push(String(t.text));
}
i++; // move to next token
}
else {
i = op.execute(buf); // execute operation and skip
}
}
// include stuff after end if it's last index in buffer
// So, if they did an insertAfter(lastValidIndex, "foo"), include
// foo if end==lastValidIndex.
if (stop === this.tokens.size - 1) {
// Scan any remaining operations after last token
// should be included (they will be inserts).
for (const op of indexToOp.values()) {
if (op.index >= this.tokens.size - 1) {
buf.push(op.text.toString());
}
}
}
return buf.join("");
}
/**
* @param {Rewrites} rewrites
* @returns {Map<number, RewriteOperation>} a map from token index to operation
*/
reduceToSingleOperationPerIndex(rewrites) {
// WALK REPLACES
for (let i = 0; i < rewrites.length; i++) {
let op = rewrites[i];
if (op == null) {
continue;
}
if (!(op instanceof ReplaceOp)) {
continue;
}
let rop = op;
// Wipe prior inserts within range
let inserts = this.getKindOfOps(rewrites, InsertBeforeOp, i);
for (let iop of inserts) {
if (iop.index === rop.index) {
// E.g., insert before 2, delete 2..2; update replace
// text to include insert before, kill insert
rewrites[iop.instructionIndex] = undefined;
rop.text = iop.text.toString() + (rop.text != null ? rop.text.toString() : "");
}
else if (iop.index > rop.index && iop.index <= rop.lastIndex) {
// delete insert as it's a no-op.
rewrites[iop.instructionIndex] = undefined;
}
}
// Drop any prior replaces contained within
let prevReplaces = this.getKindOfOps(rewrites, ReplaceOp, i);
for (let prevRop of prevReplaces) {
if (prevRop.index >= rop.index && prevRop.lastIndex <= rop.lastIndex) {
// delete replace as it's a no-op.
rewrites[prevRop.instructionIndex] = undefined;
continue;
}
// throw exception unless disjoint or identical
let disjoint =
prevRop.lastIndex < rop.index || prevRop.index > rop.lastIndex;
// Delete special case of replace (text==null):
// D.i-j.u D.x-y.v | boundaries overlap combine to max(min)..max(right)
if (prevRop.text == null && rop.text == null && !disjoint) {
rewrites[prevRop.instructionIndex] = undefined; // kill first delete
rop.index = Math.min(prevRop.index, rop.index);
rop.lastIndex = Math.max(prevRop.lastIndex, rop.lastIndex);
}
else if (!disjoint) {
throw new Error(`replace op boundaries of ${rop} overlap with previous ${prevRop}`);
}
}
}
// WALK INSERTS
for (let i = 0; i < rewrites.length; i++) {
let op = rewrites[i];
if (op == null) {
continue;
}
if (!(op instanceof InsertBeforeOp)) {
continue;
}
let iop = op;
// combine current insert with prior if any at same index
let prevInserts = this.getKindOfOps(rewrites, InsertBeforeOp, i);
for (let prevIop of prevInserts) {
if (prevIop.index === iop.index) {
if (prevIop instanceof InsertAfterOp) {
iop.text = this.catOpText(prevIop.text, iop.text);
rewrites[prevIop.instructionIndex] = undefined;
}
else if (prevIop instanceof InsertBeforeOp) { // combine objects
// convert to strings...we're in process of toString'ing
// whole token buffer so no lazy eval issue with any templates
iop.text = this.catOpText(iop.text, prevIop.text);
// delete redundant prior insert
rewrites[prevIop.instructionIndex] = undefined;
}
}
}
// look for replaces where iop.index is in range; error
let prevReplaces = this.getKindOfOps(rewrites, ReplaceOp, i);
for (let rop of prevReplaces) {
if (iop.index === rop.index) {
rop.text = this.catOpText(iop.text, rop.text);
rewrites[i] = undefined; // delete current insert
continue;
}
if (iop.index >= rop.index && iop.index <= rop.lastIndex) {
throw new Error(`insert op ${iop} within boundaries of previous ${rop}`);
}
}
}
/** @type {Map<number, RewriteOperation>} */
let m = new Map();
for (let op of rewrites) {
if (op == null) {
// ignore deleted ops
continue;
}
if (m.get(op.index) != null) {
throw new Error("should only be one op per index");
}
m.set(op.index, op);
}
return m;
}
/**
* @param {Text} a
* @param {Text} b
* @returns {string}
*/
catOpText(a, b) {
let x = "";
let y = "";
if (a != null) {
x = a.toString();
}
if (b != null) {
y = b.toString();
}
return x + y;
}
/**
* Get all operations before an index of a particular kind
* @param {Rewrites} rewrites
* @param {any} kind
* @param {number} before
*/
getKindOfOps(rewrites, kind, before) {
return rewrites.slice(0, before).filter(op => op && op instanceof kind);
}
}
class RewriteOperation {
/**
* @param {CommonTokenStream} tokens
* @param {number} index
* @param {number} instructionIndex
* @param {Text} text
*/
constructor(tokens, index, instructionIndex, text) {
this.tokens = tokens;
this.instructionIndex = instructionIndex;
this.index = index;
this.text = text === undefined ? "" : text;
}
toString() {
let opName = this.constructor.name;
const $index = opName.indexOf("$");
opName = opName.substring($index + 1, opName.length);
return "<" + opName + "@" + this.tokens.get(this.index) +
":\"" + this.text + "\">";
}
}
class InsertBeforeOp extends RewriteOperation {
/**
* @param {CommonTokenStream} tokens
* @param {number} index
* @param {number} instructionIndex
* @param {Text} text
*/
constructor(tokens, index, instructionIndex, text) {
super(tokens, index, instructionIndex, text);
}
/**
* @param {string[]} buf
* @returns {number} the index of the next token to operate on
*/
execute(buf) {
if (this.text) {
buf.push(this.text.toString());
}
if (this.tokens.get(this.index).type !== Token.EOF) {
buf.push(String(this.tokens.get(this.index).text));
}
return this.index + 1;
}
}
class InsertAfterOp extends InsertBeforeOp {
/**
* @param {CommonTokenStream} tokens
* @param {number} index
* @param {number} instructionIndex
* @param {Text} text
*/
constructor(tokens, index, instructionIndex, text) {
super(tokens, index + 1, instructionIndex, text); // insert after is insert before index+1
}
}
class ReplaceOp extends RewriteOperation {
/**
* @param {CommonTokenStream} tokens
* @param {number} from
* @param {number} to
* @param {number} instructionIndex
* @param {Text} text
*/
constructor(tokens, from, to, instructionIndex, text) {
super(tokens, from, instructionIndex, text);
this.lastIndex = to;
}
/**
* @param {string[]} buf
* @returns {number} the index of the next token to operate on
*/
execute(buf) {
if (this.text) {
buf.push(this.text.toString());
}
return this.lastIndex + 1;
}
toString() {
if (this.text == null) {
return "<DeleteOp@" + this.tokens.get(this.index) +
".." + this.tokens.get(this.lastIndex) + ">";
}
return "<ReplaceOp@" + this.tokens.get(this.index) +
".." + this.tokens.get(this.lastIndex) + ":\"" + this.text + "\">";
}
}