diff --git a/il65/compile.py b/il65/compile.py index 98d35b249..87661e627 100644 --- a/il65/compile.py +++ b/il65/compile.py @@ -9,13 +9,18 @@ import re import os import sys import linecache -from typing import Optional, Tuple, Set, Dict, Any, no_type_check +from typing import Optional, Tuple, Set, Dict, List, Any, no_type_check import attr from .plyparse import parse_file, ParseError, Module, Directive, Block, Subroutine, Scope, VarDef, LiteralValue, \ SubCall, Goto, Return, Assignment, InlineAssembly, Register, Expression, ProgramFormat, ZpOptions,\ SymbolName, Dereference, AddressOf from .plylex import SourceRef, print_bold from .optimize import optimize +from .datatypes import DataType, datatype_sizes + + +class CompileError(Exception): + pass class PlyParser: @@ -37,6 +42,8 @@ class PlyParser: # these shall only be done on the main module after all imports have been done: self.apply_directive_options(module) self.determine_subroutine_usage(module) + # XXX merge zero page from imported modules??? do we still have to do that? + self.allocate_zeropage_vars(module) except ParseError as x: self.handle_parse_error(x) if self.parse_errors: @@ -60,7 +67,6 @@ class PlyParser: zeropage.scope.add_node(node, 0) elif isinstance(node, VarDef): zeropage.scope.add_node(node) - print("ADDED ZP VAR", node) # XXX else: raise ParseError("only variables and directives allowed in zeropage block", node.sourceref) else: @@ -70,6 +76,19 @@ class PlyParser: # add the zero page again, as the very first block module.scope.add_node(zeropage, 0) + def allocate_zeropage_vars(self, module: Module) -> None: + # allocate zeropage variables to the available free zp addresses + if not module.scope.nodes: + return + zpnode = module.scope.nodes[0] + assert zpnode.name == "ZP", "first node should be the (only) ZP" + zeropage = Zeropage(module.zp_options) + for vardef in zpnode.scope.filter_nodes(VarDef): + try: + vardef.zp_address = zeropage.allocate(vardef.name, vardef.datatype) + except CompileError as x: + raise ParseError(str(x), vardef.sourceref) + @no_type_check def process_all_expressions(self, module: Module) -> None: # process/simplify all expressions (constant folding etc) @@ -383,6 +402,60 @@ class PlyParser: raise exc +class Zeropage: + SCRATCH_B1 = 0x02 + SCRATCH_B2 = 0x03 + SCRATCH_W1 = 0xfb # $fb/$fc + SCRATCH_W2 = 0xfd # $fd/$fe + + def __init__(self, options: ZpOptions) -> None: + self.free = [] # type: List[int] + self.allocations = {} # type: Dict[int, Tuple[str, DataType]] + if options in (ZpOptions.CLOBBER_RESTORE, ZpOptions.CLOBBER): + # clobber the zp, more free storage, yay! + self.free = list(range(0x04, 0xfb)) + [0xff] + for updated_by_irq in [0xa0, 0xa1, 0xa2, 0x91, 0xc0, 0xc5, 0xcb, 0xf5, 0xf6]: + self.free.remove(updated_by_irq) + else: + # these are valid for the C-64 (when no RS232 I/O is performed): + # ($02, $03, $fb-$fc, $fd-$fe are reserved as scratch addresses for various routines) + self.free = [0x04, 0x05, 0x06, 0x2a, 0x52, 0xf7, 0xf8, 0xf9, 0xfa] + assert self.SCRATCH_B1 not in self.free + assert self.SCRATCH_B2 not in self.free + assert self.SCRATCH_W1 not in self.free + assert self.SCRATCH_W2 not in self.free + + def allocate(self, name: str, datatype: DataType) -> int: + assert not name or name not in {a[0] for a in self.allocations.values()}, "var name is not unique" + + def sequential_free(location: int) -> bool: + return all(location + i in self.free for i in range(size)) + + def lone_byte(location: int) -> bool: + return (location-1) not in self.free and (location+1) not in self.free and location in self.free + + def make_allocation(location: int) -> int: + for loc in range(location, location + size): + self.free.remove(loc) + self.allocations[location] = (name or "", datatype) + return location + + size = datatype_sizes[datatype] + if len(self.free) > 0: + if size == 1: + for candidate in range(min(self.free), max(self.free)+1): + if lone_byte(candidate): + return make_allocation(candidate) + return make_allocation(self.free[0]) + for candidate in range(min(self.free), max(self.free)+1): + if sequential_free(candidate): + return make_allocation(candidate) + raise CompileError("ERROR: no more free space in ZP to allocate {:d} sequential bytes".format(size)) + + def available(self) -> int: + return len(self.free) + + if __name__ == "__main__": description = "Compiler for IL65 language, code name 'Sick'" print("\n" + description + "\n") diff --git a/il65/datatypes.py b/il65/datatypes.py index 3ef2e2970..9a9c9f7d1 100644 --- a/il65/datatypes.py +++ b/il65/datatypes.py @@ -47,6 +47,13 @@ class DataType(enum.Enum): return NotImplemented +datatype_sizes = { + DataType.BYTE: 1, + DataType.WORD: 2, + DataType.FLOAT: 5 +} + + STRING_DATATYPES = {DataType.STRING, DataType.STRING_P, DataType.STRING_S, DataType.STRING_PS} REGISTER_SYMBOLS = {"A", "X", "Y", "AX", "AY", "XY", "SC", "SI"} diff --git a/il65/generateasm.py b/il65/generateasm.py index 3c454fdd7..dc5a461f8 100644 --- a/il65/generateasm.py +++ b/il65/generateasm.py @@ -13,7 +13,7 @@ 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, mflpt5_to_float, to_mflpt5, STRING_DATATYPES +from .datatypes import VarType, DataType, datatype_sizes, to_hex, mflpt5_to_float, to_mflpt5, STRING_DATATYPES class CodeError(Exception): @@ -126,7 +126,7 @@ class AssemblyGenerator: 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) + 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): @@ -270,7 +270,7 @@ class AssemblyGenerator: self.p("_init_strings_size = * - _init_strings_start") self.p("") - def generate_block_vars(self, block: Block) -> None: + 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. @@ -307,39 +307,46 @@ class AssemblyGenerator: else: raise CodeError("invalid var type") self.p("; normal variables - initial values will be set by init code") - string_vars = [] - for vardef in vars_by_vartype.get(VarType.VAR, []): - # create a definition for a variable that takes up empty space and will be initialized at startup - if vardef.datatype in (DataType.BYTE, DataType.WORD, DataType.FLOAT): - 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)) + 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 + self.p("\v{:s} = {:s}\t; {:s} ({:d})".format(vardef.name, to_hex(vardef.zp_address), + vardef.datatype.name.lower(), datatype_sizes[vardef.datatype])) + 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 in (DataType.BYTE, DataType.WORD, DataType.FLOAT): + 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 in STRING_DATATYPES: + string_vars.append(vardef) 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 in STRING_DATATYPES: - string_vars.append(vardef) - else: - raise CodeError("unknown variable type " + str(vardef.datatype)) - if string_vars: - self.p("il65_string_vars_start") - for svar in sorted(string_vars, key=lambda v: v.name): # must be the same order as in the init routine!!! - self.p("{:s}\v.fill {:d}+1\t\t; {}".format(svar.name, len(svar.value), svar.datatype.name.lower())) + raise CodeError("unknown variable type " + str(vardef.datatype)) + if string_vars: + self.p("il65_string_vars_start") + for svar in sorted(string_vars, key=lambda v: v.name): # must be the same order as in the init routine!!! + self.p("{:s}\v.fill {:d}+1\t\t; {}".format(svar.name, len(svar.value), svar.datatype.name.lower())) self.p("") def _generate_string_var(self, vardef: VarDef, init: bool=False) -> None: diff --git a/il65/plyparse.py b/il65/plyparse.py index 596b4fef0..756583621 100644 --- a/il65/plyparse.py +++ b/il65/plyparse.py @@ -381,13 +381,14 @@ class InlineAssembly(AstNode): assembly = attr.ib(type=str) -@attr.s(cmp=False, repr=False) +@attr.s(cmp=False, repr=False, slots=True) class VarDef(AstNode): name = attr.ib(type=str) vartype = attr.ib() datatype = attr.ib() value = attr.ib(default=None) size = attr.ib(type=list, default=None) + zp_address = attr.ib(type=int, default=None, init=False) # the address in the zero page if this var is there, will be set later def __attrs_post_init__(self): # convert vartype to enum @@ -625,7 +626,9 @@ def process_constant_expression(expr: Any, sourceref: SourceRef, symbolscope: Sc if isinstance(value, VarDef): if value.vartype == VarType.MEMORY: return value.value - raise ExpressionEvaluationError("taking the address of this {} isn't a constant".format(value.__class__.__name__), expr.name.sourceref) + if value.vartype == VarType.CONST: + raise ExpressionEvaluationError("can't take the address of a constant", expr.name.sourceref) + raise ExpressionEvaluationError("address-of this {} isn't a compile-time constant".format(value.__class__.__name__), expr.name.sourceref) else: raise ExpressionEvaluationError("constant address required, not {}".format(value.__class__.__name__), expr.name.sourceref) except LookupError as x: diff --git a/tests/test_zp.py b/tests/test_zp.py index 7d8a29100..8b87799c1 100644 --- a/tests/test_zp.py +++ b/tests/test_zp.py @@ -1,17 +1,11 @@ import pytest -from il65.handwritten.symbols import Zeropage, SymbolError, DataType # @todo - - -def test_zp_configure_onlyonce(): - zp = Zeropage() - zp.configure() - with pytest.raises(SymbolError): - zp.configure() +from il65.compile import Zeropage, CompileError +from il65.plyparse import ZpOptions +from il65.datatypes import DataType def test_zp_names(): - zp = Zeropage() - zp.configure() + zp = Zeropage(ZpOptions.NOCLOBBER) zp.allocate("", DataType.BYTE) zp.allocate("", DataType.BYTE) zp.allocate("varname", DataType.BYTE) @@ -21,23 +15,22 @@ def test_zp_names(): def test_zp_noclobber_allocation(): - zp = Zeropage() - zp.configure(False) + zp = Zeropage(ZpOptions.NOCLOBBER) assert zp.available() == 9 - with pytest.raises(LookupError): + with pytest.raises(CompileError): zp.allocate("impossible", DataType.FLOAT) # in regular zp there aren't 5 sequential bytes free for i in range(zp.available()): - zp.allocate("bytevar"+str(i), DataType.BYTE) + loc = zp.allocate("bytevar"+str(i), DataType.BYTE) + assert loc > 0 assert zp.available() == 0 - with pytest.raises(LookupError): + with pytest.raises(CompileError): zp.allocate("", DataType.BYTE) - with pytest.raises(LookupError): + with pytest.raises(CompileError): zp.allocate("", DataType.WORD) def test_zp_clobber_allocation(): - zp = Zeropage() - zp.configure(True) + zp = Zeropage(ZpOptions.CLOBBER) assert zp.available() == 239 loc = zp.allocate("", DataType.FLOAT) assert loc > 3 and loc not in zp.free @@ -45,15 +38,28 @@ def test_zp_clobber_allocation(): for _ in range(num-3): zp.allocate("", DataType.FLOAT) assert zp.available() == 19 - with pytest.raises(LookupError): + with pytest.raises(CompileError): zp.allocate("", DataType.FLOAT) # can't allocate because no more sequential bytes, only fragmented for _ in range(14): zp.allocate("", DataType.BYTE) zp.allocate("", DataType.WORD) zp.allocate("", DataType.WORD) - with pytest.raises(LookupError): + with pytest.raises(CompileError): zp.allocate("", DataType.WORD) assert zp.available() == 1 zp.allocate("last", DataType.BYTE) - with pytest.raises(LookupError): + with pytest.raises(CompileError): zp.allocate("impossible", DataType.BYTE) + + +def test_zp_efficient_allocation(): + # free = [0x04, 0x05, 0x06, 0x2a, 0x52, 0xf7, 0xf8, 0xf9, 0xfa] + zp = Zeropage(ZpOptions.NOCLOBBER) + assert zp.available() == 9 + assert 0x2a == zp.allocate("", DataType.BYTE) + assert 0x52 == zp.allocate("", DataType.BYTE) + assert 0x04 == zp.allocate("", DataType.WORD) + assert 0xf7 == zp.allocate("", DataType.WORD) + assert 0x06 == zp.allocate("", DataType.BYTE) + assert 0xf9 == zp.allocate("", DataType.WORD) + assert zp.available() == 0 diff --git a/testsource/dtypes.ill b/testsource/dtypes.ill index 3c79681b3..857db1ec4 100644 --- a/testsource/dtypes.ill +++ b/testsource/dtypes.ill @@ -126,9 +126,7 @@ ; because zp-vars get assigned a specific address (from a pool). Also, it's a byte. var .word initword0a = &ZP.zpmem1 - var .word initword0 = &ZP.zpvar1 ; @todo should work, reference this symbols' generated address (@todo generate address for ZP) var initbytea0 = &ZP.zpmem1 - var .word initworda1 = &ZP.zpvar1 ; @todo should work, reference this symbols' generated address (@todo generate address for ZP) ; (constant) expressions