mirror of
https://github.com/irmen/prog8.git
synced 2024-11-22 15:33:02 +00:00
restructure code generator
This commit is contained in:
parent
ee9a5716b0
commit
07387f501a
@ -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
|
||||
|
120
il65/emit/__init__.py
Normal file
120
il65/emit/__init__.py
Normal file
@ -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
|
22
il65/emit/assignment.py
Normal file
22
il65/emit/assignment.py
Normal file
@ -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
|
18
il65/emit/calls.py
Normal file
18
il65/emit/calls.py
Normal file
@ -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
|
||||
|
225
il65/emit/generate.py
Normal file
225
il65/emit/generate.py
Normal file
@ -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<address>\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 "<unnamed>", registers) for name, registers in subdef.param_spec)
|
||||
returns = ",".join(sorted(register for register in subdef.result_spec if register[-1] != '?'))
|
||||
clobbers = ",".join(sorted(register for register in subdef.result_spec if register[-1] == '?'))
|
||||
out("\v; params: {}\n\v; returns: {} clobbers: {}".format(params or "-", returns or "-", clobbers or "-"))
|
||||
cur_block = self.cur_block
|
||||
self.cur_block = subdef.scope
|
||||
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)
|
222
il65/emit/incrdecr.py
Normal file
222
il65/emit/incrdecr.py
Normal file
@ -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))
|
259
il65/emit/variables.py
Normal file
259
il65/emit/variables.py
Normal file
@ -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)
|
@ -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<address>\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 "<unnamed>", registers) for name, registers in subdef.param_spec)
|
||||
returns = ",".join(sorted(register for register in subdef.result_spec if register[-1] != '?'))
|
||||
clobbers = ",".join(sorted(register for register in subdef.result_spec if register[-1] == '?'))
|
||||
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
|
47
il65/main.py
47
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:
|
||||
|
@ -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<address>\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 <header> block, it always goes second
|
||||
for block in [b for b in self.parsed.blocks if b.name == "<header>"]:
|
||||
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", "<header>"):
|
||||
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 "<unnamed>", 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
|
||||
|
@ -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:
|
||||
|
@ -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():
|
||||
|
33
todo.ill
33
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
|
||||
;ldy #>zp_s1
|
||||
;jsr c64scr.print_string
|
||||
;ldx #<zp_s2
|
||||
;ldy #>zp_s2
|
||||
;jsr c64scr.print_pstring
|
||||
return 44
|
||||
|
||||
sub foobar () -> () {
|
||||
|
||||
;ldx #<ctext
|
||||
;ldy #>ctext
|
||||
;jsr c64scr.print_string
|
||||
}
|
||||
return
|
||||
%breakpoint yep
|
||||
|
||||
; @todo check that subs/asm blocks end with return/rts
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user