mirror of
https://github.com/remko/waforth
synced 2024-12-30 22:23:31 +01:00
488 lines
12 KiB
TypeScript
Executable file
488 lines
12 KiB
TypeScript
Executable file
#!/usr/bin/env yarn exec ts-node --
|
|
|
|
import * as process from "process";
|
|
import * as fs from "fs";
|
|
import * as _ from "lodash";
|
|
import simpleEval from "simple-eval";
|
|
import { assert } from "console";
|
|
|
|
let inplace = false;
|
|
let addDict: string | undefined = undefined;
|
|
for (let i = 2; i < process.argv.length; i++) {
|
|
const arg = process.argv[i];
|
|
switch (arg) {
|
|
case "--inplace":
|
|
inplace = true;
|
|
break;
|
|
case "--add-dict":
|
|
addDict = process.argv[i + 1];
|
|
i += 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
type DictElement = {
|
|
type: "dict";
|
|
offset: number;
|
|
prev: number;
|
|
flags: number;
|
|
name: string;
|
|
index: number;
|
|
indexExpr?: string;
|
|
data?: string;
|
|
dataExpr?: string;
|
|
|
|
// Added afterwards
|
|
func?: string;
|
|
};
|
|
|
|
type StringElement = {
|
|
type: "string";
|
|
offset: number;
|
|
string: string;
|
|
};
|
|
|
|
type DataElement = DictElement | StringElement;
|
|
|
|
function unescapeString(s: string) {
|
|
return s
|
|
.replace(/\\n/g, "\n")
|
|
.replace(/\\(..)/g, (str: string, ...args: any[]) => {
|
|
return String.fromCharCode(parseInt(args[0], 16));
|
|
});
|
|
}
|
|
|
|
function escapeString(s: string) {
|
|
return s.replace("\\", "\\5c").replace('"', "\\22").replace("\n", "\\n");
|
|
}
|
|
|
|
function unpack(s: string): number {
|
|
let n = 0;
|
|
for (const ch of s
|
|
.split(/(\\..|[^\\])/)
|
|
.filter((x) => x != "")
|
|
.reverse()) {
|
|
n = n << 8;
|
|
if (ch.startsWith("\\")) {
|
|
n += parseInt(ch.slice(1), 16);
|
|
} else {
|
|
n += ch.charCodeAt(0);
|
|
}
|
|
}
|
|
return n;
|
|
}
|
|
|
|
function pack(n: number) {
|
|
const acc: number[] = [];
|
|
while (n > 0) {
|
|
acc.push(n % 256);
|
|
n = Math.floor(n / 256);
|
|
}
|
|
while (acc.length < 4) {
|
|
acc.push(0);
|
|
}
|
|
return acc.map((x) => "\\" + _.padStart(x.toString(16), 2, "0")).join("");
|
|
}
|
|
|
|
function toHex(n: number) {
|
|
return (n < 0 ? "-" : "") + "0x" + Math.abs(n).toString(16);
|
|
}
|
|
|
|
function parseExpr(s: string | undefined): string | undefined {
|
|
if (s == null) {
|
|
return undefined;
|
|
}
|
|
const m = s.match(/\(; =(.*) ;\)/);
|
|
if (m == null) {
|
|
throw new Error("unparseable expression: " + s);
|
|
}
|
|
let expr = m[1].trim();
|
|
if (expr.startsWith("body(") && expr.endsWith(")")) {
|
|
expr = 'body("' + expr.substring(5, expr.length - 1) + '")';
|
|
}
|
|
if (expr.startsWith("'") && expr.endsWith("'")) {
|
|
expr = "ord(" + expr + ")";
|
|
}
|
|
return expr;
|
|
}
|
|
|
|
function parseDataElement(line: string): DataElement | null {
|
|
if (!line.match(/^\s*\(data\s+/)) {
|
|
return null;
|
|
}
|
|
if (line.match(/= MODULE_HEADER_BASE/)) {
|
|
return null;
|
|
}
|
|
|
|
let m: RegExpMatchArray | null;
|
|
if ((m = line.match(/^\s*\(data \(i32.const (\w+)\) "\\.." "([^"]+)"/))) {
|
|
return {
|
|
type: "string",
|
|
offset: parseInt(m[1]),
|
|
string: unescapeString(m[2]),
|
|
};
|
|
} else if (
|
|
(m = line.match(
|
|
/^\s*\(data \(i32.const (\w+)\) "([^"]+)" "([^"]+)"( \(;[^;]+;\))? "([^"]+)" "([^"]+)"( \(;[^;]+;\))?( "([^"]+)"( \(;[^;]+;\))?)?\)/
|
|
))
|
|
) {
|
|
return {
|
|
type: "dict",
|
|
offset: parseInt(m[1]),
|
|
prev: unpack(m[2]),
|
|
flags: unpack(m[3]) & 0xe0,
|
|
name: unescapeString(m[5]).substring(0, unpack(m[3]) & 0x1f),
|
|
index: unpack(m[6]),
|
|
indexExpr: parseExpr(m[7]),
|
|
data: m[9],
|
|
dataExpr: parseExpr(m[10]),
|
|
};
|
|
}
|
|
throw new Error("unmatched data section: " + line);
|
|
}
|
|
|
|
const wat_file = "src/waforth.wat";
|
|
|
|
let lines = fs.readFileSync(wat_file).toString().split("\n");
|
|
|
|
const updateValues = true;
|
|
|
|
////////////////////////////////////////////////////////////////////////
|
|
// Collect information
|
|
////////////////////////////////////////////////////////////////////////
|
|
|
|
const stringElements: StringElement[] = [];
|
|
const dictElements: DictElement[] = [];
|
|
const definitions: Record<string, number> = {};
|
|
let currentFunc: string;
|
|
for (const line of lines) {
|
|
// Parse function name
|
|
let m = line.match(/\s*\(func ([^\s]+)\s/);
|
|
if (m != null) {
|
|
currentFunc = m[1];
|
|
}
|
|
|
|
const dataElement = parseDataElement(line);
|
|
if (dataElement != null) {
|
|
switch (dataElement.type) {
|
|
case "string":
|
|
stringElements.push(dataElement);
|
|
break;
|
|
case "dict":
|
|
dictElements.push({ ...dataElement, func: currentFunc! });
|
|
break;
|
|
default:
|
|
assert(false);
|
|
}
|
|
}
|
|
|
|
// Parse definitions
|
|
m = line.match(/^\s*;;\s+([!a-zA-Z0-9_]+)\s*:=\s*([^\s]+)/);
|
|
if (m) {
|
|
if (isNaN(m[2] as any)) {
|
|
throw new Error("unparseable definition: " + m[2]);
|
|
}
|
|
definitions[m[1]] = parseInt(m[2]);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////
|
|
// Add new entry
|
|
////////////////////////////////////////////////////////////////////////
|
|
|
|
let addDictElement: DictElement | undefined = undefined;
|
|
if (addDict) {
|
|
addDictElement = {
|
|
type: "dict",
|
|
offset: -1,
|
|
prev: -1,
|
|
flags: 0,
|
|
name: addDict,
|
|
index: 0xffffffff,
|
|
func: "$" + addDict,
|
|
};
|
|
let i = 0;
|
|
for (; i < dictElements.length; ++i) {
|
|
if (dictElements[i].name.localeCompare(addDictElement.name) > 0) {
|
|
break;
|
|
}
|
|
}
|
|
dictElements.splice(i, 0, addDictElement);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////
|
|
// Update data elements
|
|
////////////////////////////////////////////////////////////////////////
|
|
|
|
let offset = definitions.DATA_SPACE_BASE;
|
|
for (const el of stringElements) {
|
|
el.offset = offset;
|
|
offset += 1 + el.string.length;
|
|
}
|
|
|
|
offset = Math.ceil(offset / 4) * 4;
|
|
let prevDictOffset = 0;
|
|
let nextTableIndex = 0x10;
|
|
for (const el of dictElements) {
|
|
el.prev = prevDictOffset;
|
|
el.offset = prevDictOffset = offset;
|
|
offset +=
|
|
4 + Math.ceil((1 + el.name.length) / 4) * 4 + 4 + (el.data != null ? 4 : 0);
|
|
if (el.indexExpr == null) {
|
|
el.index = nextTableIndex;
|
|
nextTableIndex += 1;
|
|
}
|
|
}
|
|
|
|
const here = offset;
|
|
|
|
function serializeWordData(el: DictElement): string {
|
|
const paddedName = _.padEnd(
|
|
el.name,
|
|
Math.ceil((1 + el.name.length) / 4) * 4 - 1,
|
|
" "
|
|
);
|
|
const flagsLen =
|
|
"\\" + _.padStart((el.name.length | el.flags).toString(16), 2, "0");
|
|
let l = ` (data (i32.const 0x${el.offset.toString(16)})`;
|
|
l += ` "${pack(el.prev)}"`;
|
|
l += ` "${flagsLen}"`;
|
|
if (el.flags !== 0) {
|
|
const flags: string[] = [];
|
|
if (el.flags & 0x20) {
|
|
flags.push("F_HIDDEN");
|
|
}
|
|
if (el.flags & 0x40) {
|
|
flags.push("F_DATA");
|
|
}
|
|
if (el.flags & 0x80) {
|
|
flags.push("F_IMMEDIATE");
|
|
}
|
|
l += ` (; ${flags.join(" & ")} ;)`;
|
|
}
|
|
l += ` "${escapeString(paddedName)}"`;
|
|
l += ` "${pack(el.index)}"`;
|
|
if (el.indexExpr) {
|
|
l += ` (; = ${el.indexExpr} ;)`;
|
|
}
|
|
if (el.data) {
|
|
l += ` "${el.data}"`;
|
|
if (el.dataExpr) {
|
|
l += ` (; = ${el.dataExpr} ;)`;
|
|
}
|
|
}
|
|
l += ")";
|
|
return l;
|
|
}
|
|
|
|
function serializeStringData(el: StringElement): string {
|
|
const offset = "0x" + el.offset.toString(16);
|
|
const len = "\\" + _.padStart(el.string.length.toString(16), 2, "0");
|
|
return ` (data (i32.const ${offset}) "${len}" "${escapeString(el.string)}")`;
|
|
}
|
|
|
|
const newLines: string[] = [];
|
|
for (const line of lines) {
|
|
const dataElement = parseDataElement(line);
|
|
if (dataElement != null) {
|
|
switch (dataElement.type) {
|
|
case "string": {
|
|
const el = stringElements.find((e) => e.string === dataElement.string)!;
|
|
assert(el != null);
|
|
newLines.push(serializeStringData(el));
|
|
break;
|
|
}
|
|
case "dict": {
|
|
const el = dictElements.find((e) => e.name === dataElement.name);
|
|
if (el == null) {
|
|
newLines.push(line);
|
|
continue;
|
|
}
|
|
newLines.push(serializeWordData(el));
|
|
break;
|
|
}
|
|
default:
|
|
assert(false);
|
|
}
|
|
} else {
|
|
// TODO: Update new HERE
|
|
newLines.push(line);
|
|
}
|
|
}
|
|
lines = newLines;
|
|
|
|
////////////////////////////////////////////////////////////////////////
|
|
// Validate
|
|
////////////////////////////////////////////////////////////////////////
|
|
|
|
if (!updateValues) {
|
|
for (const de of dictElements) {
|
|
const exprVals: [string | undefined, unknown][] = [
|
|
[de.indexExpr, de.index],
|
|
[de.dataExpr, de.data],
|
|
];
|
|
for (const [expr, val] of exprVals) {
|
|
if (expr != null) {
|
|
let x = simpleEval(expr, { ...definitions, pack });
|
|
if (typeof val === "number" && typeof x === "string") {
|
|
x = unpack(x);
|
|
}
|
|
if (x != val) {
|
|
throw new Error(
|
|
`expression does not match value: ${JSON.stringify(
|
|
de
|
|
)} -> ${val} != ${x}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////
|
|
// Update expression values
|
|
////////////////////////////////////////////////////////////////////////
|
|
|
|
function body(w: string) {
|
|
const el = dictElements.find((e) => e.name === w);
|
|
if (el == null) {
|
|
throw new Error("dict entry not found: " + w);
|
|
}
|
|
return (
|
|
el.offset +
|
|
4 +
|
|
Math.ceil((1 + el.name.length) / 4) * 4 +
|
|
(el.flags & 0x40 ? 4 : 0)
|
|
);
|
|
}
|
|
|
|
function str(s: string) {
|
|
const sel = stringElements.find((e) => e.string === s);
|
|
if (sel == null) {
|
|
throw new Error("string not found: " + s);
|
|
}
|
|
return sel.offset;
|
|
}
|
|
|
|
function len(s: string) {
|
|
return s.length;
|
|
}
|
|
|
|
function ord(s: string) {
|
|
return s.charCodeAt(0);
|
|
}
|
|
|
|
function index(w: string) {
|
|
const el = dictElements.find((e) => e.name === w);
|
|
if (el == null) {
|
|
throw new Error("dict entry not found: " + w);
|
|
}
|
|
return el.index;
|
|
}
|
|
|
|
if (updateValues) {
|
|
const updatedLines: string[] = [];
|
|
for (let line of lines) {
|
|
line = line.replace(
|
|
new RegExp("(\\s)([^\\s]+)(\\s)+(\\(; =[^;]+;\\))", "g"),
|
|
(s: string, ...args: any[]) => {
|
|
const expr = parseExpr(args[3]);
|
|
const val = simpleEval(expr!, {
|
|
...definitions,
|
|
pack,
|
|
body,
|
|
index,
|
|
str,
|
|
len,
|
|
ord,
|
|
});
|
|
const sval = _.isString(val) ? '"' + val + '"' : toHex(val as number);
|
|
return args[0] + sval + args[2] + args[3];
|
|
}
|
|
);
|
|
|
|
let m = line.match(
|
|
/(.*\(global ([^\s]+) \(mut i32\) \(i32.const )([^)\s]+)(\).*)/
|
|
);
|
|
if (m != null) {
|
|
const [prefix, global, , suffix] = m.slice(1);
|
|
if (global === "$here") {
|
|
line = prefix + "0x" + here.toString(16) + suffix;
|
|
} else if (global === "$latest") {
|
|
line =
|
|
prefix +
|
|
"0x" +
|
|
dictElements[dictElements.length - 1].offset.toString(16) +
|
|
suffix;
|
|
} else if (global === "$nextTableIndex") {
|
|
line = prefix + "0x" + nextTableIndex.toString(16) + suffix;
|
|
}
|
|
}
|
|
|
|
m = line.match(/(.*\(table .* )([^\s]+)( funcref\))/);
|
|
if (m != null) {
|
|
line = m[1] + "0x" + nextTableIndex.toString(16) + m[3];
|
|
}
|
|
|
|
m = line.match(/\s*\(elem \(i32.const ([^)]+)\) ([^)]+)\)/);
|
|
if (m != null) {
|
|
const func = m[2];
|
|
const el = dictElements.find((e) => e.func === func);
|
|
if (el == null) {
|
|
continue;
|
|
}
|
|
line = ` (elem (i32.const 0x${el.index.toString(16)}) ${func})`;
|
|
}
|
|
|
|
updatedLines.push(line);
|
|
}
|
|
lines = updatedLines;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////
|
|
// Strip
|
|
////////////////////////////////////////////////////////////////////////
|
|
|
|
// const strippedLines: string[] = [];
|
|
// let skipLevel = 0;
|
|
// let skippingDefinition = false;
|
|
// for (let line of lines) {
|
|
// if (enableBulkMemory) {
|
|
// line = line
|
|
// .replace(/\(call \$memcopy/g, "(memory.copy")
|
|
// .replace(/\(call \$memset/g, "(memory.fill");
|
|
// if (line.match(/\(func (\$memset|\$memcopy)/)) {
|
|
// skippingDefinition = true;
|
|
// skipLevel = 0;
|
|
// }
|
|
// }
|
|
// if (skippingDefinition) {
|
|
// skipLevel += (line.match(/\(/g) || []).length;
|
|
// skipLevel -= (line.match(/\)/g) || []).length;
|
|
// }
|
|
|
|
// // Output line
|
|
// if (!skippingDefinition) {
|
|
// strippedLines.push(line);
|
|
// }
|
|
|
|
// if (skippingDefinition && skipLevel <= 0) {
|
|
// skippingDefinition = false;
|
|
// }
|
|
// }
|
|
// lines = strippedLines;
|
|
|
|
fs.writeFileSync(
|
|
inplace ? "src/waforth.wat" : "src/waforth.out.wat",
|
|
lines.join("\n")
|
|
);
|
|
|
|
if (addDictElement) {
|
|
console.log(` (func $${addDictElement.name} (param $tos i32) (result i32))`);
|
|
console.log(serializeWordData(addDictElement));
|
|
console.log(
|
|
` (elem (i32.const 0x${addDictElement.index.toString(16)}) $${
|
|
addDictElement.name
|
|
})`
|
|
);
|
|
}
|