mirror of
https://github.com/michaelcmartin/Ophis.git
synced 2024-10-12 15:23:38 +00:00
c3d48da59d
Due to whitespace vagaries, etc., a sample correct output has been added to the test case files, but isn't directly checked as part of verification.
1094 lines
37 KiB
Python
1094 lines
37 KiB
Python
"""The Ophis Assembler passes
|
|
|
|
Ophis's design philosophy is to build the IR once, then run a great
|
|
many assembler passes over the result. Thus, each pass does a
|
|
single, specialized job. When strung together, the full
|
|
translation occurs. This structure also makes the assembler
|
|
very extensible; additional analyses or optimizations may be
|
|
added as new subclasses of Pass."""
|
|
|
|
# Copyright 2002-2014 Michael C. Martin and additional contributors.
|
|
# You may use, modify, and distribute this file under the MIT
|
|
# license: See README for details.
|
|
|
|
import sys
|
|
import Ophis.Errors as Err
|
|
import Ophis.IR as IR
|
|
import Ophis.Opcodes as Ops
|
|
import Ophis.CmdLine as Cmd
|
|
import Ophis.Listing as Listing
|
|
import Ophis.Macro as Macro
|
|
|
|
|
|
class Pass(object):
|
|
"""Superclass for all assembler passes. Automatically handles IR
|
|
types that modify the environent's structure, and by default
|
|
raises an error on anything else. Override visitUnknown in your
|
|
extension pass to produce a pass that accepts everything."""
|
|
name = "Default Pass"
|
|
|
|
def __init__(self):
|
|
self.writeOK = True
|
|
|
|
def visitNone(self, node, env):
|
|
pass
|
|
|
|
def visitSEQUENCE(self, node, env):
|
|
Err.currentpoint = node.ppt
|
|
for n in node.data:
|
|
n.accept(self, env)
|
|
|
|
def visitDataSegment(self, node, env):
|
|
self.writeOK = False
|
|
env.setsegment(node.data[0])
|
|
|
|
def visitTextSegment(self, node, env):
|
|
self.writeOK = True
|
|
env.setsegment(node.data[0])
|
|
|
|
def visitScopeBegin(self, node, env):
|
|
env.newscope()
|
|
|
|
def visitScopeEnd(self, node, env):
|
|
env.endscope()
|
|
|
|
def visitUnknown(self, node, env):
|
|
Err.log("Internal error! " + self.name +
|
|
" cannot understand node type " + node.nodetype)
|
|
|
|
def prePass(self):
|
|
pass
|
|
|
|
def postPass(self):
|
|
pass
|
|
|
|
def go(self, node, env):
|
|
"""Prepares the environment and runs this pass, possibly
|
|
printing debugging information."""
|
|
if Err.count == 0:
|
|
if Cmd.print_pass:
|
|
print>>sys.stderr, "Running: " + self.name
|
|
env.reset()
|
|
self.prePass()
|
|
node.accept(self, env)
|
|
self.postPass()
|
|
env.reset()
|
|
if Cmd.print_labels:
|
|
print>>sys.stderr, "Current labels:"
|
|
print>>sys.stderr, env
|
|
if Cmd.print_ir:
|
|
print>>sys.stderr, "Current IR:"
|
|
print>>sys.stderr, node
|
|
|
|
|
|
class FixPoint(object):
|
|
"""A specialized class that is not a pass but can be run like one.
|
|
This class takes a list of passes and a "fixpoint" function."""
|
|
def __init__(self, name, passes, fixpoint):
|
|
self.name = name
|
|
self.passes = passes
|
|
self.fixpoint = fixpoint
|
|
|
|
def go(self, node, env):
|
|
"""Runs this FixPoint's passes, in order, until the fixpoint
|
|
is true. Always runs the passes at least once."""
|
|
for i in xrange(100):
|
|
if Err.count != 0:
|
|
break
|
|
for p in self.passes:
|
|
p.go(node, env)
|
|
if Err.count != 0:
|
|
break
|
|
if self.fixpoint():
|
|
break
|
|
if Cmd.print_pass:
|
|
print>>sys.stderr, "Fixpoint failed, looping back"
|
|
else:
|
|
Err.log("Can't make %s converge! Maybe there's a recursive "
|
|
"dependency somewhere?" % self.name)
|
|
|
|
|
|
class DefineMacros(Pass):
|
|
"Extract macro definitions and remove them from the IR"
|
|
name = "Macro definition pass"
|
|
|
|
def prePass(self):
|
|
self.inDef = False
|
|
self.nestedError = False
|
|
|
|
def postPass(self):
|
|
if self.inDef:
|
|
Err.log("Unmatched .macro")
|
|
elif Cmd.print_ir:
|
|
print>>sys.stderr, "Macro definitions:"
|
|
Macro.dump()
|
|
|
|
def visitMacroBegin(self, node, env):
|
|
if self.inDef:
|
|
Err.log("Nested macro definition")
|
|
self.nestedError = True
|
|
else:
|
|
Macro.newMacro(node.data[0])
|
|
node.nodetype = "None"
|
|
node.data = []
|
|
self.inDef = True
|
|
|
|
def visitMacroEnd(self, node, env):
|
|
if self.inDef:
|
|
Macro.endMacro()
|
|
node.nodetype = "None"
|
|
node.data = []
|
|
self.inDef = False
|
|
elif not self.nestedError:
|
|
Err.log("Unmatched .macend")
|
|
|
|
def visitUnknown(self, node, env):
|
|
if self.inDef:
|
|
Macro.registerNode(node)
|
|
node.nodetype = "None"
|
|
node.data = []
|
|
|
|
|
|
class ExpandMacros(Pass):
|
|
"Replace macro invocations with the appropriate text"
|
|
name = "Macro expansion pass"
|
|
|
|
def prePass(self):
|
|
self.changed = False
|
|
|
|
def visitMacroInvoke(self, node, env):
|
|
replacement = Macro.expandMacro(node.ppt, node.data[0], node.data[1:])
|
|
node.nodetype = replacement.nodetype
|
|
node.data = replacement.data
|
|
self.changed = True
|
|
|
|
def visitUnknown(self, node, env):
|
|
pass
|
|
|
|
|
|
class InitLabels(Pass):
|
|
"Finds all reachable labels"
|
|
name = "Label initialization pass"
|
|
|
|
def __init__(self):
|
|
Pass.__init__(self)
|
|
self.labelmap = {}
|
|
self.runcount = 0
|
|
|
|
def prePass(self):
|
|
self.changed = False
|
|
self.PCvalid = True
|
|
self.runcount += 1
|
|
|
|
def visitAdvance(self, node, env):
|
|
self.PCvalid = node.data[0].valid(env, self.PCvalid)
|
|
|
|
def visitSetPC(self, node, env):
|
|
self.PCvalid = node.data[0].valid(env, self.PCvalid)
|
|
|
|
def visitLabel(self, node, env):
|
|
(label, val) = node.data
|
|
fulllabel = "%d:%s" % (env.stack[0], label)
|
|
if fulllabel in self.labelmap and self.labelmap[fulllabel] is not node:
|
|
Err.log("Duplicate label definition '%s'" % label)
|
|
if fulllabel not in self.labelmap:
|
|
self.labelmap[fulllabel] = node
|
|
if val.valid(env, self.PCvalid) and label not in env:
|
|
env[label] = 0
|
|
self.changed = True
|
|
if label in ['a', 'x', 'y'] and self.runcount == 1:
|
|
print>>sys.stderr, str(node.ppt) + ": WARNING: " \
|
|
"using register name as label"
|
|
if label in Ops.opcodes and self.runcount == 1:
|
|
print>>sys.stderr, str(node.ppt) + ": WARNING: " \
|
|
"using opcode name as label"
|
|
|
|
def visitUnknown(self, node, env):
|
|
pass
|
|
|
|
|
|
class CircularityCheck(Pass):
|
|
"Checks for circular label dependencies"
|
|
name = "Circularity check pass"
|
|
|
|
def prePass(self):
|
|
self.changed = False
|
|
self.PCvalid = True
|
|
|
|
def visitAdvance(self, node, env):
|
|
PCvalid = self.PCvalid
|
|
self.PCvalid = node.data[0].valid(env, self.PCvalid)
|
|
if not node.data[0].valid(env, PCvalid):
|
|
Err.log("Undefined or circular reference on .advance")
|
|
|
|
def visitSetPC(self, node, env):
|
|
PCvalid = self.PCvalid
|
|
self.PCvalid = node.data[0].valid(env, self.PCvalid)
|
|
if not node.data[0].valid(env, PCvalid):
|
|
Err.log("Undefined or circular reference on program counter set")
|
|
|
|
def visitCheckPC(self, node, env):
|
|
if not node.data[0].valid(env, self.PCvalid):
|
|
Err.log("Undefined or circular reference on program counter check")
|
|
|
|
def visitLabel(self, node, env):
|
|
(label, val) = node.data
|
|
if not val.valid(env, self.PCvalid):
|
|
Err.log("Undefined or circular dependency for label '%s'" % label)
|
|
|
|
def visitUnknown(self, node, env):
|
|
pass
|
|
|
|
|
|
class CheckExprs(Pass):
|
|
"Ensures all expressions can resolve"
|
|
name = "Expression checking pass"
|
|
|
|
def visitUnknown(self, node, env):
|
|
# Throw away result, just confirm validity of all expressions
|
|
for i in [x for x in node.data if isinstance(x, IR.Expr)]:
|
|
i.value(env)
|
|
|
|
|
|
class EasyModes(Pass):
|
|
"Assigns address modes to hardcoded and branch instructions"
|
|
name = "Easy addressing modes pass"
|
|
|
|
def visitMemory(self, node, env):
|
|
if Ops.opcodes[node.data[0]][Ops.modes.index("Relative")] is not None:
|
|
node.nodetype = "Relative"
|
|
return
|
|
if Ops.opcodes[node.data[0]][Ops.modes.index("RelativeLong")] is not None:
|
|
node.nodetype = "RelativeLong"
|
|
return
|
|
if node.data[1].hardcoded:
|
|
if not collapse_no_index(node, env):
|
|
node.nodetype = "Absolute"
|
|
|
|
def visitMemoryX(self, node, env):
|
|
if node.data[1].hardcoded:
|
|
if not collapse_x(node, env):
|
|
node.nodetype = "AbsoluteX"
|
|
|
|
def visitMemoryY(self, node, env):
|
|
if node.data[1].hardcoded:
|
|
if not collapse_y(node, env):
|
|
node.nodetype = "AbsoluteY"
|
|
|
|
def visitMemory2(self, node, env):
|
|
node.nodetype = "ZPRelative"
|
|
|
|
def visitPointer(self, node, env):
|
|
if node.data[1].hardcoded:
|
|
if not collapse_no_index_ind(node, env):
|
|
node.nodetype = "Indirect"
|
|
|
|
def visitPointerX(self, node, env):
|
|
if node.data[1].hardcoded:
|
|
if not collapse_x_ind(node, env):
|
|
node.nodetype = "AbsIndX"
|
|
|
|
def visitPointerY(self, node, env):
|
|
if node.data[1].hardcoded:
|
|
if not collapse_y_ind(node, env):
|
|
node.nodetype = "AbsIndY"
|
|
|
|
def visitPointerSPY(self, node, env):
|
|
if node.data[1].hardcoded:
|
|
if not collapse_spy_ind(node, env):
|
|
node.nodetype = "AbsIndSPY"
|
|
|
|
def visitPointerZ(self, node, env):
|
|
if node.data[1].hardcoded:
|
|
if not collapse_z_ind(node, env):
|
|
node.nodetype = "AbsIndZ"
|
|
|
|
def visitUnknown(self, node, env):
|
|
pass
|
|
|
|
|
|
class PCTracker(Pass):
|
|
"Superclass for passes that need an accurate program counter."
|
|
name = "**BUG** PC Tracker Superpass used directly"
|
|
|
|
def visitSetPC(self, node, env):
|
|
env.setPC(node.data[0].value(env))
|
|
|
|
def visitAdvance(self, node, env):
|
|
env.setPC(node.data[0].value(env))
|
|
|
|
def visitImplied(self, node, env):
|
|
env.incPC(1)
|
|
|
|
def visitImmediate(self, node, env):
|
|
env.incPC(2)
|
|
|
|
def visitImmediateLong(self, node, env):
|
|
env.incPC(3)
|
|
|
|
def visitIndirectX(self, node, env):
|
|
env.incPC(2)
|
|
|
|
def visitIndirectY(self, node, env):
|
|
env.incPC(2)
|
|
|
|
def visitIndirectSPY(self, node, env):
|
|
env.incPC(2)
|
|
|
|
def visitIndirectZ(self, node, env):
|
|
env.incPC(2)
|
|
|
|
def visitZPIndirect(self, node, env):
|
|
env.incPC(2)
|
|
|
|
def visitZeroPage(self, node, env):
|
|
env.incPC(2)
|
|
|
|
def visitZeroPageX(self, node, env):
|
|
env.incPC(2)
|
|
|
|
def visitZeroPageY(self, node, env):
|
|
env.incPC(2)
|
|
|
|
def visitRelative(self, node, env):
|
|
env.incPC(2)
|
|
|
|
def visitRelativeLong(self, node, env):
|
|
env.incPC(3)
|
|
|
|
def visitZPRelative(self, node, env):
|
|
env.incPC(3)
|
|
|
|
def visitIndirect(self, node, env):
|
|
env.incPC(3)
|
|
|
|
def visitAbsolute(self, node, env):
|
|
env.incPC(3)
|
|
|
|
def visitAbsoluteX(self, node, env):
|
|
env.incPC(3)
|
|
|
|
def visitAbsoluteY(self, node, env):
|
|
env.incPC(3)
|
|
|
|
def visitAbsIndX(self, node, env):
|
|
env.incPC(3)
|
|
|
|
def visitAbsIndY(self, node, env):
|
|
env.incPC(3)
|
|
|
|
def visitAbsIndZ(self, node, env):
|
|
env.incPC(3)
|
|
|
|
def visitMemory(self, node, env):
|
|
env.incPC(3)
|
|
|
|
def visitMemoryX(self, node, env):
|
|
env.incPC(3)
|
|
|
|
def visitMemoryY(self, node, env):
|
|
env.incPC(3)
|
|
|
|
def visitMemoryZ(self, node, env):
|
|
env.incPC(3)
|
|
|
|
def visitPointer(self, node, env):
|
|
env.incPC(3)
|
|
|
|
def visitPointerX(self, node, env):
|
|
env.incPC(3)
|
|
|
|
def visitPointerY(self, node, env):
|
|
env.incPC(3)
|
|
|
|
def visitCheckPC(self, node, env):
|
|
pass
|
|
|
|
def visitLabel(self, node, env):
|
|
pass
|
|
|
|
def visitByte(self, node, env):
|
|
env.incPC(len(node.data))
|
|
|
|
def visitByteRange(self, node, env):
|
|
if node.data[1].valid(env):
|
|
env.incPC(node.data[1].value(env))
|
|
|
|
def visitWord(self, node, env):
|
|
env.incPC(len(node.data) * 2)
|
|
|
|
def visitDword(self, node, env):
|
|
env.incPC(len(node.data) * 4)
|
|
|
|
def visitWordBE(self, node, env):
|
|
env.incPC(len(node.data) * 2)
|
|
|
|
def visitDwordBE(self, node, env):
|
|
env.incPC(len(node.data) * 4)
|
|
|
|
|
|
class UpdateLabels(PCTracker):
|
|
"Computes the new values for all entries in the symbol table"
|
|
name = "Label Update Pass"
|
|
|
|
def prePass(self):
|
|
self.changed = False
|
|
|
|
def visitLabel(self, node, env):
|
|
(label, val) = node.data
|
|
old = env[label]
|
|
env[label] = val.value(env)
|
|
if old != env[label]:
|
|
self.changed = True
|
|
|
|
|
|
class Collapse(PCTracker):
|
|
"Selects as many zero-page instructions to convert as possible."
|
|
name = "Instruction Collapse Pass"
|
|
|
|
def prePass(self):
|
|
self.changed = False
|
|
|
|
def visitMemory(self, node, env):
|
|
self.changed |= collapse_no_index(node, env)
|
|
PCTracker.visitMemory(self, node, env)
|
|
|
|
def visitMemoryX(self, node, env):
|
|
self.changed |= collapse_x(node, env)
|
|
PCTracker.visitMemoryX(self, node, env)
|
|
|
|
def visitMemoryY(self, node, env):
|
|
self.changed |= collapse_y(node, env)
|
|
PCTracker.visitMemoryY(self, node, env)
|
|
|
|
def visitMemoryZ(self, node, env):
|
|
PCTracker.visitMemoryZ(self, node, env)
|
|
|
|
def visitPointer(self, node, env):
|
|
self.changed |= collapse_no_index_ind(node, env)
|
|
PCTracker.visitPointer(self, node, env)
|
|
|
|
def visitPointerX(self, node, env):
|
|
self.changed |= collapse_x_ind(node, env)
|
|
PCTracker.visitPointerX(self, node, env)
|
|
|
|
def visitPointerY(self, node, env):
|
|
self.changed |= collapse_y_ind(node, env)
|
|
PCTracker.visitPointerY(self, node, env)
|
|
|
|
# Previously zero-paged elements may end up un-zero-paged by
|
|
# the branch extension pass. Force them to Absolute equivalents
|
|
# if this happens.
|
|
|
|
def visitImmediate(self, node, env):
|
|
if node.data[1].value(env) >= 0x100:
|
|
if Ops.opcodes[node.data[0]][Ops.modes.index("ImmediateLong")] is not None:
|
|
node.nodetype = "ImmediateLong"
|
|
PCTracker.visitImmediateLong(self, node, env)
|
|
self.changed = True
|
|
return
|
|
PCTracker.visitImmediate(self, node, env)
|
|
|
|
def visitZeroPage(self, node, env):
|
|
if node.data[1].value(env) >= 0x100:
|
|
if Ops.opcodes[node.data[0]][Ops.modes.index("Absolute")] is not None:
|
|
node.nodetype = "Absolute"
|
|
PCTracker.visitAbsolute(self, node, env)
|
|
self.changed = True
|
|
return
|
|
PCTracker.visitZeroPage(self, node, env)
|
|
|
|
def visitZeroPageX(self, node, env):
|
|
if node.data[1].value(env) >= 0x100:
|
|
if Ops.opcodes[node.data[0]][Ops.modes.index("Absolute, X")] is not None:
|
|
node.nodetype = "AbsoluteX"
|
|
PCTracker.visitAbsoluteX(self, node, env)
|
|
self.changed = True
|
|
return
|
|
PCTracker.visitZeroPageX(self, node, env)
|
|
|
|
def visitZeroPageY(self, node, env):
|
|
if node.data[1].value(env) >= 0x100:
|
|
if Ops.opcodes[node.data[0]][Ops.modes.index("Absolute, Y")] is not None:
|
|
node.nodetype = "AbsoluteY"
|
|
PCTracker.visitAbsoluteY(self, node, env)
|
|
self.changed = True
|
|
return
|
|
PCTracker.visitZeroPageY(self, node, env)
|
|
|
|
|
|
def collapse_no_index(node, env):
|
|
"""Transforms a Memory node into a ZeroPage one if possible.
|
|
Returns boolean indicating whether or not it made the collapse."""
|
|
if node.data[1].value(env) < 0x100:
|
|
if Ops.opcodes[node.data[0]][Ops.modes.index("Zero Page")] is not None:
|
|
node.nodetype = "ZeroPage"
|
|
return True
|
|
return False
|
|
|
|
|
|
def collapse_x(node, env):
|
|
"""Transforms a MemoryX node into a ZeroPageX one if possible.
|
|
Returns boolean indicating whether or not it made the collapse."""
|
|
if node.data[1].value(env) < 0x100:
|
|
if Ops.opcodes[node.data[0]][Ops.modes.index("Zero Page, X")] is not None:
|
|
node.nodetype = "ZeroPageX"
|
|
return True
|
|
return False
|
|
|
|
|
|
def collapse_y(node, env):
|
|
"""Transforms a MemoryY node into a ZeroPageY one if possible.
|
|
Returns boolean indicating whether or not it made the collapse."""
|
|
if node.data[1].value(env) < 0x100:
|
|
if Ops.opcodes[node.data[0]][Ops.modes.index("Zero Page, Y")] is not None:
|
|
node.nodetype = "ZeroPageY"
|
|
return True
|
|
return False
|
|
|
|
def collapse_no_index_ind(node, env):
|
|
"""Transforms a Pointer node into a ZPIndirect one if possible.
|
|
Returns boolean indicating whether or not it made the collapse."""
|
|
if node.data[1].value(env) < 0x100:
|
|
if Ops.opcodes[node.data[0]][Ops.modes.index("(Zero Page)")] is not None:
|
|
node.nodetype = "ZPIndirect"
|
|
return True
|
|
return False
|
|
|
|
|
|
def collapse_x_ind(node, env):
|
|
"""Transforms a PointerX node into an IndirectX one if possible.
|
|
Returns boolean indicating whether or not it made the collapse."""
|
|
if node.data[1].value(env) < 0x100:
|
|
if Ops.opcodes[node.data[0]][Ops.modes.index("(Zero Page, X)")] is not None:
|
|
node.nodetype = "IndirectX"
|
|
return True
|
|
return False
|
|
|
|
|
|
def collapse_y_ind(node, env):
|
|
"""Transforms a PointerY node into an IndirectY one if possible.
|
|
Returns boolean indicating whether or not it made the collapse."""
|
|
if node.data[1].value(env) < 0x100:
|
|
if Ops.opcodes[node.data[0]][Ops.modes.index("(Zero Page), Y")] is not None:
|
|
node.nodetype = "IndirectY"
|
|
return True
|
|
return False
|
|
|
|
def collapse_spy_ind(node, env):
|
|
"""Transforms a PointerSPY node into an IndirectY one if possible.
|
|
Returns boolean indicating whether or not it made the collapse."""
|
|
if node.data[1].value(env) < 0x100:
|
|
if Ops.opcodes[node.data[0]][Ops.modes.index("(Zero Page, SP), Y")] is not None:
|
|
node.nodetype = "IndirectSPY"
|
|
return True
|
|
return False
|
|
|
|
def collapse_z_ind(node, env):
|
|
"""Transforms a PointerZ node into an IndirectZ one if possible.
|
|
Returns boolean indicating whether or not it made the collapse."""
|
|
if node.data[1].value(env) < 0x100:
|
|
if Ops.opcodes[node.data[0]][Ops.modes.index("(Zero Page), Z")] is not None:
|
|
node.nodetype = "IndirectZ"
|
|
return True
|
|
return False
|
|
|
|
|
|
class ExtendBranches(PCTracker):
|
|
"""Eliminates any branch instructions that would end up going past
|
|
the 128-byte range, and replaces them with a branch-jump pair."""
|
|
name = "Branch Expansion Pass"
|
|
reversed = {'bcc': 'bcs',
|
|
'bcs': 'bcc',
|
|
'beq': 'bne',
|
|
'bmi': 'bpl',
|
|
'bne': 'beq',
|
|
'bpl': 'bmi',
|
|
'bvc': 'bvs',
|
|
'bvs': 'bvc',
|
|
# 65c02 ones. 'bra' is special, though, having no inverse
|
|
'bbr0': 'bbs0',
|
|
'bbs0': 'bbr0',
|
|
'bbr1': 'bbs1',
|
|
'bbs1': 'bbr1',
|
|
'bbr2': 'bbs2',
|
|
'bbs2': 'bbr2',
|
|
'bbr3': 'bbs3',
|
|
'bbs3': 'bbr3',
|
|
'bbr4': 'bbs4',
|
|
'bbs4': 'bbr4',
|
|
'bbr5': 'bbs5',
|
|
'bbs5': 'bbr5',
|
|
'bbr6': 'bbs6',
|
|
'bbs6': 'bbr6',
|
|
'bbr7': 'bbs7',
|
|
'bbs7': 'bbr7'
|
|
}
|
|
|
|
def prePass(self):
|
|
self.changed = False
|
|
|
|
def visitRelative(self, node, env):
|
|
(opcode, expr) = node.data[:2]
|
|
arg = expr.value(env)
|
|
arg = arg - (env.getPC() + 2)
|
|
if arg < -128 or arg > 127:
|
|
if Cmd.enable_4502_exts:
|
|
node.nodetype = "RelativeLong"
|
|
if Cmd.warn_on_branch_extend:
|
|
print>>sys.stderr, str(node.ppt) + ": WARNING: " \
|
|
"branch out of range, replacing with 16-bit relative branch"
|
|
else:
|
|
if opcode == 'bra':
|
|
# If BRA - BRanch Always - is out of range, it's a JMP.
|
|
node.data = ('jmp', expr, None)
|
|
node.nodetype = "Absolute"
|
|
if Cmd.warn_on_branch_extend:
|
|
print>>sys.stderr, str(node.ppt) + ": WARNING: " \
|
|
"bra out of range, replacing with jmp"
|
|
else:
|
|
# Otherwise, we replace it with a 'macro' of sorts by hand:
|
|
# $branch LOC -> $reversed_branch ^+5; JMP LOC
|
|
# We don't use temp labels here because labels need to have
|
|
# been fixed in place by this point, and JMP is always 3
|
|
# bytes long.
|
|
expansion = [IR.Node(node.ppt, "Relative",
|
|
ExtendBranches.reversed[opcode],
|
|
IR.SequenceExpr([IR.PCExpr(), "+",
|
|
IR.ConstantExpr(5)]),
|
|
None),
|
|
IR.Node(node.ppt, "Absolute", 'jmp', expr, None)]
|
|
node.nodetype = 'SEQUENCE'
|
|
node.data = expansion
|
|
if Cmd.warn_on_branch_extend:
|
|
print>>sys.stderr, str(node.ppt) + ": WARNING: " + \
|
|
opcode + " out of range, " \
|
|
"replacing with " + \
|
|
ExtendBranches.reversed[opcode] + \
|
|
"/jmp combo"
|
|
self.changed = True
|
|
node.accept(self, env)
|
|
else:
|
|
PCTracker.visitRelative(self, node, env)
|
|
|
|
def visitZPRelative(self, node, env):
|
|
(opcode, tested, expr) = node.data
|
|
arg = expr.value(env)
|
|
arg = arg - (env.getPC() + 3)
|
|
if arg < -128 or arg > 127:
|
|
# Otherwise, we replace it with a 'macro' of sorts by hand:
|
|
# $branch LOC -> $reversed_branch ^+6; JMP LOC
|
|
# We don't use temp labels here because labels need to have
|
|
# been fixed in place by this point, and JMP is always 3
|
|
# bytes long.
|
|
expansion = [IR.Node(node.ppt, "ZPRelative",
|
|
ExtendBranches.reversed[opcode],
|
|
tested,
|
|
IR.SequenceExpr([IR.PCExpr(), "+",
|
|
IR.ConstantExpr(6)])),
|
|
IR.Node(node.ppt, "Absolute", 'jmp', expr, None)]
|
|
node.nodetype = 'SEQUENCE'
|
|
node.data = expansion
|
|
if Cmd.warn_on_branch_extend:
|
|
print>>sys.stderr, str(node.ppt) + ": WARNING: " + \
|
|
opcode + " out of range, " \
|
|
"replacing with " + \
|
|
ExtendBranches.reversed[opcode] + \
|
|
"/jmp combo"
|
|
self.changed = True
|
|
node.accept(self, env)
|
|
else:
|
|
PCTracker.visitZPRelative(self, node, env)
|
|
|
|
|
|
class NormalizeModes(Pass):
|
|
"""Eliminates the intermediate "Memory" and "Pointer" nodes,
|
|
converting them to "Absolute"."""
|
|
name = "Mode Normalization pass"
|
|
|
|
def visitMemory(self, node, env):
|
|
node.nodetype = "Absolute"
|
|
|
|
def visitMemoryX(self, node, env):
|
|
node.nodetype = "AbsoluteX"
|
|
|
|
def visitMemoryY(self, node, env):
|
|
node.nodetype = "AbsoluteY"
|
|
|
|
def visitPointer(self, node, env):
|
|
node.nodetype = "Indirect"
|
|
|
|
def visitPointerX(self, node, env):
|
|
node.nodetype = "AbsIndX"
|
|
|
|
# If we ever hit a PointerY by this point, we have a bug.
|
|
|
|
def visitPointerY(self, node, env):
|
|
node.nodetype = "AbsIndY"
|
|
|
|
def visitPointerZ(self, node, env):
|
|
node.nodetype = "AbsIndZ"
|
|
|
|
def visitUnknown(self, node, env):
|
|
pass
|
|
|
|
|
|
class Assembler(Pass):
|
|
"""Converts the IR into a list of bytes, suitable for writing to
|
|
a file."""
|
|
name = "Assembler"
|
|
|
|
# The self.listing field defined in prePass and referred to elsewhere
|
|
# holds the necessary information to build the program listing later.
|
|
# Each member of this list is either a string (in which case it is
|
|
# a listed instruction to be echoed) or it is a tuple of
|
|
# (startPC, bytes). Byte listings need to be interrupted when the
|
|
# PC is changed by an .org, which may happen in the middle of
|
|
# data definition blocks.
|
|
def prePass(self):
|
|
self.output = []
|
|
self.code = 0
|
|
self.data = 0
|
|
self.filler = 0
|
|
if Cmd.listfile is not None:
|
|
self.listing = Listing.Listing(Cmd.listfile)
|
|
else:
|
|
self.listing = Listing.NullLister()
|
|
if Cmd.mapfile is not None:
|
|
self.mapper = Listing.LabelMapper(Cmd.mapfile)
|
|
else:
|
|
self.mapper = Listing.NullLabelMapper()
|
|
|
|
def postPass(self):
|
|
self.listing.dump()
|
|
self.mapper.dump()
|
|
if Cmd.print_summary and Err.count == 0:
|
|
print>>sys.stderr, "Assembly complete: %s bytes output " \
|
|
"(%s code, %s data, %s filler)" \
|
|
% (len(self.output),
|
|
self.code, self.data, self.filler)
|
|
|
|
def outputbyte(self, expr, env, tee=None):
|
|
'Outputs a byte, with range checking'
|
|
if self.writeOK:
|
|
val = expr.value(env)
|
|
if val < 0x00 or val > 0xff:
|
|
Err.log("Byte constant " + str(expr) + " out of range")
|
|
val = 0
|
|
self.output.append(int(val))
|
|
if tee is not None:
|
|
tee.append(self.output[-1])
|
|
else:
|
|
Err.log("Attempt to write to data segment")
|
|
|
|
def outputword(self, expr, env, tee=None):
|
|
'Outputs a little-endian word, with range checking'
|
|
if self.writeOK:
|
|
val = expr.value(env)
|
|
if val < 0x0000 or val > 0xFFFF:
|
|
Err.log("Word constant " + str(expr) + " out of range")
|
|
val = 0
|
|
self.output.append(int(val & 0xFF))
|
|
self.output.append(int((val >> 8) & 0xFF))
|
|
if tee is not None:
|
|
tee.extend(self.output[-2:])
|
|
else:
|
|
Err.log("Attempt to write to data segment")
|
|
|
|
def outputdword(self, expr, env, tee=None):
|
|
'Outputs a little-endian dword, with range checking'
|
|
if self.writeOK:
|
|
val = expr.value(env)
|
|
if val < 0x00000000 or val > 0xFFFFFFFFL:
|
|
Err.log("DWord constant " + str(expr) + " out of range")
|
|
val = 0
|
|
self.output.append(int(val & 0xFF))
|
|
self.output.append(int((val >> 8) & 0xFF))
|
|
self.output.append(int((val >> 16) & 0xFF))
|
|
self.output.append(int((val >> 24) & 0xFF))
|
|
if tee is not None:
|
|
tee.extend(self.output[-4:])
|
|
else:
|
|
Err.log("Attempt to write to data segment")
|
|
|
|
def outputword_be(self, expr, env, tee=None):
|
|
'Outputs a big-endian word, with range checking'
|
|
if self.writeOK:
|
|
val = expr.value(env)
|
|
if val < 0x0000 or val > 0xFFFF:
|
|
Err.log("Word constant " + str(expr) + " out of range")
|
|
val = 0
|
|
self.output.append(int((val >> 8) & 0xFF))
|
|
self.output.append(int(val & 0xFF))
|
|
if tee is not None:
|
|
tee.extend(self.output[-2:])
|
|
else:
|
|
Err.log("Attempt to write to data segment")
|
|
|
|
def outputdword_be(self, expr, env, tee=None):
|
|
'Outputs a big-endian dword, with range checking'
|
|
if self.writeOK:
|
|
val = expr.value(env)
|
|
if val < 0x00000000 or val > 0xFFFFFFFFL:
|
|
Err.log("DWord constant " + str(expr) + " out of range")
|
|
val = 0
|
|
self.output.append(int((val >> 24) & 0xFF))
|
|
self.output.append(int((val >> 16) & 0xFF))
|
|
self.output.append(int((val >> 8) & 0xFF))
|
|
self.output.append(int(val & 0xFF))
|
|
if tee is not None:
|
|
tee.extend(self.output[-4:])
|
|
else:
|
|
Err.log("Attempt to write to data segment")
|
|
|
|
def relativize(self, expr, env, arglen):
|
|
"Convert an expression into one for use in relative addressing"
|
|
arg = expr.value(env)
|
|
arg = arg - (env.getPC() + arglen + 1)
|
|
if arg < -128 or arg > 127:
|
|
Err.log("Branch target out of bounds")
|
|
arg = 0
|
|
if arg < 0:
|
|
arg += 256
|
|
return IR.ConstantExpr(arg)
|
|
|
|
def relativizelong(self, expr, env, arglen):
|
|
"Convert an expression into one for use in relative addressing"
|
|
arg = expr.value(env)
|
|
arg = arg - (env.getPC() + arglen)
|
|
if arg < 0:
|
|
arg += 65536
|
|
return IR.ConstantExpr(arg)
|
|
|
|
def listing_string(self, pc, binary, mode, opcode, val1, val2):
|
|
base = " %04X " % pc
|
|
base += (" %02X" * len(binary)) % tuple(binary)
|
|
formats = ["",
|
|
"#$%02X",
|
|
"#$%04X",
|
|
"$%02X",
|
|
"$%02X, X",
|
|
"$%02X, Y",
|
|
"$%04X",
|
|
"$%04X, X",
|
|
"$%04X, Y",
|
|
"($%04X)",
|
|
"($%04X, X)",
|
|
"($%04X), Y",
|
|
"($%04X), Z",
|
|
"($%02X)",
|
|
"($%02X, X)",
|
|
"($%02X), Y",
|
|
"($%02X, SP), Y",
|
|
"($%02X), Z",
|
|
"$%04X",
|
|
"$%04X",
|
|
"$%02X, $%04X"]
|
|
fmt = ("%-16s %-5s" % (base, opcode.upper())) + formats[mode]
|
|
if val1 is None:
|
|
return fmt
|
|
elif val2 is None:
|
|
arglen = Ops.lengths[mode]
|
|
mask = 0xFF
|
|
# Relative is a full address in a byte, so it also has the
|
|
# 0xFFFF mask.
|
|
if arglen == 2 or mode == Ops.modes.index("Relative"):
|
|
mask = 0xFFFF
|
|
return fmt % (val1 & mask)
|
|
else:
|
|
# Mode is "Zero Page, Relative"
|
|
return fmt % (val1 & 0xFF, val2 & 0xFFFF)
|
|
|
|
def assemble(self, node, mode, env):
|
|
"A generic instruction called by the visitor methods themselves"
|
|
(opcode, expr, expr2) = node.data
|
|
bin_op = Ops.opcodes[opcode][mode]
|
|
if bin_op is None:
|
|
Err.log('%s does not have mode "%s"' % (opcode.upper(),
|
|
Ops.modes[mode]))
|
|
return
|
|
inst_bytes = []
|
|
self.outputbyte(IR.ConstantExpr(bin_op), env, inst_bytes)
|
|
arglen = Ops.lengths[mode]
|
|
val1 = None
|
|
val2 = None
|
|
if expr is not None:
|
|
val1 = expr.value(env)
|
|
if expr2 is not None:
|
|
val2 = expr2.value(env)
|
|
if mode == Ops.modes.index("Zero Page, Relative"):
|
|
expr2 = self.relativize(expr2, env, arglen)
|
|
self.outputbyte(expr, env, inst_bytes)
|
|
self.outputbyte(expr2, env, inst_bytes)
|
|
else:
|
|
if mode == Ops.modes.index("Relative"):
|
|
expr = self.relativize(expr, env, arglen)
|
|
elif mode == Ops.modes.index("RelativeLong"):
|
|
expr = self.relativizelong(expr, env, arglen)
|
|
if arglen == 1:
|
|
self.outputbyte(expr, env, inst_bytes)
|
|
elif arglen == 2:
|
|
self.outputword(expr, env, inst_bytes)
|
|
self.listing.listInstruction(self.listing_string(env.getPC(),
|
|
inst_bytes,
|
|
mode, opcode,
|
|
val1, val2))
|
|
env.incPC(1 + arglen)
|
|
self.code += 1 + arglen
|
|
|
|
def visitImplied(self, node, env):
|
|
self.assemble(node, Ops.modes.index("Implied"), env)
|
|
|
|
def visitImmediate(self, node, env):
|
|
self.assemble(node, Ops.modes.index("Immediate"), env)
|
|
|
|
def visitImmediateLong(self, node, env):
|
|
self.assemble(node, Ops.modes.index("ImmediateLong"), env)
|
|
|
|
def visitZeroPage(self, node, env):
|
|
self.assemble(node, Ops.modes.index("Zero Page"), env)
|
|
|
|
def visitZeroPageX(self, node, env):
|
|
self.assemble(node, Ops.modes.index("Zero Page, X"), env)
|
|
|
|
def visitZeroPageY(self, node, env):
|
|
self.assemble(node, Ops.modes.index("Zero Page, Y"), env)
|
|
|
|
def visitAbsolute(self, node, env):
|
|
self.assemble(node, Ops.modes.index("Absolute"), env)
|
|
|
|
def visitAbsoluteX(self, node, env):
|
|
self.assemble(node, Ops.modes.index("Absolute, X"), env)
|
|
|
|
def visitAbsoluteY(self, node, env):
|
|
self.assemble(node, Ops.modes.index("Absolute, Y"), env)
|
|
|
|
def visitIndirect(self, node, env):
|
|
self.assemble(node, Ops.modes.index("(Absolute)"), env)
|
|
|
|
def visitAbsIndX(self, node, env):
|
|
self.assemble(node, Ops.modes.index("(Absolute, X)"), env)
|
|
|
|
def visitAbsIndY(self, node, env):
|
|
self.assemble(node, Ops.modes.index("(Absolute), Y"), env)
|
|
|
|
def visitAbsIndZ(self, node, env):
|
|
self.assemble(node, Ops.modes.index("(Absolute), Z"), env)
|
|
|
|
def visitZPIndirect(self, node, env):
|
|
self.assemble(node, Ops.modes.index("(Zero Page)"), env)
|
|
|
|
def visitIndirectX(self, node, env):
|
|
self.assemble(node, Ops.modes.index("(Zero Page, X)"), env)
|
|
|
|
def visitIndirectY(self, node, env):
|
|
self.assemble(node, Ops.modes.index("(Zero Page), Y"), env)
|
|
|
|
def visitIndirectZ(self, node, env):
|
|
self.assemble(node, Ops.modes.index("(Zero Page), Z"), env)
|
|
|
|
def visitIndirectSPY(self, node, env):
|
|
self.assemble(node, Ops.modes.index("(Zero Page, SP), Y"), env)
|
|
|
|
def visitRelative(self, node, env):
|
|
self.assemble(node, Ops.modes.index("Relative"), env)
|
|
|
|
def visitRelativeLong(self, node, env):
|
|
self.assemble(node, Ops.modes.index("RelativeLong"), env)
|
|
|
|
def visitZPRelative(self, node, env):
|
|
self.assemble(node, Ops.modes.index("Zero Page, Relative"), env)
|
|
|
|
def visitLabel(self, node, env):
|
|
(label, val) = node.data
|
|
location = val.value(env)
|
|
self.mapper.mapLabel(label, str(node.ppt), location)
|
|
|
|
def visitByte(self, node, env):
|
|
created = []
|
|
for expr in node.data:
|
|
self.outputbyte(expr, env, created)
|
|
self.registerData(created, env.getPC())
|
|
env.incPC(len(node.data))
|
|
self.data += len(node.data)
|
|
|
|
def visitByteRange(self, node, env):
|
|
offset = node.data[0].value(env) + 2
|
|
length = node.data[1].value(env)
|
|
if offset < 2:
|
|
Err.log("Negative offset in .incbin")
|
|
elif offset > len(node.data):
|
|
Err.log("Offset extends past end of file")
|
|
elif length < 0:
|
|
Err.log("Negative length")
|
|
elif offset + length > len(node.data):
|
|
Err.log("File too small for .incbin subrange")
|
|
else:
|
|
created = []
|
|
for expr in node.data[offset:(offset + length)]:
|
|
self.outputbyte(expr, env, created)
|
|
self.registerData(created, env.getPC())
|
|
env.incPC(length)
|
|
self.data += length
|
|
|
|
def visitWord(self, node, env):
|
|
created = []
|
|
for expr in node.data:
|
|
self.outputword(expr, env, created)
|
|
self.registerData(created, env.getPC())
|
|
env.incPC(len(node.data) * 2)
|
|
self.data += len(node.data) * 2
|
|
|
|
def visitDword(self, node, env):
|
|
created = []
|
|
for expr in node.data:
|
|
self.outputdword(expr, env, created)
|
|
self.registerData(created, env.getPC())
|
|
env.incPC(len(node.data) * 4)
|
|
self.data += len(node.data) * 4
|
|
|
|
def visitWordBE(self, node, env):
|
|
created = []
|
|
for expr in node.data:
|
|
self.outputword_be(expr, env, created)
|
|
self.registerData(created, env.getPC())
|
|
env.incPC(len(node.data) * 2)
|
|
self.data += len(node.data) * 2
|
|
|
|
def visitDwordBE(self, node, env):
|
|
created = []
|
|
for expr in node.data:
|
|
self.outputdword_be(expr, env, created)
|
|
self.registerData(created, env.getPC())
|
|
env.incPC(len(node.data) * 4)
|
|
self.data += len(node.data) * 4
|
|
|
|
def visitSetPC(self, node, env):
|
|
val = node.data[0].value(env)
|
|
if self.writeOK and env.getPC() != val:
|
|
self.listing.listDivider(val)
|
|
env.setPC(val)
|
|
|
|
def visitCheckPC(self, node, env):
|
|
pc = env.getPC()
|
|
target = node.data[0].value(env)
|
|
if (pc > target):
|
|
Err.log(".checkpc assertion failed: $%x > $%x" % (pc, target))
|
|
|
|
def visitAdvance(self, node, env):
|
|
pc = env.getPC()
|
|
target = node.data[0].value(env)
|
|
if (pc > target):
|
|
Err.log("Attempted to .advance backwards: $%x to $%x" %
|
|
(pc, target))
|
|
else:
|
|
created = []
|
|
for i in xrange(target - pc):
|
|
self.outputbyte(node.data[1], env, created)
|
|
self.filler += target - pc
|
|
self.registerData(created, env.getPC())
|
|
env.setPC(target)
|
|
|
|
def registerData(self, vals, pc):
|
|
self.listing.listData(vals, pc)
|