allow floats in ZP, if_pos and if_neg added, ZP allocations more flexible

This commit is contained in:
Irmen de Jong 2017-12-31 00:48:00 +01:00
parent e67e4c0b13
commit a5283bfc7b
7 changed files with 183 additions and 42 deletions

View File

@ -13,7 +13,7 @@ import datetime
import subprocess import subprocess
import contextlib import contextlib
from functools import partial from functools import partial
from typing import TextIO, Set, Union, List, Tuple, Callable from typing import TextIO, Set, Union, List, Callable
from .parse import ProgramFormat, ParseResult, Parser from .parse import ProgramFormat, ParseResult, Parser
from .symbols import Zeropage, DataType, ConstantDef, VariableDef, SubroutineDef, \ from .symbols import Zeropage, DataType, ConstantDef, VariableDef, SubroutineDef, \
STRING_DATATYPES, REGISTER_WORDS, REGISTER_BYTES, FLOAT_MAX_NEGATIVE, FLOAT_MAX_POSITIVE STRING_DATATYPES, REGISTER_WORDS, REGISTER_BYTES, FLOAT_MAX_NEGATIVE, FLOAT_MAX_POSITIVE
@ -127,6 +127,7 @@ class CodeGenerator:
must_save_zp = self.parsed.clobberzp and self.parsed.restorezp must_save_zp = self.parsed.clobberzp and self.parsed.restorezp
if must_save_zp: if must_save_zp:
self.p("\t\tjsr il65_lib_zp.save_zeropage") self.p("\t\tjsr il65_lib_zp.save_zeropage")
zp_float_bytes = {}
# Only the vars from the ZeroPage need to be initialized here, # Only the vars from the ZeroPage need to be initialized here,
# the vars in all other blocks are just defined and pre-filled there. # 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"] zpblocks = [b for b in self.parsed.blocks if b.name == "ZP"]
@ -157,7 +158,16 @@ class CodeGenerator:
self.p("\t\tsta {:s}".format(vname)) self.p("\t\tsta {:s}".format(vname))
self.p("\t\tstx {:s}+1".format(vname)) self.p("\t\tstx {:s}+1".format(vname))
elif variable.type == DataType.FLOAT: elif variable.type == DataType.FLOAT:
raise CodeError("floats cannot be stored in the zp") 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") self.p("; end init zp vars")
else: else:
self.p("\t\t; there are no zp vars to initialize") self.p("\t\t; there are no zp vars to initialize")
@ -170,6 +180,10 @@ class CodeGenerator:
self.p("\t\tjmp il65_lib_zp.restore_zeropage") self.p("\t\tjmp il65_lib_zp.restore_zeropage")
else: else:
self.p("\t\tjmp {:s}.start\t\t; call user code".format(main_block_label)) 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: def blocks(self) -> None:
# if there's a <header> block, it always goes second # if there's a <header> block, it always goes second
@ -634,6 +648,10 @@ class CodeGenerator:
self.p("\t\tbeq " + targetstr) self.p("\t\tbeq " + targetstr)
elif ifs in ("cc", "cs", "vc", "vs", "eq", "ne"): elif ifs in ("cc", "cs", "vc", "vs", "eq", "ne"):
self.p("\t\tb{:s} {:s}".format(ifs, targetstr)) self.p("\t\tb{:s} {:s}".format(ifs, targetstr))
elif ifs == "pos":
self.p("\t\tbpl " + targetstr)
elif ifs == "neg":
self.p("\t\tbmi " + targetstr)
elif ifs == "lt": elif ifs == "lt":
self.p("\t\tbcc " + targetstr) self.p("\t\tbcc " + targetstr)
elif ifs == "gt": elif ifs == "gt":
@ -1463,19 +1481,18 @@ class CodeGenerator:
raise CodeError("invalid assignment value type", str(stmt)) raise CodeError("invalid assignment value type", str(stmt))
def generate_assign_float_to_mem(self, mmv: ParseResult.MemMappedValue, def generate_assign_float_to_mem(self, mmv: ParseResult.MemMappedValue,
rvalue: Union[ParseResult.FloatValue, ParseResult.IntegerValue], save_reg: bool=True) -> None: rvalue: Union[ParseResult.FloatValue, ParseResult.IntegerValue]) -> None:
floatvalue = float(rvalue.value) floatvalue = float(rvalue.value)
mflpt = self.to_mflpt5(floatvalue) mflpt = self.to_mflpt5(floatvalue)
target = mmv.name or Parser.to_hex(mmv.address) target = mmv.name or Parser.to_hex(mmv.address)
if save_reg: self.p("\t\tpha\t\t\t; {:s} = {}".format(target, rvalue.name or floatvalue))
self.p("\t\tpha\t\t\t; {:s} = {}".format(target, rvalue.name or floatvalue)) a_reg_value = None
else: for i, byte in enumerate(mflpt):
self.p("\t\t\t\t\t; {:s} = {}".format(target, rvalue.name or floatvalue)) if byte != a_reg_value:
for num in range(5): self.p("\t\tlda #${:02x}".format(byte))
self.p("\t\tlda #${:02x}".format(mflpt[num])) a_reg_value = byte
self.p("\t\tsta {:s}+{:d}".format(target, num)) self.p("\t\tsta {:s}+{:d}".format(target, i))
if save_reg: self.p("\t\tpla")
self.p("\t\tpla")
def generate_assign_reg_to_memory(self, lv: ParseResult.MemMappedValue, r_register: str) -> None: def generate_assign_reg_to_memory(self, lv: ParseResult.MemMappedValue, r_register: str) -> None:
# Memory = Register # Memory = Register
@ -1680,7 +1697,7 @@ class CodeGenerator:
elif lvdatatype == DataType.FLOAT: elif lvdatatype == DataType.FLOAT:
if rvalue.value is not None and not DataType.FLOAT.assignable_from_value(rvalue.value): if rvalue.value is not None and not DataType.FLOAT.assignable_from_value(rvalue.value):
raise CodeError("value cannot be assigned to a float") raise CodeError("value cannot be assigned to a float")
self.generate_assign_float_to_mem(lv, rvalue, False) self.generate_assign_float_to_mem(lv, rvalue)
else: else:
raise CodeError("invalid lvalue type " + str(lvdatatype)) raise CodeError("invalid lvalue type " + str(lvdatatype))

View File

@ -508,7 +508,7 @@ class ParseResult:
">=": "<=", ">=": "<=",
"<": ">", "<": ">",
">": "<"} ">": "<"}
IF_STATUSES = {"cc", "cs", "vc", "vs", "eq", "ne", "true", "not", "zero", "lt", "gt", "le", "ge"} IF_STATUSES = {"cc", "cs", "vc", "vs", "eq", "ne", "true", "not", "zero", "pos", "neg", "lt", "gt", "le", "ge"}
def __init__(self, ifstatus: str, leftvalue: Optional['ParseResult.Value'], def __init__(self, ifstatus: str, leftvalue: Optional['ParseResult.Value'],
operator: str, rightvalue: Optional['ParseResult.Value'], lineno: int) -> None: operator: str, rightvalue: Optional['ParseResult.Value'], lineno: int) -> None:

View File

@ -212,39 +212,56 @@ class Zeropage:
SCRATCH_W2 = 0xfd # $fd/$fe SCRATCH_W2 = 0xfd # $fd/$fe
def __init__(self) -> None: def __init__(self) -> None:
self.unused_bytes = [] # type: List[int] self.free = [] # type: List[int]
self.unused_words = [] # type: List[int] self.allocations = {} # type: Dict[int, Tuple[str, DataType]]
self._configured = False self._configured = False
def configure(self, clobber_zp: bool = False) -> None: def configure(self, clobber_zp: bool = False) -> None:
if self._configured: if self._configured:
raise SymbolError("cannot configure the ZP multiple times") raise SymbolError("cannot configure the ZP multiple times")
if clobber_zp: if clobber_zp:
self.unused_bytes = list(range(0x04, 0x80)) + [0xff] self.free = list(range(0x04, 0xfb)) + [0xff]
self.unused_words = list(range(0x80, 0xfb, 2))
else: else:
# these are valid for the C-64 (when no RS232 I/O is performed): # 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) # ($02, $03, $fb-$fc, $fd-$fe are reserved as scratch addresses for various routines)
self.unused_bytes = [0x04, 0x05, 0x06, 0x2a, 0x52] # 5 zp variables (1 byte each) self.free = [0x04, 0x05, 0x06, 0x2a, 0x52, 0xf7, 0xf8, 0xf9, 0xfa]
self.unused_words = [0xf7, 0xf9] # 2 zp word variables (2 bytes each) assert self.SCRATCH_B1 not in self.free
# @todo more clever allocating, don't have fixed bytes and words assert self.SCRATCH_B2 not in self.free
assert self.SCRATCH_B1 not in self.unused_bytes and self.SCRATCH_B1 not in self.unused_words assert self.SCRATCH_W1 not in self.free
assert self.SCRATCH_B2 not in self.unused_bytes and self.SCRATCH_B2 not in self.unused_words assert self.SCRATCH_W2 not in self.free
self._configured = True self._configured = True
def get_unused_byte(self): def allocate(self, name: str, datatype: DataType) -> int:
return self.unused_bytes.pop() assert self._configured
size = {
DataType.BYTE: 1,
DataType.WORD: 2,
DataType.FLOAT: 5
}[datatype]
def get_unused_word(self): def sequential(loc: int) -> bool:
return self.unused_words.pop() for i in range(size):
if loc+i not in self.free:
return False
return True
@property if len(self.free) > 0:
def available_byte_vars(self) -> int: if size == 1:
return len(self.unused_bytes) assert not name or name not in {a[0] for a in self.allocations.values()}
loc = self.free.pop()
self.allocations[loc] = (name or "<unnamed>", datatype)
return loc
for candidate in range(min(self.free), max(self.free)+1):
if sequential(candidate):
assert not name or name not in {a[0] for a in self.allocations.values()}
for loc in range(candidate, candidate+size):
self.free.remove(loc)
self.allocations[candidate] = (name or "<unnamed>", datatype)
return candidate
raise LookupError("no more free space in ZP to allocate {:d} sequential bytes".format(size))
@property def available(self) -> int:
def available_word_vars(self) -> int: return len(self.free)
return len(self.unused_words)
class SymbolTable: class SymbolTable:
@ -368,22 +385,25 @@ class SymbolTable:
if datatype == DataType.BYTE: if datatype == DataType.BYTE:
if allocate and self.name == "ZP": if allocate and self.name == "ZP":
try: try:
address = self._zeropage.get_unused_byte() address = self._zeropage.allocate(name, datatype)
except LookupError: except LookupError:
raise SymbolError("no space in ZP left for more global 8-bit variables (try zp clobber)") raise SymbolError("no space in ZP left for global 8-bit variable (try zp clobber)")
self.symbols[name] = VariableDef(self.name, name, sourceref, DataType.BYTE, allocate, self.symbols[name] = VariableDef(self.name, name, sourceref, DataType.BYTE, allocate,
value=value, length=1, address=address) value=value, length=1, address=address)
elif datatype == DataType.WORD: elif datatype == DataType.WORD:
if allocate and self.name == "ZP": if allocate and self.name == "ZP":
try: try:
address = self._zeropage.get_unused_word() address = self._zeropage.allocate(name, datatype)
except LookupError: except LookupError:
raise SymbolError("no space in ZP left for more global 16-bit variables (try zp clobber)") raise SymbolError("no space in ZP left for global 16-bit word variable (try zp clobber)")
self.symbols[name] = VariableDef(self.name, name, sourceref, DataType.WORD, allocate, self.symbols[name] = VariableDef(self.name, name, sourceref, DataType.WORD, allocate,
value=value, length=1, address=address) value=value, length=1, address=address)
elif datatype == DataType.FLOAT: elif datatype == DataType.FLOAT:
if allocate and self.name == "ZP": if allocate and self.name == "ZP":
raise SymbolError("floats cannot be stored in the ZP") try:
address = self._zeropage.allocate(name, datatype)
except LookupError:
raise SymbolError("no space in ZP left for global 5-byte MFLT float variable (try zp clobber)")
self.symbols[name] = VariableDef(self.name, name, sourceref, DataType.FLOAT, allocate, self.symbols[name] = VariableDef(self.name, name, sourceref, DataType.FLOAT, allocate,
value=value, length=1, address=address) value=value, length=1, address=address)
elif datatype == DataType.BYTEARRAY: elif datatype == DataType.BYTEARRAY:

View File

@ -106,6 +106,11 @@ machine, in which case all of the zero page locations are suddenly available for
IL65 can generate a special routine that saves and restores the zero page to let your program run IL65 can generate a special routine that saves and restores the zero page to let your program run
and return safely back to the system afterwards - you don't have to take care of that yourself. and return safely back to the system afterwards - you don't have to take care of that yourself.
@todo some global way (in ZP block) to promote certian other blocks/variables from that block or even
subroutine to the zeropage. Don't do this in the block itself because it's a global optimization
and if blocks require it themselves you can't combine various modules anymore once ZP runs out.
DATA TYPES DATA TYPES
---------- ----------
@ -361,8 +366,9 @@ that is translated into a comparison (if needed) and then a conditional branch i
if[_XX] [<expression>] goto <label> if[_XX] [<expression>] goto <label>
The if-status XX is one of: [cc, cs, vc, vs, eq, ne, true, not, zero, lt, gt, le, ge] The if-status XX is one of: [cc, cs, vc, vs, eq, ne, true, not, zero, pos, neg, lt, gt, le, ge]
It defaults to 'true' (=='ne', not-zero) if omitted. @todo signed: pos, neg, lts==neg?, gts==eq+pos?, les==neg+eq?, ges==pos? It defaults to 'true' (=='ne', not-zero) if omitted. ('pos' will translate into 'pl', 'neg' into 'mi')
@todo signed: lts==neg?, gts==eq+pos?, les==neg+eq?, ges==pos?
The <expression> is optional. If it is provided, it will be evaluated first. Only the [true] and [not] and [zero] The <expression> is optional. If it is provided, it will be evaluated first. Only the [true] and [not] and [zero]
if-statuses can be used when such a *comparison expression* is used. An example is: if-statuses can be used when such a *comparison expression* is used. An example is:

53
tests/test_zp.py Normal file
View File

@ -0,0 +1,53 @@
import pytest
from il65.symbols import Zeropage, SymbolError, DataType
def test_zp_configure_onlyonce():
zp = Zeropage()
zp.configure()
with pytest.raises(SymbolError):
zp.configure()
def test_zp_names():
zp = Zeropage()
zp.configure()
zp.allocate("", DataType.BYTE)
zp.allocate("", DataType.BYTE)
zp.allocate("varname", DataType.BYTE)
with pytest.raises(AssertionError):
zp.allocate("varname", DataType.BYTE)
zp.allocate("varname2", DataType.BYTE)
def test_zp_noclobber_allocation():
zp = Zeropage()
zp.configure(False)
assert zp.available() == 9
with pytest.raises(LookupError):
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)
assert zp.available() == 0
with pytest.raises(LookupError):
zp.allocate("", DataType.BYTE)
with pytest.raises(LookupError):
zp.allocate("", DataType.WORD)
def test_zp_clobber_allocation():
zp = Zeropage()
zp.configure(True)
assert zp.available() == 248
loc = zp.allocate("", DataType.FLOAT)
assert loc > 3 and loc not in zp.free
num, rest = divmod(zp.available(), 5)
for _ in range(num):
zp.allocate("", DataType.FLOAT)
assert zp.available() == rest
for _ in range(rest // 2):
zp.allocate("", DataType.WORD)
assert zp.available() == 1
zp.allocate("last", DataType.BYTE)
with pytest.raises(LookupError):
zp.allocate("impossible", DataType.BYTE)

View File

@ -15,8 +15,15 @@ import "c64lib"
~ ZP $0004 { ~ ZP $0004 {
var zpvar1 var zpvar1
var zpvar2
memory zpmem1 = $f0 memory zpmem1 = $f0
const zpconst = 1.234 const zpconst = 1.234
var .word zpvarw1
var .word zpvarw2
var .float zpvarflt1 = 11.11
var .float zpvarflt2 = 22.22
var .float zpvarflt3
} }
~ ZP { ~ ZP {

View File

@ -1,11 +1,49 @@
output prg,basic output prg,basic
zp clobber, restore
;reg_preserve off ; @todo global option off/on default off? ;reg_preserve off ; @todo global option off/on default off?
~ main { import "c64lib"
; zpvar myvar @todo allow for zp vars like this ~ ZP {
var .float fl1 = 3.1415927
var .float fl2 = 99.999999
var .float fl3 = 100000
}
~ main {
var .float fl1 = 3.1415927
var .float fl2 = 99.999999
var .float fl3 = 10
start start
fl1 = 111111.22222
fl2 = 0
fl3 = 1
fl3 = -1
fl3 = 0.5
fl3 = -0.5
X=6
loop1
A=X
c64util.print_byte_hex(0, A)
c64.CHROUT!(" ")
X--
if_pos goto loop1
Y=6
loop2
A=Y
c64util.print_byte_hex(0, A)
c64.CHROUT!(" ")
Y--
if_neg goto stop
goto loop2
stop
return return
} }