""" 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 io import re import subprocess import datetime from typing import Union from .plyparse import Module, ProgramFormat, Block, Directive, VarDef, Label, ZpOptions, DataType from .symbols import to_hex class CodeError(Exception): pass 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.generated_code = io.StringIO() def p(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.generated_code, **vargs) def generate(self, filename: str) -> None: self._generate() with open(filename, "wt") as out: out.write(self.generated_code.getvalue()) self.generated_code.close() def _generate(self) -> None: self.sanitycheck() self.header() self.initialize_variables() self.blocks() self.footer() def sanitycheck(self): # duplicate block names? 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): self.p("; code generated by il65.py - codename 'Sick'") self.p("; source file:", self.module.sourceref.file) self.p("; compiled on:", datetime.datetime.now()) self.p("; output options:", self.module.format, self.module.zp_options) self.p("; assembler syntax is for the 64tasm cross-assembler") self.p("\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") self.p("; ---- basic program with sys call ----") self.p("* = " + to_hex(self.module.address)) year = datetime.datetime.now().year self.p("\v.word (+), {:d}".format(year)) self.p("\v.null $9e, format(' %d ', _il65_sysaddr), $3a, $8f, ' il65 by idj'") self.p("+\v.word 0") self.p("_il65_sysaddr\v; assembly code starts here\n") else: self.p("; ---- program without sys call ----") self.p("* = " + to_hex(self.module.address) + "\n") elif self.module.format == ProgramFormat.RAW: self.p("; ---- raw assembler program ----") self.p("* = " + to_hex(self.module.address) + "\n") def initialize_variables(self) -> None: if self.module.zp_options == ZpOptions.CLOBBER_RESTORE: self.p("\vjsr il65_lib_zp.save_zeropage") zp_float_bytes = {} # Only the vars from the ZeroPage need to be initialized here, # the vars in all other blocks are just defined and pre-filled there. zpblock = self.module.zeropage() if zpblock: vars_to_init = [v for v in zpblock.scope.filter_nodes(VarDef) if v.allocate and v.type in (DataType.BYTE, DataType.WORD, DataType.FLOAT)] # @todo optimize sort order (sort on value first, then type, then blockname, then address/name) # (str(self.value) or "", self.blockname, self.name or "", self.address or 0, self.seq_nr) prev_value = 0 # type: Union[str, int, float] if vars_to_init: self.p("; init zp vars") self.p("\vlda #0\n\vldx #0") for variable in vars_to_init: vname = zpblock.label + '.' + variable.name vvalue = variable.value if variable.type == DataType.BYTE: if vvalue != prev_value: self.p("\vlda #${:02x}".format(vvalue)) prev_value = vvalue self.p("\vsta {:s}".format(vname)) elif variable.type == DataType.WORD: if vvalue != prev_value: self.p("\vlda #<${:04x}".format(vvalue)) self.p("\vldx #>${:04x}".format(vvalue)) prev_value = vvalue self.p("\vsta {:s}".format(vname)) self.p("\vstx {:s}+1".format(vname)) elif variable.type == DataType.FLOAT: bytes = self.to_mflpt5(vvalue) # type: ignore zp_float_bytes[variable.name] = (vname, bytes, vvalue) if zp_float_bytes: self.p("\vldx #4") self.p("-") for varname, (vname, b, fv) in zp_float_bytes.items(): self.p("\vlda _float_bytes_{:s},x".format(varname)) self.p("\vsta {:s},x".format(vname)) self.p("\vdex") self.p("\vbpl -") self.p("; end init zp vars") else: self.p("\v; there are no zp vars to initialize") else: self.p("\v; there is no zp block to initialize") if self.module.zp_options == ZpOptions.CLOBBER_RESTORE: self.p("\vjsr {:s}.start\v; call user code".format(self.module.main().label)) self.p("\vcld") self.p("\vjmp il65_lib_zp.restore_zeropage") else: self.p("\vjmp {:s}.start\v; call user code".format(self.module.main().label)) self.p("") for varname, (vname, bytes, fpvalue) in zp_float_bytes.items(): self.p("_float_bytes_{:s}\v.byte ${:02x}, ${:02x}, ${:02x}, ${:02x}, ${:02x}\t; {}".format(varname, *bytes, fpvalue)) self.p("\n") def blocks(self): self.p("; @todo") # @todo pass def footer(self): self.p("; @todo") # @todo pass class Assembler64Tass: def __init__(self, format: ProgramFormat) -> None: self.format = format def assemble(self, inputfilename: str, outputfilename: str) -> None: args = ["64tass", "--ascii", "--case-sensitive", "-Wall", "-Wno-strict-bool", "--dump-labels", "--vice-labels", "-l", outputfilename+".vice-mon-list", "-L", outputfilename+".final-asm", "--no-monitor", "--output", outputfilename, inputfilename] if self.format in (ProgramFormat.PRG, ProgramFormat.BASIC): args.append("--cbm-prg") elif self.format == ProgramFormat.RAW: args.append("--nostart") else: raise ValueError("don't know how to create format "+str(self.format)) try: if self.format == ProgramFormat.PRG: print("\nCreating C-64 prg.") elif self.format == ProgramFormat.RAW: print("\nCreating raw binary.") try: subprocess.check_call(args) except FileNotFoundError as x: raise SystemExit("ERROR: cannot run assembler program: "+str(x)) except subprocess.CalledProcessError as x: raise SystemExit("assembler failed with returncode " + str(x.returncode)) def generate_breakpoint_list(self, program_filename: str) -> str: breakpoints = [] with open(program_filename + ".final-asm", "rU") as f: for line in f: match = re.fullmatch(AssemblyGenerator.BREAKPOINT_COMMENT_DETECTOR, line, re.DOTALL) if match: breakpoints.append("$" + match.group("address")) cmdfile = program_filename + ".vice-mon-list" with open(cmdfile, "at") as f: print("; vice monitor breakpoint list now follows", file=f) print("; {:d} breakpoints have been defined here".format(len(breakpoints)), file=f) print("del", file=f) for b in breakpoints: print("break", b, file=f) return cmdfile