diff --git a/il65/astparse.py b/il65/astparse.py index 67c409ad3..169c53103 100644 --- a/il65/astparse.py +++ b/il65/astparse.py @@ -69,24 +69,6 @@ def parse_arguments(text: str, sourceref: SourceRef) -> List[Tuple[str, Primitiv except SyntaxError as x: raise src.to_error(str(x)) - 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 - print("error", ast.dump(node)) - raise TypeError("invalid arg ast node type", node) - args = [] # type: List[Tuple[str, Any]] if isinstance(nodes, ast.Expression): for arg in nodes.body.args: @@ -100,14 +82,37 @@ def parse_arguments(text: str, sourceref: SourceRef) -> List[Tuple[str, Primitiv raise TypeError("ast.Expression expected") -def parse_expr_as_comparison(text: str, context: Optional[SymbolTable], ppcontext: Optional[SymbolTable], sourceref: SourceRef) -> None: +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)) - print("AST NODE", node) + 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-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, *, @@ -240,3 +245,22 @@ class ExpressionTransformer(EvaluatingTransformer): 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 + print("error", ast.dump(node)) + raise TypeError("invalid arg ast node type", node) diff --git a/il65/codegen.py b/il65/codegen.py index 17389f51c..cf1ce8a5b 100644 --- a/il65/codegen.py +++ b/il65/codegen.py @@ -160,7 +160,7 @@ class CodeGenerator: self.p("\t\tsta {:s}".format(vname)) self.p("\t\tstx {:s}+1".format(vname)) elif variable.type == DataType.FLOAT: - raise TypeError("floats cannot be stored in the zp") + raise CodeError("floats cannot be stored in the zp") self.p("; end init zp vars") else: self.p("\t\t; there are no zp vars to initialize") @@ -213,8 +213,9 @@ class CodeGenerator: asmlines = [ "\t\tcld\t\t\t; clear decimal flag", "\t\tclc\t\t\t; clear carry flag" + "\t\tclv\t\t\t; clear overflow flag" ] - statements.insert(index+1, ParseResult.InlineAsm(0, asmlines)) + statements.insert(index+1, ParseResult.InlineAsm(asmlines, 0)) break block.statements = statements # generate @@ -286,7 +287,7 @@ class CodeGenerator: self.p("\t\t{:s} = {:s}\t; matrix {:d} by {:d} = {:d} bytes" .format(vardef.name, Parser.to_hex(vardef.address), vardef.matrixsize[0], vardef.matrixsize[1], vardef.length)) else: - raise ValueError("invalid var type") + raise CodeError("invalid var type") non_mem_vars = [vi for vi in block.symbols.iter_variables() if vi.allocate] if non_mem_vars: self.p("; normal variables") @@ -305,7 +306,7 @@ class CodeGenerator: self.p("{:s}\t\t.byte ${:02x}, ${:02x}, ${:02x}, ${:02x}, ${:02x}" .format(vardef.name, *self.to_mflpt5(float(vardef.value)))) else: - raise TypeError("weird datatype") + raise CodeError("weird datatype") elif vardef.type in (DataType.BYTEARRAY, DataType.WORDARRAY): if vardef.address: raise CodeError("array or wordarray vars must not have address; will be allocated by assembler") @@ -316,7 +317,7 @@ class CodeGenerator: self.p("{:s}\t\t.fill {:d}, [${:02x}, ${:02x}]\t; {:d} words of ${:04x}" .format(vardef.name, vardef.length * 2, f_lo, f_hi, vardef.length, vardef.value or 0)) else: - raise TypeError("invalid datatype", vardef.type) + raise CodeError("invalid datatype", vardef.type) elif vardef.type == DataType.MATRIX: if vardef.address: raise CodeError("matrix vars must not have address; will be allocated by assembler") @@ -434,6 +435,164 @@ class CodeGenerator: raise NotImplementedError("decr by > 1") # XXX def generate_call(self, stmt: ParseResult.CallStmt) -> None: + self.p("\t\t\t\t\t; src l. {:d}".format(stmt.lineno)) + if stmt.condition: + assert stmt.is_goto + line_after_goto = "" + if stmt.condition.lvalue: + if stmt.condition.comparison_op: + # the condition is lvalue operator rvalue + assert stmt.condition.ifstatus in ("true", "not") + assert stmt.condition.lvalue != stmt.condition.rvalue # so we know we actually have to compare different things + lv, op, rv = stmt.condition.lvalue, stmt.condition.comparison_op, stmt.condition.rvalue + if lv.constant and not rv.constant: + # if lv is a constant, swap the whole thing around so the constant is on the right + lv, op, rv = stmt.condition.swap() + if isinstance(rv, ParseResult.RegisterValue): + # if rv is a register, make sure it comes first instead + lv, op, rv = stmt.condition.swap() + if lv.datatype != DataType.BYTE or rv.datatype != DataType.BYTE: + raise CodeError("can only generate comparison code for byte values for now") # @todo compare non-bytes + if isinstance(lv, ParseResult.RegisterValue): + if isinstance(rv, ParseResult.RegisterValue): + self.p("\t\tst{:s} {:s}".format(rv.register.lower(), Parser.to_hex(Zeropage.SCRATCH_B1))) + if lv.register == "A": + self.p("\t\tcmp " + Parser.to_hex(Zeropage.SCRATCH_B1)) + elif lv.register == "X": + self.p("\t\tcpx " + Parser.to_hex(Zeropage.SCRATCH_B1)) + elif lv.register == "Y": + self.p("\t\tcpy " + Parser.to_hex(Zeropage.SCRATCH_B1)) + else: + raise CodeError("wrong lvalue register") + elif isinstance(rv, ParseResult.IntegerValue): + rvstr = rv.name or Parser.to_hex(rv.value) + if lv.register == "A": + self.p("\t\tcmp #" + rvstr) + elif lv.register == "X": + self.p("\t\tcpx #" + rvstr) + elif lv.register == "Y": + self.p("\t\tcpy #" + rvstr) + else: + raise CodeError("wrong lvalue register") + elif isinstance(rv, ParseResult.MemMappedValue): + rvstr = rv.name or Parser.to_hex(rv.address) + if lv.register == "A": + self.p("\t\tcmp " + rvstr) + elif lv.register == "X": + self.p("\t\tcpx #" + rvstr) + elif lv.register == "Y": + self.p("\t\tcpy #" + rvstr) + else: + raise CodeError("wrong lvalue register") + else: + raise CodeError("invalid rvalue type in comparison", rv) + elif isinstance(lv, ParseResult.MemMappedValue): + assert not isinstance(rv, ParseResult.RegisterValue), "registers as rvalue should have been swapped with lvalue" + if isinstance(rv, ParseResult.IntegerValue): + self.p("\t\tsta " + Parser.to_hex(Zeropage.SCRATCH_B1)) # need to save A, because the goto may not be taken + self.p("\t\tlda " + (lv.name or Parser.to_hex(lv.address))) + self.p("\t\tcmp #" + (rv.name or Parser.to_hex(rv.value))) + line_after_goto = "\t\tlda " + Parser.to_hex(Zeropage.SCRATCH_B1) # restore A + elif isinstance(rv, ParseResult.MemMappedValue): + rvstr = rv.name or Parser.to_hex(rv.address) + self.p("\t\tsta " + Parser.to_hex(Zeropage.SCRATCH_B1)) # need to save A, because the goto may not be taken + self.p("\t\tlda " + (lv.name or Parser.to_hex(lv.address))) + self.p("\t\tcmp " + rvstr) + line_after_goto = "\t\tlda " + Parser.to_hex(Zeropage.SCRATCH_B1) # restore A + else: + raise CodeError("invalid rvalue type in comparison", rv) + else: + raise CodeError("invalid lvalue type in comparison", lv) + else: + # the condition is just the 'truth value' of the single value, or rather bool(value) + # this is translated into assembly by comparing the argument to zero. + cv = stmt.condition.lvalue + inverse_status = stmt.condition.inverse_ifstatus() + if isinstance(cv, ParseResult.RegisterValue): + if cv.register == 'A': + self.p("\t\tcmp #0") + elif cv.register == 'X': + self.p("\t\tcpx #0") + elif cv.register == 'Y': + self.p("\t\tcpy #0") + else: + # @todo combined register, not all if statuses are supported yet + opcode = self._branchopcode(inverse_status) + if opcode not in ("beq", "bne"): + raise CodeError("cannot yet generate code for register pair that is not a true/false/eq/ne comparison", + self.cur_block.sourceref.file, stmt.lineno) # @todo + if cv.register == 'AX': + line_after_goto = "+" + self.p("\t\tcmp #0") + self.p("\t\t{:s} {:s}".format(opcode, line_after_goto)) + self.p("\t\tcpx #0") + elif cv.register == 'AY': + line_after_goto = "+" + self.p("\t\tcmp #0") + self.p("\t\t{:s} {:s}".format(opcode, line_after_goto)) + self.p("\t\tcpy #0") + elif cv.register == 'XY': + line_after_goto = "+" + self.p("\t\tcpx #0") + self.p("\t\t{:s} {:s}".format(opcode, line_after_goto)) + self.p("\t\tcpy #0") + else: + raise CodeError("invalid register", cv.register) + elif isinstance(cv, ParseResult.MemMappedValue): + if cv.datatype == DataType.BYTE: + self.p("\t\tsta " + Parser.to_hex(Zeropage.SCRATCH_B1)) # need to save A, because the goto may not be taken + self.p("\t\tlda " + (cv.name or Parser.to_hex(cv.address))) + line_after_goto = "\t\tlda " + Parser.to_hex(Zeropage.SCRATCH_B1) # restore A + elif cv.datatype == DataType.WORD: + # @todo word value, not all if statuses are supported yet + opcode = self._branchopcode(inverse_status) + if opcode not in ("beq", "bne"): + raise CodeError("cannot yet generate code for word value that is not a true/false/eq/ne comparison", + self.cur_block.sourceref.file, stmt.lineno) # @todo + self.p("\t\tsta " + Parser.to_hex(Zeropage.SCRATCH_B1)) # need to save A, because the goto may not be taken + self.p("\t\tlda " + (cv.name or Parser.to_hex(cv.address))) + self.p("\t\t{:s} +".format(opcode)) + self.p("\t\tlda " + (cv.name or Parser.to_hex(cv.address))) + line_after_goto = "+\t\tlda " + Parser.to_hex(Zeropage.SCRATCH_B1) # restore A + else: + raise CodeError("conditions cannot yet use other types than byte or word", # XXX + str(cv), self.cur_block.sourceref.file, stmt.lineno) + else: + raise CodeError("need register or memmapped value") + status = stmt.condition.ifstatus + + self._generate_call(stmt, conditional_goto_opcode=self._branchopcode(status), line_after_goto=line_after_goto) + else: + self._generate_call(stmt) + + def _branchopcode(self, status: str) -> str: + if status == "true": + status = "ne" + elif status in ("not", "zero"): + status = "eq" + elif status == "lt": + status = "cc" + elif status == "gt": + status = "eq + cs" # @todo + elif status == "le": + status = "cc + eq" # @todo + elif status == "ge": + status = "cs" + opcodes = {"cc": "bcc", + "cs": "bcs", + "vc": "bvc", + "vs": "bvs", + "eq": "beq", + "ne": "bne", + "pos": "bpl", # @todo correct? + "neg": "bmi"} # @todo correct? + return opcodes[status] + + def _generate_call(self, stmt: ParseResult.CallStmt, conditional_goto_opcode: str=None, line_after_goto: str=None) -> None: + if stmt.arguments and conditional_goto_opcode: + raise CodeError("cannot use a conditional goto when the target requires parameters") + goto_opcode = conditional_goto_opcode or "jmp" + def generate_param_assignments() -> None: for assign_stmt in stmt.desugared_call_arguments: self.generate_assignment(assign_stmt) @@ -469,10 +628,12 @@ class CodeGenerator: if isinstance(stmt.target, ParseResult.MemMappedValue): targetstr = stmt.target.name or Parser.to_hex(stmt.address) else: - raise TypeError("call sub target should be mmapped") + raise CodeError("call sub target should be mmapped") if stmt.is_goto: generate_param_assignments() - self.p("\t\tjmp " + targetstr) + self.p("\t\t{:s} {:s}".format(goto_opcode, targetstr)) + if line_after_goto: + self.p(line_after_goto) # no result assignments because it's a goto return clobbered = set() # type: Set[str] @@ -504,10 +665,12 @@ class CodeGenerator: if targetstr in REGISTER_WORDS: self.p("\t\tst{:s} {:s}".format(targetstr[0].lower(), Parser.to_hex(Zeropage.SCRATCH_B1))) self.p("\t\tst{:s} {:s}".format(targetstr[1].lower(), Parser.to_hex(Zeropage.SCRATCH_B2))) - self.p("\t\tjmp ({:s})".format(Parser.to_hex(Zeropage.SCRATCH_B1))) + self.p("\t\t{:s} ({:s})".format(goto_opcode, Parser.to_hex(Zeropage.SCRATCH_B1))) else: - self.p("\t\tjmp ({:s})".format(targetstr)) + self.p("\t\t{:s} ({:s})".format(goto_opcode, targetstr)) # no result assignments because it's a goto + if line_after_goto: + self.p(line_after_goto) else: preserve_regs = {'A', 'X', 'Y'} if stmt.preserve_regs else set() unclobber_result_registers(preserve_regs, stmt.desugared_output_assignments) @@ -551,8 +714,10 @@ class CodeGenerator: if stmt.is_goto: # no need to preserve registers for a goto generate_param_assignments() - self.p("\t\tjmp " + targetstr) + self.p("\t\t{:s} {:s}".format(goto_opcode, targetstr)) # no result assignments because it's a goto + if line_after_goto: + self.p(line_after_goto) else: preserve_regs = {'A', 'X', 'Y'} if stmt.preserve_regs else set() unclobber_result_registers(preserve_regs, stmt.desugared_output_assignments) @@ -813,7 +978,7 @@ class CodeGenerator: if lv.name: symblock, sym = self.cur_block.lookup(lv.name) if not isinstance(sym, VariableDef): - raise TypeError("invalid lvalue type " + str(sym)) + raise CodeError("invalid lvalue type " + str(sym)) assign_target = symblock.label + '.' + sym.name if symblock is not self.cur_block else lv.name lvdatatype = sym.type else: @@ -836,10 +1001,10 @@ class CodeGenerator: self.p("\t\tsta {}+1".format(assign_target)) elif lvdatatype == DataType.FLOAT: if rvalue.value is not None and not DataType.FLOAT.assignable_from_value(rvalue.value): - raise ValueError("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) else: - raise TypeError("invalid lvalue type " + str(lvdatatype)) + raise CodeError("invalid lvalue type " + str(lvdatatype)) def generate_assign_mem_to_reg(self, l_register: str, rvalue: ParseResult.MemMappedValue) -> None: r_str = rvalue.name if rvalue.name else "${:x}".format(rvalue.address) @@ -934,9 +1099,9 @@ class CodeGenerator: self.p("\t\tlda #0") self.p("\t\tsta {}+1".format(assign_target)) else: - raise TypeError("invalid lvalue type " + str(sym)) + raise CodeError("invalid lvalue type " + str(sym)) else: - raise TypeError("invalid lvalue type " + str(sym)) + raise CodeError("invalid lvalue type " + str(sym)) def generate_assign_integer_to_reg(self, l_register: str, rvalue: ParseResult.IntegerValue) -> None: r_str = rvalue.name if rvalue.name else "${:x}".format(rvalue.value) diff --git a/il65/parse.py b/il65/parse.py index 8018e6205..98126cbc4 100644 --- a/il65/parse.py +++ b/il65/parse.py @@ -13,9 +13,9 @@ import sys import shutil import enum from collections import defaultdict -from typing import Set, List, Tuple, Optional, Any, Dict, Union, Sequence +from typing import Set, List, Tuple, Optional, Any, Dict, Union from .astparse import ParseError, parse_expr_as_int, parse_expr_as_number, parse_expr_as_primitive,\ - parse_expr_as_string, parse_arguments + parse_expr_as_string, parse_arguments, parse_expr_as_comparison from .symbols import SourceRef, SymbolTable, DataType, SymbolDefinition, SubroutineDef, LabelDef, \ zeropage, check_value_in_range, char_to_bytevalue, \ PrimitiveType, VariableDef, ConstantDef, SymbolError, STRING_DATATYPES, \ @@ -254,8 +254,11 @@ class ParseResult: return False, "can only assign an integer constant value of 0 or 1 to SC and SI" if self.constant: return False, "cannot assign to a constant" - if isinstance(other, ParseResult.RegisterValue) and len(self.register) < len(other.register): - return False, "register size mismatch" + if isinstance(other, ParseResult.RegisterValue): + if other.register in {"SI", "SC", "SZ"}: + return False, "cannot explicitly assign from a status bit register alias" + if len(self.register) < len(other.register): + return False, "register size mismatch" if isinstance(other, ParseResult.StringValue) and self.register in REGISTER_BYTES: return False, "string address requires 16 bits combined register" if isinstance(other, ParseResult.IntegerValue): @@ -345,22 +348,24 @@ class ParseResult: return False, "incompatible value for assignment" class _AstNode: # @todo merge Value with this? - pass + def __init__(self, lineno: int) -> None: + self.lineno = lineno class Comment(_AstNode): - def __init__(self, text: str) -> None: + def __init__(self, text: str, lineno: int) -> None: + super().__init__(lineno) self.text = text class Label(_AstNode): def __init__(self, name: str, lineno: int) -> None: + super().__init__(lineno) self.name = name - self.lineno = lineno class AssignmentStmt(_AstNode): def __init__(self, leftvalues: List['ParseResult.Value'], right: 'ParseResult.Value', lineno: int) -> None: + super().__init__(lineno) self.leftvalues = leftvalues self.right = right - self.lineno = lineno def __str__(self): return "".format(str(self.right), ",".join(str(lv) for lv in self.leftvalues)) @@ -395,28 +400,34 @@ class ParseResult: return all(lv == self.right for lv in self.leftvalues) class ReturnStmt(_AstNode): - def __init__(self, a: Optional['ParseResult.Value']=None, + def __init__(self, lineno: int, a: Optional['ParseResult.Value']=None, x: Optional['ParseResult.Value']=None, y: Optional['ParseResult.Value']=None) -> None: + super().__init__(lineno) self.a = a self.x = x self.y = y class IncrDecrStmt(_AstNode): - def __init__(self, what: 'ParseResult.Value', howmuch: int) -> None: + def __init__(self, what: 'ParseResult.Value', howmuch: int, lineno: int) -> None: + super().__init__(lineno) self.what = what self.howmuch = howmuch class CallStmt(_AstNode): def __init__(self, lineno: int, target: Optional['ParseResult.Value']=None, *, address: Optional[int]=None, arguments: List[Tuple[str, Any]]=None, - outputs: List[Tuple[str, 'ParseResult.Value']]=None, is_goto: bool=False, preserve_regs: bool=True) -> None: - self.lineno = lineno + outputs: List[Tuple[str, 'ParseResult.Value']]=None, is_goto: bool=False, + preserve_regs: bool=True, condition: 'ParseResult.IfCondition'=None) -> None: + if not is_goto: + assert condition is None + super().__init__(lineno) self.target = target self.address = address self.arguments = arguments self.outputvars = outputs self.is_goto = is_goto + self.condition = condition self.preserve_regs = preserve_regs self.desugared_call_arguments = [] # type: List[ParseResult.AssignmentStmt] self.desugared_output_assignments = [] # type: List[ParseResult.AssignmentStmt] @@ -438,10 +449,56 @@ class ParseResult: self.desugared_output_assignments.append(assignment) class InlineAsm(_AstNode): - def __init__(self, lineno: int, asmlines: List[str]) -> None: - self.lineno = lineno + def __init__(self, asmlines: List[str], lineno: int) -> None: + super().__init__(lineno) self.asmlines = asmlines + class IfCondition(_AstNode): + SWAPPED_OPERATOR = {"==": "==", + "!=": "!=", + "<=": ">=", + ">=": "<=", + "<": ">", + ">": "<"} + INVERSE_IFSTATUS = {"cc": "cs", + "cs": "cc", + "vc": "vs", + "vs": "vc", + "eq": "ne", + "ne": "eq", + "pos": "neg", + "neg": "pos", + "true": "not", + "not": "true", + "zero": "true", + "lt": "ge", + "gt": "le", + "le": "gt", + "ge": "lt"} + + def __init__(self, ifstatus: str, leftvalue: Optional['ParseResult.Value'], + operator: str, rightvalue: Optional['ParseResult.Value'], lineno: int) -> None: + assert ifstatus in self.INVERSE_IFSTATUS + assert operator in (None, "") or operator in self.SWAPPED_OPERATOR + if operator: + assert ifstatus in ("true", "not") + super().__init__(lineno) + self.ifstatus = ifstatus + self.lvalue = leftvalue + self.comparison_op = operator + self.rvalue = rightvalue + + def __str__(self): + return "".format(self.lvalue, self.comparison_op, self.rvalue) + + def inverse_ifstatus(self) -> str: + # to be able to generate a branch when the status does NOT apply + return self.INVERSE_IFSTATUS[self.ifstatus] + + def swap(self) -> Tuple['ParseResult.Value', str, 'ParseResult.Value']: + self.lvalue, self.comparison_op, self.rvalue = self.rvalue, self.SWAPPED_OPERATOR[self.comparison_op], self.lvalue + return self.lvalue, self.comparison_op, self.rvalue + def add_block(self, block: 'ParseResult.Block', position: Optional[int]=None) -> None: if position is not None: self.blocks.insert(position, block) @@ -550,7 +607,7 @@ class Parser: while True: line = self.next_line().lstrip() if line.startswith(';'): - self.cur_block.statements.append(ParseResult.Comment(line)) + self.cur_block.statements.append(ParseResult.Comment(line, self.sourceref.line)) continue self.prev_line() break @@ -1040,6 +1097,17 @@ class Parser: return varname, datatype, length, matrix_dimensions, valuetext def parse_statement(self, line: str) -> ParseResult._AstNode: + match = re.fullmatch(r"(?Pif(_[a-z]+)?)\s+(?P.+)?goto\s+(?P[\S]+?)\s*(\((?P.*)\))?\s*", line) + if match: + # conditional goto + groups = match.groupdict() + subname = groups["subname"] + if '!' in subname: + raise self.PError("goto is always without register preservation, should not have exclamation mark") + if groups["if"] == "if" and not groups["cond"]: + raise self.PError("need explicit if status when a condition is not present") + condition = self.parse_if_condition(groups["if"], groups["cond"]) + return self.parse_call_or_goto(subname, groups["arguments"], None, False, True, condition=condition) match = re.fullmatch(r"goto\s+(?P[\S]+?)\s*(\((?P.*)\))?\s*", line) if match: # goto @@ -1047,8 +1115,7 @@ class Parser: subname = groups["subname"] if '!' in subname: raise self.PError("goto is always without register preservation, should not have exclamation mark") - arguments = groups["arguments"] - return self.parse_call_or_goto(subname, arguments, None, False, True) + return self.parse_call_or_goto(subname, groups["arguments"], None, False, True) match = re.fullmatch(r"(?P[^\(]*\s*=)?\s*(?P[\S]+?)\s*(?P[!]?)\s*(\((?P.*)\))?\s*", line) if match: # subroutine call (not a goto) with possible output param assignment @@ -1070,7 +1137,7 @@ class Parser: what = self.parse_expression(line[:-2].rstrip()) if isinstance(what, ParseResult.IntegerValue): raise self.PError("cannot in/decrement a constant value") - return ParseResult.IncrDecrStmt(what, 1 if incr else -1) + return ParseResult.IncrDecrStmt(what, 1 if incr else -1, self.sourceref.line) else: # perhaps it is an assignment statment lhs, sep, rhs = line.partition("=") @@ -1079,7 +1146,9 @@ class Parser: raise self.PError("invalid statement") def parse_call_or_goto(self, targetstr: str, argumentstr: str, outputstr: str, - preserve_regs=True, is_goto=False) -> ParseResult.CallStmt: + preserve_regs=True, is_goto=False, condition: ParseResult.IfCondition=None) -> ParseResult.CallStmt: + if not is_goto: + assert condition is None argumentstr = argumentstr.strip() if argumentstr else "" outputstr = outputstr.strip() if outputstr else "" arguments = None @@ -1106,6 +1175,8 @@ class Parser: except ParseError: symbol = None # it's probably a number or a register then if isinstance(symbol, SubroutineDef): + if condition and symbol.parameters: + raise self.PError("cannot use a subroutine that requires parameters as a target for conditional goto") # verify subroutine arguments if len(arguments or []) != len(symbol.parameters): raise self.PError("invalid number of arguments ({:d}, expected {:d})" @@ -1144,7 +1215,7 @@ class Parser: if isinstance(target, (type(None), ParseResult.Value)): if is_goto: return ParseResult.CallStmt(self.sourceref.line, target, address=address, - arguments=arguments, outputs=outputvars, is_goto=True) + arguments=arguments, outputs=outputvars, is_goto=True, condition=condition) else: return ParseResult.CallStmt(self.sourceref.line, target, address=address, arguments=arguments, outputs=outputvars, preserve_regs=preserve_regs) @@ -1190,7 +1261,7 @@ class Parser: if len(parts) > 1: values = parts[1].split(",") if len(values) == 0: - return ParseResult.ReturnStmt() + return ParseResult.ReturnStmt(self.sourceref.line) else: a = self.parse_expression(values[0]) if values[0] else None if len(values) > 1: @@ -1199,7 +1270,7 @@ class Parser: y = self.parse_expression(values[2]) if values[2] else None if len(values) > 3: raise self.PError("too many returnvalues") - return ParseResult.ReturnStmt(a, x, y) + return ParseResult.ReturnStmt(self.sourceref.line, a, x, y) def parse_asm(self) -> ParseResult.InlineAsm: line = self.next_line() @@ -1211,7 +1282,7 @@ class Parser: while True: line = self.next_line() if line.strip() == "}": - return ParseResult.InlineAsm(lineno, asmlines) + return ParseResult.InlineAsm(asmlines, lineno) # asm can refer to other symbols as well, track subroutine usage splits = line.split(maxsplit=1) if len(splits) == 2: @@ -1252,7 +1323,7 @@ class Parser: lines = ['{:s}\t.binclude "{:s}"'.format(scopename, filename)] else: raise self.PError("invalid asminclude statement") - return ParseResult.InlineAsm(self.sourceref.line, lines) + return ParseResult.InlineAsm(lines, self.sourceref.line) elif aline[0] == "asmbinary": if len(aline) == 4: offset = parse_expr_as_int(aline[2], None, None, self.sourceref) @@ -1265,7 +1336,7 @@ class Parser: lines = ['\t.binary "{:s}"'.format(filename)] else: raise self.PError("invalid asmbinary statement") - return ParseResult.InlineAsm(self.sourceref.line, lines) + return ParseResult.InlineAsm(lines, self.sourceref.line) else: raise self.PError("invalid statement") @@ -1462,6 +1533,33 @@ class Parser: result = [sentence[i:j].strip(separators) for i, j in zip(indices, indices[1:])] return list(filter(None, result)) # remove empty strings + def parse_if_condition(self, ifpart: str, conditionpart: str) -> ParseResult.IfCondition: + if ifpart == "if": + ifstatus = "true" + else: + ifstatus = ifpart[3:] + if ifstatus not in ParseResult.IfCondition.INVERSE_IFSTATUS: + raise self.PError("invalid if form") + if conditionpart: + left, operator, right = parse_expr_as_comparison(conditionpart, self.sourceref) + leftv = self.parse_expression(left) + if not operator and isinstance(leftv, (ParseResult.IntegerValue, ParseResult.FloatValue, ParseResult.StringValue)): + raise self.PError("condition is a constant value") + if isinstance(leftv, ParseResult.RegisterValue): + if leftv.register in {"SC", "SZ", "SI"}: + raise self.PError("cannot use a status bit register explicitly in a condition") + if operator: + rightv = self.parse_expression(right) + if ifstatus not in ("true", "not"): + raise self.PError("can only use if[_true] or if_not with a comparison expression") + else: + rightv = None + if leftv == rightv: + raise self.PError("left and right values in comparison are identical") + return ParseResult.IfCondition(ifstatus, leftv, operator, rightv, self.sourceref.line) + else: + return ParseResult.IfCondition(ifstatus, None, "", None, self.sourceref.line) + class Optimizer: def __init__(self, parseresult: ParseResult) -> None: @@ -1474,12 +1572,23 @@ class Optimizer: self.combine_assignments_into_multi(block) self.optimize_multiassigns(block) self.remove_unused_subroutines(block) + self.optimize_compare_with_zero(block) for sub in block.symbols.iter_subroutines(True): self.remove_identity_assigns(sub.sub_block) self.combine_assignments_into_multi(sub.sub_block) self.optimize_multiassigns(sub.sub_block) + self.optimize_compare_with_zero(sub.sub_block) return self.parsed + def optimize_compare_with_zero(self, block: ParseResult.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, ParseResult.CallStmt): + if stmt.condition and isinstance(stmt.condition.rvalue, (ParseResult.IntegerValue, ParseResult.FloatValue)): + if stmt.condition.rvalue.value == 0: + print("ZOMG COMPARE WITH ZERO", stmt.lineno) # XXX + def combine_assignments_into_multi(self, block: ParseResult.Block) -> None: # fold multiple consecutive assignments with the same rvalue into one multi-assignment statements = [] # type: List[ParseResult._AstNode] diff --git a/il65/preprocess.py b/il65/preprocess.py index e6e75b043..fe824c776 100644 --- a/il65/preprocess.py +++ b/il65/preprocess.py @@ -41,7 +41,7 @@ class PreprocessingParser(Parser): return self.result def parse_asminclude(self, line: str) -> ParseResult.InlineAsm: - return ParseResult.InlineAsm(self.sourceref.line, []) + return ParseResult.InlineAsm([], self.sourceref.line) def parse_statement(self, line: str) -> ParseResult._AstNode: return None # type: ignore diff --git a/lib/c64lib.ill b/lib/c64lib.ill index 5e166f7ac..de0355521 100644 --- a/lib/c64lib.ill +++ b/lib/c64lib.ill @@ -243,6 +243,7 @@ sub init_state () -> (A?, X?, Y?) { tax tay clc + clv cli rts } diff --git a/reference.md b/reference.md index 8fb53b7d4..9e9d56400 100644 --- a/reference.md +++ b/reference.md @@ -311,22 +311,18 @@ TODOS ### Flow Control -Required building blocks: additional forms of 'go' statement: including an if clause, comparison statement. +Required building blocks: additional forms of 'goto' statement: including an if clause, comparison statement. -- a primitive conditional branch instruction (special case of 'go'): directly translates to a branch instruction: - if[_XX] goto