Skip to content

Instantly share code, notes, and snippets.

@raphtlw
Last active March 26, 2022 17:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save raphtlw/b2a8ab392b89c75e9313853420cfb59e to your computer and use it in GitHub Desktop.
Save raphtlw/b2a8ab392b89c75e9313853420cfb59e to your computer and use it in GitHub Desktop.
TypeScript macro preprocessor
#!/usr/bin/env ts-node
import assert from "assert";
import fs from "fs";
import readline from "readline";
const argv = process.argv.slice(2);
const argFileName = argv[0];
function error(msg: string) {
console.log(`ERROR: ${msg}`);
process.exit(1);
}
Object.defineProperty(Array.prototype, "last", {
value: function () {
return this[this.length - 1];
},
});
declare global {
interface Array<T> {
last(): T;
}
}
const MATCH = {
MACRO_DECL: /macro`(.*?)->(.*?)`/gm,
MACRO_DECL_IDENTIFIER: /macro`.*?|[`;]/gm,
MACRO_USAGE_DECL_START: /^`/gm,
MACRO_USAGE_DECL_END: /^(?!macro).*?(`;?)/,
};
enum MacroType {
BLOCK,
FUNCTION,
CONSTANT,
}
type MacroDecl = {
expr: string;
expanded: string;
type: MacroType;
startLn: number;
};
type MacroUseBlock = {
startLn: number;
lines: string[];
};
class MacroEngine {
decls: MacroDecl[] = [];
usages: MacroUseBlock[] = [];
pointerInBlock = false;
define(decl: MacroDecl) {
if (this.decls.findIndex((v) => v.expr === decl.expr) === -1) {
this.decls.push(decl);
}
}
parse(line: string, lineNumber: number) {
if (line.match(MATCH.MACRO_DECL)) {
console.log(`line ${line} is a macro declaration`);
const [expr, expanded] = line
.replace(MATCH.MACRO_DECL_IDENTIFIER, "")
.split("->")
.map((l) => l.trim());
this.decls.push({
expr,
expanded,
type: MacroType.BLOCK,
startLn: lineNumber,
});
}
if (
line.match(MATCH.MACRO_USAGE_DECL_START) ||
line.match(MATCH.MACRO_USAGE_DECL_END) ||
this.pointerInBlock
) {
this.use(line, lineNumber);
}
}
use(line: string, lineNumber: number) {
if (line.match(MATCH.MACRO_USAGE_DECL_START)) {
const lineWithoutStart = line.replace(MATCH.MACRO_USAGE_DECL_START, "");
const macroExpr = lineWithoutStart.split(" ")[0];
console.log("macroExpr", macroExpr);
const macro = this.decls.find((decl) => decl.expr === macroExpr);
assert(macro, "Trying to use macro which is not defined");
// replace macro block contents
let generatedLine = lineWithoutStart.replace(macroExpr, macro.expanded);
// keep track of current block
this.usages.push({
startLn: lineNumber,
lines: [generatedLine],
});
this.pointerInBlock = true;
return;
}
const declEndMatch = line.match(MATCH.MACRO_USAGE_DECL_END);
if (declEndMatch) {
const lineWithoutEnd = line.replace(declEndMatch[1], "");
this.usages.last().lines.push(lineWithoutEnd);
this.pointerInBlock = false;
return;
}
if (this.pointerInBlock) {
this.usages.last().lines.push(line);
return;
}
}
}
const lineCounter = (
(i = 0) =>
() =>
++i
)();
async function main() {
const engine = new MacroEngine();
const rl = readline.createInterface({
input: fs.createReadStream(argFileName),
crlfDelay: Infinity,
});
for await (const line of rl) {
const lineNumber = lineCounter();
engine.parse(line, lineNumber);
}
console.log(engine.decls);
console.log(engine.usages);
const fileLines = fs.readFileSync(argFileName).toString().split("\n");
console.log(fileLines);
for (const usage of engine.usages) {
fileLines.splice(usage.startLn - 1, usage.lines.length, ...usage.lines);
}
for (const decl of engine.decls) {
fileLines[decl.startLn - 1] = "";
}
console.log(fileLines);
console.log(fileLines.join("\n").trim());
}
if (argv.length > 0) {
main();
} else {
error("input file not specified");
}
if (!argFileName.endsWith(".ts")) {
error("input file is not a TypeScript file");
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment