diff --git a/il65/datatypes.py b/il65/datatypes.py index 50ae93abe..1620fb74c 100644 --- a/il65/datatypes.py +++ b/il65/datatypes.py @@ -12,9 +12,6 @@ from functools import total_ordering from .plylex import print_warning, SourceRef -PrimitiveType = Union[int, float, str] - - @total_ordering class VarType(enum.Enum): CONST = 1 @@ -70,58 +67,9 @@ FLOAT_MAX_POSITIVE = 1.7014118345e+38 FLOAT_MAX_NEGATIVE = -1.7014118345e+38 -def to_hex(number: int) -> str: - # 0..15 -> "0".."15" - # 16..255 -> "$10".."$ff" - # 256..65536 -> "$0100".."$ffff" - if number is None: - raise ValueError("number") - if 0 <= number < 16: - return str(number) - if 0 <= number < 0x100: - return "${:02x}".format(number) - if 0 <= number < 0x10000: - return "${:04x}".format(number) - raise OverflowError(number) - - -def to_mflpt5(number: float) -> bytearray: - # algorithm here https://sourceforge.net/p/acme-crossass/code-0/62/tree/trunk/ACME_Lib/cbm/mflpt.a - number = float(number) - if number < FLOAT_MAX_NEGATIVE or number > FLOAT_MAX_POSITIVE: - raise OverflowError("floating point number out of 5-byte mflpt range", number) - if number == 0.0: - return bytearray([0, 0, 0, 0, 0]) - if number < 0.0: - sign = 0x80000000 - number = -number - else: - sign = 0x00000000 - mant, exp = math.frexp(number) - exp += 128 - if exp < 1: - # underflow, use zero instead - return bytearray([0, 0, 0, 0, 0]) - if exp > 255: - raise OverflowError("floating point number out of 5-byte mflpt range", number) - mant = sign | int(mant * 0x100000000) & 0x7fffffff - return bytearray([exp]) + int.to_bytes(mant, 4, "big") - - -def mflpt5_to_float(mflpt: bytearray) -> float: - # algorithm here https://sourceforge.net/p/acme-crossass/code-0/62/tree/trunk/ACME_Lib/cbm/mflpt.a - if mflpt == bytearray([0, 0, 0, 0, 0]): - return 0.0 - exp = mflpt[0] - 128 - sign = mflpt[1] & 0x80 - number = 0x80000000 | int.from_bytes(mflpt[1:], "big") - number = float(number) * 2**exp / 0x100000000 - return -number if sign else number - - -def coerce_value(datatype: DataType, value: PrimitiveType, sourceref: SourceRef=None) -> Tuple[bool, PrimitiveType]: +def coerce_value(datatype: DataType, value: Union[int, float, str], sourceref: SourceRef=None) -> Tuple[bool, Union[int, float, str]]: # if we're a BYTE type, and the value is a single character, convert it to the numeric value - def verify_bounds(value: PrimitiveType) -> None: + def verify_bounds(value: Union[int, float, str]) -> None: # if the value is out of bounds, raise an overflow exception if isinstance(value, (int, float)): if datatype == DataType.BYTE and not (0 <= value <= 0xff): # type: ignore diff --git a/il65/emit/__init__.py b/il65/emit/__init__.py new file mode 100644 index 000000000..d86de2282 --- /dev/null +++ b/il65/emit/__init__.py @@ -0,0 +1,120 @@ +""" +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 contextlib +import math +from typing import Set, Callable +from ..datatypes import FLOAT_MAX_POSITIVE, FLOAT_MAX_NEGATIVE +from ..plyparse import Scope +from ..compile import Zeropage + + +class CodeError(Exception): + pass + + +def to_hex(number: int) -> str: + # 0..15 -> "0".."15" + # 16..255 -> "$10".."$ff" + # 256..65536 -> "$0100".."$ffff" + if number is None: + raise ValueError("number") + if 0 <= number < 16: + return str(number) + if 0 <= number < 0x100: + return "${:02x}".format(number) + if 0 <= number < 0x10000: + return "${:04x}".format(number) + raise OverflowError(number) + + +def to_mflpt5(number: float) -> bytearray: + # algorithm here https://sourceforge.net/p/acme-crossass/code-0/62/tree/trunk/ACME_Lib/cbm/mflpt.a + number = float(number) + if number < FLOAT_MAX_NEGATIVE or number > FLOAT_MAX_POSITIVE: + raise OverflowError("floating point number out of 5-byte mflpt range", number) + if number == 0.0: + return bytearray([0, 0, 0, 0, 0]) + if number < 0.0: + sign = 0x80000000 + number = -number + else: + sign = 0x00000000 + mant, exp = math.frexp(number) + exp += 128 + if exp < 1: + # underflow, use zero instead + return bytearray([0, 0, 0, 0, 0]) + if exp > 255: + raise OverflowError("floating point number out of 5-byte mflpt range", number) + mant = sign | int(mant * 0x100000000) & 0x7fffffff + return bytearray([exp]) + int.to_bytes(mant, 4, "big") + + +def mflpt5_to_float(mflpt: bytearray) -> float: + # algorithm here https://sourceforge.net/p/acme-crossass/code-0/62/tree/trunk/ACME_Lib/cbm/mflpt.a + if mflpt == bytearray([0, 0, 0, 0, 0]): + return 0.0 + exp = mflpt[0] - 128 + sign = mflpt[1] & 0x80 + number = 0x80000000 | int.from_bytes(mflpt[1:], "big") + number = float(number) * 2**exp / 0x100000000 + return -number if sign else number + + +@contextlib.contextmanager +def preserving_registers(registers: Set[str], scope: Scope, out: Callable, loads_a_within: bool=False, force_preserve: bool=False): + # this sometimes clobbers a ZP scratch register and is therefore NOT safe to use in interrupts + # see http://6502.org/tutorials/register_preservation.html + if not scope.save_registers and not force_preserve: + yield + return + if registers == {'A'}: + out("\t\tpha") + yield + out("\t\tpla") + elif registers: + if not loads_a_within: + out("\t\tsta ${:02x}".format(Zeropage.SCRATCH_B2)) + if 'A' in registers: + out("\t\tpha") + if 'X' in registers: + out("\t\ttxa\n\t\tpha") + if 'Y' in registers: + out("\t\ttya\n\t\tpha") + if not loads_a_within: + out("\t\tlda ${:02x}".format(Zeropage.SCRATCH_B2)) + yield + if 'X' in registers and 'Y' in registers: + if 'A' not in registers: + out("\t\tsta ${:02x}".format(Zeropage.SCRATCH_B2)) + out("\t\tpla\n\t\ttay") + out("\t\tpla\n\t\ttax") + out("\t\tlda ${:02x}".format(Zeropage.SCRATCH_B2)) + else: + out("\t\tpla\n\t\ttay") + out("\t\tpla\n\t\ttax") + else: + if 'Y' in registers: + if 'A' not in registers: + out("\t\tsta ${:02x}".format(Zeropage.SCRATCH_B2)) + out("\t\tpla\n\t\ttay") + out("\t\tlda ${:02x}".format(Zeropage.SCRATCH_B2)) + else: + out("\t\tpla\n\t\ttay") + if 'X' in registers: + if 'A' not in registers: + out("\t\tsta ${:02x}".format(Zeropage.SCRATCH_B2)) + out("\t\tpla\n\t\ttax") + out("\t\tlda ${:02x}".format(Zeropage.SCRATCH_B2)) + else: + out("\t\tpla\n\t\ttax") + if 'A' in registers: + out("\t\tpla") + else: + yield diff --git a/il65/emit/assignment.py b/il65/emit/assignment.py new file mode 100644 index 000000000..403df6f94 --- /dev/null +++ b/il65/emit/assignment.py @@ -0,0 +1,22 @@ +""" +Programming Language for 6502/6510 microprocessors, codename 'Sick' +This is the code generator for assignment statements. + +Written by Irmen de Jong (irmen@razorvine.net) - license: GNU GPL 3.0 +""" + +from typing import Callable +from ..plyparse import LiteralValue, Assignment, AugAssignment + + +def generate_assignment(out: Callable, stmt: Assignment) -> None: + assert stmt.right is not None + rvalue = stmt.right + if isinstance(stmt.right, LiteralValue): + rvalue = stmt.right.value + # @todo + + +def generate_aug_assignment(out: Callable, stmt: AugAssignment) -> None: + assert stmt.right is not None + pass # @todo diff --git a/il65/emit/calls.py b/il65/emit/calls.py new file mode 100644 index 000000000..a4c77853e --- /dev/null +++ b/il65/emit/calls.py @@ -0,0 +1,18 @@ +""" +Programming Language for 6502/6510 microprocessors, codename 'Sick' +This is the code generator for gotos and subroutine calls. + +Written by Irmen de Jong (irmen@razorvine.net) - license: GNU GPL 3.0 +""" + +from typing import Callable +from ..plyparse import Goto, SubCall + + +def generate_goto(out: Callable, stmt: Goto) -> None: + pass # @todo + + +def generate_subcall(out: Callable, stmt: SubCall) -> None: + pass # @todo + diff --git a/il65/emit/generate.py b/il65/emit/generate.py new file mode 100644 index 000000000..cdb20ab0f --- /dev/null +++ b/il65/emit/generate.py @@ -0,0 +1,225 @@ +""" +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, 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
\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) + 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 "", 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 + print(subdef.scope.nodes) + for stmt in subdef.scope.nodes: + self.generate_statement(out, stmt) + self.cur_block = cur_block + out("") + out("; -- end block subroutines") + out("\n\v.pend\n") + + def generate_statement(self, out: Callable, stmt: AstNode) -> 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) + 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) diff --git a/il65/emit/incrdecr.py b/il65/emit/incrdecr.py new file mode 100644 index 000000000..8c658ae0b --- /dev/null +++ b/il65/emit/incrdecr.py @@ -0,0 +1,222 @@ +""" +Programming Language for 6502/6510 microprocessors, codename 'Sick' +This is the code generator for the in-place incr and decr instructions. + +Written by Irmen de Jong (irmen@razorvine.net) - license: GNU GPL 3.0 +""" + +from typing import Callable +from ..plyparse import Scope, AstNode, Register, IncrDecr, TargetRegisters, SymbolName, Dereference +from ..datatypes import DataType, REGISTER_BYTES +from . import CodeError, to_hex, preserving_registers + + +def datatype_of(node: AstNode, scope: Scope) -> DataType: + if isinstance(node, (Dereference, Register)): + return node.datatype + if isinstance(node, SymbolName): + symdef = scope[node.name] + + raise TypeError("cannot determine datatype", node) + + +def generate_incrdecr(out: Callable, stmt: IncrDecr) -> None: + assert isinstance(stmt.howmuch, (int, float)) and stmt.howmuch >= 0 + assert stmt.operator in ("++", "--") + target = stmt.target + if isinstance(target, TargetRegisters): + if len(target.registers) != 1: + raise CodeError("incr/decr can operate on one register at a time only") + target = target[0] + # target = Register/SymbolName/Dereference + if stmt.howmuch > 255: + if isinstance(stmt.target, TargetRegisters) + if stmt.what.datatype != DataType.FLOAT and not stmt.value.name and stmt.value.value > 0xff: + raise CodeError("only supports integer incr/decr by up to 255 for now") # XXX + howmuch = stmt.value.value + value_str = stmt.value.name or str(howmuch) + if isinstance(stmt.what, RegisterValue): + reg = stmt.what.register + # note: these operations below are all checked to be ok + if stmt.operator == "++": + if reg == 'A': + # a += 1..255 + out("\t\tclc") + out("\t\tadc #" + value_str) + elif reg in REGISTER_BYTES: + if howmuch == 1: + # x/y += 1 + out("\t\tin{:s}".format(reg.lower())) + else: + # x/y += 2..255 + with preserving_registers({'A'}): + out("\t\tt{:s}a".format(reg.lower())) + out("\t\tclc") + out("\t\tadc #" + value_str) + out("\t\tta{:s}".format(reg.lower())) + elif reg == "AX": + # AX += 1..255 + out("\t\tclc") + out("\t\tadc #" + value_str) + out("\t\tbcc +") + out("\t\tinx") + out("+") + elif reg == "AY": + # AY += 1..255 + out("\t\tclc") + out("\t\tadc # " + value_str) + out("\t\tbcc +") + out("\t\tiny") + out("+") + elif reg == "XY": + if howmuch == 1: + # XY += 1 + out("\t\tinx") + out("\t\tbne +") + out("\t\tiny") + out("+") + else: + # XY += 2..255 + with preserving_registers({'A'}): + out("\t\ttxa") + out("\t\tclc") + out("\t\tadc #" + value_str) + out("\t\ttax") + out("\t\tbcc +") + out("\t\tiny") + out("+") + else: + raise CodeError("invalid incr register: " + reg) + else: + if reg == 'A': + # a -= 1..255 + out("\t\tsec") + out("\t\tsbc #" + value_str) + elif reg in REGISTER_BYTES: + if howmuch == 1: + # x/y -= 1 + out("\t\tde{:s}".format(reg.lower())) + else: + # x/y -= 2..255 + with preserving_registers({'A'}): + out("\t\tt{:s}a".format(reg.lower())) + out("\t\tsec") + out("\t\tsbc #" + value_str) + out("\t\tta{:s}".format(reg.lower())) + elif reg == "AX": + # AX -= 1..255 + out("\t\tsec") + out("\t\tsbc #" + value_str) + out("\t\tbcs +") + out("\t\tdex") + out("+") + elif reg == "AY": + # AY -= 1..255 + out("\t\tsec") + out("\t\tsbc #" + value_str) + out("\t\tbcs +") + out("\t\tdey") + out("+") + elif reg == "XY": + if howmuch == 1: + # XY -= 1 + out("\t\tcpx #0") + out("\t\tbne +") + out("\t\tdey") + out("+\t\tdex") + else: + # XY -= 2..255 + with preserving_registers({'A'}): + out("\t\ttxa") + out("\t\tsec") + out("\t\tsbc #" + value_str) + out("\t\ttax") + out("\t\tbcs +") + out("\t\tdey") + out("+") + else: + raise CodeError("invalid decr register: " + reg) + elif isinstance(stmt.what, (MemMappedValue, IndirectValue)): + what = stmt.what + if isinstance(what, IndirectValue): + if isinstance(what.value, IntegerValue): + what_str = what.value.name or to_hex(what.value.value) + else: + raise CodeError("invalid incr indirect type", what.value) + else: + what_str = what.name or to_hex(what.address) + if what.datatype == DataType.BYTE: + if howmuch == 1: + out("\t\t{:s} {:s}".format("inc" if stmt.operator == "++" else "dec", what_str)) + else: + with preserving_registers({'A'}): + out("\t\tlda " + what_str) + if stmt.operator == "++": + out("\t\tclc") + out("\t\tadc #" + value_str) + else: + out("\t\tsec") + out("\t\tsbc #" + value_str) + out("\t\tsta " + what_str) + elif what.datatype == DataType.WORD: + if howmuch == 1: + # mem.word +=/-= 1 + if stmt.operator == "++": + out("\t\tinc " + what_str) + out("\t\tbne +") + out("\t\tinc {:s}+1".format(what_str)) + out("+") + else: + with preserving_registers({'A'}): + out("\t\tlda " + what_str) + out("\t\tbne +") + out("\t\tdec {:s}+1".format(what_str)) + out("+\t\tdec " + what_str) + else: + # mem.word +=/-= 2..255 + if stmt.operator == "++": + with preserving_registers({'A'}): + out("\t\tclc") + out("\t\tlda " + what_str) + out("\t\tadc #" + value_str) + out("\t\tsta " + what_str) + out("\t\tbcc +") + out("\t\tinc {:s}+1".format(what_str)) + out("+") + else: + with preserving_registers({'A'}): + out("\t\tsec") + out("\t\tlda " + what_str) + out("\t\tsbc #" + value_str) + out("\t\tsta " + what_str) + out("\t\tbcs +") + out("\t\tdec {:s}+1".format(what_str)) + out("+") + elif what.datatype == DataType.FLOAT: + if howmuch == 1.0: + # special case for +/-1 + with preserving_registers({'A', 'X', 'Y'}, loads_a_within=True): + out("\t\tldx #<" + what_str) + out("\t\tldy #>" + what_str) + if stmt.operator == "++": + out("\t\tjsr c64flt.float_add_one") + else: + out("\t\tjsr c64flt.float_sub_one") + elif stmt.value.name: + with preserving_registers({'A', 'X', 'Y'}, loads_a_within=True): + out("\t\tlda #<" + stmt.value.name) + out("\t\tsta c64.SCRATCH_ZPWORD1") + out("\t\tlda #>" + stmt.value.name) + out("\t\tsta c64.SCRATCH_ZPWORD1+1") + out("\t\tldx #<" + what_str) + out("\t\tldy #>" + what_str) + if stmt.operator == "++": + out("\t\tjsr c64flt.float_add_SW1_to_XY") + else: + out("\t\tjsr c64flt.float_sub_SW1_from_XY") + else: + raise CodeError("incr/decr missing float constant definition") + else: + raise CodeError("cannot in/decrement memory of type " + str(what.datatype), howmuch) + else: + raise CodeError("cannot in/decrement " + str(stmt.what)) diff --git a/il65/emit/variables.py b/il65/emit/variables.py new file mode 100644 index 000000000..9b4e51adb --- /dev/null +++ b/il65/emit/variables.py @@ -0,0 +1,259 @@ +""" +Programming Language for 6502/6510 microprocessors, codename 'Sick' +This is the code generator for variable declarations and initialization. + +Written by Irmen de Jong (irmen@razorvine.net) - license: GNU GPL 3.0 +""" + +from collections import defaultdict +from typing import Dict, List, Callable, Any +from ..plyparse import Block, VarType, VarDef +from ..datatypes import DataType, STRING_DATATYPES +from . import to_hex, to_mflpt5, CodeError + + +def generate_block_init(out: Callable, block: Block) -> None: + # generate the block initializer + # @todo add a block initializer subroutine that can contain custom reset/init code? (static initializer) + + def _memset(varname: str, value: int, size: int) -> None: + if size > 6: + out("\vlda #<" + varname) + out("\vsta il65_lib.SCRATCH_ZPWORD1") + out("\vlda #>" + varname) + out("\vsta il65_lib.SCRATCH_ZPWORD1+1") + out("\vlda #" + to_hex(value)) + out("\vldx #<" + to_hex(size)) + out("\vldy #>" + to_hex(size)) + out("\vjsr il65_lib.memset") + else: + out("\vlda #" + to_hex(value)) + for i in range(size): + out("\vsta {:s}+{:d}".format(varname, i)) + + def _memsetw(varname: str, value: int, size: int) -> None: + if size > 4: + out("\vlda #<" + varname) + out("\vsta il65_lib.SCRATCH_ZPWORD1") + out("\vlda #>" + varname) + out("\vsta il65_lib.SCRATCH_ZPWORD1+1") + out("\vlda #<" + to_hex(size)) + out("\vsta il65_lib.SCRATCH_ZPWORD2") + out("\vlda #>" + to_hex(size)) + out("\vsta il65_lib.SCRATCH_ZPWORD2+1") + out("\vlda #<" + to_hex(value)) + out("\vldx #>" + to_hex(value)) + out("\vjsr il65_lib.memsetw") + else: + out("\vlda #<" + to_hex(value)) + out("\vldy #>" + to_hex(value)) + for i in range(size): + out("\vsta {:s}+{:d}".format(varname, i * 2)) + out("\vsty {:s}+{:d}".format(varname, i * 2 + 1)) + + out("_il65_init_block\v; (re)set vars to initial values") + float_inits = {} + prev_value_a, prev_value_x = None, None + vars_by_datatype = defaultdict(list) # type: Dict[DataType, List[VarDef]] + for vardef in block.scope.filter_nodes(VarDef): + if vardef.vartype == VarType.VAR: + vars_by_datatype[vardef.datatype].append(vardef) + for bytevar in sorted(vars_by_datatype[DataType.BYTE], key=lambda vd: vd.value): + assert isinstance(bytevar.value, int) + if bytevar.value != prev_value_a: + out("\vlda #${:02x}".format(bytevar.value)) + prev_value_a = bytevar.value + out("\vsta {:s}".format(bytevar.name)) + for wordvar in sorted(vars_by_datatype[DataType.WORD], key=lambda vd: vd.value): + assert isinstance(wordvar.value, int) + v_hi, v_lo = divmod(wordvar.value, 256) + if v_hi != prev_value_a: + out("\vlda #${:02x}".format(v_hi)) + prev_value_a = v_hi + if v_lo != prev_value_x: + out("\vldx #${:02x}".format(v_lo)) + prev_value_x = v_lo + out("\vsta {:s}".format(wordvar.name)) + out("\vstx {:s}+1".format(wordvar.name)) + for floatvar in vars_by_datatype[DataType.FLOAT]: + assert isinstance(floatvar.value, (int, float)) + fpbytes = to_mflpt5(floatvar.value) # type: ignore + float_inits[floatvar.name] = (floatvar.name, fpbytes, floatvar.value) + for arrayvar in vars_by_datatype[DataType.BYTEARRAY]: + assert isinstance(arrayvar.value, int) + _memset(arrayvar.name, arrayvar.value, arrayvar.size[0]) + for arrayvar in vars_by_datatype[DataType.WORDARRAY]: + assert isinstance(arrayvar.value, int) + _memsetw(arrayvar.name, arrayvar.value, arrayvar.size[0]) + for arrayvar in vars_by_datatype[DataType.MATRIX]: + assert isinstance(arrayvar.value, int) + _memset(arrayvar.name, arrayvar.value, arrayvar.size[0] * arrayvar.size[1]) + if float_inits: + out("\vldx #4") + out("-") + for varname, (vname, b, fv) in sorted(float_inits.items()): + out("\vlda _init_float_{:s},x".format(varname)) + out("\vsta {:s},x".format(vname)) + out("\vdex") + out("\vbpl -") + out("\vrts\n") + for varname, (vname, fpbytes, fpvalue) in sorted(float_inits.items()): + out("_init_float_{:s}\t\t.byte ${:02x}, ${:02x}, ${:02x}, ${:02x}, ${:02x}\t; {}".format(varname, *fpbytes, fpvalue)) + all_string_vars = [] + for svtype in STRING_DATATYPES: + all_string_vars.extend(vars_by_datatype[svtype]) + for strvar in all_string_vars: + # string vars are considered to be a constant, and are statically initialized. + _generate_string_var(out, strvar) + out("") + + +def generate_block_vars(out: Callable, block: Block, zeropage: bool=False) -> None: + # Generate the block variable storage. + # The memory bytes of the allocated variables is set to zero (so it compresses very well), + # their actual starting values are set by the block init code. + vars_by_vartype = defaultdict(list) # type: Dict[VarType, List[VarDef]] + for vardef in block.scope.filter_nodes(VarDef): + vars_by_vartype[vardef.vartype].append(vardef) + out("; constants") + for vardef in vars_by_vartype.get(VarType.CONST, []): + if vardef.datatype == DataType.FLOAT: + out("\v{:s} = {}".format(vardef.name, _numeric_value_str(vardef.value))) + elif vardef.datatype in (DataType.BYTE, DataType.WORD): + out("\v{:s} = {:s}".format(vardef.name, _numeric_value_str(vardef.value, True))) + elif vardef.datatype.isstring(): + # a const string is just a string variable in the generated assembly + _generate_string_var(out, vardef) + else: + raise CodeError("invalid const type", vardef) + out("; memory mapped variables") + for vardef in vars_by_vartype.get(VarType.MEMORY, []): + # create a definition for variables at a specific place in memory (memory-mapped) + if vardef.datatype.isnumeric(): + assert vardef.size == [1] + out("\v{:s} = {:s}\t; {:s}".format(vardef.name, to_hex(vardef.value), vardef.datatype.name.lower())) + elif vardef.datatype == DataType.BYTEARRAY: + assert len(vardef.size) == 1 + out("\v{:s} = {:s}\t; array of {:d} bytes".format(vardef.name, to_hex(vardef.value), vardef.size[0])) + elif vardef.datatype == DataType.WORDARRAY: + assert len(vardef.size) == 1 + out("\v{:s} = {:s}\t; array of {:d} words".format(vardef.name, to_hex(vardef.value), vardef.size[0])) + elif vardef.datatype == DataType.MATRIX: + assert len(vardef.size) in (2, 3) + if len(vardef.size) == 2: + comment = "matrix of {:d} by {:d} = {:d} bytes".format(vardef.size[0], vardef.size[1], vardef.size[0]*vardef.size[1]) + elif len(vardef.size) == 3: + comment = "matrix of {:d} by {:d}, interleave {:d}".format(vardef.size[0], vardef.size[1], vardef.size[2]) + else: + raise CodeError("matrix size should be 2 or 3 numbers") + out("\v{:s} = {:s}\t; {:s}".format(vardef.name, to_hex(vardef.value), comment)) + else: + raise CodeError("invalid var type") + out("; normal variables - initial values will be set by init code") + if zeropage: + # zeropage uses the zp_address we've allocated, instead of allocating memory here + for vardef in vars_by_vartype.get(VarType.VAR, []): + assert vardef.zp_address is not None + if vardef.datatype.isstring(): + raise CodeError("cannot put strings in the zeropage", vardef.sourceref) + if vardef.datatype.isarray(): + size_str = "size " + str(vardef.size) + else: + size_str = "" + out("\v{:s} = {:s}\t; {:s} {:s}".format(vardef.name, to_hex(vardef.zp_address), vardef.datatype.name.lower(), size_str)) + else: + # create definitions for the variables that takes up empty space and will be initialized at startup + string_vars = [] + for vardef in vars_by_vartype.get(VarType.VAR, []): + if vardef.datatype.isnumeric(): + assert vardef.size == [1] + if vardef.datatype == DataType.BYTE: + out("{:s}\v.byte ?".format(vardef.name)) + elif vardef.datatype == DataType.WORD: + out("{:s}\v.word ?".format(vardef.name)) + elif vardef.datatype == DataType.FLOAT: + out("{:s}\v.fill 5\t\t; float".format(vardef.name)) + else: + raise CodeError("weird datatype") + elif vardef.datatype in (DataType.BYTEARRAY, DataType.WORDARRAY): + assert len(vardef.size) == 1 + if vardef.datatype == DataType.BYTEARRAY: + out("{:s}\v.fill {:d}\t\t; bytearray".format(vardef.name, vardef.size[0])) + elif vardef.datatype == DataType.WORDARRAY: + out("{:s}\v.fill {:d}*2\t\t; wordarray".format(vardef.name, vardef.size[0])) + else: + raise CodeError("invalid datatype", vardef.datatype) + elif vardef.datatype == DataType.MATRIX: + assert len(vardef.size) == 2 + out("{:s}\v.fill {:d}\t\t; matrix {:d}*{:d} bytes" + .format(vardef.name, vardef.size[0] * vardef.size[1], vardef.size[0], vardef.size[1])) + elif vardef.datatype.isstring(): + string_vars.append(vardef) + else: + raise CodeError("unknown variable type " + str(vardef.datatype)) + # string vars are considered to be a constant, and are not re-initialized. + out("") + + +def _generate_string_var(out: Callable, vardef: VarDef) -> None: + if vardef.datatype == DataType.STRING: + # 0-terminated string + out("{:s}\n\v.null {:s}".format(vardef.name, _format_string(str(vardef.value)))) + elif vardef.datatype == DataType.STRING_P: + # pascal string + out("{:s}\n\v.ptext {:s}".format(vardef.name, _format_string(str(vardef.value)))) + elif vardef.datatype == DataType.STRING_S: + # 0-terminated string in screencode encoding + out(".enc 'screen'") + out("{:s}\n\v.null {:s}".format(vardef.name, _format_string(str(vardef.value), True))) + out(".enc 'none'") + elif vardef.datatype == DataType.STRING_PS: + # 0-terminated pascal string in screencode encoding + out(".enc 'screen'") + out("{:s}n\v.ptext {:s}".format(vardef.name, _format_string(str(vardef.value), True))) + out(".enc 'none'") + + +def _format_string(value: str, screencodes: bool = False) -> str: + if len(value) == 1 and screencodes: + if value[0].isprintable() and ord(value[0]) < 128: + return "'{:s}'".format(value[0]) + else: + return str(ord(value[0])) + result = '"' + for char in value: + if char in "{}": + result += '", {:d}, "'.format(ord(char)) + elif char.isprintable() and ord(char) < 128: + result += char + else: + if screencodes: + result += '", {:d}, "'.format(ord(char)) + else: + if char == '\f': + result += "{clear}" + elif char == '\b': + result += "{delete}" + elif char == '\n': + result += "{cr}" + elif char == '\r': + result += "{down}" + elif char == '\t': + result += "{tab}" + else: + result += '", {:d}, "'.format(ord(char)) + return result + '"' + + +def _numeric_value_str(value: Any, as_hex: bool=False) -> str: + if isinstance(value, bool): + return "1" if value else "0" + if isinstance(value, int): + if as_hex: + return to_hex(value) + return str(value) + if isinstance(value, (int, float)): + if as_hex: + raise TypeError("cannot output float as hex") + return str(value) + raise TypeError("no numeric representation possible", value) diff --git a/il65/generateasm.py b/il65/generateasm.py deleted file mode 100644 index 900e43bc2..000000000 --- a/il65/generateasm.py +++ /dev/null @@ -1,499 +0,0 @@ -""" -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 re -import subprocess -import datetime -from collections import defaultdict -from typing import Dict, TextIO, List, Any -from .plylex import print_bold -from .plyparse import Module, ProgramFormat, Block, Directive, VarDef, Label, Subroutine, AstNode, ZpOptions, \ - InlineAssembly, Return, Register, LiteralValue -from .datatypes import VarType, DataType, to_hex, to_mflpt5, STRING_DATATYPES - - -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.cur_block = None - self.output = None # type: TextIO - - 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.output, **vargs) - - def generate(self, filename: str) -> None: - with open(filename, "wt") as self.output: - try: - self._generate() - except Exception as x: - self.output.write(".error \"****** ABORTED DUE TO ERROR: " + str(x) + "\"\n") - raise - - def _generate(self) -> None: - self.sanitycheck() - self.header() - self.init_vars_and_start() - self.blocks() - self.footer() - - def sanitycheck(self): - start_found = False - for block, parent in self.module.all_scopes(): - 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): - 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_entrypoint), $3a, $8f, ' il65 by idj'") - self.p("+\v.word 0") - self.p("_il65_entrypoint\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 init_vars_and_start(self) -> None: - if self.module.zp_options == ZpOptions.CLOBBER_RESTORE: - self.p("\vjsr _il65_save_zeropage") - self.p("\v; initialize all blocks (reset vars)") - if self.module.zeropage(): - self.p("\vjsr ZP._il65_init_block") - for block in self.module.nodes: - if isinstance(block, Block) and block.name != "ZP": - self.p("\vjsr {}._il65_init_block".format(block.name)) - self.p("\v; call user code") - if self.module.zp_options == ZpOptions.CLOBBER_RESTORE: - self.p("\vjsr {:s}.start".format(self.module.main().label)) - self.p("\vcld") - self.p("\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(): - self.p(line.rstrip("\n")) - else: - self.p("\vjmp {:s}.start".format(self.module.main().label)) - self.p("") - - def blocks(self) -> None: - zpblock = self.module.zeropage() - if zpblock: - # if there's a Zeropage block, it always goes first - self.cur_block = zpblock # type: ignore - self.p("\n; ---- zero page block: '{:s}' ----".format(zpblock.name)) - self.p("; file: '{:s}' src l. {:d}\n".format(zpblock.sourceref.file, zpblock.sourceref.line)) - self.p("{:s}\t.proc\n".format(zpblock.label)) - self.generate_block_init(zpblock) - self.generate_block_vars(zpblock, True) - # there's no code in the zero page block. - self.p("\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 - self.p("\n; ---- block: '{:s}' ----".format(block.name)) - self.p("; file: '{:s}' src l. {:d}\n".format(block.sourceref.file, block.sourceref.line)) - if block.address: - self.p(".cerror * > ${0:04x}, 'block address overlaps by ', *-${0:04x},' bytes'".format(block.address)) - self.p("* = ${:04x}".format(block.address)) - self.p("{:s}\t.proc\n".format(block.label)) - self.generate_block_init(block) - self.generate_block_vars(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 - self.p("; external subroutines") - for subdef in subroutines: - assert subdef.scope is None - self.p("\v{:s} = {:s}".format(subdef.name, to_hex(subdef.address))) - self.p("; end external subroutines\n") - for stmt in block.scope.nodes: - if isinstance(stmt, (VarDef, Directive, Subroutine)): - continue # should have been handled already or will be later - self.generate_statement(stmt) - 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 - self.p("\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 - self.p("; -- block subroutines") - for subdef in subroutines: - assert subdef.scope is not None - self.p("{: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] == '?')) - self.p("\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: - if isinstance(stmt, (VarDef, Directive)): - continue # should have been handled already - self.generate_statement(stmt) - self.cur_block = cur_block - self.p("") - self.p("; -- end block subroutines") - self.p("\n\v.pend\n") - - def footer(self) -> None: - self.p("\t.end") - - def output_string(self, value: str, screencodes: bool = False) -> str: - if len(value) == 1 and screencodes: - if value[0].isprintable() and ord(value[0]) < 128: - return "'{:s}'".format(value[0]) - else: - return str(ord(value[0])) - result = '"' - for char in value: - if char in "{}": - result += '", {:d}, "'.format(ord(char)) - elif char.isprintable() and ord(char) < 128: - result += char - else: - if screencodes: - result += '", {:d}, "'.format(ord(char)) - else: - if char == '\f': - result += "{clear}" - elif char == '\b': - result += "{delete}" - elif char == '\n': - result += "{cr}" - elif char == '\r': - result += "{down}" - elif char == '\t': - result += "{tab}" - else: - result += '", {:d}, "'.format(ord(char)) - return result + '"' - - def generate_block_init(self, block: Block) -> None: - # generate the block initializer - # @todo add a block initializer subroutine that can contain custom reset/init code? (static initializer) - - def _memset(varname: str, value: int, size: int) -> None: - if size > 6: - self.p("\vlda #<" + varname) - self.p("\vsta il65_lib.SCRATCH_ZPWORD1") - self.p("\vlda #>" + varname) - self.p("\vsta il65_lib.SCRATCH_ZPWORD1+1") - self.p("\vlda #" + to_hex(value)) - self.p("\vldx #<" + to_hex(size)) - self.p("\vldy #>" + to_hex(size)) - self.p("\vjsr il65_lib.memset") - else: - self.p("\vlda #" + to_hex(value)) - for i in range(size): - self.p("\vsta {:s}+{:d}".format(varname, i)) - - def _memsetw(varname: str, value: int, size: int) -> None: - if size > 4: - self.p("\vlda #<" + varname) - self.p("\vsta il65_lib.SCRATCH_ZPWORD1") - self.p("\vlda #>" + varname) - self.p("\vsta il65_lib.SCRATCH_ZPWORD1+1") - self.p("\vlda #<" + to_hex(size)) - self.p("\vsta il65_lib.SCRATCH_ZPWORD2") - self.p("\vlda #>" + to_hex(size)) - self.p("\vsta il65_lib.SCRATCH_ZPWORD2+1") - self.p("\vlda #<" + to_hex(value)) - self.p("\vldx #>" + to_hex(value)) - self.p("\vjsr il65_lib.memsetw") - else: - self.p("\vlda #<" + to_hex(value)) - self.p("\vldy #>" + to_hex(value)) - for i in range(size): - self.p("\vsta {:s}+{:d}".format(varname, i*2)) - self.p("\vsty {:s}+{:d}".format(varname, i*2+1)) - - self.p("_il65_init_block\v; (re)set vars to initial values") - float_inits = {} - prev_value_a, prev_value_x = None, None - vars_by_datatype = defaultdict(list) # type: Dict[DataType, List[VarDef]] - for vardef in block.scope.filter_nodes(VarDef): - if vardef.vartype == VarType.VAR: - vars_by_datatype[vardef.datatype].append(vardef) - for bytevar in sorted(vars_by_datatype[DataType.BYTE], key=lambda vd: vd.value): - assert isinstance(bytevar.value, int) - if bytevar.value != prev_value_a: - self.p("\vlda #${:02x}".format(bytevar.value)) - prev_value_a = bytevar.value - self.p("\vsta {:s}".format(bytevar.name)) - for wordvar in sorted(vars_by_datatype[DataType.WORD], key=lambda vd: vd.value): - assert isinstance(wordvar.value, int) - v_hi, v_lo = divmod(wordvar.value, 256) - if v_hi != prev_value_a: - self.p("\vlda #${:02x}".format(v_hi)) - prev_value_a = v_hi - if v_lo != prev_value_x: - self.p("\vldx #${:02x}".format(v_lo)) - prev_value_x = v_lo - self.p("\vsta {:s}".format(wordvar.name)) - self.p("\vstx {:s}+1".format(wordvar.name)) - for floatvar in vars_by_datatype[DataType.FLOAT]: - assert isinstance(floatvar.value, (int, float)) - fpbytes = to_mflpt5(floatvar.value) # type: ignore - float_inits[floatvar.name] = (floatvar.name, fpbytes, floatvar.value) - for arrayvar in vars_by_datatype[DataType.BYTEARRAY]: - assert isinstance(arrayvar.value, int) - _memset(arrayvar.name, arrayvar.value, arrayvar.size[0]) - for arrayvar in vars_by_datatype[DataType.WORDARRAY]: - assert isinstance(arrayvar.value, int) - _memsetw(arrayvar.name, arrayvar.value, arrayvar.size[0]) - for arrayvar in vars_by_datatype[DataType.MATRIX]: - assert isinstance(arrayvar.value, int) - _memset(arrayvar.name, arrayvar.value, arrayvar.size[0] * arrayvar.size[1]) - if float_inits: - self.p("\vldx #4") - self.p("-") - for varname, (vname, b, fv) in sorted(float_inits.items()): - self.p("\vlda _init_float_{:s},x".format(varname)) - self.p("\vsta {:s},x".format(vname)) - self.p("\vdex") - self.p("\vbpl -") - self.p("\vrts\n") - for varname, (vname, fpbytes, fpvalue) in sorted(float_inits.items()): - self.p("_init_float_{:s}\t\t.byte ${:02x}, ${:02x}, ${:02x}, ${:02x}, ${:02x}\t; {}".format(varname, *fpbytes, fpvalue)) - all_string_vars = [] - for svtype in STRING_DATATYPES: - all_string_vars.extend(vars_by_datatype[svtype]) - for strvar in all_string_vars: - # string vars are considered to be a constant, and are statically initialized. - self._generate_string_var(strvar) - self.p("") - - def _numeric_value_str(self, value: Any, as_hex: bool=False) -> str: - if isinstance(value, bool): - return "1" if value else "0" - if isinstance(value, int): - if as_hex: - return to_hex(value) - return str(value) - if isinstance(value, (int, float)): - if as_hex: - raise TypeError("cannot output float as hex") - return str(value) - raise TypeError("no numeric representation possible", value) - - def generate_block_vars(self, block: Block, zeropage: bool=False) -> None: - # Generate the block variable storage. - # The memory bytes of the allocated variables is set to zero (so it compresses very well), - # their actual starting values are set by the block init code. - vars_by_vartype = defaultdict(list) # type: Dict[VarType, List[VarDef]] - for vardef in block.scope.filter_nodes(VarDef): - vars_by_vartype[vardef.vartype].append(vardef) - self.p("; constants") - for vardef in vars_by_vartype.get(VarType.CONST, []): - if vardef.datatype == DataType.FLOAT: - self.p("\v{:s} = {}".format(vardef.name, self._numeric_value_str(vardef.value))) - elif vardef.datatype in (DataType.BYTE, DataType.WORD): - self.p("\v{:s} = {:s}".format(vardef.name, self._numeric_value_str(vardef.value, True))) - elif vardef.datatype.isstring(): - # a const string is just a string variable in the generated assembly - self._generate_string_var(vardef) - else: - raise CodeError("invalid const type", vardef) - self.p("; memory mapped variables") - for vardef in vars_by_vartype.get(VarType.MEMORY, []): - # create a definition for variables at a specific place in memory (memory-mapped) - if vardef.datatype.isnumeric(): - assert vardef.size == [1] - self.p("\v{:s} = {:s}\t; {:s}".format(vardef.name, to_hex(vardef.value), vardef.datatype.name.lower())) - elif vardef.datatype == DataType.BYTEARRAY: - assert len(vardef.size) == 1 - self.p("\v{:s} = {:s}\t; array of {:d} bytes".format(vardef.name, to_hex(vardef.value), vardef.size[0])) - elif vardef.datatype == DataType.WORDARRAY: - assert len(vardef.size) == 1 - self.p("\v{:s} = {:s}\t; array of {:d} words".format(vardef.name, to_hex(vardef.value), vardef.size[0])) - elif vardef.datatype == DataType.MATRIX: - assert len(vardef.size) in (2, 3) - if len(vardef.size) == 2: - comment = "matrix of {:d} by {:d} = {:d} bytes".format(vardef.size[0], vardef.size[1], vardef.size[0]*vardef.size[1]) - elif len(vardef.size) == 3: - comment = "matrix of {:d} by {:d}, interleave {:d}".format(vardef.size[0], vardef.size[1], vardef.size[2]) - else: - raise CodeError("matrix size should be 2 or 3 numbers") - self.p("\v{:s} = {:s}\t; {:s}".format(vardef.name, to_hex(vardef.value), comment)) - else: - raise CodeError("invalid var type") - self.p("; normal variables - initial values will be set by init code") - if zeropage: - # zeropage uses the zp_address we've allocated, instead of allocating memory here - for vardef in vars_by_vartype.get(VarType.VAR, []): - assert vardef.zp_address is not None - if vardef.datatype.isstring(): - raise CodeError("cannot put strings in the zeropage", vardef.sourceref) - if vardef.datatype.isarray(): - size_str = "size " + str(vardef.size) - else: - size_str = "" - self.p("\v{:s} = {:s}\t; {:s} {:s}".format(vardef.name, to_hex(vardef.zp_address), - vardef.datatype.name.lower(), size_str)) - else: - # create definitions for the variables that takes up empty space and will be initialized at startup - string_vars = [] - for vardef in vars_by_vartype.get(VarType.VAR, []): - if vardef.datatype.isnumeric(): - assert vardef.size == [1] - if vardef.datatype == DataType.BYTE: - self.p("{:s}\v.byte ?".format(vardef.name)) - elif vardef.datatype == DataType.WORD: - self.p("{:s}\v.word ?".format(vardef.name)) - elif vardef.datatype == DataType.FLOAT: - self.p("{:s}\v.fill 5\t\t; float".format(vardef.name)) - else: - raise CodeError("weird datatype") - elif vardef.datatype in (DataType.BYTEARRAY, DataType.WORDARRAY): - assert len(vardef.size) == 1 - if vardef.datatype == DataType.BYTEARRAY: - self.p("{:s}\v.fill {:d}\t\t; bytearray".format(vardef.name, vardef.size[0])) - elif vardef.datatype == DataType.WORDARRAY: - self.p("{:s}\v.fill {:d}*2\t\t; wordarray".format(vardef.name, vardef.size[0])) - else: - raise CodeError("invalid datatype", vardef.datatype) - elif vardef.datatype == DataType.MATRIX: - assert len(vardef.size) == 2 - self.p("{:s}\v.fill {:d}\t\t; matrix {:d}*{:d} bytes" - .format(vardef.name, vardef.size[0] * vardef.size[1], vardef.size[0], vardef.size[1])) - elif vardef.datatype.isstring(): - string_vars.append(vardef) - else: - raise CodeError("unknown variable type " + str(vardef.datatype)) - # string vars are considered to be a constant, and are not re-initialized. - self.p("") - - def _generate_string_var(self, vardef: VarDef) -> None: - if vardef.datatype == DataType.STRING: - # 0-terminated string - self.p("{:s}\n\v.null {:s}".format(vardef.name, self.output_string(str(vardef.value)))) - elif vardef.datatype == DataType.STRING_P: - # pascal string - self.p("{:s}\n\v.ptext {:s}".format(vardef.name, self.output_string(str(vardef.value)))) - elif vardef.datatype == DataType.STRING_S: - # 0-terminated string in screencode encoding - self.p(".enc 'screen'") - self.p("{:s}\n\v.null {:s}".format(vardef.name, self.output_string(str(vardef.value), True))) - self.p(".enc 'none'") - elif vardef.datatype == DataType.STRING_PS: - # 0-terminated pascal string in screencode encoding - self.p(".enc 'screen'") - self.p("{:s}n\v.ptext {:s}".format(vardef.name, self.output_string(str(vardef.value), True))) - self.p(".enc 'none'") - - def generate_statement(self, stmt: AstNode) -> None: - if isinstance(stmt, Label): - self.p("\n{:s}\v\t\t; {:s}".format(stmt.name, stmt.lineref)) - elif isinstance(stmt, Return): - if stmt.value_A: - self.generate_assignment(Register(name="A", sourceref=stmt.sourceref), '=', stmt.value_A) # type: ignore - if stmt.value_X: - self.generate_assignment(Register(name="X", sourceref=stmt.sourceref), '=', stmt.value_X) # type: ignore - if stmt.value_Y: - self.generate_assignment(Register(name="Y", sourceref=stmt.sourceref), '=', stmt.value_Y) # type: ignore - self.p("\vrts") - elif isinstance(stmt, InlineAssembly): - self.p("\n\v; inline asm, " + stmt.lineref) - self.p(stmt.assembly) - self.p("\v; end inline asm, " + stmt.lineref + "\n") - else: - self.p("\vrts; " + str(stmt)) # @todo rest of the statement nodes - - def generate_assignment(self, lvalue: AstNode, operator: str, rvalue: Any) -> None: - assert rvalue is not None - if isinstance(rvalue, LiteralValue): - rvalue = rvalue.value - print("ASSIGN", lvalue, lvalue.datatype, operator, rvalue) # @todo - - -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 CodeError("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 diff --git a/il65/main.py b/il65/main.py index 1060f464b..8bea70a30 100644 --- a/il65/main.py +++ b/il65/main.py @@ -7,12 +7,57 @@ Written by Irmen de Jong (irmen@razorvine.net) - license: GNU GPL 3.0 import time import os +import re import argparse import subprocess from .compile import PlyParser from .optimize import optimize -from .generateasm import AssemblyGenerator, Assembler64Tass +from .emit.generate import AssemblyGenerator from .plylex import print_bold +from .plyparse import ProgramFormat + + +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 code 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 def main() -> None: diff --git a/il65/oldstuff/codegen.py b/il65/oldstuff/codegen.py index c09a460ea..50e9207ea 100644 --- a/il65/oldstuff/codegen.py +++ b/il65/oldstuff/codegen.py @@ -1,396 +1,9 @@ -""" -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 datetime -import subprocess -import contextlib -from functools import partial -from typing import TextIO, Callable -from .parse import ProgramFormat, ParseResult, Parser -from .symbols import * - - -class CodeError(Exception): - pass - +# old deprecated code class CodeGenerator: BREAKPOINT_COMMENT_SIGNATURE = "~~~BREAKPOINT~~~" BREAKPOINT_COMMENT_DETECTOR = r".(?P
\w+)\s+ea\s+nop\s+;\s+{:s}.*".format(BREAKPOINT_COMMENT_SIGNATURE) - def __init__(self, parsed: ParseResult) -> None: - self.parsed = parsed - self.generated_code = io.StringIO() - self.p = partial(print, file=self.generated_code) - self.previous_stmt_was_assignment = False - self.cur_block = None # type: Block - - def generate(self) -> None: - print("\ngenerating assembly code") - self.sanitycheck() - self.header() - self.initialize_variables() - self.blocks() - self.footer() - - def sanitycheck(self) -> None: - # duplicate block names? - all_blocknames = [b.name for b in self.parsed.blocks if b.name] - 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) - # ZP block contains no code? - for zpblock in [b for b in self.parsed.blocks if b.name == "ZP"]: - if zpblock.label_names: - raise CodeError("ZP block cannot contain labels") - # can only contain code comments, or nothing at all - if not all(isinstance(s, Comment) for s in zpblock.statements): - raise CodeError("ZP block cannot contain code statements, only definitions and comments") - - def optimize(self) -> None: - # optimize the generated assembly code - pass - - def write_assembly(self, out: TextIO) -> None: - out.write(self.generated_code.getvalue()) - - def header(self) -> None: - self.p("; code generated by il65.py - codename 'Sick'") - self.p("; source file:", self.parsed.sourcefile) - if self.parsed.with_sys: - self.p("; output format:", self.parsed.format.value, " (with basic program SYS)") - else: - self.p("; output format:", self.parsed.format.value) - self.p("; assembler syntax is for 64tasm") - self.p(".cpu '6502'\n.enc 'none'\n") - if self.parsed.format == ProgramFormat.PRG: - if self.parsed.with_sys: - self.p("; ---- basic program with sys call ----") - self.p("* = " + Parser.to_hex(self.parsed.start_address)) - year = datetime.datetime.now().year - self.p("\t\t.word (+), {:d}".format(year)) - self.p("\t\t.null $9e, format(' %d ', _il65_sysaddr), $3a, $8f, ' il65 by idj'") - self.p("+\t\t.word 0") - self.p("_il65_sysaddr\t\t; assembly code starts here\n") - else: - self.p("; ---- program without sys call ----") - self.p("* = " + Parser.to_hex(self.parsed.start_address) + "\n") - if self.parsed.format == ProgramFormat.RAW: - self.p("; ---- raw assembler program ----") - self.p("* = " + Parser.to_hex(self.parsed.start_address) + "\n") - - @staticmethod - def to_mflpt5(number: float) -> bytearray: - # algorithm here https://sourceforge.net/p/acme-crossass/code-0/62/tree/trunk/ACME_Lib/cbm/mflpt.a - number = float(number) - if number < FLOAT_MAX_NEGATIVE or number > FLOAT_MAX_POSITIVE: - raise OverflowError("floating point number out of 5-byte mflpt range", number) - if number == 0.0: - return bytearray([0, 0, 0, 0, 0]) - if number < 0.0: - sign = 0x80000000 - number = -number - else: - sign = 0x00000000 - mant, exp = math.frexp(number) - exp += 128 - if exp < 1: - # underflow, use zero instead - return bytearray([0, 0, 0, 0, 0]) - if exp > 255: - raise OverflowError("floating point number out of 5-byte mflpt range", number) - mant = sign | int(mant * 0x100000000) & 0x7fffffff - return bytearray([exp]) + int.to_bytes(mant, 4, "big") - - @staticmethod - def mflpt5_to_float(mflpt: bytearray) -> float: - if mflpt == bytearray([0, 0, 0, 0, 0]): - return 0.0 - exp = mflpt[0] - 128 - sign = mflpt[1] & 0x80 - number = 0x80000000 | int.from_bytes(mflpt[1:], "big") - number = float(number) * 2**exp / 0x100000000 - return -number if sign else number - - def initialize_variables(self) -> None: - must_save_zp = self.parsed.clobberzp and self.parsed.restorezp - if must_save_zp: - self.p("\t\tjsr 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. - zpblocks = [b for b in self.parsed.blocks if b.name == "ZP"] - if zpblocks: - assert len(zpblocks) == 1 - zpblock = zpblocks[0] - vars_to_init = [v for v in zpblock.symbols.iter_variables() - 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("\t\tlda #0\n\t\tldx #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("\t\tlda #${:02x}".format(vvalue)) - prev_value = vvalue - self.p("\t\tsta {:s}".format(vname)) - elif variable.type == DataType.WORD: - if vvalue != prev_value: - self.p("\t\tlda #<${:04x}".format(vvalue)) - self.p("\t\tldx #>${:04x}".format(vvalue)) - prev_value = vvalue - self.p("\t\tsta {:s}".format(vname)) - self.p("\t\tstx {: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("\t\tldx #4") - self.p("-") - for varname, (vname, b, fv) in zp_float_bytes.items(): - self.p("\t\tlda _float_bytes_{:s},x".format(varname)) - self.p("\t\tsta {:s},x".format(vname)) - self.p("\t\tdex") - self.p("\t\tbpl -") - self.p("; end init zp vars") - else: - self.p("\t\t; there are no zp vars to initialize") - else: - self.p("\t\t; there is no zp block to initialize") - main_block_label = [b.label for b in self.parsed.blocks if b.name == "main"][0] - if must_save_zp: - self.p("\t\tjsr {:s}.start\t\t; call user code".format(main_block_label)) - self.p("\t\tcld") - self.p("\t\tjmp il65_lib_zp.restore_zeropage") - else: - self.p("\t\tjmp {:s}.start\t\t; call user code".format(main_block_label)) - self.p("") - for varname, (vname, bytes, fpvalue) in zp_float_bytes.items(): - self.p("_float_bytes_{:s}\t\t.byte ${:02x}, ${:02x}, ${:02x}, ${:02x}, ${:02x}\t; {}".format(varname, *bytes, fpvalue)) - self.p("\n") - - def blocks(self) -> None: - # if there's a
block, it always goes second - for block in [b for b in self.parsed.blocks if b.name == "
"]: - self.cur_block = block - for s in block.statements: - if isinstance(s, Comment): - self.p(s.text) - else: - raise CodeError("header block cannot contain any other statements beside comments") - self.p("\n") - # if there's a Zeropage block, it always goes second - for zpblock in [b for b in self.parsed.blocks if b.name == "ZP"]: - self.cur_block = zpblock - self.p("\n; ---- zero page block: '{:s}' ----\t\t; src l. {:d}\n".format(zpblock.sourceref.file, zpblock.sourceref.line)) - for s in zpblock.statements: - if isinstance(s, Comment): - self.p(s.text) - else: - raise CodeError("zp cannot contain any other statements beside comments") - self.p("{:s}\t.proc\n".format(zpblock.label)) - self.generate_block_vars(zpblock) - self.p("\t.pend\n") - # make sure the main.start routine clears the decimal and carry flags as first steps - block = self.parsed.find_block("main") - statements = list(block.statements) - for index, stmt in enumerate(statements): - if isinstance(stmt, Label) and stmt.name == "start": - asmlines = [ - "\t\tcld\t\t\t; clear decimal flag", - "\t\tclc\t\t\t; clear carry flag", - "\t\tclv\t\t\t; clear overflow flag", - ] - statements.insert(index+1, InlineAsm(asmlines, stmt.sourceref)) - break - block.statements = statements - # generate - for block in sorted(self.parsed.blocks, key=lambda b: b.address): - if block.name in ("ZP", "
"): - continue # these blocks are already processed - self.cur_block = block - self.p("\n; ---- next block: '{:s}' ----\t\t; src l. {:d}\n".format(block.sourceref.file, block.sourceref.line)) - if block.address: - self.p(".cerror * > ${0:04x}, 'block address overlaps by ', *-${0:04x},' bytes'".format(block.address)) - self.p("* = ${:04x}".format(block.address)) - self.p("{:s}\t.proc\n".format(block.label)) - self.generate_block_vars(block) - subroutines = list(sub for sub in block.symbols.iter_subroutines() if sub.address is not None) - if subroutines: - self.p("\n; external subroutines") - for subdef in subroutines: - assert subdef.sub_block is None - self.p("\t\t{:s} = {:s}".format(subdef.name, Parser.to_hex(subdef.address))) - self.p("; end external subroutines") - for stmt in block.statements: - self.generate_statement(stmt) - subroutines = list(sub for sub in block.symbols.iter_subroutines(True)) - if subroutines: - self.p("\n; block subroutines") - for subdef in subroutines: - assert subdef.sub_block is not None - self.p("{:s}\t\t; src l. {:d}".format(subdef.name, subdef.sourceref.line)) - params = ", ".join("{:s} -> {:s}".format(p[0] or "", p[1]) for p in subdef.parameters) - returns = ",".join(sorted(subdef.return_registers)) - clobbers = ",".join(sorted(subdef.clobbered_registers)) - self.p("\t\t; params: {}\n\t\t; returns: {} clobbers: {}" - .format(params or "-", returns or "-", clobbers or "-")) - cur_block = self.cur_block - self.cur_block = subdef.sub_block - for stmt in subdef.sub_block.statements: - self.generate_statement(stmt) - self.cur_block = cur_block - self.p("") - self.p("; end external subroutines") - self.p("\t.pend\n") - - def generate_block_vars(self, block: Block) -> None: - consts = [c for c in block.symbols.iter_constants()] - if consts: - self.p("; constants") - for constdef in consts: - if constdef.type == DataType.FLOAT: - self.p("\t\t{:s} = {}".format(constdef.name, constdef.value)) - elif constdef.type in (DataType.BYTE, DataType.WORD): - self.p("\t\t{:s} = {:s}".format(constdef.name, Parser.to_hex(constdef.value))) # type: ignore - elif constdef.type in STRING_DATATYPES: - # a const string is just a string variable in the generated assembly - self._generate_string_var(constdef) - else: - raise CodeError("invalid const type", constdef) - mem_vars = [vi for vi in block.symbols.iter_variables() if not vi.allocate and not vi.register] - if mem_vars: - self.p("; memory mapped variables") - for vardef in mem_vars: - # create a definition for variables at a specific place in memory (memory-mapped) - if vardef.type in (DataType.BYTE, DataType.WORD, DataType.FLOAT): - self.p("\t\t{:s} = {:s}\t; {:s}".format(vardef.name, Parser.to_hex(vardef.address), vardef.type.name.lower())) - elif vardef.type == DataType.BYTEARRAY: - self.p("\t\t{:s} = {:s}\t; array of {:d} bytes".format(vardef.name, Parser.to_hex(vardef.address), vardef.length)) - elif vardef.type == DataType.WORDARRAY: - self.p("\t\t{:s} = {:s}\t; array of {:d} words".format(vardef.name, Parser.to_hex(vardef.address), vardef.length)) - elif vardef.type == DataType.MATRIX: - self.p("\t\t{:s} = {:s}\t; matrix {:d} by {:d} = {:d} bytes" - .format(vardef.name, Parser.to_hex(vardef.address), vardef.matrixsize[0], vardef.matrixsize[1], vardef.length)) - else: - raise CodeError("invalid var type") - non_mem_vars = [vi for vi in block.symbols.iter_variables() if vi.allocate] - if non_mem_vars: - self.p("; normal variables") - for vardef in non_mem_vars: - # create a definition for a variable that takes up space and will be initialized at startup - sourcecomment = "\t; " + vardef.sourcecomment if vardef.sourcecomment else "" - if vardef.type in (DataType.BYTE, DataType.WORD, DataType.FLOAT): - if vardef.address: - assert block.name == "ZP", "only ZP-variables can be put on an address" - self.p("\t\t{:s} = {:s}".format(vardef.name, Parser.to_hex(vardef.address))) - else: - if vardef.type == DataType.BYTE: - self.p("{:s}\t\t.byte {:s}{:s}".format(vardef.name, Parser.to_hex(int(vardef.value)), sourcecomment)) - elif vardef.type == DataType.WORD: - self.p("{:s}\t\t.word {:s}{:s}".format(vardef.name, Parser.to_hex(int(vardef.value)), sourcecomment)) - elif vardef.type == DataType.FLOAT: - self.p("{:s}\t\t.byte ${:02x}, ${:02x}, ${:02x}, ${:02x}, ${:02x}{:s}" - .format(vardef.name, *self.to_mflpt5(float(vardef.value)), sourcecomment)) - else: - raise CodeError("weird datatype") - elif vardef.type in (DataType.BYTEARRAY, DataType.WORDARRAY): - if vardef.address: - raise CodeError("array or wordarray vars must not have address; will be allocated by assembler") - if vardef.type == DataType.BYTEARRAY: - self.p("{:s}\t\t.fill {:d}, ${:02x}{:s}".format(vardef.name, vardef.length, vardef.value or 0, sourcecomment)) - elif vardef.type == DataType.WORDARRAY: - f_hi, f_lo = divmod(vardef.value or 0, 256) # type: ignore - self.p("{:s}\t\t.fill {:d}, [${:02x}, ${:02x}]\t; {:d} words of ${:04x}" - .format(vardef.name, vardef.length * 2, f_lo, f_hi, vardef.length, vardef.value or 0)) - else: - raise CodeError("invalid datatype", vardef.type) - elif vardef.type == DataType.MATRIX: - if vardef.address: - raise CodeError("matrix vars must not have address; will be allocated by assembler") - self.p("{:s}\t\t.fill {:d}, ${:02x}\t\t; matrix {:d}*{:d} bytes" - .format(vardef.name, - vardef.matrixsize[0] * vardef.matrixsize[1], - vardef.value or 0, - vardef.matrixsize[0], vardef.matrixsize[1])) - elif vardef.type in STRING_DATATYPES: - self._generate_string_var(vardef) - else: - raise CodeError("unknown variable type " + str(vardef.type)) - - def _generate_string_var(self, vardef: Union[ConstantDef, VariableDef]) -> None: - if vardef.type == DataType.STRING: - # 0-terminated string - self.p("{:s}\n\t\t.null {:s}".format(vardef.name, self.output_string(str(vardef.value)))) - elif vardef.type == DataType.STRING_P: - # pascal string - self.p("{:s}\n\t\t.ptext {:s}".format(vardef.name, self.output_string(str(vardef.value)))) - elif vardef.type == DataType.STRING_S: - # 0-terminated string in screencode encoding - self.p(".enc 'screen'") - self.p("{:s}\n\t\t.null {:s}".format(vardef.name, self.output_string(str(vardef.value), True))) - self.p(".enc 'none'") - elif vardef.type == DataType.STRING_PS: - # 0-terminated pascal string in screencode encoding - self.p(".enc 'screen'") - self.p("{:s}\n\t\t.ptext {:s}".format(vardef.name, self.output_string(str(vardef.value), True))) - self.p(".enc 'none'") - - def generate_statement(self, stmt: AstNode) -> None: - if isinstance(stmt, ReturnStmt): - if stmt.a: - if isinstance(stmt.a, IntegerValue): - self.p("\t\tlda #{:d}".format(stmt.a.value)) - else: - raise CodeError("can only return immediate values for now") # XXX - if stmt.x: - if isinstance(stmt.x, IntegerValue): - self.p("\t\tldx #{:d}".format(stmt.x.value)) - else: - raise CodeError("can only return immediate values for now") # XXX - if stmt.y: - if isinstance(stmt.y, IntegerValue): - self.p("\t\tldy #{:d}".format(stmt.y.value)) - else: - raise CodeError("can only return immediate values for now") # XXX - self.p("\t\trts") - elif isinstance(stmt, AugmentedAssignmentStmt): - self.generate_augmented_assignment(stmt) - elif isinstance(stmt, AssignmentStmt): - self.generate_assignment(stmt) - elif isinstance(stmt, Label): - self.p("\n{:s}\t\t\t\t; {:s}".format(stmt.name, stmt.lineref)) - elif isinstance(stmt, (InplaceIncrStmt, InplaceDecrStmt)): - self.generate_incr_or_decr(stmt) - elif isinstance(stmt, CallStmt): - self.generate_call(stmt) - elif isinstance(stmt, InlineAsm): - self.p("\t\t; inline asm, " + stmt.lineref) - for line in stmt.asmlines: - self.p(line) - self.p("\t\t; end inline asm, " + stmt.lineref) - elif isinstance(stmt, Comment): - self.p(stmt.text) - elif isinstance(stmt, BreakpointStmt): - # put a marker in the source so that we can generate a list of breakpoints later - self.p("\t\tnop\t; {:s} {:s}".format(self.BREAKPOINT_COMMENT_SIGNATURE, stmt.lineref)) - else: - raise CodeError("unknown statement " + repr(stmt)) - self.previous_stmt_was_assignment = isinstance(stmt, AssignmentStmt) - def generate_incr_or_decr(self, stmt: Union[InplaceIncrStmt, InplaceDecrStmt]) -> None: assert stmt.value.constant assert (stmt.value.value is None and stmt.value.name) or stmt.value.value > 0 diff --git a/il65/plyparse.py b/il65/plyparse.py index 71f196fd2..1a4b8f534 100644 --- a/il65/plyparse.py +++ b/il65/plyparse.py @@ -101,7 +101,7 @@ class Scope(AstNode): symbols = attr.ib(init=False) name = attr.ib(init=False) # will be set by enclosing block, or subroutine etc. parent_scope = attr.ib(init=False, default=None) # will be wired up later - save_registers = attr.ib(type=bool, default=None, init=False) # None = look in parent scope's setting + save_registers = attr.ib(type=bool, default=None, init=False) # None = look in parent scope's setting # @todo property that does that def __attrs_post_init__(self): # populate the symbol table for this scope for fast lookups via scope["name"] or scope["dotted.name"] @@ -325,6 +325,7 @@ class Assignment(AstNode): def simplify_targetregisters(self) -> None: # optimize TargetRegisters down to single Register if it's just one register new_targets = [] + assert isinstance(self.left, (list, tuple)), "assignment lvalue must be sequence" for t in self.left: if isinstance(t, TargetRegisters) and len(t.registers) == 1: t = t.registers[0] @@ -439,7 +440,7 @@ class VarDef(AstNode): # if the value is an expression, mark it as a *constant* expression here if isinstance(self.value, AstNode): self.value.processed_expr_must_be_constant = True - elif self.value is None and self.datatype.isnumeric(): + elif self.value is None and (self.datatype.isnumeric() or self.datatype.isarray()): self.value = 0 # if it's a matrix with interleave, it must be memory mapped if self.datatype == DataType.MATRIX and len(self.size) == 3: diff --git a/tests/test_core.py b/tests/test_core.py index f0dc912ae..cd095cb49 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -2,6 +2,7 @@ import pytest from il65 import datatypes from il65.compile import ParseError from il65.plylex import SourceRef +from il65.emit import to_hex, to_mflpt5 def test_datatypes(): @@ -30,60 +31,60 @@ def test_parseerror(): def test_to_hex(): - assert datatypes.to_hex(0) == "0" - assert datatypes.to_hex(1) == "1" - assert datatypes.to_hex(10) == "10" - assert datatypes.to_hex(15) == "15" - assert datatypes.to_hex(16) == "$10" - assert datatypes.to_hex(255) == "$ff" - assert datatypes.to_hex(256) == "$0100" - assert datatypes.to_hex(20060) == "$4e5c" - assert datatypes.to_hex(65535) == "$ffff" + assert to_hex(0) == "0" + assert to_hex(1) == "1" + assert to_hex(10) == "10" + assert to_hex(15) == "15" + assert to_hex(16) == "$10" + assert to_hex(255) == "$ff" + assert to_hex(256) == "$0100" + assert to_hex(20060) == "$4e5c" + assert to_hex(65535) == "$ffff" with pytest.raises(OverflowError): - datatypes.to_hex(-1) + to_hex(-1) with pytest.raises(OverflowError): - datatypes.to_hex(65536) + to_hex(65536) def test_float_to_mflpt5(): - mflpt = datatypes.to_mflpt5(1.0) + mflpt = to_mflpt5(1.0) assert type(mflpt) is bytearray - assert b"\x00\x00\x00\x00\x00" == datatypes.to_mflpt5(0) - assert b"\x82\x49\x0F\xDA\xA1" == datatypes.to_mflpt5(3.141592653) - assert b"\x82\x49\x0F\xDA\xA2" == datatypes.to_mflpt5(3.141592653589793) - assert b"\x90\x80\x00\x00\x00" == datatypes.to_mflpt5(-32768) - assert b"\x81\x00\x00\x00\x00" == datatypes.to_mflpt5(1) - assert b"\x80\x35\x04\xF3\x34" == datatypes.to_mflpt5(0.7071067812) - assert b"\x80\x35\x04\xF3\x33" == datatypes.to_mflpt5(0.7071067811865476) - assert b"\x81\x35\x04\xF3\x34" == datatypes.to_mflpt5(1.4142135624) - assert b"\x81\x35\x04\xF3\x33" == datatypes.to_mflpt5(1.4142135623730951) - assert b"\x80\x80\x00\x00\x00" == datatypes.to_mflpt5(-.5) - assert b"\x80\x31\x72\x17\xF8" == datatypes.to_mflpt5(0.69314718061) - assert b"\x80\x31\x72\x17\xF7" == datatypes.to_mflpt5(0.6931471805599453) - assert b"\x84\x20\x00\x00\x00" == datatypes.to_mflpt5(10) - assert b"\x9E\x6E\x6B\x28\x00" == datatypes.to_mflpt5(1000000000) - assert b"\x80\x00\x00\x00\x00" == datatypes.to_mflpt5(.5) - assert b"\x81\x38\xAA\x3B\x29" == datatypes.to_mflpt5(1.4426950408889634) - assert b"\x81\x49\x0F\xDA\xA2" == datatypes.to_mflpt5(1.5707963267948966) - assert b"\x83\x49\x0F\xDA\xA2" == datatypes.to_mflpt5(6.283185307179586) - assert b"\x7F\x00\x00\x00\x00" == datatypes.to_mflpt5(.25) + assert b"\x00\x00\x00\x00\x00" == to_mflpt5(0) + assert b"\x82\x49\x0F\xDA\xA1" == to_mflpt5(3.141592653) + assert b"\x82\x49\x0F\xDA\xA2" == to_mflpt5(3.141592653589793) + assert b"\x90\x80\x00\x00\x00" == to_mflpt5(-32768) + assert b"\x81\x00\x00\x00\x00" == to_mflpt5(1) + assert b"\x80\x35\x04\xF3\x34" == to_mflpt5(0.7071067812) + assert b"\x80\x35\x04\xF3\x33" == to_mflpt5(0.7071067811865476) + assert b"\x81\x35\x04\xF3\x34" == to_mflpt5(1.4142135624) + assert b"\x81\x35\x04\xF3\x33" == to_mflpt5(1.4142135623730951) + assert b"\x80\x80\x00\x00\x00" == to_mflpt5(-.5) + assert b"\x80\x31\x72\x17\xF8" == to_mflpt5(0.69314718061) + assert b"\x80\x31\x72\x17\xF7" == to_mflpt5(0.6931471805599453) + assert b"\x84\x20\x00\x00\x00" == to_mflpt5(10) + assert b"\x9E\x6E\x6B\x28\x00" == to_mflpt5(1000000000) + assert b"\x80\x00\x00\x00\x00" == to_mflpt5(.5) + assert b"\x81\x38\xAA\x3B\x29" == to_mflpt5(1.4426950408889634) + assert b"\x81\x49\x0F\xDA\xA2" == to_mflpt5(1.5707963267948966) + assert b"\x83\x49\x0F\xDA\xA2" == to_mflpt5(6.283185307179586) + assert b"\x7F\x00\x00\x00\x00" == to_mflpt5(.25) def test_float_range(): - assert b"\xff\x7f\xff\xff\xff" == datatypes.to_mflpt5(datatypes.FLOAT_MAX_POSITIVE) - assert b"\xff\xff\xff\xff\xff" == datatypes.to_mflpt5(datatypes.FLOAT_MAX_NEGATIVE) + assert b"\xff\x7f\xff\xff\xff" == to_mflpt5(datatypes.FLOAT_MAX_POSITIVE) + assert b"\xff\xff\xff\xff\xff" == to_mflpt5(datatypes.FLOAT_MAX_NEGATIVE) with pytest.raises(OverflowError): - datatypes.to_mflpt5(1.7014118346e+38) + to_mflpt5(1.7014118346e+38) with pytest.raises(OverflowError): - datatypes.to_mflpt5(-1.7014118346e+38) + to_mflpt5(-1.7014118346e+38) with pytest.raises(OverflowError): - datatypes.to_mflpt5(1.7014118347e+38) + to_mflpt5(1.7014118347e+38) with pytest.raises(OverflowError): - datatypes.to_mflpt5(-1.7014118347e+38) - assert b"\x03\x39\x1d\x15\x63" == datatypes.to_mflpt5(1.7e-38) - assert b"\x00\x00\x00\x00\x00" == datatypes.to_mflpt5(1.7e-39) - assert b"\x03\xb9\x1d\x15\x63" == datatypes.to_mflpt5(-1.7e-38) - assert b"\x00\x00\x00\x00\x00" == datatypes.to_mflpt5(-1.7e-39) + to_mflpt5(-1.7014118347e+38) + assert b"\x03\x39\x1d\x15\x63" == to_mflpt5(1.7e-38) + assert b"\x00\x00\x00\x00\x00" == to_mflpt5(1.7e-39) + assert b"\x03\xb9\x1d\x15\x63" == to_mflpt5(-1.7e-38) + assert b"\x00\x00\x00\x00\x00" == to_mflpt5(-1.7e-39) def test_char_to_bytevalue(): diff --git a/todo.ill b/todo.ill index e72223eb8..02dbaa4d3 100644 --- a/todo.ill +++ b/todo.ill @@ -1,5 +1,4 @@ %output basic -%import c64lib ~ main { @@ -16,32 +15,18 @@ var .wordarray(20) arr2 = $ea start: - %asm { + %breakpoint abc,def - lda zp1_1 - jsr c64scr.print_byte_decimal0 - inc zp1_1 - lda zp1_1 - jsr c64scr.print_byte_decimal0 - inc zp1_1 - lda zp1_1 - jsr c64scr.print_byte_decimal0 - inc zp1_1 - lda zp1_1 - jsr c64scr.print_byte_decimal0 - inc zp1_1 + foobar() - ;ldx #zp_s1 - ;jsr c64scr.print_string - ;ldx #zp_s2 - ;jsr c64scr.print_pstring + return 44 + +sub foobar () -> () { - ;ldx #ctext - ;jsr c64scr.print_string - } return + %breakpoint yep + + ; @todo check that subs/asm blocks end with return/rts +} }