diff --git a/il65/compile.py b/il65/compile.py index 4df3a8da1..ae92ecca1 100644 --- a/il65/compile.py +++ b/il65/compile.py @@ -16,7 +16,7 @@ from .plyparse import parse_file, ParseError, Module, Directive, Block, Subrouti SymbolName, Dereference, AddressOf from .plylex import SourceRef, print_bold from .optimize import optimize -from .datatypes import DataType, STRING_DATATYPES +from .datatypes import DataType, VarType, STRING_DATATYPES class CompileError(Exception): @@ -97,7 +97,7 @@ class PlyParser: for block, parent in module.all_scopes(): parentname = (parent.name + ".") if parent else "" blockname = parentname + block.name - if blockname in encountered_blocks: + if blockname in encountered_blocks: raise ValueError("block names not unique:", blockname) encountered_blocks.add(blockname) for node in block.nodes: @@ -429,6 +429,7 @@ class Zeropage: def allocate(self, vardef: VarDef) -> int: assert not vardef.name or vardef.name not in {a[0] for a in self.allocations.values()}, "var name is not unique" + assert vardef.vartype == VarType.VAR, "can only allocate var" def sequential_free(location: int) -> bool: return all(location + i in self.free for i in range(size)) diff --git a/il65/generateasm.py b/il65/generateasm.py index 72cdaf48c..96ed01c27 100644 --- a/il65/generateasm.py +++ b/il65/generateasm.py @@ -245,7 +245,7 @@ class AssemblyGenerator: self.p("_il65_init_block\v; (re)set vars to initial values") float_inits = {} - string_inits = [] + string_inits = [] # type: List[VarDef] prev_value_a, prev_value_x = None, None vars_by_datatype = defaultdict(list) # type: Dict[DataType, List[VarDef]] for vardef in block.scope.filter_nodes(VarDef): diff --git a/il65/handwritten/__init__.py b/il65/handwritten/__init__.py deleted file mode 100644 index 5bb534f79..000000000 --- a/il65/handwritten/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# package diff --git a/il65/handwritten/exprparse.py b/il65/handwritten/exprparse.py deleted file mode 100644 index 421cbfa8b..000000000 --- a/il65/handwritten/exprparse.py +++ /dev/null @@ -1,284 +0,0 @@ -""" -Programming Language for 6502/6510 microprocessors -This is the expression parser/evaluator. - -Written by Irmen de Jong (irmen@razorvine.net) - license: GNU GPL 3.0 -""" - -import ast -import attr -from typing import Union, Optional, List, Tuple, Any -from .symbols import FLOAT_MAX_POSITIVE, FLOAT_MAX_NEGATIVE, SourceRef, SymbolTable, SymbolError, PrimitiveType - - -class ParseError(Exception): - def __init__(self, message: str, sourcetext: Optional[str], sourceref: SourceRef) -> None: - self.sourceref = sourceref - self.msg = message - self.sourcetext = sourcetext - - def __str__(self): - return "{} {:s}".format(self.sourceref, self.msg) - - -class SourceLine: - def __init__(self, text: str, sourceref: SourceRef) -> None: - self.sourceref = sourceref - self.text = text.strip() - - def to_error(self, message: str) -> ParseError: - return ParseError(message, self.text, self.sourceref) - - def preprocess(self) -> str: - # transforms the source text into valid Python syntax by bending some things, so ast can parse it. - # $d020 -> 0xd020 - # %101001 -> 0xb101001 - # #something -> __ptr@something (matmult operator) - text = "" - quotes_stack = "" - characters = enumerate(self.text + " ") - for i, c in characters: - if c in ("'", '"'): - if quotes_stack and quotes_stack[-1] == c: - quotes_stack = quotes_stack[:-1] - else: - quotes_stack += c - text += c - continue - if not quotes_stack: - if c == '%' and self.text[i + 1] in "01": - text += "0b" - continue - if c == '$' and self.text[i + 1] in "0123456789abcdefABCDEF": - text += "0x" - continue - if c == '&': - if i > 0: - text += " " - text += "__ptr@" - continue - text += c - return text - - -def parse_arguments(text: str, sourceref: SourceRef) -> List[Tuple[str, PrimitiveType]]: - src = SourceLine(text, sourceref) - text = src.preprocess() - try: - nodes = ast.parse("__func({:s})".format(text), sourceref.file, "eval") - except SyntaxError as x: - raise src.to_error(str(x)) - - args = [] # type: List[Tuple[str, Any]] - if isinstance(nodes, ast.Expression): - for arg in nodes.body.args: - reprvalue = astnode_to_repr(arg) - args.append((None, reprvalue)) - for kwarg in nodes.body.keywords: - reprvalue = astnode_to_repr(kwarg.value) - args.append((kwarg.arg, reprvalue)) - return args - else: - raise TypeError("ast.Expression expected") - - -def parse_expr_as_comparison(text: str, sourceref: SourceRef) -> Tuple[str, str, str]: - src = SourceLine(text, sourceref) - text = src.preprocess() - try: - node = ast.parse(text, sourceref.file, mode="eval") - except SyntaxError as x: - raise src.to_error(str(x)) - if not isinstance(node, ast.Expression): - raise TypeError("ast.Expression expected") - if isinstance(node.body, ast.Compare): - if len(node.body.ops) != 1: - raise src.to_error("only one comparison operator at a time is supported") - operator = { - "Eq": "==", - "NotEq": "!=", - "Lt": "<", - "LtE": "<=", - "Gt": ">", - "GtE": ">=", - "Is": None, - "IsNot": None, - "In": None, - "NotIn": None - }[node.body.ops[0].__class__.__name__] - if not operator: - raise src.to_error("unsupported comparison operator") - left = text[node.body.left.col_offset:node.body.comparators[0].col_offset].rstrip()[:-len(operator)] - right = text[node.body.comparators[0].col_offset:] - return left.strip(), operator, right.strip() - left = astnode_to_repr(node.body) - return left, "", "" - - -def parse_expr_as_int(text: str, context: Optional[SymbolTable], ppcontext: Optional[SymbolTable], sourceref: SourceRef, *, - minimum: int=0, maximum: int=0xffff) -> int: - result = parse_expr_as_primitive(text, context, ppcontext, sourceref, minimum=minimum, maximum=maximum) - if isinstance(result, int): - return result - src = SourceLine(text, sourceref) - raise src.to_error("int expected, not " + type(result).__name__) - - -def parse_expr_as_number(text: str, context: Optional[SymbolTable], ppcontext: Optional[SymbolTable], sourceref: SourceRef, *, - minimum: float=FLOAT_MAX_NEGATIVE, maximum: float=FLOAT_MAX_POSITIVE) -> Union[int, float]: - result = parse_expr_as_primitive(text, context, ppcontext, sourceref, minimum=minimum, maximum=maximum) - if isinstance(result, (int, float)): - return result - src = SourceLine(text, sourceref) - raise src.to_error("int or float expected, not " + type(result).__name__) - - -def parse_expr_as_string(text: str, context: Optional[SymbolTable], ppcontext: Optional[SymbolTable], sourceref: SourceRef) -> str: - result = parse_expr_as_primitive(text, context, ppcontext, sourceref) - if isinstance(result, str): - return result - src = SourceLine(text, sourceref) - raise src.to_error("string expected, not " + type(result).__name__) - - -def parse_expr_as_primitive(text: str, context: Optional[SymbolTable], ppcontext: Optional[SymbolTable], sourceref: SourceRef, *, - minimum: float = FLOAT_MAX_NEGATIVE, maximum: float = FLOAT_MAX_POSITIVE) -> PrimitiveType: - src = SourceLine(text, sourceref) - text = src.preprocess() - try: - node = ast.parse(text, sourceref.file, mode="eval") - except SyntaxError as x: - raise src.to_error(str(x)) - if isinstance(node, ast.Expression): - result = ExpressionTransformer(src, context, ppcontext).evaluate(node) - else: - raise TypeError("ast.Expression expected") - if isinstance(result, bool): - return int(result) - if isinstance(result, (int, float)): - if minimum <= result <= maximum: - return result - raise src.to_error("number too large") - if isinstance(result, str): - return result - raise src.to_error("int or float or string expected, not " + type(result).__name__) - - -class EvaluatingTransformer(ast.NodeTransformer): - def __init__(self, src: SourceLine, context: SymbolTable, ppcontext: SymbolTable) -> None: - super().__init__() - self.src = src - self.context = context - self.ppcontext = ppcontext - - def error(self, message: str, column: int=0) -> ParseError: - ref = attr.evolve(self.src.sourceref, column=column) - return ParseError(message, self.src.text, ref) - - def evaluate(self, node: ast.Expression) -> PrimitiveType: - node = self.visit(node) - code = compile(node, self.src.sourceref.file, mode="eval") - if self.context: - globals = None - locals = self.context.as_eval_dict(self.ppcontext) - else: - globals = {"__builtins__": {}} - locals = None - try: - result = eval(code, globals, locals) # XXX unsafe... - except Exception as x: - raise self.src.to_error(str(x)) from x - else: - if type(result) is bool: - return int(result) - return result - - -class ExpressionTransformer(EvaluatingTransformer): - def _dotted_name_from_attr(self, node: ast.Attribute) -> str: - if isinstance(node.value, ast.Name): - return node.value.id + '.' + node.attr - if isinstance(node.value, ast.Attribute): - return self._dotted_name_from_attr(node.value) + '.' + node.attr - raise self.error("dotted name error") - - def visit_Name(self, node: ast.Name): - # convert true/false names to True/False constants - if node.id == "true": - return ast.copy_location(ast.NameConstant(True), node) - if node.id == "false": - return ast.copy_location(ast.NameConstant(False), node) - return node - - def visit_UnaryOp(self, node): - if isinstance(node.operand, ast.Num): - if isinstance(node.op, ast.USub): - node = self.generic_visit(node) - return ast.copy_location(ast.Num(-node.operand.n), node) - if isinstance(node.op, ast.UAdd): - node = self.generic_visit(node) - return ast.copy_location(ast.Num(node.operand.n), node) - if isinstance(node.op, ast.Invert): - if isinstance(node.operand, ast.Num): - node = self.generic_visit(node) - return ast.copy_location(ast.Num(~node.operand.n), node) - else: - raise self.error("can only bitwise invert a number") - raise self.error("expected unary + or - or ~") - elif isinstance(node.operand, ast.UnaryOp): - # nested unary ops, for instance: "~-2" = invert(minus(2)) - node = self.generic_visit(node) - return self.visit_UnaryOp(node) - else: - print(node.operand) - raise self.error("expected constant numeric operand for unary operator") - - def visit_BinOp(self, node): - node = self.generic_visit(node) - if isinstance(node.op, ast.MatMult): - if isinstance(node.left, ast.Name) and node.left.id == "__ptr": - if isinstance(node.right, ast.Attribute): - symbolname = self._dotted_name_from_attr(node.right) - elif isinstance(node.right, ast.Name): - symbolname = node.right.id - else: - raise self.error("can only take address of a named variable") - try: - address = self.context.get_address(symbolname) - except SymbolError as x: - raise self.error(str(x)) - else: - return ast.copy_location(ast.Num(address), node) - else: - raise self.error("invalid MatMult/Pointer node in AST") - return node - - -def astnode_to_repr(node: ast.AST) -> str: - if isinstance(node, ast.Name): - return node.id - if isinstance(node, ast.Num): - return repr(node.n) - if isinstance(node, ast.Str): - return repr(node.s) - if isinstance(node, ast.BinOp): - if node.left.id == "__ptr" and isinstance(node.op, ast.MatMult): # type: ignore - return '&' + astnode_to_repr(node.right) - else: - print("error", ast.dump(node)) - raise TypeError("invalid arg ast node type", node) - if isinstance(node, ast.Attribute): - return astnode_to_repr(node.value) + "." + node.attr - if isinstance(node, ast.UnaryOp): - if isinstance(node.op, ast.USub): - return "-" + astnode_to_repr(node.operand) - if isinstance(node.op, ast.UAdd): - return "+" + astnode_to_repr(node.operand) - if isinstance(node.op, ast.Invert): - return "~" + astnode_to_repr(node.operand) - if isinstance(node.op, ast.Not): - return "not " + astnode_to_repr(node.operand) - if isinstance(node, ast.List): - # indirect values get turned into a list... - return "[" + ",".join(astnode_to_repr(elt) for elt in node.elts) + "]" - raise TypeError("invalid arg ast node type", node) diff --git a/il65/handwritten/optimize.py b/il65/handwritten/optimize.py deleted file mode 100644 index 0368eab4d..000000000 --- a/il65/handwritten/optimize.py +++ /dev/null @@ -1,154 +0,0 @@ -""" -Programming Language for 6502/6510 microprocessors -This is the code to optimize the parse tree. - -Written by Irmen de Jong (irmen@razorvine.net) - license: GNU GPL 3.0 -""" - -from typing import List -from .parse import ParseResult -from .symbols import Block, AugmentedAssignmentStmt, IntegerValue, FloatValue, AssignmentStmt, CallStmt, \ - Value, MemMappedValue, RegisterValue, AstNode - - -class Optimizer: - def __init__(self, parseresult: ParseResult) -> None: - self.parsed = parseresult - - def optimize(self) -> ParseResult: - print("\noptimizing parse tree") - for block in self.parsed.all_blocks(): - self.remove_augmentedassign_incrdecr_nops(block) - self.remove_identity_assigns(block) - self.combine_assignments_into_multi(block) - self.optimize_multiassigns(block) - self.remove_unused_subroutines(block) - self.optimize_compare_with_zero(block) - return self.parsed - - def remove_augmentedassign_incrdecr_nops(self, block: Block) -> None: - have_removed_stmts = False - for index, stmt in enumerate(list(block.statements)): - if isinstance(stmt, AugmentedAssignmentStmt): - if isinstance(stmt.right, (IntegerValue, FloatValue)): - if stmt.right.value == 0 and stmt.operator in ("+=", "-=", "|=", "<<=", ">>=", "^="): - print("{}: removed statement that has no effect".format(stmt.sourceref)) - have_removed_stmts = True - block.statements[index] = None - if stmt.right.value >= 8 and stmt.operator in ("<<=", ">>="): - print("{}: shifting that many times always results in zero".format(stmt.sourceref)) - new_stmt = AssignmentStmt(stmt.leftvalues, IntegerValue(0, stmt.sourceref), stmt.sourceref) - block.statements[index] = new_stmt - if have_removed_stmts: - # remove the Nones - block.statements = [s for s in block.statements if s is not None] - - def optimize_compare_with_zero(self, block: Block) -> None: - # a conditional goto that compares a value to zero will be simplified - # the comparison operator and rvalue (0) will be removed and the if-status changed accordingly - for stmt in block.statements: - if isinstance(stmt, CallStmt): - cond = stmt.condition - if cond and isinstance(cond.rvalue, (IntegerValue, FloatValue)) and cond.rvalue.value == 0: - simplified = False - if cond.ifstatus in ("true", "ne"): - if cond.comparison_op == "==": - # if_true something == 0 -> if_not something - cond.ifstatus = "not" - cond.comparison_op, cond.rvalue = "", None - simplified = True - elif cond.comparison_op == "!=": - # if_true something != 0 -> if_true something - cond.comparison_op, cond.rvalue = "", None - simplified = True - elif cond.ifstatus in ("not", "eq"): - if cond.comparison_op == "==": - # if_not something == 0 -> if_true something - cond.ifstatus = "true" - cond.comparison_op, cond.rvalue = "", None - simplified = True - elif cond.comparison_op == "!=": - # if_not something != 0 -> if_not something - cond.comparison_op, cond.rvalue = "", None - simplified = True - if simplified: - print("{}: simplified comparison with zero".format(stmt.sourceref)) - - def combine_assignments_into_multi(self, block: Block) -> None: - # fold multiple consecutive assignments with the same rvalue into one multi-assignment - statements = [] # type: List[AstNode] - multi_assign_statement = None - for stmt in block.statements: - if isinstance(stmt, AssignmentStmt) and not isinstance(stmt, AugmentedAssignmentStmt): - if multi_assign_statement and multi_assign_statement.right == stmt.right: - multi_assign_statement.leftvalues.extend(stmt.leftvalues) - print("{}: joined with previous line into multi-assign statement".format(stmt.sourceref)) - else: - if multi_assign_statement: - statements.append(multi_assign_statement) - multi_assign_statement = stmt - else: - if multi_assign_statement: - statements.append(multi_assign_statement) - multi_assign_statement = None - statements.append(stmt) - if multi_assign_statement: - statements.append(multi_assign_statement) - block.statements = statements - - def optimize_multiassigns(self, block: Block) -> None: - # optimize multi-assign statements. - for stmt in block.statements: - if isinstance(stmt, AssignmentStmt) and len(stmt.leftvalues) > 1: - # remove duplicates - lvalues = list(set(stmt.leftvalues)) - if len(lvalues) != len(stmt.leftvalues): - print("{}: removed duplicate assignment targets".format(stmt.sourceref)) - # change order: first registers, then zp addresses, then non-zp addresses, then the rest (if any) - stmt.leftvalues = list(sorted(lvalues, key=_value_sortkey)) - - def remove_identity_assigns(self, block: Block) -> None: - have_removed_stmts = False - for index, stmt in enumerate(list(block.statements)): - if isinstance(stmt, AssignmentStmt): - stmt.remove_identity_lvalues() - if not stmt.leftvalues: - print("{}: removed identity assignment statement".format(stmt.sourceref)) - have_removed_stmts = True - block.statements[index] = None - if have_removed_stmts: - # remove the Nones - block.statements = [s for s in block.statements if s is not None] - - def remove_unused_subroutines(self, block: Block) -> None: - # some symbols are used by the emitted assembly code from the code generator, - # and should never be removed or the assembler will fail - never_remove = {"c64.FREADUY", "c64.FTOMEMXY", "c64.FADD", "c64.FSUB", - "c64flt.GIVUAYF", "c64flt.copy_mflt", "c64flt.float_add_one", "c64flt.float_sub_one", - "c64flt.float_add_SW1_to_XY", "c64flt.float_sub_SW1_from_XY"} - discarded = [] - for sub in list(block.symbols.iter_subroutines()): - usages = self.parsed.subroutine_usage[(sub.blockname, sub.name)] - if not usages and sub.blockname + '.' + sub.name not in never_remove: - block.symbols.remove_node(sub.name) - discarded.append(sub.name) - if discarded: - print("{}: discarded {:d} unused subroutines from block '{:s}'".format(block.sourceref, len(discarded), block.name)) - - -def _value_sortkey(value: Value) -> int: - if isinstance(value, RegisterValue): - num = 0 - for char in value.register: - num *= 100 - num += ord(char) - return num - elif isinstance(value, MemMappedValue): - if value.address is None: - return 99999999 - if value.address < 0x100: - return 10000 + value.address - else: - return 20000 + value.address - else: - return 99999999 diff --git a/il65/handwritten/parse.py b/il65/handwritten/parse.py deleted file mode 100644 index 3e669e280..000000000 --- a/il65/handwritten/parse.py +++ /dev/null @@ -1,1365 +0,0 @@ -""" -Programming Language for 6502/6510 microprocessors -This is the hand-written parser of the IL65 code, that generates a parse tree. - -Written by Irmen de Jong (irmen@razorvine.net) - license: GNU GPL 3.0 -""" - -import re -import os -import sys -import shutil -from collections import defaultdict -from .exprparse import ParseError, parse_expr_as_int, parse_expr_as_number, parse_expr_as_primitive,\ - parse_expr_as_string, parse_arguments, parse_expr_as_comparison -from .symbols import * - - -class ProgramFormat(enum.Enum): - PRG = "prg" - RAW = "raw" - - -class ParseResult: - def __init__(self, sourcefile: str) -> None: - self.format = ProgramFormat.RAW - self.with_sys = False - self.sourcefile = sourcefile - self.clobberzp = False - self.restorezp = False - self.start_address = 0 - self.blocks = [] # type: List[Block] - self.subroutine_usage = defaultdict(set) # type: Dict[Tuple[str, str], Set[str]] - self.zeropage = Zeropage() - self.preserve_registers = False - - def all_blocks(self) -> Generator[Block, None, None]: - for block in self.blocks: - yield block - for sub in block.symbols.iter_subroutines(True): - yield sub.sub_block - - def add_block(self, block: Block, position: Optional[int]=None) -> None: - if position is not None: - self.blocks.insert(position, block) - else: - self.blocks.append(block) - - def merge(self, parsed: 'ParseResult') -> None: - existing_blocknames = set(block.name for block in self.blocks) - other_blocknames = set(block.name for block in parsed.blocks) - overlap = existing_blocknames & other_blocknames - if overlap != {"
"}: - raise SymbolError("double block names: {}".format(overlap)) - for block in parsed.blocks: - if block.name != "
": - self.blocks.append(block) - - def find_block(self, name: str) -> Block: - for block in self.blocks: - if block.name == name: - return block - raise KeyError("block not found: " + name) - - def sub_used_by(self, sub: SubroutineDef, sourceref: SourceRef) -> None: - self.subroutine_usage[(sub.blockname, sub.name)].add(str(sourceref)) - - -class Parser: - def __init__(self, filename: str, outputdir: str, existing_imports: Set[str], parsing_import: bool = False, - sourcelines: List[Tuple[int, str]] = None, ppsymbols: SymbolTable = None, sub_usage: Dict=None) -> None: - self.result = ParseResult(filename) - if sub_usage is not None: - # re-use the (global) subroutine usage tracking - self.result.subroutine_usage = sub_usage - self.sourceref = SourceRef(filename, -1, 0) # type: ignore - if sourcelines: - self.lines = sourcelines - else: - self.lines = self.load_source(filename) - self.outputdir = outputdir - self.parsing_import = parsing_import # are we parsing a import file? - self._cur_lineidx = -1 # used to efficiently go to next/previous line in source - self.cur_block = None # type: Block - self.root_scope = SymbolTable("", None, None) - self.root_scope.set_zeropage(self.result.zeropage) - self.ppsymbols = ppsymbols # symboltable from preprocess phase - self.print_block_parsing = True - self.existing_imports = existing_imports - self.parse_errors = 0 - - def load_source(self, filename: str) -> List[Tuple[int, str]]: - with open(filename, "rU") as source: - sourcelines = source.readlines() - # store all lines that aren't empty - # comments are kept (end-of-line comments are stripped though) - lines = [] - for num, line in enumerate(sourcelines, start=1): - line = line.rstrip() - if line.lstrip().startswith(';'): - lines.append((num, line.lstrip())) - else: - line2, sep, comment = line.rpartition(';') - if sep: - line = line2.rstrip() - if line: - lines.append((num, line)) - return lines - - def parse(self) -> Optional[ParseResult]: - # start the parsing - try: - result = self.parse_file() - except ParseError as x: - self.handle_parse_error(x) - except Exception as x: - if sys.stderr.isatty(): - print("\x1b[1m", file=sys.stderr) - print("\nERROR: internal parser error: ", x, file=sys.stderr) - if self.cur_block: - print(" file:", self.sourceref.file, "block:", self.cur_block.name, "line:", self.sourceref.line, file=sys.stderr) - else: - print(" file:", self.sourceref.file, file=sys.stderr) - if sys.stderr.isatty(): - print("\x1b[0m", file=sys.stderr, end="", flush=True) - raise - if self.parse_errors: - self.print_bold("\nNo output; there were {:d} errors in file {:s}\n".format(self.parse_errors, self.sourceref.file)) - raise SystemExit(1) - return result - - def handle_parse_error(self, exc: ParseError) -> None: - self.parse_errors += 1 - if sys.stderr.isatty(): - print("\x1b[1m", file=sys.stderr) - if exc.sourcetext: - print("\t" + exc.sourcetext, file=sys.stderr) - if exc.sourceref.column: - print("\t" + ' ' * exc.sourceref.column + ' ^', file=sys.stderr) - if self.parsing_import: - print("Error (in imported file):", str(exc), file=sys.stderr) - else: - print("Error:", str(exc), file=sys.stderr) - if sys.stderr.isatty(): - print("\x1b[0m", file=sys.stderr, end="", flush=True) - - def parse_file(self) -> ParseResult: - print("\nparsing", self.sourceref.file) - self._parse_1() - self._parse_import_file("il65lib") # compiler support library is always imported. - self._parse_2() - return self.result - - def print_warning(self, text: str, sourceref: SourceRef=None) -> None: - self.print_bold("warning: {}: {:s}".format(sourceref or self.sourceref, text)) - - def print_bold(self, text: str) -> None: - if sys.stdout.isatty(): - print("\x1b[1m" + text + "\x1b[0m", flush=True) - else: - print(text) - - def _parse_comments(self) -> None: - while True: - line = self.next_line().lstrip() - if line.startswith(';'): - self.cur_block.statements.append(Comment(line, self.sourceref)) - continue - self.prev_line() - break - - def _parse_1(self) -> None: - self.cur_block = Block("
", self.sourceref, self.root_scope, self.result.preserve_registers) - self.result.add_block(self.cur_block) - self.parse_header() - if not self.parsing_import: - self.result.zeropage.configure(self.result.clobberzp) - while True: - self._parse_comments() - next_line = self.peek_next_line().lstrip() - if next_line.startswith("~"): - block = self.parse_block() - if block: - self.result.add_block(block) - elif next_line.startswith(("%import ", "%import\t")): - self.parse_import() - else: - break - line = self.next_line() - if line: - raise self.PError("invalid statement or characters, block expected") - if not self.parsing_import: - # check if we have a proper main block to contain the program's entry point - main_found = False - for block in self.result.blocks: - if block.name == "main": - main_found = True - if "start" not in block.label_names: - self.sourceref.line = block.sourceref.line - self.sourceref.column = 0 - raise self.PError("block 'main' should contain the program entry point 'start'") - self._check_return_statement(block, "'main' block") - for sub in block.symbols.iter_subroutines(True): - self._check_return_statement(sub.sub_block, "subroutine '{:s}'".format(sub.name)) - if not main_found: - raise self.PError("a block 'main' must be defined and contain the program's entry point label 'start'") - - def _check_return_statement(self, block: Block, message: str) -> None: - # find last statement that isn't a comment - for stmt in reversed(block.statements): - if isinstance(stmt, Comment): - continue - if isinstance(stmt, ReturnStmt) or isinstance(stmt, CallStmt) and stmt.is_goto: - return - if isinstance(stmt, InlineAsm): - # check that the last asm line is a jmp or a rts - for asmline in reversed(stmt.asmlines): - if asmline.strip().replace(' ', '').startswith(";returns"): - return - if asmline.lstrip().startswith(';'): - continue - if " rts" in asmline or "\trts" in asmline or " jmp" in asmline or "\tjmp" in asmline: - return - if asmline.strip(): - if asmline.split()[0].isidentifier(): - continue - break - break - self.print_warning("{:s} doesn't end with a return statement".format(message), block.sourceref) - - _immediate_floats = {} # type: Dict[float, Tuple[str, str]] - _immediate_string_vars = {} # type: Dict[str, Tuple[str, str]] - - def _parse_2(self) -> None: - # parsing pass 2 (not done during preprocessing!) - self.cur_block = None - self.sourceref = SourceRef(self.sourceref.file, -1) # type: ignore - - def imm_string_to_var(stmt: AssignmentStmt, containing_block: Block) -> None: - if stmt.right.name or not isinstance(stmt.right, StringValue): - return - if stmt.right.value in self._immediate_string_vars: - blockname, stringvar_name = self._immediate_string_vars[stmt.right.value] - if blockname: - stmt.right.name = blockname + '.' + stringvar_name - else: - stmt.right.name = stringvar_name - else: - stringvar_name = "il65_str_{:d}".format(id(stmt)) - value = stmt.right.value - containing_block.symbols.define_constant(stringvar_name, stmt.sourceref, DataType.STRING, value=value) - stmt.right.name = stringvar_name - self._immediate_string_vars[stmt.right.value] = (containing_block.name, stringvar_name) - - def desugar_immediate_strings(stmt: AstNode, containing_block: Block) -> None: - if isinstance(stmt, CallStmt): - for s in stmt.desugared_call_arguments: - self.sourceref = s.sourceref - imm_string_to_var(s, containing_block) - for s in stmt.desugared_output_assignments: - self.sourceref = s.sourceref - imm_string_to_var(s, containing_block) - if isinstance(stmt, AssignmentStmt): - self.sourceref = stmt.sourceref - imm_string_to_var(stmt, containing_block) - - def desugar_immediate_floats(stmt: AstNode, containing_block: Block) -> None: - if isinstance(stmt, (InplaceIncrStmt, InplaceDecrStmt)): - howmuch = stmt.value.value - if howmuch is None: - assert stmt.value.name - return - if howmuch in (0, 1) or type(howmuch) is int: - return # 1 is special cased in the code generator - rom_floats = { - 1: "c64.FL_FONE", - .25: "c64.FL_FR4", - .5: "c64.FL_FHALF", - -.5: "c64.FL_NEGHLF", - 10: "c64.FL_TENC", - -32768: "c64.FL_N32768", - 1e9: "c64.FL_NZMIL", - math.pi: "c64.FL_PIVAL", - math.pi / 2: "c64.FL_PIHALF", - math.pi * 2: "c64.FL_TWOPI", - math.sqrt(2)/2.0: "c64.FL_SQRHLF", - math.sqrt(2): "c64.FL_SQRTWO", - math.log(2): "c64.FL_LOG2", - 1.0 / math.log(2): "c64.FL_LOGEB2", - } - for fv, name in rom_floats.items(): - if math.isclose(howmuch, fv, rel_tol=0, abs_tol=1e-9): - # use one of the constants available in ROM - stmt.value.name = name - return - if howmuch in self._immediate_floats: - # reuse previously defined float constant - blockname, floatvar_name = self._immediate_floats[howmuch] - if blockname: - stmt.value.name = blockname + '.' + floatvar_name - else: - stmt.value.name = floatvar_name - else: - # define new float variable to hold the incr/decr value - # note: not a constant, because we need the MFLT bytes - floatvar_name = "il65_float_{:d}".format(id(stmt)) - containing_block.symbols.define_variable(floatvar_name, stmt.sourceref, DataType.FLOAT, value=howmuch) - self._immediate_floats[howmuch] = (containing_block.name, floatvar_name) - stmt.value.name = floatvar_name - - for block in self.result.blocks: - self.cur_block = block - self.sourceref = attr.evolve(block.sourceref, column=0) - for _, sub, stmt in block.all_statements(): - if isinstance(stmt, CallStmt): - self.sourceref = stmt.sourceref - self.desugar_call_arguments_and_outputs(stmt) - desugar_immediate_strings(stmt, self.cur_block) - desugar_immediate_floats(stmt, self.cur_block) - - def desugar_call_arguments_and_outputs(self, stmt: CallStmt) -> None: - stmt.desugared_call_arguments.clear() - stmt.desugared_output_assignments.clear() - for name, value in stmt.arguments or []: - assert name is not None, "all call arguments should have a name or be matched on a named parameter" - assignment = self.parse_assignment(name, value) - assignment.sourceref = stmt.sourceref - if assignment.leftvalues[0].datatype != DataType.BYTE: - if isinstance(assignment.right, IntegerValue) and assignment.right.constant: - # a call that doesn't expect a BYTE argument but gets one, converted from a 1-byte string most likely - if value.startswith("'") and value.endswith("'"): - self.print_warning("possible problematic string to byte conversion (use a .text var instead?)") - if not assignment.is_identity(): - stmt.desugared_call_arguments.append(assignment) - if all(not isinstance(v, RegisterValue) for r, v in stmt.outputvars or []): - # if none of the output variables are registers, we can simply generate the assignments without issues - for register, value in stmt.outputvars or []: - rvalue = self.parse_expression(register) - assignment = AssignmentStmt([value], rvalue, stmt.sourceref) - stmt.desugared_output_assignments.append(assignment) - else: - result_reg_mapping = [(register, value.register, value) for register, value in stmt.outputvars or [] - if isinstance(value, RegisterValue)] - if any(r[0] != r[1] for r in result_reg_mapping): - # not all result parameter registers line up with the correct order of registers in the statement, - # reshuffling call results is not supported yet. - raise self.PError("result registers and/or their ordering is not the same as in the " - "subroutine definition, this isn't supported yet") - else: - # no register alignment issues, just generate the assignments - # note: do not remove the identity assignment here or the output register handling generates buggy code - for register, value in stmt.outputvars or []: - rvalue = self.parse_expression(register) - assignment = AssignmentStmt([value], rvalue, stmt.sourceref) - stmt.desugared_output_assignments.append(assignment) - - def next_line(self) -> str: - self._cur_lineidx += 1 - try: - lineno, line = self.lines[self._cur_lineidx] - self.sourceref = SourceRef(file=self.sourceref.file, line=lineno) # type: ignore - return line - except IndexError: - return "" - - def prev_line(self) -> str: - self._cur_lineidx -= 1 - lineno, line = self.lines[self._cur_lineidx] - self.sourceref = SourceRef(file=self.sourceref.file, line=lineno) # type: ignore - return line - - def peek_next_line(self) -> str: - if (self._cur_lineidx + 1) < len(self.lines): - return self.lines[self._cur_lineidx + 1][1] - return "" - - def PError(self, message: str, lineno: int=0, column: int=0) -> ParseError: - sourceline = "" - lineno = lineno or self.sourceref.line - column = column or self.sourceref.column - for num, text in self.lines: - if num == lineno: - sourceline = text.strip() - break - return ParseError(message, sourceline, SourceRef(self.sourceref.file, lineno, column)) # type: ignore - - def get_datatype(self, typestr: str) -> Tuple[DataType, int, Optional[Tuple[int, int]]]: - if typestr == ".byte": - return DataType.BYTE, 1, None - elif typestr == ".word": - return DataType.WORD, 1, None - elif typestr == ".float": - return DataType.FLOAT, 1, None - elif typestr.endswith("text"): - if typestr == ".text": - return DataType.STRING, 0, None - elif typestr == ".ptext": - return DataType.STRING_P, 0, None - elif typestr == ".stext": - return DataType.STRING_S, 0, None - elif typestr == ".pstext": - return DataType.STRING_PS, 0, None - elif typestr.startswith(".array(") and typestr.endswith(")"): - return DataType.BYTEARRAY, self._size_from_arraydecl(typestr), None - elif typestr.startswith(".wordarray(") and typestr.endswith(")"): - return DataType.WORDARRAY, self._size_from_arraydecl(typestr), None - elif typestr.startswith(".matrix(") and typestr.endswith(")"): - dimensions = self._size_from_matrixdecl(typestr) - return DataType.MATRIX, dimensions[0] * dimensions[1], dimensions - raise self.PError("invalid data type: " + typestr) - - def parse_header(self) -> None: - self.result.with_sys = False - self.result.format = ProgramFormat.RAW - output_specified = False - zp_specified = False - preserve_specified = False - while True: - self._parse_comments() - line = self.next_line() - if line.startswith('%'): - directive = line.split(maxsplit=1)[0][1:] - if directive == "output": - if output_specified: - raise self.PError("can only specify output options once") - output_specified = True - _, _, optionstr = line.partition(" ") - options = set(optionstr.replace(' ', '').split(',')) - self.result.with_sys = False - self.result.format = ProgramFormat.RAW - if "raw" in options: - options.remove("raw") - if "prg" in options: - options.remove("prg") - self.result.format = ProgramFormat.PRG - if "basic" in options: - options.remove("basic") - if self.result.format == ProgramFormat.PRG: - self.result.with_sys = True - else: - raise self.PError("can only use basic output option with prg, not raw") - if options: - raise self.PError("invalid output option(s): " + str(options)) - continue - elif directive == "zp": - if zp_specified: - raise self.PError("can only specify ZP options once") - zp_specified = True - _, _, optionstr = line.partition(" ") - options = set(optionstr.replace(' ', '').split(',')) - self.result.clobberzp = False - self.result.restorezp = False - if "clobber" in options: - options.remove("clobber") - self.result.clobberzp = True - if "restore" in options: - options.remove("restore") - if self.result.clobberzp: - self.result.restorezp = True - else: - raise self.PError("can only use restore zp option if clobber zp is used as well") - if options: - raise self.PError("invalid zp option(s): " + str(options)) - continue - elif directive == "address": - if self.result.start_address: - raise self.PError("multiple occurrences of 'address'") - _, _, arg = line.partition(" ") - try: - self.result.start_address = parse_expr_as_int(arg, None, None, self.sourceref) - except ParseError: - raise self.PError("invalid address") - if self.result.format == ProgramFormat.PRG and self.result.with_sys and self.result.start_address != 0x0801: - raise self.PError("cannot use non-default 'address' when output format includes basic SYS program") - continue - elif directive == "saveregisters": - if preserve_specified: - raise self.PError("can only specify saveregisters option once") - preserve_specified = True - _, _, optionstr = line.partition(" ") - self.result.preserve_registers = optionstr in ("", "true", "yes") - continue - elif directive == "import": - break # the first import directive actually is not part of the header anymore - else: - raise self.PError("invalid directive") - break # no more directives, header parsing finished! - self.prev_line() - if not self.result.start_address: - # set the proper default start address - if self.result.format == ProgramFormat.PRG: - self.result.start_address = 0x0801 # normal C-64 basic program start address - elif self.result.format == ProgramFormat.RAW: - self.result.start_address = 0xc000 # default start for raw assembly - if self.result.format == ProgramFormat.PRG and self.result.with_sys and self.result.start_address != 0x0801: - raise self.PError("cannot use non-default 'address' when output format includes basic SYS program") - - def parse_import(self) -> None: - line = self.next_line() - line = line.lstrip() - if not line.startswith(("%import ", "%import\t")): - raise self.PError("expected import") - try: - _, filename = line.split(maxsplit=1) - except ValueError: - raise self.PError("invalid import statement") - if filename[0] in "'\"" and filename[-1] in "'\"": - filename = filename[1:-1] - if not filename: - raise self.PError("invalid filename") - self._parse_import_file(filename) - - def _parse_import_file(self, filename: str) -> None: - candidates = [filename+".ill", filename] - filename_at_source_location = os.path.join(os.path.split(self.sourceref.file)[0], filename) - if filename_at_source_location not in candidates: - candidates.append(filename_at_source_location+".ill") - candidates.append(filename_at_source_location) - filename_at_libs_location = os.path.join(os.path.split(__file__)[0], "../lib", filename) - if filename_at_libs_location not in candidates: - candidates.append(filename_at_libs_location+".ill") - candidates.append(filename_at_libs_location) - for filename in candidates: - if os.path.isfile(filename): - if not self.check_import_okay(filename): - return - self.print_import_progress("importing", filename) - parser = self.create_import_parser(filename, self.outputdir) - result = parser.parse() - self.print_import_progress("\ncontinuing", self.sourceref.file) - if result: - # merge the symbol table of the imported file into our own - try: - self.root_scope.merge_roots(parser.root_scope) - self.result.merge(result) - except SymbolError as x: - raise self.PError(str(x)) - return - else: - raise self.PError("Error while parsing imported file") - raise self.PError("imported file not found") - - def print_import_progress(self, message: str, *args: str) -> None: - print(message, *args) - - def create_import_parser(self, filename: str, outputdir: str) -> 'Parser': - return Parser(filename, outputdir, self.existing_imports, True, ppsymbols=self.ppsymbols, sub_usage=self.result.subroutine_usage) - - def parse_block(self) -> Optional[Block]: - # first line contains block header "~ [name] [addr]" followed by a '{' - self._parse_comments() - line = self.next_line() - line = line.lstrip() - if not line.startswith("~"): - raise self.PError("expected '~' (block)") - block_args = line[1:].split() - arg = "" - self.cur_block = Block("", self.sourceref, self.root_scope, self.result.preserve_registers) - is_zp_block = False - while block_args: - arg = block_args.pop(0) - if arg.isidentifier(): - if arg.lower() == "zeropage" or arg in ("zp", "zP", "Zp"): - raise self.PError("zero page block must be named 'ZP'") - is_zp_block = arg == "ZP" - if arg in set(b.name for b in self.result.blocks): - orig = [b for b in self.result.blocks if b.name == arg][0] - if not is_zp_block: - raise self.PError("duplicate block name '{:s}', original definition at {}".format(arg, orig.sourceref)) - self.cur_block = orig # zero page block occurrences are merged - else: - self.cur_block = Block(arg, self.sourceref, self.root_scope, self.result.preserve_registers) - try: - self.root_scope.define_scope(self.cur_block.symbols, self.cur_block.sourceref) - except SymbolError as x: - raise self.PError(str(x)) - elif arg == "{": - break - elif arg.endswith("{"): - # when there is no whitespace before the { - block_args.insert(0, "{") - block_args.insert(0, arg[:-1]) - continue - else: - try: - block_address = parse_expr_as_int(arg, self.cur_block.symbols, self.ppsymbols, self.sourceref) - except ParseError: - raise self.PError("Invalid block address") - if block_address == 0 or (block_address < 0x0200 and not is_zp_block): - raise self.PError("block address must be >= $0200 (or omitted)") - if is_zp_block: - if block_address not in (0, 0x04): - raise self.PError("zero page block address must be $04 (or omittted)") - block_address = 0x04 - self.cur_block.address = block_address - if arg != "{": - line = self.peek_next_line() - if line != "{": - raise self.PError("expected '{' after block") - else: - self.next_line() - if self.print_block_parsing: - if self.cur_block.address: - print(" parsing block '{:s}' at ${:04x}".format(self.cur_block.name, self.cur_block.address)) - else: - print(" parsing block '{:s}'".format(self.cur_block.name)) - if self.cur_block.ignore: - # just skip the lines until we hit a '}' that closes the block - nesting_level = 1 - while True: - line = self.next_line().strip() - if line.endswith("{"): - nesting_level += 1 - elif line == "}": - nesting_level -= 1 - if nesting_level == 0: - self.print_warning("ignoring block without name and address", self.cur_block.sourceref) - return None - else: - raise self.PError("invalid statement in block") - while True: - try: - go_on, resultblock = self._parse_block_statement(is_zp_block) - if not go_on: - return resultblock - except ParseError as x: - self.handle_parse_error(x) - - def _parse_block_statement(self, is_zp_block: bool) -> Tuple[bool, Optional[Block]]: - # parse the statements inside a block - self._parse_comments() - line = self.next_line() - unstripped_line = line - line = line.strip() - if line.startswith('%'): - directive, _, optionstr = line.partition(" ") - directive = directive[1:] - self.cur_block.preserve_registers = optionstr in ("", "true", "yes") - if directive in ("asminclude", "asmbinary"): - if is_zp_block: - raise self.PError("ZP block cannot contain assembler directives") - self.cur_block.statements.append(self.parse_asminclude(line)) - elif directive == "asm": - if is_zp_block: - raise self.PError("ZP block cannot contain code statements") - self.prev_line() - self.cur_block.statements.append(self.parse_asm()) - elif directive == "breakpoint": - self.cur_block.statements.append(BreakpointStmt(self.sourceref)) - self.print_warning("breakpoint defined") - elif directive == "saveregisters": - self.result.preserve_registers = optionstr in ("", "true", "yes") - else: - raise self.PError("invalid directive") - elif line == "}": - if is_zp_block and any(b.name == "ZP" for b in self.result.blocks): - return False, None # we already have the ZP block - if self.cur_block.ignore: - self.print_warning("ignoring block without name and address", self.cur_block.sourceref) - return False, None - return False, self.cur_block - elif line.startswith(("var ", "var\t")): - self.parse_var_def(line) - elif line.startswith(("const ", "const\t")): - self.parse_const_def(line) - elif line.startswith(("memory ", "memory\t")): - self.parse_memory_def(line, is_zp_block) - elif line.startswith(("sub ", "sub\t")): - if is_zp_block: - raise self.PError("ZP block cannot contain subroutines") - self.parse_subroutine_def(line) - elif unstripped_line.startswith((" ", "\t")): - if line.endswith("{"): - raise self.PError("invalid statement") - if is_zp_block: - raise self.PError("ZP block cannot contain code statements") - self.cur_block.statements.append(self.parse_statement(line)) - elif line: - match = re.fullmatch(r"(?P