diff --git a/il65/compile.py b/il65/compile.py index 63d26d31a..7790993a9 100644 --- a/il65/compile.py +++ b/il65/compile.py @@ -126,7 +126,7 @@ class PlyParser: block.address = 0xc000 elif directive.args[0] == "prg": block.format = ProgramFormat.PRG - block.address = 0x0801 + block.address = 0xc000 elif directive.args[0] == "basic": block.format = ProgramFormat.BASIC block.address = 0x0801 diff --git a/il65/datatypes.py b/il65/datatypes.py new file mode 100644 index 000000000..d1f3ae28c --- /dev/null +++ b/il65/datatypes.py @@ -0,0 +1,240 @@ +""" +Programming Language for 6502/6510 microprocessors, codename 'Sick' +Here are the data type definitions and -conversions. + +Written by Irmen de Jong (irmen@razorvine.net) - license: GNU GPL 3.0 +""" + +import math +import enum +from typing import Tuple, Union +from functools import total_ordering +from .plylex import print_warning, SourceRef + + +PrimitiveType = Union[int, float, str] + + +@total_ordering +class VarType(enum.Enum): + CONST = 1 + MEMORY = 2 + VAR = 3 + + def __lt__(self, other): + if self.__class__ == other.__class__: + return self.value < other.value + return NotImplemented + + +@total_ordering +class DataType(enum.Enum): + """The possible data types of values""" + BYTE = 1 + WORD = 2 + FLOAT = 3 + BYTEARRAY = 4 + WORDARRAY = 5 + MATRIX = 6 + STRING = 7 + STRING_P = 8 + STRING_S = 9 + STRING_PS = 10 + + def __lt__(self, other): + if self.__class__ == other.__class__: + return self.value < other.value + return NotImplemented + + +STRING_DATATYPES = {DataType.STRING, DataType.STRING_P, DataType.STRING_S, DataType.STRING_PS} + +# 5-byte cbm MFLPT format limitations: +FLOAT_MAX_POSITIVE = 1.7014118345e+38 +FLOAT_MAX_NEGATIVE = -1.7014118345e+38 + + +def to_hex(number: int) -> str: + # 0..255 -> "$00".."$ff" + # 256..65536 -> "$0100".."$ffff" + if number is None: + raise ValueError("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 + def verify_bounds(value: PrimitiveType) -> None: + # if the value is out of bounds, raise an overflow exception + if datatype == DataType.BYTE and not (0 <= value <= 0xff): # type: ignore + raise OverflowError("value out of range for byte") + if datatype == DataType.WORD and not (0 <= value <= 0xffff): # type: ignore + raise OverflowError("value out of range for word") + if datatype == DataType.FLOAT and not (FLOAT_MAX_NEGATIVE <= value <= FLOAT_MAX_POSITIVE): # type: ignore + raise OverflowError("value out of range for float") + + if datatype in (DataType.BYTE, DataType.BYTEARRAY, DataType.MATRIX) and isinstance(value, str): + if len(value) == 1: + return True, char_to_bytevalue(value) + # if we're an integer value and the passed value is float, truncate it (and give a warning) + if datatype in (DataType.BYTE, DataType.WORD, DataType.MATRIX) and isinstance(value, float): + frac = math.modf(value) + if frac != 0: + print_warning("float value truncated ({} to datatype {})".format(value, datatype.name), sourceref=sourceref) + value = int(value) + verify_bounds(value) + return True, value + verify_bounds(value) + return False, value + + +def char_to_bytevalue(character: str, petscii: bool=True) -> int: + assert len(character) == 1 + if petscii: + return ord(character.translate(ascii_to_petscii_trans)) + else: + raise NotImplementedError("screencode conversion not yet implemented for chars") + + +# ASCII/UNICODE-to-PETSCII translation table +# Unicode symbols supported that map to a PETSCII character: £ ↑ ← ♠ ♥ ♦ ♣ π ● ○ and various others +ascii_to_petscii_trans = str.maketrans({ + '\f': 147, # form feed becomes ClearScreen "{clear}" + '\n': 13, # line feed becomes a RETURN "{cr}" (not a line feed) + '\r': 17, # CR becomes CursorDown "{down}" + 'a': 65, + 'b': 66, + 'c': 67, + 'd': 68, + 'e': 69, + 'f': 70, + 'g': 71, + 'h': 72, + 'i': 73, + 'j': 74, + 'k': 75, + 'l': 76, + 'm': 77, + 'n': 78, + 'o': 79, + 'p': 80, + 'q': 81, + 'r': 82, + 's': 83, + 't': 84, + 'u': 85, + 'v': 86, + 'w': 87, + 'x': 88, + 'y': 89, + 'z': 90, + 'A': 97, + 'B': 98, + 'C': 99, + 'D': 100, + 'E': 101, + 'F': 102, + 'G': 103, + 'H': 104, + 'I': 105, + 'J': 106, + 'K': 107, + 'L': 108, + 'M': 109, + 'N': 110, + 'O': 111, + 'P': 112, + 'Q': 113, + 'R': 114, + 'S': 115, + 'T': 116, + 'U': 117, + 'V': 118, + 'W': 119, + 'X': 120, + 'Y': 121, + 'Z': 122, + '{': 179, # left squiggle + '}': 235, # right squiggle + '£': 92, # pound currency sign + '^': 94, # up arrow + '~': 126, # pi math symbol + 'π': 126, # pi symbol + '`': 39, # single quote + '✓': 250, # check mark + + '|': 221, # vertical bar + '│': 221, # vertical bar + '─': 96, # horizontal bar + '┼': 123, # vertical and horizontal bar + + '↑': 94, # up arrow + '←': 95, # left arrow + + '▔': 163, # upper bar + '_': 164, # lower bar (underscore) + '▁': 164, # lower bar + '▎': 165, # left bar + + '♠': 97, # spades + '●': 113, # circle + '♥': 115, # hearts + '○': 119, # open circle + '♣': 120, # clubs + '♦': 122, # diamonds + + '├': 171, # vertical and right + '┤': 179, # vertical and left + '┴': 177, # horiz and up + '┬': 178, # horiz and down + '└': 173, # up right + '┐': 174, # down left + '┌': 175, # down right + '┘': 189, # up left + '▗': 172, # block lr + '▖': 187, # block ll + '▝': 188, # block ur + '▘': 190, # block ul + '▚': 191, # block ul and lr + '▌': 161, # left half + '▄': 162, # lower half + '▒': 230, # raster +}) diff --git a/il65/generateasm.py b/il65/generateasm.py index 4487987cf..f60ce8afe 100644 --- a/il65/generateasm.py +++ b/il65/generateasm.py @@ -5,13 +5,13 @@ 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 subprocess import datetime -from typing import Union -from .plyparse import Module, ProgramFormat, Block, Directive, VarDef, Label, ZpOptions, DataType -from .symbols import to_hex +import itertools +from typing import Union, TextIO, List, Tuple, Iterator +from .plyparse import Module, ProgramFormat, Block, Directive, VarDef, Label, Subroutine, AstNode, ZpOptions +from .datatypes import VarType, DataType, to_hex, mflpt5_to_float, to_mflpt5, STRING_DATATYPES class CodeError(Exception): @@ -24,17 +24,20 @@ class AssemblyGenerator: def __init__(self, module: Module) -> None: self.module = module - self.generated_code = io.StringIO() + 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.generated_code, **vargs) + print(text.replace("\v", "\t\t"), *args, file=self.output, **vargs) def generate(self, filename: str) -> None: - self._generate() - with open(filename, "wt") as out: - out.write(self.generated_code.getvalue()) - self.generated_code.close() + 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() @@ -93,7 +96,7 @@ class AssemblyGenerator: zpblock = self.module.zeropage() if zpblock: vars_to_init = [v for v in zpblock.scope.filter_nodes(VarDef) - if v.allocate and v.type in (DataType.BYTE, DataType.WORD, DataType.FLOAT)] + if v.vartype == VarType.VAR and v.vartype 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] @@ -131,6 +134,7 @@ class AssemblyGenerator: self.p("\v; there are no zp vars to initialize") else: self.p("\v; there is no zp block to initialize") + # @todo all block vars should be (re)initialized here as well! if self.module.zp_options == ZpOptions.CLOBBER_RESTORE: self.p("\vjsr {:s}.start\v; call user code".format(self.module.main().label)) self.p("\vcld") @@ -142,13 +146,190 @@ class AssemblyGenerator: self.p("_float_bytes_{:s}\v.byte ${:02x}, ${:02x}, ${:02x}, ${:02x}, ${:02x}\t; {}".format(varname, *bytes, fpvalue)) self.p("\n") - def blocks(self): - self.p("; @todo") # @todo - pass + 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}' ----\t\t; src l. {:d}\n".format(zpblock.sourceref.file, zpblock.sourceref.line)) + self.p("{:s}\t.proc\n".format(zpblock.label)) + self.generate_block_vars(zpblock) + 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; ---- 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.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, Directive): + continue # should have been handled already + 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\t\t\t; clear decimal flag") + self.p("\vclc\t\t\t; clear carry flag") + self.p("\vclv\t\t\t; clear overflow flag") + 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 "", 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, 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): - self.p("; @todo") # @todo - pass + 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_vars(self, block: Block) -> None: + # @todo block vars should be re-initialized when the program is run again, and not depend on statically prefilled data! + vars_by_vartype = itertools.groupby(block.scope.filter_nodes(VarDef), lambda c: c.vartype) + variable_definitions = sorted(vars_by_vartype, key=lambda gt: gt[0]) # type: List[Tuple[VarType, Iterator[VarDef]]] + for vartype, varnodes in variable_definitions: + if vartype == VarType.CONST: + self.p("; constants") + for vardef in varnodes: + if vardef.datatype == DataType.FLOAT: + self.p("\t\t{:s} = {}".format(vardef.name, vardef.value)) + elif vardef.datatype in (DataType.BYTE, DataType.WORD): + self.p("\t\t{:s} = {:s}".format(vardef.name, to_hex(vardef.value))) + elif vardef.datatype in STRING_DATATYPES: + # a const string is just a string variable in the generated assembly + self._generate_string_var(vardef) + else: + raise CodeError("invalid const type", vardef) + elif vartype == VarType.MEMORY: + self.p("; memory mapped variables") + for vardef in varnodes: + # create a definition for variables at a specific place in memory (memory-mapped) + if vardef.datatype in (DataType.BYTE, DataType.WORD, DataType.FLOAT): + assert vardef.size == [1] + self.p("\t\t{: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("\t\t{: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("\t\t{: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) == 2 + self.p("\t\t{:s} = {:s}\t; matrix {:d} by {:d} = {:d} bytes" + .format(vardef.name, to_hex(vardef.value), vardef.size[0], vardef.size[1], vardef.size[0]*vardef.size[1])) + else: + raise CodeError("invalid var type") + elif vartype == VarType.VAR: + self.p("; normal variables") + for vardef in varnodes: + # create a definition for a variable that takes up 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}\t\t.byte {:s}".format(vardef.name, to_hex(int(vardef.value or 0)))) + elif vardef.datatype == DataType.WORD: + self.p("{:s}\t\t.word {:s}".format(vardef.name, to_hex(int(vardef.value or 0)))) + elif vardef.datatype == DataType.FLOAT: + self.p("{:s}\t\t.byte ${:02x}, ${:02x}, ${:02x}, ${:02x}, ${:02x}{:s}" + .format(vardef.name, *to_mflpt5(float(vardef.value or 0.0)))) + 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}\t\t.fill {:d}, ${:02x}".format(vardef.name, vardef.size[0], vardef.value or 0)) + elif vardef.datatype == 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.size[0] * 2, f_lo, f_hi, vardef.size[0], vardef.value or 0)) + else: + raise CodeError("invalid datatype", vardef.datatype) + elif vardef.datatype == DataType.MATRIX: + assert len(vardef.size) == 2 + self.p("{:s}\t\t.fill {:d}, ${:02x}\t\t; matrix {:d}*{:d} bytes" + .format(vardef.name, vardef.size[0] * vardef.size[1], vardef.value or 0, vardef.size[0], vardef.size[1])) + elif vardef.datatype in STRING_DATATYPES: + self._generate_string_var(vardef) + else: + raise CodeError("unknown variable type " + str(vardef.datatype)) + + 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)) + # @todo rest of the statement nodes class Assembler64Tass: @@ -164,7 +345,7 @@ class Assembler64Tass: elif self.format == ProgramFormat.RAW: args.append("--nostart") else: - raise ValueError("don't know how to create format "+str(self.format)) + raise CodeError("don't know how to create format "+str(self.format)) try: if self.format == ProgramFormat.PRG: print("\nCreating C-64 prg.") diff --git a/il65/plyparse.py b/il65/plyparse.py index 435bb51b7..940b49e50 100644 --- a/il65/plyparse.py +++ b/il65/plyparse.py @@ -11,7 +11,7 @@ from typing import Union, Generator, Tuple, List, Optional, Dict import attr from ply.yacc import yacc from .plylex import SourceRef, tokens, lexer, find_tok_column -from .symbols import DataType +from .datatypes import DataType, VarType, coerce_value class ProgramFormat(enum.Enum): @@ -71,7 +71,7 @@ class AstNode: def process_expressions(self) -> None: # process/simplify all expressions (constant folding etc) @todo - # override in node types that have expression(s) + # @todo override in node types that have expression(s) pass @@ -319,21 +319,45 @@ class VarDef(AstNode): vartype = attr.ib() datatype = attr.ib() value = attr.ib(default=None) - size = attr.ib(type=int, default=None) + size = attr.ib(type=list, default=None) def __attrs_post_init__(self): + # convert vartype to enum + if self.vartype == "const": + self.vartype = VarType.CONST + elif self.vartype == "var": + self.vartype = VarType.VAR + elif self.vartype == "memory": + self.vartype = VarType.MEMORY + else: + raise ValueError("invalid vartype", self.vartype) # convert datatype node to enum + size if self.datatype is None: assert self.size is None - self.size = 1 + self.size = [1] self.datatype = DataType.BYTE elif isinstance(self.datatype, DatatypeNode): assert self.size is None - self.size = self.datatype.dimensions + self.size = self.datatype.dimensions or [1] self.datatype = self.datatype.to_enum() # if the value is an expression, mark it as a *constant* expression here if isinstance(self.value, Expression): self.value.processed_must_be_constant = True + elif self.value is None and self.datatype in (DataType.BYTE, DataType.WORD, DataType.FLOAT): + self.value = 0 + # note: value coercion is done later, when all expressions are evaluated + + def process_expressions(self) -> None: + if isinstance(self.value, Expression): + # process/simplify all expressions (constant folding etc) # @todo + # verify that the expression yields a single constant value, replace value by that value # @todo + self.value = 123 # XXX + assert not isinstance(self.value, Expression) + if self.vartype in (VarType.CONST, VarType.VAR): + try: + _, self.value = coerce_value(self.datatype, self.value, self.sourceref) + except OverflowError as x: + raise ParseError(str(x), self.sourceref) from None @attr.s(cmp=False, slots=True, repr=False) @@ -359,8 +383,8 @@ class DatatypeNode(AstNode): @attr.s(cmp=False, repr=False) class Subroutine(AstNode): name = attr.ib(type=str) - param_spec = attr.ib() - result_spec = attr.ib() + param_spec = attr.ib(type=list) + result_spec = attr.ib(type=list) scope = attr.ib(type=Scope, default=None) address = attr.ib(type=int, default=None, validator=validate_address) @@ -638,9 +662,9 @@ def p_subroutine(p): """ body = p[10] if isinstance(body, Scope): - p[0] = Subroutine(name=p[2], param_spec=p[4], result_spec=p[8], scope=body, sourceref=_token_sref(p, 1)) + p[0] = Subroutine(name=p[2], param_spec=p[4] or [], result_spec=p[8] or [], scope=body, sourceref=_token_sref(p, 1)) elif isinstance(body, int): - p[0] = Subroutine(name=p[2], param_spec=p[4], result_spec=p[8], address=body, sourceref=_token_sref(p, 1)) + p[0] = Subroutine(name=p[2], param_spec=p[4] or [], result_spec=p[8] or [], address=body, sourceref=_token_sref(p, 1)) else: raise TypeError("subroutine_body", p.slice) diff --git a/il65/symbols.py b/il65/symbols.py deleted file mode 100644 index def9e2702..000000000 --- a/il65/symbols.py +++ /dev/null @@ -1,38 +0,0 @@ -""" -Programming Language for 6502/6510 microprocessors, codename 'Sick' -Here are the symbol (name) operations such as lookups, datatype definitions. - -Written by Irmen de Jong (irmen@razorvine.net) - license: GNU GPL 3.0 -""" - - -import enum - - -class DataType(enum.Enum): - """The possible data types of values""" - BYTE = 1 - WORD = 2 - FLOAT = 3 - BYTEARRAY = 4 - WORDARRAY = 5 - MATRIX = 6 - STRING = 7 - STRING_P = 8 - STRING_S = 9 - STRING_PS = 10 - - -STRING_DATATYPES = {DataType.STRING, DataType.STRING_P, DataType.STRING_S, DataType.STRING_PS} - - -def to_hex(number: int) -> str: - # 0..255 -> "$00".."$ff" - # 256..65536 -> "$0100".."$ffff" - if number is None: - raise ValueError("number") - if 0 <= number < 0x100: - return "${:02x}".format(number) - if 0 <= number < 0x10000: - return "${:04x}".format(number) - raise OverflowError(number) diff --git a/tests/test_core.py b/tests/test_core.py index 06490cc22..59f6f9734 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,11 +1,11 @@ import pytest -from il65.symbols import DataType, STRING_DATATYPES, to_hex +from il65 import datatypes from il65.compile import ParseError from il65.plylex import SourceRef def test_datatypes(): - assert all(isinstance(s, DataType) for s in STRING_DATATYPES) + assert all(isinstance(s, datatypes.DataType) for s in datatypes.STRING_DATATYPES) def test_sourceref(): @@ -22,13 +22,93 @@ def test_parseerror(): def test_to_hex(): - assert to_hex(0) == "$00" - assert to_hex(1) == "$01" - assert to_hex(255) == "$ff" - assert to_hex(256) == "$0100" - assert to_hex(20060) == "$4e5c" - assert to_hex(65535) == "$ffff" + assert datatypes.to_hex(0) == "$00" + assert datatypes.to_hex(1) == "$01" + assert datatypes.to_hex(255) == "$ff" + assert datatypes.to_hex(256) == "$0100" + assert datatypes.to_hex(20060) == "$4e5c" + assert datatypes.to_hex(65535) == "$ffff" with pytest.raises(OverflowError): - to_hex(-1) + datatypes.to_hex(-1) with pytest.raises(OverflowError): - to_hex(65536) + datatypes.to_hex(65536) + + +def test_float_to_mflpt5(): + mflpt = datatypes.to_mflpt5(1.0) + assert type(mflpt) is bytearray + assert b"\x00\x00\x00\x00\x00" == datatypes.to_mflpt5(0) + assert b"\x82\x49\x0F\xDA\xA1" == datatypes.to_mflpt5(3.141592653) + assert b"\x82\x49\x0F\xDA\xA2" == datatypes.to_mflpt5(3.141592653589793) + assert b"\x90\x80\x00\x00\x00" == datatypes.to_mflpt5(-32768) + assert b"\x81\x00\x00\x00\x00" == datatypes.to_mflpt5(1) + assert b"\x80\x35\x04\xF3\x34" == datatypes.to_mflpt5(0.7071067812) + assert b"\x80\x35\x04\xF3\x33" == datatypes.to_mflpt5(0.7071067811865476) + assert b"\x81\x35\x04\xF3\x34" == datatypes.to_mflpt5(1.4142135624) + assert b"\x81\x35\x04\xF3\x33" == datatypes.to_mflpt5(1.4142135623730951) + assert b"\x80\x80\x00\x00\x00" == datatypes.to_mflpt5(-.5) + assert b"\x80\x31\x72\x17\xF8" == datatypes.to_mflpt5(0.69314718061) + assert b"\x80\x31\x72\x17\xF7" == datatypes.to_mflpt5(0.6931471805599453) + assert b"\x84\x20\x00\x00\x00" == datatypes.to_mflpt5(10) + assert b"\x9E\x6E\x6B\x28\x00" == datatypes.to_mflpt5(1000000000) + assert b"\x80\x00\x00\x00\x00" == datatypes.to_mflpt5(.5) + assert b"\x81\x38\xAA\x3B\x29" == datatypes.to_mflpt5(1.4426950408889634) + assert b"\x81\x49\x0F\xDA\xA2" == datatypes.to_mflpt5(1.5707963267948966) + assert b"\x83\x49\x0F\xDA\xA2" == datatypes.to_mflpt5(6.283185307179586) + assert b"\x7F\x00\x00\x00\x00" == datatypes.to_mflpt5(.25) + + +def test_float_range(): + assert b"\xff\x7f\xff\xff\xff" == datatypes.to_mflpt5(datatypes.FLOAT_MAX_POSITIVE) + assert b"\xff\xff\xff\xff\xff" == datatypes.to_mflpt5(datatypes.FLOAT_MAX_NEGATIVE) + with pytest.raises(OverflowError): + datatypes.to_mflpt5(1.7014118346e+38) + with pytest.raises(OverflowError): + datatypes.to_mflpt5(-1.7014118346e+38) + with pytest.raises(OverflowError): + datatypes.to_mflpt5(1.7014118347e+38) + with pytest.raises(OverflowError): + datatypes.to_mflpt5(-1.7014118347e+38) + assert b"\x03\x39\x1d\x15\x63" == datatypes.to_mflpt5(1.7e-38) + assert b"\x00\x00\x00\x00\x00" == datatypes.to_mflpt5(1.7e-39) + assert b"\x03\xb9\x1d\x15\x63" == datatypes.to_mflpt5(-1.7e-38) + assert b"\x00\x00\x00\x00\x00" == datatypes.to_mflpt5(-1.7e-39) + + +def test_char_to_bytevalue(): + assert datatypes.char_to_bytevalue('a') == 65 + assert datatypes.char_to_bytevalue('\n') == 13 + assert datatypes.char_to_bytevalue('π') == 126 + assert datatypes.char_to_bytevalue('▒') == 230 + assert datatypes.char_to_bytevalue('\x00') == 0 + assert datatypes.char_to_bytevalue('\xff') == 255 + with pytest.raises(AssertionError): + datatypes.char_to_bytevalue('') + # screencodes not yet implemented: assert datatypes.char_to_bytevalue('a', False) == 65 + + +def test_coerce_value(): + assert datatypes.coerce_value(datatypes.DataType.BYTE, 0) == (False, 0) + assert datatypes.coerce_value(datatypes.DataType.BYTE, 255) == (False, 255) + assert datatypes.coerce_value(datatypes.DataType.WORD, 0) == (False, 0) + assert datatypes.coerce_value(datatypes.DataType.WORD, 65535) == (False, 65535) + assert datatypes.coerce_value(datatypes.DataType.FLOAT, -999.22) == (False, -999.22) + assert datatypes.coerce_value(datatypes.DataType.FLOAT, 123.45) == (False, 123.45) + assert datatypes.coerce_value(datatypes.DataType.BYTE, 5.678) == (True, 5) + assert datatypes.coerce_value(datatypes.DataType.WORD, 5.678) == (True, 5) + with pytest.raises(OverflowError): + datatypes.coerce_value(datatypes.DataType.BYTE, -1) + with pytest.raises(OverflowError): + datatypes.coerce_value(datatypes.DataType.BYTE, 256) + with pytest.raises(OverflowError): + datatypes.coerce_value(datatypes.DataType.BYTE, 256.12345) + with pytest.raises(OverflowError): + datatypes.coerce_value(datatypes.DataType.WORD, -1) + with pytest.raises(OverflowError): + datatypes.coerce_value(datatypes.DataType.WORD, 65536) + with pytest.raises(OverflowError): + datatypes.coerce_value(datatypes.DataType.WORD, 65536.12345) + with pytest.raises(OverflowError): + datatypes.coerce_value(datatypes.DataType.FLOAT, -1.7014118346e+38) + with pytest.raises(OverflowError): + datatypes.coerce_value(datatypes.DataType.FLOAT, 1.7014118347e+38) diff --git a/tests/test_floats.py b/tests/test_floats.py deleted file mode 100644 index 86927a1fb..000000000 --- a/tests/test_floats.py +++ /dev/null @@ -1,45 +0,0 @@ -import pytest -from il65.handwritten import symbols, codegen - - -def test_float_to_mflpt5(): - mflpt = codegen.CodeGenerator.to_mflpt5(1.0) - assert type(mflpt) is bytearray - assert b"\x00\x00\x00\x00\x00" == codegen.CodeGenerator.to_mflpt5(0) - assert b"\x82\x49\x0F\xDA\xA1" == codegen.CodeGenerator.to_mflpt5(3.141592653) - assert b"\x82\x49\x0F\xDA\xA2" == codegen.CodeGenerator.to_mflpt5(3.141592653589793) - assert b"\x90\x80\x00\x00\x00" == codegen.CodeGenerator.to_mflpt5(-32768) - assert b"\x81\x00\x00\x00\x00" == codegen.CodeGenerator.to_mflpt5(1) - assert b"\x80\x35\x04\xF3\x34" == codegen.CodeGenerator.to_mflpt5(0.7071067812) - assert b"\x80\x35\x04\xF3\x33" == codegen.CodeGenerator.to_mflpt5(0.7071067811865476) - assert b"\x81\x35\x04\xF3\x34" == codegen.CodeGenerator.to_mflpt5(1.4142135624) - assert b"\x81\x35\x04\xF3\x33" == codegen.CodeGenerator.to_mflpt5(1.4142135623730951) - assert b"\x80\x80\x00\x00\x00" == codegen.CodeGenerator.to_mflpt5(-.5) - assert b"\x80\x31\x72\x17\xF8" == codegen.CodeGenerator.to_mflpt5(0.69314718061) - assert b"\x80\x31\x72\x17\xF7" == codegen.CodeGenerator.to_mflpt5(0.6931471805599453) - assert b"\x84\x20\x00\x00\x00" == codegen.CodeGenerator.to_mflpt5(10) - assert b"\x9E\x6E\x6B\x28\x00" == codegen.CodeGenerator.to_mflpt5(1000000000) - assert b"\x80\x00\x00\x00\x00" == codegen.CodeGenerator.to_mflpt5(.5) - assert b"\x81\x38\xAA\x3B\x29" == codegen.CodeGenerator.to_mflpt5(1.4426950408889634) - assert b"\x81\x49\x0F\xDA\xA2" == codegen.CodeGenerator.to_mflpt5(1.5707963267948966) - assert b"\x83\x49\x0F\xDA\xA2" == codegen.CodeGenerator.to_mflpt5(6.283185307179586) - assert b"\x7F\x00\x00\x00\x00" == codegen.CodeGenerator.to_mflpt5(.25) - - -def test_float_range(): - assert b"\xff\x7f\xff\xff\xff" == codegen.CodeGenerator.to_mflpt5(symbols.FLOAT_MAX_POSITIVE) - assert b"\xff\xff\xff\xff\xff" == codegen.CodeGenerator.to_mflpt5(symbols.FLOAT_MAX_NEGATIVE) - with pytest.raises(OverflowError): - codegen.CodeGenerator.to_mflpt5(1.7014118346e+38) - with pytest.raises(OverflowError): - codegen.CodeGenerator.to_mflpt5(-1.7014118346e+38) - with pytest.raises(OverflowError): - codegen.CodeGenerator.to_mflpt5(1.7014118347e+38) - with pytest.raises(OverflowError): - codegen.CodeGenerator.to_mflpt5(-1.7014118347e+38) - assert b"\x03\x39\x1d\x15\x63" == codegen.CodeGenerator.to_mflpt5(1.7e-38) - assert b"\x00\x00\x00\x00\x00" == codegen.CodeGenerator.to_mflpt5(1.7e-39) - assert b"\x03\xb9\x1d\x15\x63" == codegen.CodeGenerator.to_mflpt5(-1.7e-38) - assert b"\x00\x00\x00\x00\x00" == codegen.CodeGenerator.to_mflpt5(-1.7e-39) - - diff --git a/tests/test_zp.py b/tests/test_zp.py index 167d32f9a..7d8a29100 100644 --- a/tests/test_zp.py +++ b/tests/test_zp.py @@ -1,5 +1,5 @@ import pytest -from il65.handwritten.symbols import Zeropage, SymbolError, DataType +from il65.handwritten.symbols import Zeropage, SymbolError, DataType # @todo def test_zp_configure_onlyonce(): diff --git a/todo.ill b/todo.ill index 32dea9694..54f9748db 100644 --- a/todo.ill +++ b/todo.ill @@ -1,19 +1,18 @@ -%output prg +%output basic %saveregisters %import c64lib %import mathlib -%address 22222 -~ main $4444 { +~ main { %saveregisters true const num = 2 var var1 =2 - var .word wvar1 = 2 + foo() ; @todo constant + var .word wvar1 = 2 + foo() ; @todo constant check error start: