gbasm
Version:
A GameBoy Assembler.
483 lines (355 loc) • 14.9 kB
JavaScript
// Dependencies ---------------------------------------------------------------
// ----------------------------------------------------------------------------
var Instruction = require('../data/Instruction'),
optimize = require('./Optimizer'),
Expression = require('../parser/Expression'),
Linker = require('./Linker'),
Errors = require('../Errors');
// Source File Linking Logic --------------------------------------------------
// ----------------------------------------------------------------------------
var FileLinker = {
// Static Methods ---------------------------------------------------------
init: function(file) {
// Recursively expand macros in all sections
file.sections.forEach(FileLinker.expandMacros);
// Resolve any outstanding sizes for data and variables
if (file.unresolvedSizes.length) {
file.unresolvedSizes.forEach(function(entry) {
var size = Linker.resolveValue(
file,
entry.size,
entry.offset,
entry.index,
false,
[]
);
entry.size = typeof size === 'string' ? size.length : size;
});
// Clear the list
file.unresolvedSizes.length = 0;
}
// Now recalculate the offsets of all entries within all sections
file.sections.forEach(function(section) {
section.calculateOffsets();
});
// For relative jumps, we check if the target is a OFFSET and switch
// it with the actual instruction it points to.
// This is required to preserve relative jump target through code
// optimization were instructions and thus their size and address might
// be altered.
if (file.relativeJumpTargets.length) {
file.relativeJumpTargets.forEach(function(instr) {
var target = findInstructionByOffset(file, instr.offset, instr.arg.value);
if (!target) {
new Errors.AddressError(
file,
'Invalid jump offset, must point at the address of a valid instruction',
instr.index
);
} else {
instr.arg = target;
}
});
// Clear the list
file.relativeJumpTargets.length = 0;
}
},
link: function(file) {
FileLinker.resolveInstructions(file);
FileLinker.resolveDataBlocks(file);
},
expandMacros: function(section) {
var expanded,
depth = 0;
do {
expanded = false;
for(var i = 0, l = section.entries.length; i < l; i++) {
var entry = section.entries[i];
if (entry instanceof Expression.Call) {
// Remove the macro entry
section.entries.splice(i, 1);
i--;
var macro = Linker.resolveMacro(entry, section.file, 0, []);
if (macro.isBuiltin) {
new Errors.ArgumentError(
section.file,
'Cannot expand built-in MACRO ' + macro.name,
entry.callee.index
);
} else if (macro.isExpression) {
new Errors.ArgumentError(
section.file,
'Cannot expand user defined expression MACRO ' + macro.name,
entry.callee.index
);
} else {
// Parse and expand the macro body into the current position in the file
expanded = true;
macro.callee.expand(
macro.name, section, i + 1, macro.args
);
}
// Break out when there are too many levels of recursion
if (depth > 32) {
new Errors.MacroError(
section.file,
'Maximum macro expansion depth reached (32 levels)',
entry.callee.index
);
}
}
}
depth++;
} while(expanded);
},
// Name Resolution --------------------------------------------------------
resolveInstructions: function(file) {
for(var i = 0, l = file.instructions.length; i < l; i++) {
var instr = file.instructions[i];
if (!instr.arg) {
continue;
}
// Handle targets of relative jump instructions
var value;
if (instr.arg instanceof Instruction) {
value = instr.arg.offset - instr.offset;
// Resolve the value of the instructions argument
} else {
value = Linker.resolveValue(
file,
instr.arg,
instr.offset,
instr.arg.index,
instr.mnemonic === 'jr',
[]
);
}
// Check if we could resolve the value
if (value === null) {
new Errors.ReferenceError(
file,
'"' + instr.arg.value + '" could not be resolved',
instr.index
);
// Validate signed argument range
} else if (instr.isSigned && (value < -127 || value > 128)) {
if (instr.mnemonic === 'jr') {
new Errors.AddressError(
file,
'Invalid relative jump value of ' + value + ' bytes, must be -127 to 128 bytes',
instr.index
);
} else {
new Errors.ArgumentError(
file,
'Invalid signed byte argument value of ' + value + ', must be between -127 and 128',
instr.index
);
}
} else if (instr.isBit && (value < 0 || value > 7)) {
new Errors.ArgumentError(
file,
'Invalid bit index value of ' + value + ', must be between 0 and 7',
instr.index
);
} else if (instr.bits === 8 && (value < -127 || value > 255)) {
new Errors.ArgumentError(
file,
'Invalid byte argument value of ' + value + ', must be between -128 and 255',
instr.index
);
} else if (instr.bits === 16 && (value < -32767 || value > 65535)) {
if (instr.mnemonic === 'jp' || instr.mnemonic === 'call') {
new Errors.AddressError(
file,
'Invalid jump address value of ' + value + ', must be between 0 and 65535',
instr.index
);
} else {
new Errors.ArgumentError(
file,
'Invalid word argument value of ' + value + ', must be between -32767 and 65535',
instr.index
);
}
// Convert signed values to twos complement
} else if (value < 0) {
if (instr.bits === 8) {
// Correct jump offsets for relative jumps
if (instr.mnemonic === 'jr') {
if (value < 0) {
value -= 2;
}
}
value = 256 - Math.abs(value);
} else {
value = 65536 - Math.abs(value);
}
} else {
// Correct jump offsets for relative jumps
if (instr.mnemonic === 'jr') {
if (value > 0) {
value -= 2;
}
}
}
// Replace arg with resolved value
instr.resolvedArg = value;
}
},
resolveDataBlocks: function(file) {
file.dataBlocks.forEach(function(data) {
for(var i = 0, l = data.values.length; i < l; i++) {
var value = data.values[i];
// Resolve the correct value
var resolved = Linker.resolveValue(
file,
value,
value.offset,
value.index,
false,
[]
);
// DS can also store strings by splitting them
if (data.isFixedSize) {
// Only strings can be contained in fixed sized sections
if (typeof resolved !== 'string') {
new Errors.ArgumentError(
file,
'Only string values are allow for fixed sized data storage',
data.index
);
} else if (resolved.length > data.size) {
new Errors.ArgumentError(
file,
'String length of ' + resolved.length
+ ' exceeds allocated storage size of ' + data.size + ' bytes',
data.index
);
}
// Pad strings with 0x00
value = new Array(data.size);
for(var e = 0; e < data.size; e++) {
if (e < resolved.length) {
value[e] = resolved.charCodeAt(e);
} else {
value[e] = 0;
}
}
data.resolvedValues = value;
// Check bit width
} else if (data.bits === 8 && (resolved < -127 || resolved > 255)) {
new Errors.ArgumentError(
file,
'Invalid byte argument value of ' + resolved
+ ' for data storage, must be between -128 and 255',
data.index
);
} else if (data.bits === 16 && (resolved < -32767 || resolved > 65535)) {
new Errors.ArgumentError(
file,
'Invalid word argument value of ' + resolved
+ ' for data storage, must be between -32767 and 65535',
data.index
);
// Convert signed values to twos complement
} else if (resolved < 0) {
if (data.bits === 8) {
data.resolvedValues[i] = 256 - Math.abs(resolved);
} else {
data.resolvedValues[i] = 65536 - Math.abs(resolved);
}
} else {
data.resolvedValues[i] = resolved;
}
}
});
},
resolveLocalLabel: function(file, localLabel) {
// Find the first global label which sits infront of the target localLabel
var i, l, parent = null;
for(i = 0, l = file.labels.length; i < l; i++) {
var label = file.labels[i];
if (!label.parent) {
if (label.index > localLabel.index) {
break;
} else {
parent = label;
}
}
}
if (parent) {
// Now find the first children with the labels name
for(i = 0, l = parent.children.length; i < l; i++) {
if (parent.children[i].name === localLabel.value) {
return parent.children[i];
}
}
}
return null;
},
// Optimization -----------------------------------------------------------
optimize: function(file, unsafe) {
var optimized,
droppedInstructions = 0;
do {
optimized = false;
for(var i = 0, l = file.instructions.length; i < l; i++) {
var affectedInstructions = optimize(
file.instructions[i],
unsafe,
i < l - 1 ? file.instructions[i + 1] : null,
i < l - 2 ? file.instructions[i + 2] : null,
i < l - 3 ? file.instructions[i + 3] : null
);
if (affectedInstructions > 0) {
optimized = true;
// If more than 1 instruction is affected remove
// the superfluous ones
if (affectedInstructions > 1) {
// Reduce total instruction count
l -= affectedInstructions - 1;
file.instructions.splice(
i + 1,
affectedInstructions - 1
).forEach(function(instr) {
instr.remove();
});
droppedInstructions += affectedInstructions - 1;
}
}
}
} while(optimized);
// If we dropped any instructions recalculate the offsets of all
// entries within all sections
if (droppedInstructions > 0) {
file.sections.forEach(function(section) {
section.calculateOffsets();
});
}
}
};
// Helpers --------------------------------------------------------------------
function findInstructionByOffset(file, address, offset) {
// Correct for instruction size
if (offset < 0) {
offset -= 1;
}
var target = address + offset,
min = 0,
max = file.instructions.length;
while(max >= min) {
var mid = min + Math.round((max - min) * 0.5),
instr = file.instructions[mid];
if (instr.offset === target) {
return instr;
} else if (instr.offset < target) {
min = mid + 1;
} else if (instr.offset > target) {
max = mid - 1;
}
}
return null;
}
// Exports --------------------------------------------------------------------
module.exports = FileLinker;