killawat
Version:
KillaWAT is a WebAssembly Text Macro Assembler
572 lines (519 loc) • 16.9 kB
JavaScript
const { logError, moduleStack, importedModules, moduleMap, main, YELLOW, GREEN, macroMap, macroTable } = require("./shared");
const { Tokenizer } = require("./tokenizer");
const fs = require('fs');
const binaryen = require("binaryen");
const ParamType = {
LOCAL: 0,
GLOBAL: 1,
CONST: 2,
}
class Macro {
constructor(tokenArray) {
let token = tokenArray[0] || {};
if (token.type !== 'macro_name') {
logError(`!macro must be followed by a macro name`, token);
return;
}
this.name = token.value;
this.paramArray = [];
this.body = "";
for (let i = 1; i < tokenArray.length; i++) {
token = tokenArray[i];
let nextToken = tokenArray[i + 1] || {};
if (token.type === 'lp') {
if (nextToken.text === '!local' ||
nextToken.text === '!const' ||
nextToken.text === '!global') { // !local|!const|!global|!body
this.parseParam(tokenArray.slice(i + 1, i + token.endTokenOffset));
}
else if (nextToken.text === '!body') {
this.body = tokenArray.slice(i + 2, i + token.endTokenOffset);
}
}
}
}
parseParam(tokenArray) {
let token = tokenArray[0];
let nameToken = tokenArray[1] || {};
let typeToken = tokenArray[2] || {};
if (nameToken.type !== 'name') {
logError('!local must be followed by a name', nameToken);
return;
}
if (typeToken.type !== 'valtype') {
logError('!local must include a type', typeToken);
return;
}
if (token.text === '!local') {
this.paramArray.push({
paramType: ParamType.LOCAL,
name: nameToken.text,
type: typeToken.value,
});
}
else if (token.text === '!const') {
this.paramArray.push({
paramType: ParamType.CONST,
name: nameToken.text,
type: typeToken.value,
});
}
else if (token.text === '!global') {
this.paramArray.push({
paramType: ParamType.GLOBAL,
name: nameToken.text,
type: typeToken.value,
});
}
}
}
class Preprocess {
constructor(tokenizer, fileName) {
let tokenArray = tokenizer.tokenArray;
let processedTokens = [];
let processedIndex = 0;
this.importedModules = [];
this.fileName = fileName;
for (let i = 0; i < tokenArray.length; i++) {
let prevToken = tokenArray[i - 1] || {};
let token = tokenArray[i];
let nextToken = tokenArray[i + 1] || {};
if (token.type === 'preprocess') {
// !merge|!macro|!module|!private|!mem|!inline|!address|!len|!process|##|#
switch (token.text) {
case '!merge':
if (prevToken.type !== 'lp') {
logError(`!merge preprocessor directive must be preceeded by '('`, prevToken);
return;
}
this.processImport(tokenArray.slice(i + 1, i + prevToken.endTokenOffset));
break;
case '!macro':
if (prevToken.type !== 'lp') {
logError(`!macro preprocessor directive must be preceeded by '('`, prevToken);
return;
}
this.processMacro(tokenArray.slice(i + 1, i + prevToken.endTokenOffset), processedTokens);
break;
case '!inline':
if (prevToken.type !== 'lp') {
logError(`!inline preprocessor directive must be preceeded by '('`, prevToken);
return;
}
this.processInline(tokenArray.slice(i + 1, i - 1 + prevToken.endTokenOffset), processedTokens);
break;
case '!process':
if (prevToken.type !== 'lp') {
logError(`!process preprocessor directive must be preceeded by '('`, prevToken);
return;
}
break;
default:
logError(`preprocessor directive ${token.text} found in wrong location`, token);
}
}
else {
token.processedIndex = processedIndex++;
processedTokens.push(token);
}
}
this.tokens = processedTokens;
}
processImport(tokenArray) {
let i = 0;
for (let i = 0; i < tokenArray.length; i++) {
let token = tokenArray[i];
if (token.type === 'rp') {
break;
}
if (token.type !== 'string_literal') {
logError(`!merge preprocessor command must be followed by a string literal`, token);
return;
}
let file_name = token.value;
if (!moduleMap.get(file_name)) {
console.log(GREEN, `importing file: ${file_name}`);
var bytes = fs.readFileSync(file_name);
var code = bytes.toString();
let importTokens = [];
let tokenizer = new Tokenizer(code, importTokens)
// not sure what to do with importTokens
let module = new Module(tokenizer, file_name);
this.importedModules.push(module);
if (main.fileName !== module.fileName) {
module.funcExpressionTokens.forEach(exp => main.module.funcExpressionTokens.push(exp));
module.globalExpressionTokens.forEach(exp => main.module.globalExpressionTokens.push(exp));
module.importExpressionTokens.forEach(exp => main.module.importExpressionTokens.push(exp));
// how to tie data expressions to memory, how to tie memory to file
module.memoryExpressionTokens.forEach(exp => main.module.memoryExpressionTokens.push(exp));
module.dataExpressionTokens.forEach(exp => main.module.dataExpressionTokens.push(exp));
}
}
}
}
processMacro(tokenArray) {
let macro = new Macro(tokenArray);
macroMap.set(macro.name, macro);
macroTable.push(macro);
}
processInline(tokenArray) {
let tokenIndex = 0;
let token = tokenArray[tokenIndex];
let newGlobalTokens = [];
let newDataTokens = [];
let globalName = null;
let address = null;
let fileName = null;
if (token.type === 'name') {
globalName = token.value;
token = tokenArray[++tokenIndex];
}
if (token.type === 'int_literal' ||
token.type === 'hex_literal' ||
token.type === 'bin_literal') {
address = token.value;
token = tokenArray[++tokenIndex];
}
if (token.type !== 'string_literal') {
logError(`expected to find a file name`, token);
return;
}
fileName = token.value;
this.preprocessFunc = null;
token = tokenArray[++tokenIndex] || {};
if (token.type === 'lp') {
// is the next token !process
let processToken = tokenArray[++tokenIndex] || {};
let fileNameToken = tokenArray[++tokenIndex] || {};
if (processToken.text === '!process' &&
fileNameToken.type === 'string_literal') {
console.log(`dir: ${__dirname}`);
if (fileNameToken.value.substring(0, 2) === './') {
this.preprocessFunc = require(`${process.cwd()}/${fileNameToken.value.slice(2)}`);
}
else {
this.preprocessFunc = require(`${__dirname}/preprocess/${fileNameToken.value}`);
}
}
}
token = tokenArray[0];
if (globalName != null && address != null) {
newGlobalTokens.push(
{
type: 'lp',
value: '(',
text: '(',
index: token.index,
line: token.line,
col: token.col,
endTokenOffset: 8,
},
{
type: 'module_definitions',
value: 'global',
text: 'global',
index: token.index,
line: token.line,
col: token.col,
},
{
type: 'name',
value: globalName,
text: globalName,
index: token.index,
line: token.line,
col: token.col,
},
{
type: 'valtype',
value: binaryen.i32,
text: 'i32',
index: token.index,
line: token.line,
col: token.col,
},
{
type: 'lp',
value: '(',
text: '(',
index: token.index,
line: token.line,
col: token.col,
endTokenOffset: 3,
},
{
type: 'terminal',
value: 'i32.const',
text: 'i32.const',
index: token.index,
line: token.line,
col: token.col,
},
{
type: 'int_literal',
value: address,
text: address.toString(),
index: token.index,
line: token.line,
col: token.col,
},
{
type: 'rp',
value: ')',
text: ')',
index: token.index,
line: token.line,
col: token.col,
},
{
type: 'rp',
value: ')',
text: ')',
index: token.index,
line: token.line,
col: token.col,
},
{
type: 'meta',
value: 'meta',
text: 'meta',
file: this.fileName
});
main.module.globalExpressionTokens.push(newGlobalTokens);
//console.log(main.module.globalExpressionTokens)
}
if (address === null) {
logError(`!inline requires an address`, token);
return;
}
let data = new Buffer.from(fs.readFileSync(fileName, { encoding: "binary" }));
console.log(`typeof data: ${typeof data}`);
if (this.preprocessFunc != null) {
data = this.preprocessFunc(data);
}
let dataString = '';
for (let i = 0; i < data.length; i++) {
dataString += '\\' + data[i].toString(16).padStart(2, '0');
}
newDataTokens.push(
{
type: 'lp',
value: '(',
text: '(',
index: token.index,
line: token.line,
col: token.col,
endTokenOffset: 7,
},
{
type: 'module_definitions',
value: 'data',
text: 'data',
index: token.index,
line: token.line,
col: token.col,
},
{
type: 'lp',
value: '(',
text: '(',
index: token.index,
line: token.line,
col: token.col,
endTokenOffset: 3,
},
{
type: 'terminal',
value: 'i32.const',
text: 'i32.const',
index: token.index,
line: token.line,
col: token.col,
},
{
type: 'int_literal',
value: address,
text: address.toString(),
index: token.index,
line: token.line,
col: token.col,
},
{
type: 'rp',
value: ')',
text: ')',
index: token.index,
line: token.line,
col: token.col,
},
{
type: 'string_literal',
value: dataString,
text: `"${dataString}"`,
index: token.index,
line: token.line,
col: token.col,
},
{
type: 'rp',
value: ')',
text: ')',
index: token.index,
line: token.line,
col: token.col,
},
{
type: 'meta',
value: 'meta',
text: 'meta',
file: this.fileName
});
main.module.dataExpressionTokens.push(newDataTokens);
}
}
class Module {
constructor(tokenizer, fileName) {
let moduleExists = moduleMap.get(fileName);
if (moduleExists) {
logError(`module has already been imported for ${fileName}`, this.tokenArray[0]);
return;
}
this.funcExpressionTokens = [];
this.tableExpressionTokens = [];
this.memoryExpressionTokens = [];
this.globalExpressionTokens = [];
this.elemExpressionTokens = [];
this.dataExpressionTokens = [];
this.startExpressionTokens = [];
this.importExpressionTokens = [];
this.exportExpressionTokens = [];
this.fileName = fileName;
if (main.module == null) {
main.module = this;
main.fileName = fileName;
}
this.preprocessor = new Preprocess(tokenizer, fileName);
this.tokenArray = this.preprocessor.tokens;
this.unprocessedTokens = tokenizer.tokenArray;
importedModules.push(this);
moduleMap.set(fileName, this);
for (let i = 0; i < this.tokenArray.length; i++) {
let token = this.tokenArray[i];
let nextToken = this.tokenArray[i + 1];
if (token.level === 2 && token.type === 'lp') {
let endTokenIndex = i + token.endTokenOffset;
switch (nextToken.text) {
case "func":
this.funcExpressionTokens.push(
this.tokenArray.slice(i, endTokenIndex).concat({
type: 'meta',
value: 'meta',
text: 'meta',
file: this.fileName
})
);
break;
case "table":
this.tableExpressionTokens.push(
this.tokenArray.slice(i, endTokenIndex).concat({
type: 'meta',
value: 'meta',
text: 'meta',
file: this.fileName
})
);
break;
case "memory":
this.memoryExpressionTokens.push(
this.tokenArray.slice(i, endTokenIndex).concat({
type: 'meta',
value: 'meta',
text: 'meta',
file: this.fileName
})
);
break;
case "global":
this.globalExpressionTokens.push(
this.tokenArray.slice(i, endTokenIndex).concat({
type: 'meta',
value: 'meta',
text: 'meta',
file: this.fileName
}));
break;
case "elem":
this.elemExpressionTokens.push(
this.tokenArray.slice(i, endTokenIndex).concat({
type: 'meta',
value: 'meta',
text: 'meta',
file: this.fileName
})
);
break;
case "data":
this.dataExpressionTokens.push(
this.tokenArray.slice(i, endTokenIndex).concat({
type: 'meta',
value: 'meta',
text: 'meta',
file: this.fileName
})
);
break;
case "start":
if (this.startExpressionTokens.length !== 0) {
logError('start is defined more than once', this.startExpressionTokens[0]);
return;
}
this.startExpressionTokens = this.tokenArray.slice(i + 1, endTokenIndex);
break;
case "import":
this.importExpressionTokens.push(
this.tokenArray.slice(i, endTokenIndex).concat({
type: 'meta',
value: 'meta',
text: 'meta',
file: this.fileName
})
);
break;
case "export":
this.exportExpressionTokens.push(
this.tokenArray.slice(i, endTokenIndex).concat({
type: 'meta',
value: 'meta',
text: 'meta',
file: this.fileName
})
);
break;
default:
//console.log(nextToken.text);
}
}
}
}
toString() {
let tokenMap = row => {
return row.map(
token => {
return token.type !== 'string_literal' ? token.text : token.value;
}
);
};
return `
funcExpressionTokens: ${JSON.stringify(this.funcExpressionTokens.map(tokenMap), null, 2)}
tableExpressionTokens: ${JSON.stringify(this.tableExpressionTokens.map(tokenMap), null, 2)}
memoryExpressionTokens: ${JSON.stringify(this.memoryExpressionTokens.map(tokenMap), null, 2)}
globalExpressionTokens: ${JSON.stringify(this.globalExpressionTokens.map(tokenMap), null, 2)}
elemExpressionTokens: ${JSON.stringify(this.elemExpressionTokens.map(tokenMap), null, 2)}
dataExpressionTokens: ${JSON.stringify(this.dataExpressionTokens.map(tokenMap), null, 2)}
startExpressionTokens: ${JSON.stringify(this.startExpressionTokens.map(tokenMap), null, 2)}
importExpressionTokens: ${JSON.stringify(this.importExpressionTokens.map(tokenMap), null, 2)}
exportExpressionTokens: ${JSON.stringify(this.exportExpressionTokens.map(tokenMap), null, 2)}
`;
}
}
module.exports.Module = Module;