1
0
mirror of https://github.com/sehugg/8bitworkshop.git synced 2024-05-31 13:41:32 +00:00
8bitworkshop/src/worker/assembler.ts

480 lines
13 KiB
TypeScript
Raw Normal View History

2018-02-18 05:12:09 +00:00
2021-07-12 23:05:37 +00:00
type Endian = 'big' | 'little';
type Symbol = {
value: number
}
type AssemblerVar = {
bits : number,
toks : string[],
2021-07-12 23:05:37 +00:00
endian? : Endian,
iprel? : boolean,
2021-06-22 16:20:11 +00:00
ipofs? : number,
ipmul? : number,
}
type AssemblerRuleSlice = {
a : number; // argument index
b : number; // bit index
n : number; // # of bits
}
type AssemblerRule = {
fmt : string,
2021-06-22 16:20:11 +00:00
bits : (string | number | AssemblerRuleSlice)[],
// added at runtime
re? : RegExp,
prefix? : string,
varlist? : string[]
}
type AssemblerVarList = {[name:string] : AssemblerVar};
type AssemblerLine = {line:number, offset:number, nbits:number, insns?:string};
type AssemblerFixup = {
sym:string,
ofs:number,
2021-06-22 16:20:11 +00:00
size:number;
srcofs:number,
dstofs:number,
dstlen:number,
line:number,
iprel:boolean,
2021-06-22 16:20:11 +00:00
ipofs:number,
ipmul:number,
2021-07-12 23:05:37 +00:00
endian:Endian
};
type AssemblerSpec = {
name : string,
width : number,
vars : AssemblerVarList,
rules : AssemblerRule[]
}
type AssemblerInstruction = {opcode:number, nbits : number};
type AssemblerErrorResult = {error:string};
type AssemblerLineResult = AssemblerErrorResult | AssemblerInstruction;
type AssemblerError = {msg:string, line:number};
type AssemblerState = {
ip: number,
line: number,
origin: number,
codelen: number,
intermediate: any,
output: number[],
lines: AssemblerLine[],
errors: AssemblerError[],
fixups: AssemblerFixup[]
}
const isError = (o: AssemblerLineResult): o is AssemblerErrorResult => (<AssemblerErrorResult>o).error !== undefined
function hex(v:number, nd:number) {
try {
if (!nd) nd = 2;
2021-06-22 16:20:11 +00:00
if (nd == 8) {
return hex((v>>16)&0xffff,4) + hex(v&0xffff,4);
}
var s = v.toString(16).toUpperCase();
while (s.length < nd)
s = "0" + s;
return s;
} catch (e) {
return v+"";
}
}
function stringToData(s:string) : number[] {
var data = [];
for (var i=0; i<s.length; i++) {
data[i] = s.charCodeAt(i);
}
return data;
}
export class Assembler {
spec : AssemblerSpec;
ip = 0;
origin = 0;
linenum = 0;
2021-07-12 23:05:37 +00:00
symbols : {[name:string] : Symbol} = {};
errors : AssemblerError[] = [];
outwords : number[] = [];
asmlines : AssemblerLine[] = [];
fixups : AssemblerFixup[] = [];
width = 8;
codelen = 0;
aborted = false;
constructor(spec : AssemblerSpec) {
this.spec = spec;
if (spec) {
this.preprocessRules();
}
}
2018-02-18 05:12:09 +00:00
rule2regex(rule : AssemblerRule, vars : AssemblerVarList) {
2018-02-18 05:12:09 +00:00
var s = rule.fmt;
2018-12-05 13:33:40 +00:00
if (!s || !(typeof s === 'string'))
throw Error('Each rule must have a "fmt" string field');
if (!rule.bits || !(rule.bits instanceof Array))
throw Error('Each rule must have a "bits" array field');
2018-02-18 05:12:09 +00:00
var varlist = [];
rule.prefix = s.split(/\s+/)[0];
2018-05-27 18:13:06 +00:00
s = s.replace(/\+/g, '\\+');
s = s.replace(/\*/g, '\\*');
2018-02-18 05:12:09 +00:00
s = s.replace(/\s+/g, '\\s+');
s = s.replace(/\[/g, '\\[');
s = s.replace(/\]/g, '\\]');
2021-06-24 18:19:36 +00:00
s = s.replace(/\(/g, '\\(');
s = s.replace(/\)/g, '\\)');
2018-02-18 05:12:09 +00:00
s = s.replace(/\./g, '\\.');
2018-05-27 18:13:06 +00:00
// TODO: more escapes?
s = s.replace(/~\w+/g, (varname:string) => {
2018-02-18 05:12:09 +00:00
varname = varname.substr(1);
var v = vars[varname];
varlist.push(varname);
if (!v)
2018-12-05 13:33:40 +00:00
throw Error('Could not find variable definition for "~' + varname + '"');
2018-02-18 05:12:09 +00:00
else if (v.toks)
return '(\\w+)';
else
return '([0-9]+|[$][0-9a-f]+|\\w+)';
});
2018-12-05 13:33:40 +00:00
try {
rule.re = new RegExp('^'+s+'$', 'i');
} catch (e) {
throw Error("Bad regex for rule \"" + rule.fmt + "\": /" + s + "/ -- " + e);
}
2018-02-18 05:12:09 +00:00
rule.varlist = varlist;
// TODO: check rule constraints
return rule;
}
preprocessRules() {
if (this.spec.width) {
2021-07-12 23:05:37 +00:00
this.width = this.spec.width || 8;
}
for (var rule of this.spec.rules) {
this.rule2regex(rule, this.spec.vars);
}
2018-02-18 05:12:09 +00:00
}
warning(msg:string, line?:number) {
this.errors.push({msg:msg, line:line?line:this.linenum});
2018-02-18 05:12:09 +00:00
}
fatal(msg:string, line?:number) {
this.warning(msg, line);
this.aborted = true;
2018-03-02 05:15:33 +00:00
}
fatalIf(msg?:string, line?:number) {
if (msg) this.fatal(msg, line);
2018-02-18 05:12:09 +00:00
}
addBytes(result:AssemblerInstruction) {
this.asmlines.push({
line:this.linenum,
offset:this.ip,
2018-02-18 05:12:09 +00:00
nbits:result.nbits
});
var op = result.opcode;
var nb = result.nbits/this.width;
2018-02-18 05:12:09 +00:00
for (var i=0; i<nb; i++) {
2021-06-22 16:20:11 +00:00
if (this.width < 32)
this.outwords[this.ip++ - this.origin] = (op >> (nb-1-i)*this.width) & ((1<<this.width)-1);
else
this.outwords[this.ip++ - this.origin] = op;
2018-02-18 05:12:09 +00:00
}
}
addWords(data:number[]) {
this.asmlines.push({
line:this.linenum,
offset:this.ip,
nbits:this.width*data.length
2018-04-16 22:12:12 +00:00
});
for (var i=0; i<data.length; i++) {
2021-06-22 16:20:11 +00:00
if (this.width < 32)
this.outwords[this.ip++ - this.origin] = data[i] & ((1<<this.width)-1);
else
this.outwords[this.ip++ - this.origin] = data[i];
2018-04-16 22:12:12 +00:00
}
}
2018-06-11 17:01:09 +00:00
parseData(toks:string[]) : number[] {
2018-04-16 22:12:12 +00:00
var data = [];
for (var i=0; i<toks.length; i++) {
data[i] = this.parseConst(toks[i]);
2018-04-16 22:12:12 +00:00
}
return data;
}
2018-06-11 17:01:09 +00:00
alignIP(align) {
if (align < 1 || align > this.codelen)
this.fatal("Invalid alignment value");
2018-04-16 22:12:12 +00:00
else
this.ip = Math.floor((this.ip+align-1)/align)*align;
2018-04-16 22:12:12 +00:00
}
2018-02-18 05:12:09 +00:00
parseConst(s:string, nbits?:number) : number {
// TODO: check bit length
if (s && s[0] == '$')
2018-02-18 05:12:09 +00:00
return parseInt(s.substr(1), 16);
else
return parseInt(s);
}
2021-07-12 23:05:37 +00:00
swapEndian(x: number, nbits: number) {
var y = 0;
while (nbits > 0) {
var n = Math.min(nbits, this.width);
var mask = (1 << n) - 1;
y <<= n;
y |= (x & mask);
x >>>= n;
nbits -= n;
}
2021-07-12 23:05:37 +00:00
return y;
}
buildInstruction(rule:AssemblerRule, m:string[]) : AssemblerLineResult {
2018-02-18 05:12:09 +00:00
var opcode = 0;
var oplen = 0;
// iterate over each component of the rule output ("bits")
for (let b of rule.bits) {
let n,x;
// is a string? then it's a bit constant
// TODO
2021-06-22 16:20:11 +00:00
if (typeof b === "string") {
2018-02-18 05:12:09 +00:00
n = b.length;
x = parseInt(b,2);
} else {
2021-06-22 16:20:11 +00:00
// is it a slice {a,b,n} or just a number?
var index = typeof b === "number" ? b : b.a;
// it's an indexed variable, look up its variable
2021-06-22 16:20:11 +00:00
var id = m[index+1];
var v = this.spec.vars[rule.varlist[index]];
2018-02-18 05:12:09 +00:00
if (!v) {
2021-06-22 16:20:11 +00:00
return {error:`Could not find matching identifier for '${m[0]}' index ${index}`};
2018-02-18 05:12:09 +00:00
}
n = v.bits;
2021-06-22 16:20:11 +00:00
var shift = 0;
if (typeof b !== "number") {
n = b.n;
shift = b.b;
}
// is it an enumerated type? look up the index of its keyword
2018-02-18 05:12:09 +00:00
if (v.toks) {
x = v.toks.indexOf(id);
if (x < 0)
return {error:"Can't use '" + id + "' here, only one of: " + v.toks.join(', ')};
2018-02-18 05:12:09 +00:00
} else {
// otherwise, parse it as a constant
x = this.parseConst(id, n);
2018-02-18 05:12:09 +00:00
// is it a label? add fixup
if (isNaN(x)) {
2021-06-22 16:20:11 +00:00
this.fixups.push({
sym:id, ofs:this.ip, size:v.bits, line:this.linenum,
dstlen:n, dstofs:oplen, srcofs:shift,
2021-07-12 23:05:37 +00:00
endian:v.endian,
2021-06-22 16:20:11 +00:00
iprel:!!v.iprel, ipofs:(v.ipofs+0), ipmul:v.ipmul||1
});
2018-02-18 05:12:09 +00:00
x = 0;
2021-06-22 16:20:11 +00:00
} else {
var mask = (1<<v.bits)-1;
if ((x&mask) != x)
return {error:"Value " + x + " does not fit in " + v.bits + " bits"};
2018-02-18 05:12:09 +00:00
}
}
2021-07-12 23:05:37 +00:00
// if little endian, we need to swap ordering
if (v.endian == 'little') x = this.swapEndian(x, v.bits);
// is it an array slice? slice the bits
2021-06-22 16:20:11 +00:00
if (typeof b !== "number") {
x = (x >>> shift) & ((1 << b.n)-1);
}
2018-02-18 05:12:09 +00:00
}
opcode = (opcode << n) | x;
oplen += n;
}
if (oplen == 0)
this.warning("Opcode had zero length");
else if (oplen > 32)
this.warning("Opcodes > 32 bits not supported");
else if ((oplen % this.width) != 0)
this.warning("Opcode was not word-aligned (" + oplen + " bits)");
2018-02-18 05:12:09 +00:00
return {opcode:opcode, nbits:oplen};
}
loadArch(arch:string) : string {
if (this.loadJSON) {
var json = this.loadJSON(arch + ".json");
2018-02-18 05:12:09 +00:00
if (json && json.vars && json.rules) {
this.spec = json;
this.preprocessRules();
2018-02-18 05:12:09 +00:00
} else {
return ("Could not load arch file '" + arch + ".json'");
2018-02-18 05:12:09 +00:00
}
}
}
parseDirective(tokens) {
2018-04-16 22:12:12 +00:00
var cmd = tokens[0].toLowerCase();
if (cmd == '.define')
this.symbols[tokens[1].toLowerCase()] = {value:tokens[2]};
2018-04-16 22:12:12 +00:00
else if (cmd == '.org')
this.ip = this.origin = parseInt(tokens[1]);
2018-04-16 22:12:12 +00:00
else if (cmd == '.len')
this.codelen = parseInt(tokens[1]);
2018-04-16 22:12:12 +00:00
else if (cmd == '.width')
this.width = parseInt(tokens[1]);
2018-04-16 22:12:12 +00:00
else if (cmd == '.arch')
this.fatalIf(this.loadArch(tokens[1]));
2018-04-16 22:12:12 +00:00
else if (cmd == '.include')
this.fatalIf(this.loadInclude(tokens[1]));
2018-04-16 22:12:12 +00:00
else if (cmd == '.module')
this.fatalIf(this.loadModule(tokens[1]));
2018-04-16 22:12:12 +00:00
else if (cmd == '.data')
this.addWords(this.parseData(tokens.slice(1)));
2018-04-16 22:12:12 +00:00
else if (cmd == '.string')
this.addWords(stringToData(tokens.slice(1).join(' ')));
2018-04-16 22:12:12 +00:00
else if (cmd == '.align')
this.alignIP(this.parseConst(tokens[1]));
2018-02-18 05:12:09 +00:00
else
this.warning("Unrecognized directive: " + tokens);
2018-02-18 05:12:09 +00:00
}
assemble(line:string) : AssemblerInstruction {
this.linenum++;
2018-02-18 05:12:09 +00:00
// remove comments
2018-04-16 22:12:12 +00:00
line = line.replace(/[;].*/g, '').trim();
2018-02-18 05:12:09 +00:00
// is it a directive?
if (line[0] == '.') {
var tokens = line.split(/\s+/);
this.parseDirective(tokens);
2018-02-18 05:12:09 +00:00
return;
}
2018-04-16 22:12:12 +00:00
// make it lowercase
line = line.toLowerCase();
2018-02-18 05:12:09 +00:00
// find labels
line = line.replace(/(\w+):/, (_label, label) => {
this.symbols[label] = {value:this.ip};
2018-02-18 05:12:09 +00:00
return ''; // replace label with blank
});
line = line.trim();
if (line == '')
return; // empty line
// look at each rule in order
if (!this.spec) { this.fatal("Need to load .arch first"); return; }
var lastError;
for (var rule of this.spec.rules) {
2018-02-18 05:12:09 +00:00
var m = rule.re.exec(line);
if (m) {
var result = this.buildInstruction(rule, m);
if (!isError(result)) {
this.addBytes(result);
2018-02-18 05:12:09 +00:00
return result;
} else {
lastError = result.error;
2018-02-18 05:12:09 +00:00
}
}
}
this.warning(lastError ? lastError : ("Could not decode instruction: " + line));
2018-02-18 05:12:09 +00:00
}
2021-07-12 23:05:37 +00:00
applyFixup(fix: AssemblerFixup, sym: Symbol) {
var ofs = fix.ofs + Math.floor(fix.dstofs/this.width);
var mask = ((1<<fix.size)-1);
var value = this.parseConst(sym.value+"", fix.dstlen);
if (fix.iprel)
value = (value - fix.ofs) * fix.ipmul - fix.ipofs;
if (fix.srcofs == 0 && (value > mask || value < -mask))
this.warning("Symbol " + fix.sym + " (" + value + ") does not fit in " + fix.dstlen + " bits", fix.line);
//console.log(hex(value,8), fix.srcofs, fix.dstofs, fix.dstlen);
if (fix.srcofs > 0)
value >>>= fix.srcofs;
value &= (1 << fix.dstlen) - 1;
// TODO: make it work for all widths
if (this.width == 32) {
var shift = 32 - fix.dstofs - fix.dstlen;
value <<= shift;
}
// TODO: check range
if (fix.size <= this.width) {
this.outwords[ofs - this.origin] ^= value;
} else {
// swap if we want big endian (we'll apply in LSB first order)
if (fix.endian == 'big') value = this.swapEndian(value, fix.size);
// apply multi-byte fixup
while (value) {
if (value & this.outwords[ofs - this.origin]) {
this.warning("Instruction bits overlapped: " + hex(this.outwords[ofs - this.origin],8), hex(value,8));
} else {
this.outwords[ofs - this.origin] ^= value & ((1<<this.width)-1);
}
value >>>= this.width;
ofs++;
}
}
}
finish() : AssemblerState {
2018-02-18 05:12:09 +00:00
// apply fixups
for (var i=0; i<this.fixups.length; i++) {
var fix = this.fixups[i];
var sym = this.symbols[fix.sym];
2018-02-18 05:12:09 +00:00
if (sym) {
2021-07-12 23:05:37 +00:00
this.applyFixup(fix, sym);
2018-02-18 05:12:09 +00:00
} else {
this.warning("Symbol '" + fix.sym + "' not found");
2018-02-18 05:12:09 +00:00
}
}
// update asmlines
for (var i=0; i<this.asmlines.length; i++) {
var al = this.asmlines[i];
2018-02-18 05:12:09 +00:00
al.insns = '';
for (var j=0; j<al.nbits/this.width; j++) {
var word = this.outwords[al.offset + j - this.origin];
2018-02-18 05:12:09 +00:00
if (j>0) al.insns += ' ';
al.insns += hex(word,this.width/4);
2018-02-18 05:12:09 +00:00
}
}
while (this.outwords.length < this.codelen) {
this.outwords.push(0);
2018-02-18 05:12:09 +00:00
}
this.fixups = [];
return this.state();
2018-02-18 05:12:09 +00:00
}
assembleFile(text) : AssemblerState {
2018-02-18 05:12:09 +00:00
var lines = text.split(/\n/g);
for (var i=0; i<lines.length && !this.aborted; i++) {
2018-04-16 22:12:12 +00:00
try {
this.assemble(lines[i]);
2018-04-16 22:12:12 +00:00
} catch (e) {
console.log(e);
this.fatal("Exception during assembly: " + e);
2018-04-16 22:12:12 +00:00
}
2018-02-18 05:12:09 +00:00
}
return this.finish();
2018-02-18 05:12:09 +00:00
}
state() : AssemblerState {
return {ip:this.ip, line:this.linenum, origin:this.origin, codelen:this.codelen,
2018-03-02 05:15:33 +00:00
intermediate:{}, // TODO: listing, symbols?
output:this.outwords,
lines:this.asmlines,
errors:this.errors,
fixups:this.fixups};
2018-02-18 05:12:09 +00:00
}
// methods to implement in subclass
loadJSON : (path : string) => any;
loadInclude : (path : string) => string;
loadModule : (path : string) => string;
2018-02-18 05:12:09 +00:00
}