mirror of
https://github.com/irmen/prog8.git
synced 2024-12-26 14:29:35 +00:00
225 lines
11 KiB
Python
225 lines
11 KiB
Python
"""
|
|
Programming Language for 6502/6510 microprocessors, codename 'Sick'
|
|
This is the assembly code generator (from the parse tree)
|
|
|
|
Written by Irmen de Jong (irmen@razorvine.net) - license: GNU GPL 3.0
|
|
"""
|
|
|
|
import os
|
|
import datetime
|
|
from typing import TextIO, Callable
|
|
from ..plylex import print_bold
|
|
from ..plyparse import Module, Scope, ProgramFormat, Block, Directive, VarDef, Label, Subroutine, AstNode, ZpOptions, \
|
|
InlineAssembly, Return, Register, Goto, SubCall, Assignment, AugAssignment, IncrDecr
|
|
from . import CodeError, to_hex
|
|
from .variables import generate_block_init, generate_block_vars
|
|
from .assignment import generate_assignment, generate_aug_assignment
|
|
from .calls import generate_goto, generate_subcall
|
|
from .incrdecr import generate_incrdecr
|
|
|
|
|
|
class Output:
|
|
def __init__(self, stream: TextIO) -> None:
|
|
self.stream = stream
|
|
|
|
def __call__(self, text, *args, **vargs):
|
|
# replace '\v' (vertical tab) char by the actual line indent (2 tabs) and write to the stringIo
|
|
print(text.replace("\v", "\t\t"), *args, file=self.stream, **vargs)
|
|
|
|
|
|
class AssemblyGenerator:
|
|
BREAKPOINT_COMMENT_SIGNATURE = "~~~BREAKPOINT~~~"
|
|
BREAKPOINT_COMMENT_DETECTOR = r".(?P<address>\w+)\s+ea\s+nop\s+;\s+{:s}.*".format(BREAKPOINT_COMMENT_SIGNATURE)
|
|
|
|
def __init__(self, module: Module) -> None:
|
|
self.module = module
|
|
self.cur_block = None
|
|
self.output = None # type: Output
|
|
|
|
def generate(self, filename: str) -> None:
|
|
with open(filename, "wt") as stream:
|
|
output = Output(stream)
|
|
try:
|
|
self._generate(output)
|
|
except Exception as x:
|
|
output(".error \"****** ABORTED DUE TO ERROR:", x, "\"\n")
|
|
raise
|
|
|
|
def _generate(self, out: Callable) -> None:
|
|
self.sanitycheck()
|
|
self.header(out)
|
|
self.blocks(out)
|
|
out("\t.end")
|
|
|
|
def sanitycheck(self) -> None:
|
|
start_found = False
|
|
for block, parent in self.module.all_scopes():
|
|
assert isinstance(block, (Module, Block, Subroutine))
|
|
for label in block.nodes:
|
|
if isinstance(label, Label) and label.name == "start" and block.name == "main":
|
|
start_found = True
|
|
break
|
|
if start_found:
|
|
break
|
|
if not start_found:
|
|
print_bold("ERROR: program entry point is missing ('start' label in 'main' block)\n")
|
|
raise SystemExit(1)
|
|
all_blocknames = [b.name for b in self.module.scope.filter_nodes(Block)]
|
|
unique_blocknames = set(all_blocknames)
|
|
if len(all_blocknames) != len(unique_blocknames):
|
|
for name in unique_blocknames:
|
|
all_blocknames.remove(name)
|
|
raise CodeError("there are duplicate block names", all_blocknames)
|
|
zpblock = self.module.zeropage()
|
|
if zpblock:
|
|
# ZP block contains no code?
|
|
for stmt in zpblock.scope.nodes:
|
|
if not isinstance(stmt, (Directive, VarDef)):
|
|
raise CodeError("ZP block can only contain directive and var")
|
|
|
|
def header(self, out: Callable) -> None:
|
|
out("; code generated by il65.py - codename 'Sick'")
|
|
out("; source file:", self.module.sourceref.file)
|
|
out("; compiled on:", datetime.datetime.now())
|
|
out("; output options:", self.module.format, self.module.zp_options)
|
|
out("; assembler syntax is for the 64tasm cross-assembler")
|
|
out("\n.cpu '6502'\n.enc 'none'\n")
|
|
assert self.module.address is not None
|
|
if self.module.format in (ProgramFormat.PRG, ProgramFormat.BASIC):
|
|
if self.module.format == ProgramFormat.BASIC:
|
|
if self.module.address != 0x0801:
|
|
raise CodeError("BASIC output mode must have load address $0801")
|
|
out("; ---- basic program with sys call ----")
|
|
out("* = " + to_hex(self.module.address))
|
|
year = datetime.datetime.now().year
|
|
out("\v.word (+), {:d}".format(year))
|
|
out("\v.null $9e, format(' %d ', _il65_entrypoint), $3a, $8f, ' il65 by idj'")
|
|
out("+\v.word 0")
|
|
out("_il65_entrypoint\v; assembly code starts here\n")
|
|
else:
|
|
out("; ---- program without sys call ----")
|
|
out("* = " + to_hex(self.module.address) + "\n")
|
|
elif self.module.format == ProgramFormat.RAW:
|
|
out("; ---- raw assembler program ----")
|
|
out("* = " + to_hex(self.module.address) + "\n")
|
|
# call the block init methods and jump to the user's main.start entrypoint
|
|
if self.module.zp_options == ZpOptions.CLOBBER_RESTORE:
|
|
out("\vjsr _il65_save_zeropage")
|
|
out("\v; initialize all blocks (reset vars)")
|
|
if self.module.zeropage():
|
|
out("\vjsr ZP._il65_init_block")
|
|
for block in self.module.nodes:
|
|
if isinstance(block, Block) and block.name != "ZP":
|
|
out("\vjsr {}._il65_init_block".format(block.name))
|
|
out("\v; call user code")
|
|
if self.module.zp_options == ZpOptions.CLOBBER_RESTORE:
|
|
out("\vjsr {:s}.start".format(self.module.main().label))
|
|
out("\vcld")
|
|
out("\vjmp _il65_restore_zeropage\n")
|
|
# include the assembly code for the save/restore zeropage routines
|
|
zprestorefile = os.path.join(os.path.split(__file__)[0], "../lib", "restorezp.asm")
|
|
with open(zprestorefile, "rU") as f:
|
|
for line in f.readlines():
|
|
out(line.rstrip("\n"))
|
|
else:
|
|
out("\vjmp {:s}.start".format(self.module.main().label))
|
|
out("")
|
|
|
|
def blocks(self, out: Callable) -> None:
|
|
zpblock = self.module.zeropage()
|
|
if zpblock:
|
|
# if there's a Zeropage block, it always goes first
|
|
self.cur_block = zpblock # type: ignore
|
|
out("\n; ---- zero page block: '{:s}' ----".format(zpblock.name))
|
|
out("; file: '{:s}' src l. {:d}\n".format(zpblock.sourceref.file, zpblock.sourceref.line))
|
|
out("{:s}\t.proc\n".format(zpblock.label))
|
|
generate_block_init(out, zpblock)
|
|
generate_block_vars(out, zpblock, True)
|
|
# there's no code in the zero page block.
|
|
out("\v.pend\n")
|
|
for block in sorted(self.module.scope.filter_nodes(Block), key=lambda b: b.address or 0):
|
|
if block.name == "ZP":
|
|
continue # already processed
|
|
self.cur_block = block
|
|
out("\n; ---- block: '{:s}' ----".format(block.name))
|
|
out("; file: '{:s}' src l. {:d}\n".format(block.sourceref.file, block.sourceref.line))
|
|
if block.address:
|
|
out(".cerror * > ${0:04x}, 'block address overlaps by ', *-${0:04x},' bytes'".format(block.address))
|
|
out("* = ${:04x}".format(block.address))
|
|
out("{:s}\t.proc\n".format(block.label))
|
|
generate_block_init(out, block)
|
|
generate_block_vars(out, block)
|
|
subroutines = list(sub for sub in block.scope.filter_nodes(Subroutine) if sub.address is not None)
|
|
if subroutines:
|
|
# these are (external) subroutines that are defined by address instead of a scope/code
|
|
out("; external subroutines")
|
|
for subdef in subroutines:
|
|
assert subdef.scope is None
|
|
out("\v{:s} = {:s}".format(subdef.name, to_hex(subdef.address)))
|
|
out("; end external subroutines\n")
|
|
for stmt in block.scope.nodes:
|
|
if isinstance(stmt, (VarDef, Subroutine)):
|
|
continue # should have been handled already or will be later
|
|
self.generate_statement(out, stmt, block.scope)
|
|
if block.name == "main" and isinstance(stmt, Label) and stmt.name == "start":
|
|
# make sure the main.start routine clears the decimal and carry flags as first steps
|
|
out("\vcld\n\vclc\n\vclv")
|
|
subroutines = list(sub for sub in block.scope.filter_nodes(Subroutine) if sub.address is None)
|
|
if subroutines:
|
|
# these are subroutines that are defined by a scope/code
|
|
out("; -- block subroutines")
|
|
for subdef in subroutines:
|
|
assert subdef.scope is not None
|
|
out("{:s}\v; src l. {:d}".format(subdef.name, subdef.sourceref.line))
|
|
params = ", ".join("{:s} -> {:s}".format(name or "<unnamed>", registers) for name, registers in subdef.param_spec)
|
|
returns = ",".join(sorted(register for register in subdef.result_spec if register[-1] != '?'))
|
|
clobbers = ",".join(sorted(register for register in subdef.result_spec if register[-1] == '?'))
|
|
out("\v; params: {}\n\v; returns: {} clobbers: {}".format(params or "-", returns or "-", clobbers or "-"))
|
|
cur_block = self.cur_block
|
|
self.cur_block = subdef.scope
|
|
for stmt in subdef.scope.nodes:
|
|
self.generate_statement(out, stmt, subdef.scope)
|
|
self.cur_block = cur_block
|
|
out("")
|
|
out("; -- end block subroutines")
|
|
out("\n\v.pend\n")
|
|
|
|
def generate_statement(self, out: Callable, stmt: AstNode, scope: Scope) -> None:
|
|
if isinstance(stmt, Label):
|
|
out("\n{:s}\v\t\t; {:s}".format(stmt.name, stmt.lineref))
|
|
elif isinstance(stmt, Return):
|
|
if stmt.value_A:
|
|
reg = Register(name="A", sourceref=stmt.sourceref) # type: ignore
|
|
assignment = Assignment(left=[reg], right=stmt.value_A, sourceref=stmt.sourceref) # type: ignore
|
|
generate_assignment(out, assignment)
|
|
if stmt.value_X:
|
|
reg = Register(name="X", sourceref=stmt.sourceref) # type: ignore
|
|
assignment = Assignment(left=[reg], right=stmt.value_X, sourceref=stmt.sourceref) # type: ignore
|
|
generate_assignment(out, assignment)
|
|
if stmt.value_Y:
|
|
reg = Register(name="Y", sourceref=stmt.sourceref) # type: ignore
|
|
assignment = Assignment(left=[reg], right=stmt.value_Y, sourceref=stmt.sourceref) # type: ignore
|
|
generate_assignment(out, assignment)
|
|
out("\vrts")
|
|
elif isinstance(stmt, InlineAssembly):
|
|
out("\n\v; inline asm, " + stmt.lineref)
|
|
out(stmt.assembly)
|
|
out("\v; end inline asm, " + stmt.lineref + "\n")
|
|
elif isinstance(stmt, IncrDecr):
|
|
generate_incrdecr(out, stmt, scope)
|
|
elif isinstance(stmt, Goto):
|
|
generate_goto(out, stmt)
|
|
elif isinstance(stmt, SubCall):
|
|
generate_subcall(out, stmt)
|
|
elif isinstance(stmt, Assignment):
|
|
generate_assignment(out, stmt)
|
|
elif isinstance(stmt, AugAssignment):
|
|
generate_aug_assignment(out, stmt)
|
|
elif isinstance(stmt, Directive):
|
|
if stmt.name == "breakpoint":
|
|
# put a marker in the source so that we can generate a list of breakpoints later
|
|
out("\vnop\t\t; {:s} {:s}".format(self.BREAKPOINT_COMMENT_SIGNATURE, stmt.lineref))
|
|
# other directives are ignored here
|
|
else:
|
|
raise NotImplementedError("statement", stmt)
|