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
+}
}