prog8/il65/emit/generate.py

239 lines
12 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, no_type_check
from ..plylex import print_bold
from ..plyparse import (Module, ProgramFormat, Block, Directive, VarDef, Label, Subroutine, 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 "<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 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)