zp variable allocation

This commit is contained in:
Irmen de Jong 2018-01-13 01:19:45 +01:00
parent e6804b2bf9
commit 62dfdace71
6 changed files with 156 additions and 62 deletions

View File

@ -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 "<unnamed>", 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")

View File

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

View File

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

View File

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

View File

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

View File

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