mirror of
https://github.com/irmen/prog8.git
synced 2024-08-17 02:28:56 +00:00
zp variable allocation
This commit is contained in:
parent
e6804b2bf9
commit
62dfdace71
@ -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")
|
||||
|
@ -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"}
|
||||
|
@ -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,9 +307,16 @@ class AssemblyGenerator:
|
||||
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
|
||||
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, []):
|
||||
# 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:
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user