restructure code generator

This commit is contained in:
Irmen de Jong 2018-01-14 15:18:50 +01:00
parent ee9a5716b0
commit 07387f501a
13 changed files with 969 additions and 1009 deletions

View File

@ -12,9 +12,6 @@ from functools import total_ordering
from .plylex import print_warning, SourceRef from .plylex import print_warning, SourceRef
PrimitiveType = Union[int, float, str]
@total_ordering @total_ordering
class VarType(enum.Enum): class VarType(enum.Enum):
CONST = 1 CONST = 1
@ -70,58 +67,9 @@ FLOAT_MAX_POSITIVE = 1.7014118345e+38
FLOAT_MAX_NEGATIVE = -1.7014118345e+38 FLOAT_MAX_NEGATIVE = -1.7014118345e+38
def to_hex(number: int) -> str: def coerce_value(datatype: DataType, value: Union[int, float, str], sourceref: SourceRef=None) -> Tuple[bool, Union[int, float, 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]:
# if we're a BYTE type, and the value is a single character, convert it to the numeric value # 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 the value is out of bounds, raise an overflow exception
if isinstance(value, (int, float)): if isinstance(value, (int, float)):
if datatype == DataType.BYTE and not (0 <= value <= 0xff): # type: ignore if datatype == DataType.BYTE and not (0 <= value <= 0xff): # type: ignore

120
il65/emit/__init__.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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)

View File

@ -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

View File

@ -7,12 +7,57 @@ Written by Irmen de Jong (irmen@razorvine.net) - license: GNU GPL 3.0
import time import time
import os import os
import re
import argparse import argparse
import subprocess import subprocess
from .compile import PlyParser from .compile import PlyParser
from .optimize import optimize from .optimize import optimize
from .generateasm import AssemblyGenerator, Assembler64Tass from .emit.generate import AssemblyGenerator
from .plylex import print_bold 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: def main() -> None:

View File

@ -1,396 +1,9 @@
""" # old deprecated code
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
class CodeGenerator: class CodeGenerator:
BREAKPOINT_COMMENT_SIGNATURE = "~~~BREAKPOINT~~~" BREAKPOINT_COMMENT_SIGNATURE = "~~~BREAKPOINT~~~"
BREAKPOINT_COMMENT_DETECTOR = r".(?P<address>\w+)\s+ea\s+nop\s+;\s+{:s}.*".format(BREAKPOINT_COMMENT_SIGNATURE) 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: def generate_incr_or_decr(self, stmt: Union[InplaceIncrStmt, InplaceDecrStmt]) -> None:
assert stmt.value.constant assert stmt.value.constant
assert (stmt.value.value is None and stmt.value.name) or stmt.value.value > 0 assert (stmt.value.value is None and stmt.value.name) or stmt.value.value > 0

View File

@ -101,7 +101,7 @@ class Scope(AstNode):
symbols = attr.ib(init=False) symbols = attr.ib(init=False)
name = attr.ib(init=False) # will be set by enclosing block, or subroutine etc. 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 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): def __attrs_post_init__(self):
# populate the symbol table for this scope for fast lookups via scope["name"] or scope["dotted.name"] # 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: def simplify_targetregisters(self) -> None:
# optimize TargetRegisters down to single Register if it's just one register # optimize TargetRegisters down to single Register if it's just one register
new_targets = [] new_targets = []
assert isinstance(self.left, (list, tuple)), "assignment lvalue must be sequence"
for t in self.left: for t in self.left:
if isinstance(t, TargetRegisters) and len(t.registers) == 1: if isinstance(t, TargetRegisters) and len(t.registers) == 1:
t = t.registers[0] 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 the value is an expression, mark it as a *constant* expression here
if isinstance(self.value, AstNode): if isinstance(self.value, AstNode):
self.value.processed_expr_must_be_constant = True 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 self.value = 0
# if it's a matrix with interleave, it must be memory mapped # if it's a matrix with interleave, it must be memory mapped
if self.datatype == DataType.MATRIX and len(self.size) == 3: if self.datatype == DataType.MATRIX and len(self.size) == 3:

View File

@ -2,6 +2,7 @@ import pytest
from il65 import datatypes from il65 import datatypes
from il65.compile import ParseError from il65.compile import ParseError
from il65.plylex import SourceRef from il65.plylex import SourceRef
from il65.emit import to_hex, to_mflpt5
def test_datatypes(): def test_datatypes():
@ -30,60 +31,60 @@ def test_parseerror():
def test_to_hex(): def test_to_hex():
assert datatypes.to_hex(0) == "0" assert to_hex(0) == "0"
assert datatypes.to_hex(1) == "1" assert to_hex(1) == "1"
assert datatypes.to_hex(10) == "10" assert to_hex(10) == "10"
assert datatypes.to_hex(15) == "15" assert to_hex(15) == "15"
assert datatypes.to_hex(16) == "$10" assert to_hex(16) == "$10"
assert datatypes.to_hex(255) == "$ff" assert to_hex(255) == "$ff"
assert datatypes.to_hex(256) == "$0100" assert to_hex(256) == "$0100"
assert datatypes.to_hex(20060) == "$4e5c" assert to_hex(20060) == "$4e5c"
assert datatypes.to_hex(65535) == "$ffff" assert to_hex(65535) == "$ffff"
with pytest.raises(OverflowError): with pytest.raises(OverflowError):
datatypes.to_hex(-1) to_hex(-1)
with pytest.raises(OverflowError): with pytest.raises(OverflowError):
datatypes.to_hex(65536) to_hex(65536)
def test_float_to_mflpt5(): def test_float_to_mflpt5():
mflpt = datatypes.to_mflpt5(1.0) mflpt = to_mflpt5(1.0)
assert type(mflpt) is bytearray assert type(mflpt) is bytearray
assert b"\x00\x00\x00\x00\x00" == datatypes.to_mflpt5(0) assert b"\x00\x00\x00\x00\x00" == to_mflpt5(0)
assert b"\x82\x49\x0F\xDA\xA1" == datatypes.to_mflpt5(3.141592653) assert b"\x82\x49\x0F\xDA\xA1" == to_mflpt5(3.141592653)
assert b"\x82\x49\x0F\xDA\xA2" == datatypes.to_mflpt5(3.141592653589793) assert b"\x82\x49\x0F\xDA\xA2" == to_mflpt5(3.141592653589793)
assert b"\x90\x80\x00\x00\x00" == datatypes.to_mflpt5(-32768) assert b"\x90\x80\x00\x00\x00" == to_mflpt5(-32768)
assert b"\x81\x00\x00\x00\x00" == datatypes.to_mflpt5(1) assert b"\x81\x00\x00\x00\x00" == to_mflpt5(1)
assert b"\x80\x35\x04\xF3\x34" == datatypes.to_mflpt5(0.7071067812) assert b"\x80\x35\x04\xF3\x34" == to_mflpt5(0.7071067812)
assert b"\x80\x35\x04\xF3\x33" == datatypes.to_mflpt5(0.7071067811865476) assert b"\x80\x35\x04\xF3\x33" == to_mflpt5(0.7071067811865476)
assert b"\x81\x35\x04\xF3\x34" == datatypes.to_mflpt5(1.4142135624) assert b"\x81\x35\x04\xF3\x34" == to_mflpt5(1.4142135624)
assert b"\x81\x35\x04\xF3\x33" == datatypes.to_mflpt5(1.4142135623730951) assert b"\x81\x35\x04\xF3\x33" == to_mflpt5(1.4142135623730951)
assert b"\x80\x80\x00\x00\x00" == datatypes.to_mflpt5(-.5) assert b"\x80\x80\x00\x00\x00" == to_mflpt5(-.5)
assert b"\x80\x31\x72\x17\xF8" == datatypes.to_mflpt5(0.69314718061) assert b"\x80\x31\x72\x17\xF8" == to_mflpt5(0.69314718061)
assert b"\x80\x31\x72\x17\xF7" == datatypes.to_mflpt5(0.6931471805599453) assert b"\x80\x31\x72\x17\xF7" == to_mflpt5(0.6931471805599453)
assert b"\x84\x20\x00\x00\x00" == datatypes.to_mflpt5(10) assert b"\x84\x20\x00\x00\x00" == to_mflpt5(10)
assert b"\x9E\x6E\x6B\x28\x00" == datatypes.to_mflpt5(1000000000) assert b"\x9E\x6E\x6B\x28\x00" == to_mflpt5(1000000000)
assert b"\x80\x00\x00\x00\x00" == datatypes.to_mflpt5(.5) assert b"\x80\x00\x00\x00\x00" == to_mflpt5(.5)
assert b"\x81\x38\xAA\x3B\x29" == datatypes.to_mflpt5(1.4426950408889634) assert b"\x81\x38\xAA\x3B\x29" == to_mflpt5(1.4426950408889634)
assert b"\x81\x49\x0F\xDA\xA2" == datatypes.to_mflpt5(1.5707963267948966) assert b"\x81\x49\x0F\xDA\xA2" == to_mflpt5(1.5707963267948966)
assert b"\x83\x49\x0F\xDA\xA2" == datatypes.to_mflpt5(6.283185307179586) assert b"\x83\x49\x0F\xDA\xA2" == to_mflpt5(6.283185307179586)
assert b"\x7F\x00\x00\x00\x00" == datatypes.to_mflpt5(.25) assert b"\x7F\x00\x00\x00\x00" == to_mflpt5(.25)
def test_float_range(): def test_float_range():
assert b"\xff\x7f\xff\xff\xff" == datatypes.to_mflpt5(datatypes.FLOAT_MAX_POSITIVE) assert b"\xff\x7f\xff\xff\xff" == to_mflpt5(datatypes.FLOAT_MAX_POSITIVE)
assert b"\xff\xff\xff\xff\xff" == datatypes.to_mflpt5(datatypes.FLOAT_MAX_NEGATIVE) assert b"\xff\xff\xff\xff\xff" == to_mflpt5(datatypes.FLOAT_MAX_NEGATIVE)
with pytest.raises(OverflowError): with pytest.raises(OverflowError):
datatypes.to_mflpt5(1.7014118346e+38) to_mflpt5(1.7014118346e+38)
with pytest.raises(OverflowError): with pytest.raises(OverflowError):
datatypes.to_mflpt5(-1.7014118346e+38) to_mflpt5(-1.7014118346e+38)
with pytest.raises(OverflowError): with pytest.raises(OverflowError):
datatypes.to_mflpt5(1.7014118347e+38) to_mflpt5(1.7014118347e+38)
with pytest.raises(OverflowError): with pytest.raises(OverflowError):
datatypes.to_mflpt5(-1.7014118347e+38) to_mflpt5(-1.7014118347e+38)
assert b"\x03\x39\x1d\x15\x63" == datatypes.to_mflpt5(1.7e-38) assert b"\x03\x39\x1d\x15\x63" == to_mflpt5(1.7e-38)
assert b"\x00\x00\x00\x00\x00" == datatypes.to_mflpt5(1.7e-39) assert b"\x00\x00\x00\x00\x00" == to_mflpt5(1.7e-39)
assert b"\x03\xb9\x1d\x15\x63" == datatypes.to_mflpt5(-1.7e-38) assert b"\x03\xb9\x1d\x15\x63" == to_mflpt5(-1.7e-38)
assert b"\x00\x00\x00\x00\x00" == datatypes.to_mflpt5(-1.7e-39) assert b"\x00\x00\x00\x00\x00" == to_mflpt5(-1.7e-39)
def test_char_to_bytevalue(): def test_char_to_bytevalue():

View File

@ -1,5 +1,4 @@
%output basic %output basic
%import c64lib
~ main { ~ main {
@ -16,32 +15,18 @@
var .wordarray(20) arr2 = $ea var .wordarray(20) arr2 = $ea
start: start:
%asm { %breakpoint abc,def
lda zp1_1 foobar()
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
;ldx #<zp_s1 return 44
;ldy #>zp_s1
;jsr c64scr.print_string sub foobar () -> () {
;ldx #<zp_s2
;ldy #>zp_s2
;jsr c64scr.print_pstring
;ldx #<ctext
;ldy #>ctext
;jsr c64scr.print_string
}
return return
%breakpoint yep
; @todo check that subs/asm blocks end with return/rts
}
} }