""" 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, AssignmentTargets 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
\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: for label in self.module.all_nodes(Label): if label.name == "start" and label.my_scope().name == "main": break else: 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.all_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.all_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.all_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.all_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 "