@defasm/core
Version:
Fast, lightweight JavaScript x64 assembler
435 lines (383 loc) • 15.8 kB
JavaScript
import { ASMError, token, next, match, loadCode, currRange, currSyntax, setSyntax, prevRange, line, comment, Range, startAbsRange, RelativeRange, ungetToken } from "./parser.js";
import { isDirective, makeDirective } from "./directives.js";
import { Instruction } from "./instructions.js";
import { SymbolDefinition, recompQueue, queueRecomp, loadSymbols, symbols } from "./symbols.js";
import { Statement, StatementNode } from "./statement.js";
import { loadSections, Section, sectionFlags, sections } from "./sections.js";
/** @type {StatementNode} */ var prevNode = null;
/** @type {Section} */ export var currSection = null;
/** @type {32 | 64} */ export var currBitness;
var addr = 0;
/** @param {Section} section */
function setSection(section)
{
currSection = section;
const prevInstr = section.cursor.prev.statement;
return prevInstr.address + prevInstr.length;
}
/** @param {Statement} instr */
function addInstruction(instr, seekEnd = true)
{
if(instr.section !== currSection)
instr.address = setSection(instr.section);
prevNode = prevNode.next = new StatementNode(instr);
currSection.cursor.prev = currSection.cursor.prev.next = instr.sectionNode;
setSyntax(instr.syntax);
if(seekEnd && token != '\n' && token != ';')
{
// Special case: this error should appear but not remove the instruction's bytes
instr.error = new ASMError("Expected end of line");
while(token != '\n' && token != ';')
next();
}
addr = instr.address + instr.length;
instr.range.length = (seekEnd ? currRange.start : currRange.end) - instr.range.start;
}
/**
* @typedef {Object} AssemblyConfig
* @property {import('@defasm/core/parser.js').Syntax} config.syntax The initial syntax to use
* @property {boolean} config.writableText Whether the .text section should be writable
* @property {32|64} config.bitness Whether to use the 64- or 32-bit instruction set
*/
export class AssemblyState
{
/** @param {AssemblyConfig} */
constructor({
syntax = {
intel: false,
prefix: true
},
writableText = false,
bitness = 64
} = {})
{
this.defaultSyntax = syntax;
this.bitness = bitness;
/** @type {Map<string, import("./symbols.js").Symbol>} */
this.symbols = new Map();
/** @type {string[]} */
this.fileSymbols = [];
setSyntax(syntax);
loadSymbols(this.symbols, this.fileSymbols);
currBitness = bitness;
/** @type {Section[]} */
this.sections = [
new Section('.text'),
new Section('.data'),
new Section('.bss')
];
if(writableText)
this.sections[0].flags |= sectionFlags.w;
this.head = new StatementNode();
/** @type {string} */
this.source = '';
/** @type {Range} */
this.compiledRange = new Range();
/** @type {ASMError[]} */
this.errors = [];
}
/** Compile Assembly from source code into machine code
* @param {string} source
*/
compile(source, {
haltOnError = false,
range: replacementRange = new Range(0, this.source.length),
doSecondPass = true } = {})
{
this.source =
/* If the given range is outside the current
code's span, fill the in-between with newlines */
this.source.slice(0, replacementRange.start).padEnd(replacementRange.start, '\n') +
source +
this.source.slice(replacementRange.end);
loadSymbols(this.symbols, this.fileSymbols);
loadSections(this.sections, replacementRange);
currBitness = this.bitness;
let { head, tail } = this.head.getAffectedArea(replacementRange, true, source.length);
setSyntax(head.statement ? head.statement.syntax : this.defaultSyntax);
addr = setSection(head.statement ? head.statement.section : this.sections[0]);
loadCode(this.source, replacementRange.start);
prevNode = head;
while(match)
{
let range = startAbsRange();
try
{
if(token != '\n' && token != ';')
{
let name = token;
next();
if(token == ':') // Label definition
addInstruction(new SymbolDefinition({ addr, name, range, isLabel: true }), false);
else if(token == '=' || currSyntax.intel && token.toLowerCase() == 'equ') // Symbol definition
addInstruction(new SymbolDefinition({ addr, name, range }));
else
{
let isDir = false;
if(currSyntax.intel)
{
if(name[0] == '%')
{
isDir = true;
name += token.toLowerCase();
next();
}
else
isDir = isDirective(name, true);
}
else
isDir = name[0] == '.';
if(isDir) // Assembler directive
addInstruction(makeDirective({ addr, range }, currSyntax.intel ? name : name.slice(1)));
else if(currSyntax.intel && isDirective(token, true)) // "<label> <directive>"
{
addInstruction(new SymbolDefinition({ addr, name, range, isLabel: true }), false);
name = token; range = startAbsRange();
next();
addInstruction(makeDirective({ addr, range }, name));
}
else // Instruction
addInstruction(new Instruction({ addr, name, range }));
}
}
}
catch(error)
{
while(token != '\n' && token != ';')
next();
if(haltOnError && !(doSecondPass && error.range))
throw `Error on line ${line}: ${error.message}`;
if(!error.range)
console.error(`Error on line ${line}:\n`, error);
else
addInstruction(new Statement({ addr, range, error }), !comment);
}
if(comment)
addInstruction(new Statement({ addr, range: startAbsRange() }), false);
next();
if(currRange.end > replacementRange.end)
break;
}
// Correct the tails' positions, in case more instructions were parsed than initially thought
for(const section of sections)
{
let node = section.cursor.tail;
while(node && node.statement.range.start < currRange.start)
node = node.next;
section.cursor.tail = node;
}
while(tail && tail.statement.range.start < currRange.start)
{
tail.statement.remove();
tail = tail.next;
}
// If a replacement causes a section's span to change, this will correct the cursors
if(tail && currSection !== tail.statement.section && !tail.statement.switchSection)
{
let tailSection = tail.statement.section;
let node = tailSection.cursor.tail;
currSection.cursor.prev.next = node;
while(node && !node.statement.switchSection)
{
node.statement.section = currSection;
if(node.statement.ipRelative)
queueRecomp(node.statement);
currSection.cursor.prev = node;
node = node.next;
}
tailSection.cursor.tail = node;
}
// Link the cursors' last nodes to their tail nodes
for(const section of sections)
{
let prev = section.cursor.prev;
prev.next = section.cursor.tail;
/* To update the addresses following the insertion, add the last
inserted statement to the recompilation queue.
We don't call queueRecomp because that function also marks the
statement as wanting recompilation, which it doesn't - it's only
queued so it can update the addresses of the proceeding statements. */
if(prev.next)
recompQueue.push(prev);
}
prevNode.next = tail;
if(tail)
{
let instr = tail.statement;
if((currSyntax.prefix != instr.syntax.prefix || currSyntax.intel != instr.syntax.intel) && !instr.switchSyntax)
{
// Syntax has been changed, we have to recompile some of the source
let nextSyntaxChange = tail;
while(nextSyntaxChange.next && !nextSyntaxChange.next.switchSyntax)
nextSyntaxChange = nextSyntaxChange.next;
const recompStart = prevNode.statement ? prevNode.statement.range.end : 0;
const recompEnd = nextSyntaxChange.statement.range.end;
const recompRange = new Range(recompStart, recompEnd - recompStart);
this.compile(recompRange.slice(this.source), {
haltOnError,
range: recompRange,
doSecondPass: false
});
}
}
this.compiledRange = replacementRange.until(prevRange);
if(doSecondPass)
this.secondPass(haltOnError);
}
// Run the second pass on the instruction list
secondPass(haltOnError = false)
{
addr = 0;
let node;
loadSymbols(this.symbols);
symbols.forEach((symbol, name) => {
symbol.references = symbol.references.filter(instr => !instr.removed);
symbol.definitions = symbol.definitions.filter(instr => !instr.removed);
if((symbol.statement === null || symbol.statement.error) && symbol.references.length == 0 && symbol.definitions.length == 0)
symbols.delete(name);
});
/* For efficiency (and also to fix certain edge cases), we'll make sure
to recompile in order of statement address. */
recompQueue.sort((a, b) => a.statement.address - b.statement.address);
while(node = recompQueue.shift())
{
addr = node.statement.address;
do
{
let instr = node.statement;
if(instr)
{
instr.address = addr;
if((instr.wantsRecomp || instr.ipRelative) && !instr.removed)
{
// Recompiling the instruction
try
{
instr.wantsRecomp = false;
instr.recompile();
}
catch(e)
{
instr.error = e;
// When a symbol is invalidated, all references to it should be too
if(instr.symbol)
for(const ref of instr.symbol.references)
queueRecomp(ref);
}
}
addr = instr.address + instr.length;
}
node = node.next;
} while(node && node.statement.address != addr);
}
// Error collection
this.errors = [];
const reportedErrors = []
this.iterate((instr, line) => {
if(instr.outline && instr.outline.operands)
for(let op of instr.outline.operands)
op.clearAttemptedSizes();
const error = instr.error;
if(error)
{
this.errors.push(error);
/* Errors whose pos can't be determined should be logged
to the console (these are usually internal compiler errors) */
if(!error.range)
{
console.error(`Error on line ${line}:\n`, error);
error.range = new RelativeRange(instr.range, instr.range.start, instr.range.length);
}
reportedErrors.push({ line, error });
}
});
if(haltOnError && reportedErrors.length > 0)
throw reportedErrors.map(({ error, line }) => {
const linePart = `Error on line ${line}: `;
return linePart + (error.range.parent ?? error.range).slice(this.source) +
'\n' + ' '.repeat(linePart.length + (error.range.parent ? error.range.start - error.range.parent.start : 0)) +
'^ ' + error.message
}).join('\n\n');
}
line(line)
{
if(line-- < 1)
throw "Invalid line";
let start = 0;
while(line)
{
start = this.source.indexOf('\n', start) + 1;
if(start == 0)
return new Range(this.source.length + line, 0);
line--;
}
let end = this.source.indexOf('\n', start);
if(end < 0)
end = this.source.length;
return new Range(start, end - start);
}
/**
* @callback instrCallback
* @param {Statement} instr
* @param {Number} line
*/
/** @param {instrCallback} func */
iterate(func)
{
let line = 1, nextLine = 0, node = this.head.next;
while(nextLine != Infinity)
{
nextLine = this.source.indexOf('\n', nextLine) + 1 || Infinity;
while(node && node.statement.range.end < nextLine)
{
func(node.statement, line);
node = node.next;
}
line++;
}
}
/**
* @callback lineCallback
* @param {Uint8Array} bytes
* @param {Number} line
*/
/** @param {lineCallback} func */
bytesPerLine(func)
{
let lineQueue = [];
let line = 1, nextLine = 0, node = this.head.next;
while(nextLine != Infinity)
{
let bytes = new Uint8Array();
let addToBytes = () => {
if(lineQueue.length > 0)
{
const line = lineQueue.shift();
if(line.length > 0)
{
let newBytes = new Uint8Array(bytes.length + line.length);
newBytes.set(bytes);
newBytes.set(line, bytes.length);
bytes = newBytes;
}
}
}
nextLine = this.source.indexOf('\n', nextLine) + 1 || Infinity;
addToBytes();
while(node && node.statement.range.start < nextLine)
{
let instr = node.statement, prevEnd = 0;
for(const end of [...instr.lineEnds, instr.length])
{
if(end <= instr.length)
lineQueue.push(instr.bytes.subarray(prevEnd, end));
prevEnd = end;
}
addToBytes();
node = node.next;
}
func(bytes, line);
line++;
}
}
}