""" 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, no_type_check 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, to_mflpt5, Context 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: def __init__(self, module: Module, enable_floats: bool) -> None: self.module = module self.floats_enabled = enable_floats 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": # type: ignore 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)] # type: ignore 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("") @no_type_check 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): ctx = Context(out=out, stmt=None, scope=block.scope, floats_enabled=self.floats_enabled) 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 ctx.stmt = stmt self.generate_statement(ctx) 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 "", 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 ctx.stmt in subdef.scope.nodes: self.generate_statement(ctx) self.cur_block = cur_block out("") out("; -- end block subroutines") if block.scope.float_const_values: if not self.floats_enabled: raise CodeError("floating point numbers not enabled via option") # generate additional float constants that are used in floating point expressions out("\n; -- float constants") for name, value in block.scope.float_const_values.items(): out("{:s}\t\t.byte ${:02x}, ${:02x}, ${:02x}, ${:02x}, ${:02x}\t; {}".format(name, *to_mflpt5(value), value)) out("\n\v.pend\n") @no_type_check def generate_statement(self, ctx: Context) -> None: stmt = ctx.stmt if isinstance(stmt, Label): ctx.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) assignment = Assignment(sourceref=stmt.sourceref) assignment.nodes.append(AssignmentTargets(nodes=[reg], sourceref=stmt.sourceref)) assignment.nodes.append(stmt.value_A) ctx.stmt = assignment generate_assignment(ctx) if stmt.value_X: reg = Register(name="X", sourceref=stmt.sourceref) assignment = Assignment(sourceref=stmt.sourceref) assignment.nodes.append(AssignmentTargets(nodes=[reg], sourceref=stmt.sourceref)) assignment.nodes.append(stmt.value_X) ctx.stmt = assignment generate_assignment(ctx) if stmt.value_Y: reg = Register(name="Y", sourceref=stmt.sourceref) assignment = Assignment(sourceref=stmt.sourceref) assignment.nodes.append(AssignmentTargets(nodes=[reg], sourceref=stmt.sourceref)) assignment.nodes.append(stmt.value_Y) ctx.stmt = assignment generate_assignment(ctx) ctx.out("\vrts") elif isinstance(stmt, InlineAssembly): ctx.out("\n\v; inline asm, " + stmt.lineref) ctx.out(stmt.assembly) ctx.out("\v; end inline asm, " + stmt.lineref + "\n") elif isinstance(stmt, IncrDecr): generate_incrdecr(ctx) elif isinstance(stmt, Goto): generate_goto(ctx) elif isinstance(stmt, SubCall): generate_subcall(ctx) elif isinstance(stmt, Assignment): generate_assignment(ctx) elif isinstance(stmt, AugAssignment): generate_aug_assignment(ctx) elif isinstance(stmt, Directive): if stmt.name == "breakpoint": # put a marker in the source so that we can generate a list of breakpoints later # this label is later extracted from the label dump file to turn it into a breakpoint instruction ctx.out("_il65_breakpoint_{:d}".format(id(stmt))) # other directives are ignored here else: raise NotImplementedError("statement", stmt)