gbasm
Version:
A GameBoy Assembler.
664 lines (495 loc) • 20.3 kB
JavaScript
// Dependencies ---------------------------------------------------------------
// ----------------------------------------------------------------------------
var FileLinker = null,
BuiltinMacro = require('./Macro'),
Errors = require('../Errors'),
Token = require('../parser/Lexer').Token,
Variable = require('../data/Variable'),
Constant = require('../data/Constant'),
Section = require('../data/Section'),
Label = require('../data/Label'),
Macro = require('../data/Macro'),
Expression = require('../parser/Expression');
// Global Linking Logic -------------------------------------------------------
// ----------------------------------------------------------------------------
var Linker = {
init: function(files) {
// Order sections based on their offsets / banks
var sections = Linker.getAllSections(files);
sections.sort(function(a, b) {
if (a.segment === b.segment) {
if (a.bank === b.bank) {
if (a.hasCustomBaseOffset && b.hasCustomBaseOffset) {
return a.baseOffset - b.baseOffset;
} else if (a.hasCustomBaseOffset) {
return -1;
} else {
return 1;
}
} else {
return a.bank - b.bank;
}
} else {
return Section.Segments[a.segment].index
- Section.Segments[b.segment].index;
}
});
},
link: function(files, verify) {
// First calculate all initial addresses for all files
files.forEach(FileLinker.init);
// Now order and re-arrange the existing section and assign bases
// addresses of sections without specific offset adresses
var sections = Linker.getAllSections(files),
sectionsLastAddresses = {};
sections.forEach(function(section) {
var id = section.segment + '#' + section.bank;
// Place sections without a specified offset after other sections
// in the machting segment / bank
if (!section.hasCustomBaseOffset) {
section.resolvedOffset = sectionsLastAddresses[id] || section.resolvedOffset;
section.calculateOffsets();
}
sectionsLastAddresses[id] = section.resolvedOffset + section.size;
});
// Check for overlapping sections
if (verify) {
this.checkOverlap(sections);
}
// Link all files with the newly calculated addresses
files.forEach(FileLinker.link);
},
checkOverlap: function(sections) {
for(var i = 0; i < sections.length; i++) {
for(var e = i + 1; e < sections.length; e++) {
var b = sections[i],
a = sections[e];
if (a.resolvedOffset + a.size > b.resolvedOffset && a.resolvedOffset < b.resolvedOffset + b.size - 1) {
if (a.resolvedOffset > b.resolvedOffset) {
new Errors.AddressError(
a.file,
'Section overlaps with previously defined section ' + b.toString(true)
+ '. Previous section is ' + ((b.resolvedOffset + b.size) - a.resolvedOffset) + ' byte(s) too long',
a.nameIndex
);
} else {
new Errors.AddressError(
a.file,
'Section overlaps with previously defined section ' + b.toString(true)
+ '. Section is ' + a.size + ' byte(s) long, but only '
+ (b.resolvedOffset - a.resolvedOffset)
+ ' bytes(s) are available until the start of the next section.',
a.nameIndex
);
}
}
}
}
},
// Name / Value / Expression Resolution -----------------------------------
resolveValue: function(sourceFile, value, sourceOffset, sourceIndex, relativeOffset, stack, returnReference) {
// Check for circular references during value resolution
if (stack.indexOf(value) === -1) {
stack.push(value);
switch(value.type) {
case 'NUMBER':
case 'STRING':
return value.value;
case 'NAME':
return Linker.resolveNameValue(sourceFile, value, sourceOffset, sourceIndex, relativeOffset, stack, returnReference);
case 'LABEL_LOCAL_REF':
return Linker.resolveLocalLabel(sourceFile, value, relativeOffset, sourceOffset);
case 'EXPRESSION':
var resolved = Linker.resolveExpression(
value.value, sourceFile, sourceOffset, sourceIndex,
relativeOffset, stack
);
return typeof resolved === 'number' ? resolved | 0 : resolved;
case 'OFFSET':
return relativeOffset ? value.value : sourceOffset + value.value;
default:
throw new TypeError('Unresolved ' + value.type + '(' + value.value + ')');
}
} else {
new Errors.ReferenceError(
stack[0].file,
'Circular reference of "'
+ stack[0].value
+ '" to itself via '
+ stack.slice(1).reverse().map(function(s) {
return s.value + ' in ' + s.file.getPath();
}).join(' -> '),
stack[0].index
);
}
},
resolveLocalLabel: function(sourceFile, value, relativeOffset, sourceOffset) {
var resolved = FileLinker.resolveLocalLabel(sourceFile, value);
if (resolved) {
resolved.references++;
if (relativeOffset) {
return resolved.offset - sourceOffset;
} else {
return resolved.offset;
}
} else {
new Errors.ReferenceError(
value.file,
'Local label "' + value.value + '" not found in current scope',
value.index
);
}
},
resolveNameValue: function(sourceFile, value, sourceOffset, sourceIndex, relativeOffset, stack, returnReference) {
var resolved = Linker.resolveName(value.value, sourceFile);
if (resolved) {
resolved.references++;
// Recursively resolve constants
if (resolved instanceof Constant) {
if (resolved.value instanceof Token) {
return Linker.resolveValue(
resolved.file, resolved.value,
sourceOffset, resolved.value.index,
relativeOffset, stack,
returnReference
);
} else {
return resolved.value;
}
// Resolve Variable Values and Label Addresses
} else if (resolved instanceof Variable || resolved instanceof Label) {
// Force the callee to use the reference it passed in
// This is needed for macros where we otherwise would pass
// in the pre-computed label and variable addresses even
// though the are subject to change during linkage
if (returnReference || resolved.offset === -1) {
return null;
} else if (relativeOffset) {
return resolved.offset - sourceOffset;
} else {
return resolved.offset;
}
// Resolve builtint macro handlers
} else if (resolved instanceof BuiltinMacro) {
return resolved;
// Resolve other pre-defined, built-in values
} else {
return resolved;
}
// Error on missing local names
} else if (value.value.charCodeAt(0) === 95) {
var resolvedGlobal = Linker.resolveName(
value.value, sourceFile, true
);
if (resolvedGlobal) {
new Errors.ReferenceError(
value.file,
'Local name "'
+ value.value
+ '" was not declared in current file, but found in '
+ resolvedGlobal.file.getPath(resolvedGlobal.index, true),
value.index
);
} else {
new Errors.ReferenceError(
value.file,
'Local name "' + value.value + '" was not declared',
value.index
);
}
// Error on missing global names
} else {
// TODO Show the reference path
new Errors.ReferenceError(
value.file,
'"' + value.value + '" was not declared',
value.index
);
}
},
resolveName: function(name, file, global) {
// Check if their is a builtin macro with the specified name
if (BuiltinMacro.isDefined(name)) {
return BuiltinMacro.get(name);
// Names prefixed with _ will only be looked up in their own file
} else if (!global && name.charCodeAt(0) === 95) {
return file.names[name];
// All other names will be searched globally
} else {
var files = file.compiler.files;
for(var i = 0, l = files.length; i < l; i++) {
file = files[i];
var value = file.names[name];
if (value) {
return value;
}
}
return null;
}
},
resolveExpression: function(node, sourceFile, sourceOffset, sourceIndex, relativeOffset, stack) {
// Binary Expressions
if (node instanceof Expression.Node) {
var left = Linker.resolveExpression(
node.left, sourceFile, sourceOffset, sourceIndex,
relativeOffset, stack
);
if (node.right) {
var right = Linker.resolveExpression(
node.right, sourceFile, sourceOffset, sourceIndex,
relativeOffset, stack
);
if (typeof left !== typeof right) {
new Errors.ExpressionError(
sourceFile,
'Incompatible operand types '
+ (typeof left).toUpperCase()
+ ' and '
+ (typeof right).toUpperCase()
+ ' for binary operator ' + node.op.id,
node.op.index
);
} else {
return Linker.evaluateBinaryOperator(node.op.id, left, right);
}
} else if (typeof left === 'number') {
return Linker.evaluateUnaryOperator(node.op.id, left);
} else{
new Errors.ExpressionError(
sourceFile,
'Invalid operand type '
+ (typeof left).toUpperCase()
+ ' for unary operator ' + node.op.id,
node.left.value.index
);
}
// Raw Values
} else if (node instanceof Expression.Leaf) {
return Linker.resolveValue(
sourceFile, node.value, sourceOffset,
node.value.index, false, stack,
false
);
// Macro Calls
} else if (node instanceof Expression.Call) {
var macro = Linker.resolveMacro(node, sourceFile, sourceOffset, stack);
// Builtin macros are basically just plain JavaScript functions
if (macro.isBuiltin) {
return macro.callee.func.apply(null, macro.args);
// For use in expressions, only macros which return a value can be
// used
} else if (macro.isExpression) {
var expr = macro.callee.getExpressionForArguments(macro.args);
return Linker.resolveExpression(
expr, sourceFile, sourceOffset, sourceIndex,
relativeOffset, stack
);
// Expansion macros are not suited
} else {
new Errors.MacroError(
sourceFile,
'User defined expansion MACRO ' + macro.name
+ ' cannot be used as a value',
node.callee.index,
macro.callee
);
}
}
},
resolveMacro: function(node, sourceFile, sourceOffset, stack) {
var callee = null;
// Check for builtin macros
if (!BuiltinMacro.isDefined(node.callee.value)) {
if (!Macro.isDefined(node.callee.value)) {
new Errors.ExpressionError(
sourceFile,
'Call of undefined MACRO function "' + node.callee.value + '"',
node.callee.index
);
} else {
callee = Macro.get(node.callee.value);
}
} else {
callee = BuiltinMacro.get(node.callee.value);
}
if (node.args.length > callee.args.length) {
new Errors.ExpressionError(
sourceFile,
'Too many arguments for '
+ callee.name
+ ', macro takes at most '
+ callee.args.length
+ ' arguments',
node.args[0].value.index
);
} else if (node.args.length < callee.args.length) {
new Errors.ExpressionError(
sourceFile,
'Too few arguments for '
+ callee.name
+ ', macro takes at least '
+ callee.args.length
+ ' arguments',
node.callee.index
);
} else {
var args = node.args.map(function(arg, index) {
return Linker.resolveMacroArgument(
arg, callee.args[index],
sourceFile,
sourceOffset,
stack
);
});
return {
name: node.callee.value,
callee: callee,
args: args,
isBuiltin: callee instanceof BuiltinMacro,
isExpression: !!callee.isExpression
};
}
},
resolveMacroArgument: function(arg, expected, sourceFile, sourceOffset, stack) {
// Support Register Arguments
if (expected.type === 'any'
&& arg.value && arg.value.type === 'NAME'
&& isRegisterArgument(arg.value.value)) {
return new Macro.RegisterArgument(arg.value.value);
} else {
var value;
// Macro call arguments
if (arg instanceof Expression.Call) {
value = Linker.resolveExpression(
arg, sourceFile, sourceOffset,
arg.callee.index, false, stack
);
// Other values
} else {
value = Linker.resolveValue(
sourceFile, arg.value, sourceOffset,
arg.value.index, false, stack,
true
);
}
// In case we resolve labels, we cannot assume a final offset here
// and have to defer the resolution of the value
if (expected.type !== 'any' && typeof value !== expected.type) {
// Error out if we couldn't resolve the value i.e. a address that
// has no valid offset just yet
if (value === null) {
new Errors.MacroError(
sourceFile,
'Cannot resolve computed address for static macro argument '
+ expected.name + '.',
arg.value.index
);
}
new Errors.ExpressionError(
sourceFile,
'Invalid type for MACRO argument: '
+ expected.name
+ ' is expected to be of type ' + expected.type.toUpperCase()
+ ' but was ' + (typeof value).toUpperCase()
+ ' instead',
arg.value.index
);
// Address values might not have been resolved yet
} else if (value === null) {
return arg.value;
} else {
return value;
}
}
},
evaluateBinaryOperator: function(op, left, right) {
switch(op) {
// Binary
case '&':
return (left & right) | 0;
case '|':
return (left | right) | 0;
case '^':
return (left ^ right) | 0;
// Math
case '+':
if (typeof left === 'string') {
return left + right;
} else {
return left + right;
}
break;
case '-':
return left - right;
case '*':
return left * right;
case '/':
return left / right;
case '%':
return left % right;
case '**':
return Math.pow(left, right);
// Shift
case '>>':
return (left >> right) | 0;
case '<<':
return (left << right) | 0;
// Comparisons
case '>':
return left > right ? 1 : 0;
case '>=':
return left >= right ? 1 : 0;
case '<':
return left < right ? 1 : 0;
case '<=':
return left <= right ? 1 : 0;
case '==':
return left === right ? 1 : 0;
case '!=':
return left !== right ? 1 : 0;
default:
throw new TypeError('Unimplemented binary operator: ' + op);
}
},
evaluateUnaryOperator: function(op, arg) {
switch(op) {
case '!':
return !arg ? 1 : 0;
case '-':
return -arg;
case '~':
return (~arg) | 0;
default:
throw new TypeError('Unimplemented unary operator: ' + op);
}
},
// Optimization -----------------------------------------------------------
optimize: function(files, unsafe) {
// Optimize instructions
files.forEach(function(file) {
FileLinker.optimize(file, unsafe);
});
// Now relink with the changed addresses
Linker.link(files, true);
},
getAllSections: function(files) {
var sections = [];
files.forEach(function(file) {
sections.push.apply(sections, file.sections);
});
return sections;
}
};
// Helpers --------------------------------------------------------------------
function isRegisterArgument(arg) {
return arg === 'a' || arg === 'b'
|| arg === 'c' || arg === 'd'
|| arg === 'e' || arg === 'h'
|| arg === 'l' || arg === 'hl'
|| arg === 'de' || arg === 'bc'
|| arg === 'af' || arg === 'sp';
}
// Exports --------------------------------------------------------------------
module.exports = Linker;
// After Dependencies ---------------------------------------------------------
FileLinker = require('./FileLinker');