From 8a998441409a5a4e1ddf73f36d550e7537c61b57 Mon Sep 17 00:00:00 2001 From: Irmen de Jong Date: Mon, 1 Jan 2018 23:24:39 +0100 Subject: [PATCH] register preserve is no longer the default --- README.md | 2 +- il65/astdefs.py | 5 +- il65/codegen.py | 571 +++++++++++++++++------------------- il65/parse.py | 233 ++++++++------- lib/c64lib.ill | 68 ++--- lib/il65lib.ill | 4 +- lib/mathlib.ill | 12 +- reference.md | 178 ++++++----- testsource/calls.ill | 15 +- testsource/conditionals.ill | 4 +- testsource/dtypes.ill | 6 +- testsource/floats.ill | 4 +- testsource/input.ill | 20 +- testsource/large.ill | 126 ++++---- testsource/numbergame.ill | 7 +- testsource/source1.ill | 8 +- testsource/source2.ill | 91 ++---- testsource/source3.ill | 4 +- testsource/source4.ill | 24 +- testsource/source5.ill | 24 -- todo.ill | 23 +- 21 files changed, 703 insertions(+), 726 deletions(-) delete mode 100644 testsource/source5.ill diff --git a/README.md b/README.md index 59b03027f..4db22e28b 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ which aims to provide many conveniences over raw assembly code (even when using - automatic variable allocations, automatic string variables and string sharing - automatic type conversions - floating point operations -- automatically preserving and restoring CPU registers state, when calling routines that otherwise would clobber these +- optional automatic preserving and restoring CPU registers state, when calling routines that otherwise would clobber these - abstracting away low level aspects such as zero page handling, program startup, explicit memory addresses - breakpoints, that let the Vice emulator drop into the monitor if execution hits them - source code labels automatically loaded in Vice emulator so it can show them in disassembly diff --git a/il65/astdefs.py b/il65/astdefs.py index b6ef739a8..9fd5b1a5d 100644 --- a/il65/astdefs.py +++ b/il65/astdefs.py @@ -27,12 +27,13 @@ class _AstNode: class Block(_AstNode): _unnamed_block_labels = {} # type: Dict[Block, str] - def __init__(self, name: str, sourceref: SourceRef, parent_scope: SymbolTable) -> None: + def __init__(self, name: str, sourceref: SourceRef, parent_scope: SymbolTable, preserve_registers: bool=False) -> None: super().__init__(sourceref) self.address = 0 self.name = name self.statements = [] # type: List[_AstNode] self.symbols = SymbolTable(name, parent_scope, self) + self.preserve_registers = preserve_registers @property def ignore(self) -> bool: @@ -484,7 +485,7 @@ class CallStmt(_AstNode): def __init__(self, sourceref: SourceRef, target: Optional[Value] = None, *, address: Optional[int] = None, arguments: List[Tuple[str, Any]] = None, outputs: List[Tuple[str, Value]] = None, is_goto: bool = False, - preserve_regs: bool = True, condition: IfCondition = None) -> None: + preserve_regs: Set[str] = None, condition: IfCondition = None) -> None: if not is_goto: assert condition is None super().__init__(sourceref) diff --git a/il65/codegen.py b/il65/codegen.py index e2ad59660..76f660e34 100644 --- a/il65/codegen.py +++ b/il65/codegen.py @@ -417,12 +417,11 @@ class CodeGenerator: self.p("\t\tin{:s}".format(reg.lower())) else: # x/y += 2..255 - self.p("\t\tpha") - self.p("\t\tt{:s}a".format(reg.lower())) - self.p("\t\tclc") - self.p("\t\tadc #" + value_str) - self.p("\t\tta{:s}".format(reg.lower())) - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\tt{:s}a".format(reg.lower())) + self.p("\t\tclc") + self.p("\t\tadc #" + value_str) + self.p("\t\tta{:s}".format(reg.lower())) elif reg == "AX": # AX += 1..255 self.p("\t\tclc") @@ -446,14 +445,14 @@ class CodeGenerator: self.p("+") else: # XY += 2..255 - self.p("\t\tpha") - self.p("\t\ttxa") - self.p("\t\tclc") - self.p("\t\tadc #" + value_str) - self.p("\t\ttax") - self.p("\t\tbcc +") - self.p("\t\tiny") - self.p("+\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttxa") + self.p("\t\tclc") + self.p("\t\tadc #" + value_str) + self.p("\t\ttax") + self.p("\t\tbcc +") + self.p("\t\tiny") + self.p("+") else: raise CodeError("invalid incr register: " + reg) else: @@ -467,12 +466,11 @@ class CodeGenerator: self.p("\t\tde{:s}".format(reg.lower())) else: # x/y -= 2..255 - self.p("\t\tpha") - self.p("\t\tt{:s}a".format(reg.lower())) - self.p("\t\tsec") - self.p("\t\tsbc #" + value_str) - self.p("\t\tta{:s}".format(reg.lower())) - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\tt{:s}a".format(reg.lower())) + self.p("\t\tsec") + self.p("\t\tsbc #" + value_str) + self.p("\t\tta{:s}".format(reg.lower())) elif reg == "AX": # AX -= 1..255 self.p("\t\tsec") @@ -496,14 +494,14 @@ class CodeGenerator: self.p("+\t\tdex") else: # XY -= 2..255 - self.p("\t\tpha") - self.p("\t\ttxa") - self.p("\t\tsec") - self.p("\t\tsbc #" + value_str) - self.p("\t\ttax") - self.p("\t\tbcs +") - self.p("\t\tdey") - self.p("+\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttxa") + self.p("\t\tsec") + self.p("\t\tsbc #" + value_str) + self.p("\t\ttax") + self.p("\t\tbcs +") + self.p("\t\tdey") + self.p("+") else: raise CodeError("invalid decr register: " + reg) elif isinstance(stmt.what, (MemMappedValue, IndirectValue)): @@ -519,16 +517,15 @@ class CodeGenerator: if howmuch == 1: self.p("\t\t{:s} {:s}".format("inc" if is_incr else "dec", what_str)) else: - self.p("\t\tpha") - self.p("\t\tlda " + what_str) - if is_incr: - self.p("\t\tclc") - self.p("\t\tadc #" + value_str) - else: - self.p("\t\tsec") - self.p("\t\tsbc #" + value_str) - self.p("\t\tsta " + what_str) - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\tlda " + what_str) + if is_incr: + self.p("\t\tclc") + self.p("\t\tadc #" + value_str) + else: + self.p("\t\tsec") + self.p("\t\tsbc #" + value_str) + self.p("\t\tsta " + what_str) elif what.datatype == DataType.WORD: if howmuch == 1: # mem.word +=/-= 1 @@ -538,32 +535,31 @@ class CodeGenerator: self.p("\t\tinc {:s}+1".format(what_str)) self.p("+") else: - self.p("\t\tpha") - self.p("\t\tlda " + what_str) - self.p("\t\tbne +") - self.p("\t\tdec {:s}+1".format(what_str)) - self.p("+\t\tdec " + what_str) - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\tlda " + what_str) + self.p("\t\tbne +") + self.p("\t\tdec {:s}+1".format(what_str)) + self.p("+\t\tdec " + what_str) else: # mem.word +=/-= 2..255 if is_incr: - self.p("\t\tpha") - self.p("\t\tclc") - self.p("\t\tlda " + what_str) - self.p("\t\tadc #" + value_str) - self.p("\t\tsta " + what_str) - self.p("\t\tbcc +") - self.p("\t\tinc {:s}+1".format(what_str)) - self.p("+\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\tclc") + self.p("\t\tlda " + what_str) + self.p("\t\tadc #" + value_str) + self.p("\t\tsta " + what_str) + self.p("\t\tbcc +") + self.p("\t\tinc {:s}+1".format(what_str)) + self.p("+") else: - self.p("\t\tpha") - self.p("\t\tsec") - self.p("\t\tlda " + what_str) - self.p("\t\tsbc #" + value_str) - self.p("\t\tsta " + what_str) - self.p("\t\tbcs +") - self.p("\t\tdec {:s}+1".format(what_str)) - self.p("+\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\tsec") + self.p("\t\tlda " + what_str) + self.p("\t\tsbc #" + value_str) + self.p("\t\tsta " + what_str) + self.p("\t\tbcs +") + self.p("\t\tdec {:s}+1".format(what_str)) + self.p("+") elif what.datatype == DataType.FLOAT: if howmuch == 1.0: # special case for +/-1 @@ -914,15 +910,17 @@ class CodeGenerator: return True return False - def unclobber_result_registers(registers: Set[str], output_assignments: List[AssignmentStmt]) -> None: + def unclobber_result_registers(registers: Set[str], output_assignments: List[AssignmentStmt]) -> Set[str]: + result = registers.copy() for a in output_assignments: for lv in a.leftvalues: if isinstance(lv, RegisterValue): if len(lv.register) == 1: - registers.discard(lv.register) + result.discard(lv.register) else: for r in lv.register: - registers.discard(r) + result.discard(r) + return result if stmt.target.name: symblock, targetdef = self.cur_block.lookup(stmt.target.name) @@ -941,10 +939,10 @@ class CodeGenerator: return clobbered = set() # type: Set[str] if targetdef.clobbered_registers: - if stmt.preserve_regs: - clobbered = targetdef.clobbered_registers - unclobber_result_registers(clobbered, stmt.desugared_output_assignments) - with self.preserving_registers(clobbered, loads_a_within=params_load_a()): + if stmt.preserve_regs is not None: + clobbered = targetdef.clobbered_registers & stmt.preserve_regs + clobbered = unclobber_result_registers(clobbered, stmt.desugared_output_assignments) + with self.preserving_registers(clobbered, loads_a_within=params_load_a(), always_preserve=stmt.preserve_regs is not None): generate_param_assignments() branch_emitter(targetstr, False, False) generate_result_assignments() @@ -974,14 +972,14 @@ class CodeGenerator: # no result assignments because it's a goto else: # indirect call to subroutine - preserve_regs = {'A', 'X', 'Y'} if stmt.preserve_regs else set() - unclobber_result_registers(preserve_regs, stmt.desugared_output_assignments) - with self.preserving_registers(preserve_regs, loads_a_within=params_load_a()): + preserve_regs = unclobber_result_registers(stmt.preserve_regs or set(), stmt.desugared_output_assignments) + with self.preserving_registers(preserve_regs, loads_a_within=params_load_a(), + always_preserve=stmt.preserve_regs is not None): generate_param_assignments() if targetstr in REGISTER_WORDS: print("warning: {}: indirect register pair call is quite inefficient, use a jump table in memory instead?" .format(stmt.sourceref)) - if stmt.preserve_regs: + if stmt.preserve_regs is not None: # cannot use zp scratch because it may be used by the register backup. This is very inefficient code! self.p("\t\tjsr il65_lib.jsr_indirect_nozpuse_"+targetstr) @@ -1009,9 +1007,9 @@ class CodeGenerator: branch_emitter(targetstr, True, False) # no result assignments because it's a goto else: - preserve_regs = {'A', 'X', 'Y'} if stmt.preserve_regs else set() - unclobber_result_registers(preserve_regs, stmt.desugared_output_assignments) - with self.preserving_registers(preserve_regs, loads_a_within=params_load_a()): + preserve_regs = unclobber_result_registers(stmt.preserve_regs or set(), stmt.desugared_output_assignments) + with self.preserving_registers(preserve_regs, loads_a_within=params_load_a(), + always_preserve=stmt.preserve_regs is not None): generate_param_assignments() branch_emitter(targetstr, False, False) generate_result_assignments() @@ -1040,19 +1038,17 @@ class CodeGenerator: self.p("\t\tclc") self.p("\t\tadc " + r_str) elif lvalue.register == "X": - self.p("\t\tpha") - self.p("\t\ttxa") - self.p("\t\tclc") - self.p("\t\tadc " + r_str) - self.p("\t\ttax") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttxa") + self.p("\t\tclc") + self.p("\t\tadc " + r_str) + self.p("\t\ttax") elif lvalue.register == "Y": - self.p("\t\tpha") - self.p("\t\ttya") - self.p("\t\tclc") - self.p("\t\tadc " + r_str) - self.p("\t\ttay") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttya") + self.p("\t\tclc") + self.p("\t\tadc " + r_str) + self.p("\t\ttay") else: raise CodeError("unsupported register for aug assign", str(lvalue)) # @todo +=.word elif operator == "-=": @@ -1060,70 +1056,62 @@ class CodeGenerator: self.p("\t\tsec") self.p("\t\tsbc " + r_str) elif lvalue.register == "X": - self.p("\t\tpha") - self.p("\t\ttxa") - self.p("\t\tsec") - self.p("\t\tsbc " + r_str) - self.p("\t\ttax") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttxa") + self.p("\t\tsec") + self.p("\t\tsbc " + r_str) + self.p("\t\ttax") elif lvalue.register == "Y": - self.p("\t\tpha") - self.p("\t\ttya") - self.p("\t\tsec") - self.p("\t\tsbc " + r_str) - self.p("\t\ttay") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttya") + self.p("\t\tsec") + self.p("\t\tsbc " + r_str) + self.p("\t\ttay") else: raise CodeError("unsupported register for aug assign", str(lvalue)) # @todo -=.word elif operator == "&=": if lvalue.register == "A": self.p("\t\tand " + r_str) elif lvalue.register == "X": - self.p("\t\tpha") - self.p("\t\ttxa") - self.p("\t\tand " + r_str) - self.p("\t\ttax") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttxa") + self.p("\t\tand " + r_str) + self.p("\t\ttax") elif lvalue.register == "Y": - self.p("\t\tpha") - self.p("\t\ttya") - self.p("\t\tand " + r_str) - self.p("\t\ttay") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttya") + self.p("\t\tand " + r_str) + self.p("\t\ttay") else: raise CodeError("unsupported register for aug assign", str(lvalue)) # @todo &=.word elif operator == "|=": if lvalue.register == "A": self.p("\t\tora " + r_str) elif lvalue.register == "X": - self.p("\t\tpha") - self.p("\t\ttxa") - self.p("\t\tora " + r_str) - self.p("\t\ttax") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttxa") + self.p("\t\tora " + r_str) + self.p("\t\ttax") elif lvalue.register == "Y": - self.p("\t\tpha") - self.p("\t\ttya") - self.p("\t\tora " + r_str) - self.p("\t\ttay") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttya") + self.p("\t\tora " + r_str) + self.p("\t\ttay") else: raise CodeError("unsupported register for aug assign", str(lvalue)) # @todo |=.word elif operator == "^=": if lvalue.register == "A": self.p("\t\teor " + r_str) elif lvalue.register == "X": - self.p("\t\tpha") - self.p("\t\ttxa") - self.p("\t\teor " + r_str) - self.p("\t\ttax") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttxa") + self.p("\t\teor " + r_str) + self.p("\t\ttax") elif lvalue.register == "Y": - self.p("\t\tpha") - self.p("\t\ttya") - self.p("\t\teor " + r_str) - self.p("\t\ttay") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttya") + self.p("\t\teor " + r_str) + self.p("\t\ttay") else: raise CodeError("unsupported register for aug assign", str(lvalue)) # @todo ^=.word elif operator == ">>=": @@ -1133,17 +1121,15 @@ class CodeGenerator: if lvalue.register == "A": self.p("\t\tlsr " + r_str) elif lvalue.register == "X": - self.p("\t\tpha") - self.p("\t\ttxa") - self.p("\t\tlsr " + r_str) - self.p("\t\ttax") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttxa") + self.p("\t\tlsr " + r_str) + self.p("\t\ttax") elif lvalue.register == "Y": - self.p("\t\tpha") - self.p("\t\ttya") - self.p("\t\tlsr " + r_str) - self.p("\t\ttay") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttya") + self.p("\t\tlsr " + r_str) + self.p("\t\ttay") else: raise CodeError("unsupported lvalue register for shift", str(lvalue)) # @todo >>=.word elif operator == "<<=": @@ -1153,17 +1139,15 @@ class CodeGenerator: if lvalue.register == "A": self.p("\t\tasl " + r_str) elif lvalue.register == "X": - self.p("\t\tpha") - self.p("\t\ttxa") - self.p("\t\tasl " + r_str) - self.p("\t\ttax") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttxa") + self.p("\t\tasl " + r_str) + self.p("\t\ttax") elif lvalue.register == "Y": - self.p("\t\tpha") - self.p("\t\ttya") - self.p("\t\tasl " + r_str) - self.p("\t\ttay") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttya") + self.p("\t\tasl " + r_str) + self.p("\t\ttay") else: raise CodeError("unsupported lvalue register for shift", str(lvalue)) # @todo >>=.word @@ -1174,19 +1158,17 @@ class CodeGenerator: self.p("\t\tclc") self.p("\t\tadc #" + r_str) elif lvalue.register == "X": - self.p("\t\tpha") - self.p("\t\ttxa") - self.p("\t\tclc") - self.p("\t\tadc #" + r_str) - self.p("\t\ttax") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttxa") + self.p("\t\tclc") + self.p("\t\tadc #" + r_str) + self.p("\t\ttax") elif lvalue.register == "Y": - self.p("\t\tpha") - self.p("\t\ttya") - self.p("\t\tclc") - self.p("\t\tadc #" + r_str) - self.p("\t\ttay") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttya") + self.p("\t\tclc") + self.p("\t\tadc #" + r_str) + self.p("\t\ttay") else: raise CodeError("unsupported register for aug assign", str(lvalue)) # @todo +=.word elif operator == "-=": @@ -1194,70 +1176,62 @@ class CodeGenerator: self.p("\t\tsec") self.p("\t\tsbc #" + r_str) elif lvalue.register == "X": - self.p("\t\tpha") - self.p("\t\ttxa") - self.p("\t\tsec") - self.p("\t\tsbc #" + r_str) - self.p("\t\ttax") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttxa") + self.p("\t\tsec") + self.p("\t\tsbc #" + r_str) + self.p("\t\ttax") elif lvalue.register == "Y": - self.p("\t\tpha") - self.p("\t\ttya") - self.p("\t\tsec") - self.p("\t\tsbc #" + r_str) - self.p("\t\ttay") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttya") + self.p("\t\tsec") + self.p("\t\tsbc #" + r_str) + self.p("\t\ttay") else: raise CodeError("unsupported register for aug assign", str(lvalue)) # @todo -=.word elif operator == "&=": if lvalue.register == "A": self.p("\t\tand #" + r_str) elif lvalue.register == "X": - self.p("\t\tpha") - self.p("\t\ttxa") - self.p("\t\tand #" + r_str) - self.p("\t\ttax") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttxa") + self.p("\t\tand #" + r_str) + self.p("\t\ttax") elif lvalue.register == "Y": - self.p("\t\tpha") - self.p("\t\ttya") - self.p("\t\tand #" + r_str) - self.p("\t\ttay") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttya") + self.p("\t\tand #" + r_str) + self.p("\t\ttay") else: raise CodeError("unsupported register for aug assign", str(lvalue)) # @todo &=.word elif operator == "|=": if lvalue.register == "A": self.p("\t\tora #" + r_str) elif lvalue.register == "X": - self.p("\t\tpha") - self.p("\t\ttxa") - self.p("\t\tora #" + r_str) - self.p("\t\ttax") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttxa") + self.p("\t\tora #" + r_str) + self.p("\t\ttax") elif lvalue.register == "Y": - self.p("\t\tpha") - self.p("\t\ttya") - self.p("\t\tora #" + r_str) - self.p("\t\ttay") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttya") + self.p("\t\tora #" + r_str) + self.p("\t\ttay") else: raise CodeError("unsupported register for aug assign", str(lvalue)) # @todo |=.word elif operator == "^=": if lvalue.register == "A": self.p("\t\teor #" + r_str) elif lvalue.register == "X": - self.p("\t\tpha") - self.p("\t\ttxa") - self.p("\t\teor #" + r_str) - self.p("\t\ttax") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttxa") + self.p("\t\teor #" + r_str) + self.p("\t\ttax") elif lvalue.register == "Y": - self.p("\t\tpha") - self.p("\t\ttya") - self.p("\t\teor #" + r_str) - self.p("\t\ttay") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttya") + self.p("\t\teor #" + r_str) + self.p("\t\ttay") else: raise CodeError("unsupported register for aug assign", str(lvalue)) # @todo ^=.word elif operator == ">>=": @@ -1271,17 +1245,15 @@ class CodeGenerator: if lvalue.register == "A": shifts_A(rvalue.value) elif lvalue.register == "X": - self.p("\t\tpha") - self.p("\t\ttxa") - shifts_A(rvalue.value) - self.p("\t\ttax") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttxa") + shifts_A(rvalue.value) + self.p("\t\ttax") elif lvalue.register == "Y": - self.p("\t\tpha") - self.p("\t\ttya") - shifts_A(rvalue.value) - self.p("\t\ttay") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttya") + shifts_A(rvalue.value) + self.p("\t\ttay") else: raise CodeError("unsupported register for aug assign", str(lvalue)) # @todo >>=.word elif operator == "<<=": @@ -1295,17 +1267,15 @@ class CodeGenerator: if lvalue.register == "A": shifts_A(rvalue.value) elif lvalue.register == "X": - self.p("\t\tpha") - self.p("\t\ttxa") - shifts_A(rvalue.value) - self.p("\t\ttax") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttxa") + shifts_A(rvalue.value) + self.p("\t\ttax") elif lvalue.register == "Y": - self.p("\t\tpha") - self.p("\t\ttya") - shifts_A(rvalue.value) - self.p("\t\ttay") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttya") + shifts_A(rvalue.value) + self.p("\t\ttay") else: raise CodeError("unsupported register for aug assign", str(lvalue)) # @todo <<=.word @@ -1319,20 +1289,18 @@ class CodeGenerator: self.p("\t\tadc " + Parser.to_hex(Zeropage.SCRATCH_B1)) elif lvalue.register == "X": self.p("\t\tst{:s} {:s}".format(rvalue.register.lower(), Parser.to_hex(Zeropage.SCRATCH_B1))) - self.p("\t\tpha") - self.p("\t\ttxa") - self.p("\t\tclc") - self.p("\t\tadc " + Parser.to_hex(Zeropage.SCRATCH_B1)) - self.p("\t\ttax") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttxa") + self.p("\t\tclc") + self.p("\t\tadc " + Parser.to_hex(Zeropage.SCRATCH_B1)) + self.p("\t\ttax") elif lvalue.register == "Y": self.p("\t\tst{:s} {:s}".format(rvalue.register.lower(), Parser.to_hex(Zeropage.SCRATCH_B1))) - self.p("\t\tpha") - self.p("\t\ttya") - self.p("\t\tclc") - self.p("\t\tadc " + Parser.to_hex(Zeropage.SCRATCH_B1)) - self.p("\t\ttay") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttya") + self.p("\t\tclc") + self.p("\t\tadc " + Parser.to_hex(Zeropage.SCRATCH_B1)) + self.p("\t\ttay") else: raise CodeError("unsupported lvalue register for aug assign", str(lvalue)) # @todo +=.word elif operator == "-=": @@ -1344,20 +1312,18 @@ class CodeGenerator: self.p("\t\tsbc " + Parser.to_hex(Zeropage.SCRATCH_B1)) elif lvalue.register == "X": self.p("\t\tst{:s} {:s}".format(rvalue.register.lower(), Parser.to_hex(Zeropage.SCRATCH_B1))) - self.p("\t\tpha") - self.p("\t\ttxa") - self.p("\t\tsec") - self.p("\t\tsbc " + Parser.to_hex(Zeropage.SCRATCH_B1)) - self.p("\t\ttax") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttxa") + self.p("\t\tsec") + self.p("\t\tsbc " + Parser.to_hex(Zeropage.SCRATCH_B1)) + self.p("\t\ttax") elif lvalue.register == "Y": self.p("\t\tst{:s} {:s}".format(rvalue.register.lower(), Parser.to_hex(Zeropage.SCRATCH_B1))) - self.p("\t\tpha") - self.p("\t\ttya") - self.p("\t\tsec") - self.p("\t\tsbc " + Parser.to_hex(Zeropage.SCRATCH_B1)) - self.p("\t\ttay") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttya") + self.p("\t\tsec") + self.p("\t\tsbc " + Parser.to_hex(Zeropage.SCRATCH_B1)) + self.p("\t\ttay") else: raise CodeError("unsupported lvalue register for aug assign", str(lvalue)) # @todo -=.word elif operator == "&=": @@ -1368,18 +1334,16 @@ class CodeGenerator: self.p("\t\tand " + Parser.to_hex(Zeropage.SCRATCH_B1)) elif lvalue.register == "X": self.p("\t\tst{:s} {:s}".format(rvalue.register.lower(), Parser.to_hex(Zeropage.SCRATCH_B1))) - self.p("\t\tpha") - self.p("\t\ttxa") - self.p("\t\tand " + Parser.to_hex(Zeropage.SCRATCH_B1)) - self.p("\t\ttax") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttxa") + self.p("\t\tand " + Parser.to_hex(Zeropage.SCRATCH_B1)) + self.p("\t\ttax") elif lvalue.register == "Y": self.p("\t\tst{:s} {:s}".format(rvalue.register.lower(), Parser.to_hex(Zeropage.SCRATCH_B1))) - self.p("\t\tpha") - self.p("\t\ttya") - self.p("\t\tand " + Parser.to_hex(Zeropage.SCRATCH_B1)) - self.p("\t\ttay") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttya") + self.p("\t\tand " + Parser.to_hex(Zeropage.SCRATCH_B1)) + self.p("\t\ttay") else: raise CodeError("unsupported lvalue register for aug assign", str(lvalue)) # @todo &=.word elif operator == "|=": @@ -1390,18 +1354,16 @@ class CodeGenerator: self.p("\t\tora " + Parser.to_hex(Zeropage.SCRATCH_B1)) elif lvalue.register == "X": self.p("\t\tst{:s} {:s}".format(rvalue.register.lower(), Parser.to_hex(Zeropage.SCRATCH_B1))) - self.p("\t\tpha") - self.p("\t\ttxa") - self.p("\t\tora " + Parser.to_hex(Zeropage.SCRATCH_B1)) - self.p("\t\ttax") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttxa") + self.p("\t\tora " + Parser.to_hex(Zeropage.SCRATCH_B1)) + self.p("\t\ttax") elif lvalue.register == "Y": self.p("\t\tst{:s} {:s}".format(rvalue.register.lower(), Parser.to_hex(Zeropage.SCRATCH_B1))) - self.p("\t\tpha") - self.p("\t\ttya") - self.p("\t\tora " + Parser.to_hex(Zeropage.SCRATCH_B1)) - self.p("\t\ttay") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttya") + self.p("\t\tora " + Parser.to_hex(Zeropage.SCRATCH_B1)) + self.p("\t\ttay") else: raise CodeError("unsupported lvalue register for aug assign", str(lvalue)) # @todo |=.word elif operator == "^=": @@ -1412,18 +1374,16 @@ class CodeGenerator: self.p("\t\teor " + Parser.to_hex(Zeropage.SCRATCH_B1)) elif lvalue.register == "X": self.p("\t\tst{:s} {:s}".format(rvalue.register.lower(), Parser.to_hex(Zeropage.SCRATCH_B1))) - self.p("\t\tpha") - self.p("\t\ttxa") - self.p("\t\teor " + Parser.to_hex(Zeropage.SCRATCH_B1)) - self.p("\t\ttax") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttxa") + self.p("\t\teor " + Parser.to_hex(Zeropage.SCRATCH_B1)) + self.p("\t\ttax") elif lvalue.register == "Y": self.p("\t\tst{:s} {:s}".format(rvalue.register.lower(), Parser.to_hex(Zeropage.SCRATCH_B1))) - self.p("\t\tpha") - self.p("\t\ttya") - self.p("\t\teor " + Parser.to_hex(Zeropage.SCRATCH_B1)) - self.p("\t\ttay") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttya") + self.p("\t\teor " + Parser.to_hex(Zeropage.SCRATCH_B1)) + self.p("\t\ttay") else: raise CodeError("unsupported lvalue register for aug assign", str(lvalue)) # @todo ^=.word elif operator == ">>=": @@ -1434,18 +1394,16 @@ class CodeGenerator: self.p("\t\tlsr " + Parser.to_hex(Zeropage.SCRATCH_B1)) elif lvalue.register == "X": self.p("\t\tst{:s} {:s}".format(rvalue.register.lower(), Parser.to_hex(Zeropage.SCRATCH_B1))) - self.p("\t\tpha") - self.p("\t\ttxa") - self.p("\t\tlsr " + Parser.to_hex(Zeropage.SCRATCH_B1)) - self.p("\t\ttax") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttxa") + self.p("\t\tlsr " + Parser.to_hex(Zeropage.SCRATCH_B1)) + self.p("\t\ttax") elif lvalue.register == "Y": self.p("\t\tst{:s} {:s}".format(rvalue.register.lower(), Parser.to_hex(Zeropage.SCRATCH_B1))) - self.p("\t\tpha") - self.p("\t\ttya") - self.p("\t\tlsr " + Parser.to_hex(Zeropage.SCRATCH_B1)) - self.p("\t\ttay") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttya") + self.p("\t\tlsr " + Parser.to_hex(Zeropage.SCRATCH_B1)) + self.p("\t\ttay") else: raise CodeError("unsupported lvalue register for aug assign", str(lvalue)) # @todo >>=.word elif operator == "<<=": @@ -1456,18 +1414,16 @@ class CodeGenerator: self.p("\t\tasl " + Parser.to_hex(Zeropage.SCRATCH_B1)) elif lvalue.register == "X": self.p("\t\tst{:s} {:s}".format(rvalue.register.lower(), Parser.to_hex(Zeropage.SCRATCH_B1))) - self.p("\t\tpha") - self.p("\t\ttxa") - self.p("\t\tasl " + Parser.to_hex(Zeropage.SCRATCH_B1)) - self.p("\t\ttax") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttxa") + self.p("\t\tasl " + Parser.to_hex(Zeropage.SCRATCH_B1)) + self.p("\t\ttax") elif lvalue.register == "Y": self.p("\t\tst{:s} {:s}".format(rvalue.register.lower(), Parser.to_hex(Zeropage.SCRATCH_B1))) - self.p("\t\tpha") - self.p("\t\ttya") - self.p("\t\tasl " + Parser.to_hex(Zeropage.SCRATCH_B1)) - self.p("\t\ttay") - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\ttya") + self.p("\t\tasl " + Parser.to_hex(Zeropage.SCRATCH_B1)) + self.p("\t\ttay") else: raise CodeError("unsupported lvalue register for aug assign", str(lvalue)) # @todo <<=.word @@ -1556,14 +1512,14 @@ class CodeGenerator: floatvalue = float(rvalue.value) mflpt = self.to_mflpt5(floatvalue) target = mmv.name or Parser.to_hex(mmv.address) - self.p("\t\tpha\t\t\t; {:s} = {}".format(target, rvalue.name or floatvalue)) - a_reg_value = None - for i, byte in enumerate(mflpt): - if byte != a_reg_value: - self.p("\t\tlda #${:02x}".format(byte)) - a_reg_value = byte - self.p("\t\tsta {:s}+{:d}".format(target, i)) - self.p("\t\tpla") + with self.preserving_registers({'A'}): + self.p("\t\t\t\t\t; {:s} = {}".format(target, rvalue.name or floatvalue)) + a_reg_value = None + for i, byte in enumerate(mflpt): + if byte != a_reg_value: + self.p("\t\tlda #${:02x}".format(byte)) + a_reg_value = byte + self.p("\t\tsta {:s}+{:d}".format(target, i)) def generate_assign_reg_to_memory(self, lv: MemMappedValue, r_register: str) -> None: # Memory = Register @@ -1692,9 +1648,12 @@ class CodeGenerator: raise CodeError("invalid register " + lv.register) @contextlib.contextmanager - def preserving_registers(self, registers: Set[str], loads_a_within: bool=False): - # this clobbers a ZP scratch register and is therefore NOT safe to use in interrupts + def preserving_registers(self, registers: Set[str], loads_a_within: bool=False, always_preserve: bool=False): + # this sometimes clobbers a ZP scratch register and is therefore NOT safe to use in interrupts # see http://6502.org/tutorials/register_preservation.html + if not self.cur_block.preserve_registers and not always_preserve: + yield + return if registers == {'A'}: self.p("\t\tpha") yield diff --git a/il65/parse.py b/il65/parse.py index 01b1b6dac..9a0ad0bde 100644 --- a/il65/parse.py +++ b/il65/parse.py @@ -39,6 +39,7 @@ class ParseResult: 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: @@ -176,7 +177,7 @@ class Parser: break def _parse_1(self) -> None: - self.cur_block = Block("
", self.sourceref, self.root_scope) + 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: @@ -188,7 +189,7 @@ class Parser: block = self.parse_block() if block: self.result.add_block(block) - elif next_line.startswith(("import ", "import\t")): + elif next_line.startswith(("%import ", "%import\t")): self.parse_import() else: break @@ -402,76 +403,91 @@ class Parser: 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(("output ", "output\t")): - 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)) - elif line.startswith(("zp ", "zp\t")): - 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)) - elif line.startswith("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") - else: - # 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") - return + 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 == "preserve_registers": + if preserve_specified: + raise self.PError("can only specify preserve_registers 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")): + if not line.startswith(("%import ", "%import\t")): raise self.PError("expected import") try: _, arg = line.split(maxsplit=1) @@ -528,7 +544,7 @@ class Parser: raise self.PError("expected '~' (block)") block_args = line[1:].split() arg = "" - self.cur_block = Block("", self.sourceref.copy(), self.root_scope) + self.cur_block = Block("", self.sourceref.copy(), self.root_scope, self.result.preserve_registers) is_zp_block = False while block_args: arg = block_args.pop(0) @@ -542,7 +558,7 @@ class Parser: 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.copy(), self.root_scope) + self.cur_block = Block(arg, self.sourceref.copy(), 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: @@ -605,14 +621,34 @@ class Parser: line = self.next_line() unstripped_line = line line = line.strip() - if line == "}": + 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 == "preserve_registers": + 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 - if line.startswith(("var ", "var\t")): + elif line.startswith(("var ", "var\t")): self.parse_var_def(line) elif line.startswith(("const ", "const\t")): self.parse_const_def(line) @@ -622,19 +658,9 @@ class Parser: if is_zp_block: raise self.PError("ZP block cannot contain subroutines") self.parse_subroutine_def(line) - elif line.startswith(("asminclude ", "asminclude\t", "asmbinary ", "asmbinary\t")): - if is_zp_block: - raise self.PError("ZP block cannot contain assembler directives") - self.cur_block.statements.append(self.parse_asminclude(line)) - elif line.startswith(("asm ", "asm\t")): - 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 line == "breakpoint": - self.cur_block.statements.append(BreakpointStmt(self.sourceref)) - self.print_warning("breakpoint defined") 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)) @@ -714,7 +740,8 @@ class Parser: if code_decl: address = None # parse the subroutine code lines (until the closing '}') - subroutine_block = Block(self.cur_block.name + "." + name, self.sourceref, self.cur_block.symbols) + subroutine_block = Block(self.cur_block.name + "." + name, self.sourceref, self.cur_block.symbols, + self.cur_block.preserve_registers) current_block = self.cur_block self.cur_block = subroutine_block while True: @@ -727,10 +754,12 @@ class Parser: break if line.startswith(("sub ", "sub\t")): raise self.PError("cannot nest subroutines") - elif line.startswith(("asm ", "asm\t")): + elif line.startswith(("%asm ", "%asm\t")): self.prev_line() subroutine_block.statements.append(self.parse_asm()) elif unstripped_line.startswith((" ", "\t")): + if line.endswith("{"): + raise self.PError("invalid statement") subroutine_block.statements.append(self.parse_statement(line)) elif line: self.parse_label(line) @@ -792,25 +821,31 @@ class Parser: # 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) + return self.parse_call_or_goto(subname, groups["arguments"], None, None, True, condition=condition) match = re.fullmatch(r"goto\s+(?P[\S]+?)\s*(\((?P.*)\))?\s*", line) if match: # goto groups = match.groupdict() subname = groups["subname"] - if '!' in subname: - raise self.PError("goto is always without register preservation, should not have exclamation mark") - 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) + return self.parse_call_or_goto(subname, groups["arguments"], None, None, True) + match = re.fullmatch(r"(?P[^\(]*\s*=)?\s*(?P[\S]+?)\s*(?P!\s*[A-Z]*)?\s*(\((?P.*)\))?\s*", line) if match: # subroutine call (not a goto) with possible output param assignment groups = match.groupdict() - preserve = not bool(groups["fcall"]) + preserve = None + preserve_str = groups["preserve"] + if preserve_str and preserve_str.startswith('!'): + preserve_str = preserve_str.replace(' ', '') + if preserve_str == "!": + preserve = {'A', 'X', 'Y'} + else: + preserve = set(preserve_str[1:]) + for r in preserve: + if r not in REGISTER_BYTES: + raise self.PError("invalid register in call preservation list") subname = groups["subname"] arguments = groups["arguments"] outputs = groups["outputs"] or "" @@ -843,7 +878,7 @@ 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, condition: IfCondition=None) -> CallStmt: + preserve_regs: Set[str]=None, is_goto: bool=False, condition: IfCondition=None) -> CallStmt: if not is_goto: assert condition is None argumentstr = argumentstr.strip() if argumentstr else "" @@ -1008,8 +1043,8 @@ class Parser: def parse_asm(self) -> InlineAsm: line = self.next_line() aline = line.split() - if not len(aline) == 2 or aline[0] != "asm" or aline[1] != "{": - raise self.PError("invalid asm start") + if not len(aline) == 2 or aline[0] != "%asm" or aline[1] != "{": + raise self.PError("invalid asm directive") asmlines = [] # type: List[str] while True: line = self.next_line() @@ -1036,7 +1071,7 @@ class Parser: def parse_asminclude(self, line: str) -> InlineAsm: aline = line.split() if len(aline) < 2: - raise self.PError("invalid asminclude or asmbinary statement") + raise self.PError("invalid asminclude or asmbinary directive") filename = aline[1] if not filename.startswith('"') or not filename.endswith('"'): raise self.PError("filename must be between quotes") @@ -1049,14 +1084,14 @@ class Parser: raise self.PError("included file not found") print("copying included file to output location:", filename) shutil.copy(filename_in_sourcedir, filename_in_output_location) - if aline[0] == "asminclude": + if aline[0] == "%asminclude": if len(aline) == 3: scopename = aline[2] lines = ['{:s}\t.binclude "{:s}"'.format(scopename, filename)] else: - raise self.PError("invalid asminclude statement") + raise self.PError("invalid asminclude directive") return InlineAsm(lines, self.sourceref) - elif aline[0] == "asmbinary": + elif aline[0] == "%asmbinary": if len(aline) == 4: offset = parse_expr_as_int(aline[2], None, None, self.sourceref) length = parse_expr_as_int(aline[3], None, None, self.sourceref) @@ -1067,7 +1102,7 @@ class Parser: elif len(aline) == 2: lines = ['\t.binary "{:s}"'.format(filename)] else: - raise self.PError("invalid asmbinary statement") + raise self.PError("invalid asmbinary directive") return InlineAsm(lines, self.sourceref) else: raise self.PError("invalid statement") diff --git a/lib/c64lib.ill b/lib/c64lib.ill index 123b0929d..03be26989 100644 --- a/lib/c64lib.ill +++ b/lib/c64lib.ill @@ -234,7 +234,7 @@ sub init_system () -> (?) { ; This means that the BASIC, KERNAL and CHARGEN ROMs are banked in, ; the VIC, SID and CIA chips are reset, screen is cleared, and the default IRQ is set. ; Also a different color scheme is chosen to identify ourselves a little. - asm { + %asm { sei cld lda #%00101111 @@ -269,7 +269,7 @@ sub init_system () -> (?) { sub FREADS32 () -> (?) { ; ---- fac1 = signed int32 from $62-$65 big endian (MSB FIRST) - asm { + %asm { lda $62 eor #$ff asl a @@ -281,7 +281,7 @@ sub FREADS32 () -> (?) { sub FREADUS32 () -> (?) { ; ---- fac1 = uint32 from $62-$65 big endian (MSB FIRST) - asm { + %asm { sec lda #0 ldx #$a0 @@ -292,7 +292,7 @@ sub FREADUS32 () -> (?) { sub FREADS24AXY (lo: A, mid: X, hi: Y) -> (?) { ; ---- fac1 = signed int24 (A/X/Y contain lo/mid/hi bytes) ; note: there is no FREADU24AXY (unsigned), use FREADUS32 instead. - asm { + %asm { sty $62 stx $63 sta $64 @@ -308,7 +308,7 @@ sub FREADS24AXY (lo: A, mid: X, hi: Y) -> (?) { sub GIVUAYF (uword: AY) -> (?) { ; ---- unsigned 16 bit word in A/Y (lo/hi) to fac1 - asm { + %asm { sty $62 sta $63 ldx #$90 @@ -319,7 +319,7 @@ sub GIVUAYF (uword: AY) -> (?) { sub GIVAYFAY (sword: AY) -> (?) { ; ---- signed 16 bit word in A/Y (lo/hi) to float in fac1 - asm { + %asm { sta c64.SCRATCH_ZP1 tya ldy c64.SCRATCH_ZP1 @@ -329,7 +329,7 @@ sub GIVAYFAY (sword: AY) -> (?) { sub FTOSWRDAY () -> (AY, X?) { ; ---- fac1 to signed word in A/Y - asm { + %asm { jsr c64.FTOSWORDYA ; note the inverse Y/A order sta c64.SCRATCH_ZP1 tya @@ -340,7 +340,7 @@ sub FTOSWRDAY () -> (AY, X?) { sub GETADRAY () -> (AY, X?) { ; ---- fac1 to unsigned word in A/Y - asm { + %asm { jsr c64.GETADR ; this uses the inverse order, Y/A sta c64.SCRATCH_ZP1 tya @@ -353,7 +353,7 @@ sub GETADRAY () -> (AY, X?) { sub copy_mflt (source: XY) -> (A?, Y?) { ; ---- copy a 5 byte MFLT floating point variable to another place ; input: X/Y = source address, c64.SCRATCH_ZPWORD1 = destination address - asm { + %asm { stx c64.SCRATCH_ZP1 sty c64.SCRATCH_ZPWORD1+1 ldy #0 @@ -378,7 +378,7 @@ sub copy_mflt (source: XY) -> (A?, Y?) { sub float_add_one (mflt: XY) -> (?) { ; ---- add 1 to the MFLT pointed to by X/Y. Clobbers A, X, Y - asm { + %asm { stx c64.SCRATCH_ZP1 sty c64.SCRATCH_ZP2 txa @@ -394,7 +394,7 @@ sub float_add_one (mflt: XY) -> (?) { sub float_sub_one (mflt: XY) -> (?) { ; ---- subtract 1 from the MFLT pointed to by X/Y. Clobbers A, X, Y - asm { + %asm { stx c64.SCRATCH_ZP1 sty c64.SCRATCH_ZP2 lda # (?) { sub float_add_SW1_to_XY (mflt: XY) -> (?) { ; ---- add MFLT pointed to by SCRATCH_ZPWORD1 to the MFLT pointed to by X/Y. Clobbers A, X, Y - asm { + %asm { stx c64.SCRATCH_ZP1 sty c64.SCRATCH_ZP2 txa @@ -427,7 +427,7 @@ sub float_add_SW1_to_XY (mflt: XY) -> (?) { sub float_sub_SW1_from_XY (mflt: XY) -> (?) { ; ---- subtract MFLT pointed to by SCRATCH_ZPWORD1 from the MFLT pointed to by X/Y. Clobbers A, X, Y - asm { + %asm { stx c64.SCRATCH_ZP1 sty c64.SCRATCH_ZP2 lda c64.SCRATCH_ZPWORD1 @@ -454,7 +454,7 @@ sub clear_screen (char:A, color: Y) -> () { ; ---- clear the character screen with the given fill character and character color. ; (assumes screen is at $0400, could be altered in the future with self-modifying code) - asm { + %asm { sta _loop + 1 ; self-modifying stx c64.SCRATCH_ZP1 ldx #0 @@ -483,7 +483,7 @@ sub scroll_left_full (alsocolors: SC) -> (A?, X?, Y?) { ; ---- scroll the whole screen 1 character to the left ; contents of the rightmost column are unchanged, you should clear/refill this yourself ; Carry flag determines if screen color data should be scrolled too - asm { + %asm { bcs + jmp _scroll_screen @@ -542,7 +542,7 @@ sub scroll_right_full (alsocolors: SC) -> (A?, X?) { ; ---- scroll the whole screen 1 character to the right ; contents of the leftmost column are unchanged, you should clear/refill this yourself ; Carry flag determines if screen color data should be scrolled too - asm { + %asm { bcs + jmp _scroll_screen @@ -593,7 +593,7 @@ sub scroll_up_full (alsocolors: SC) -> (A?, X?) { ; ---- scroll the whole screen 1 character up ; contents of the bottom row are unchanged, you should refill/clear this yourself ; Carry flag determines if screen color data should be scrolled too - asm { + %asm { bcs + jmp _scroll_screen @@ -644,7 +644,7 @@ sub scroll_down_full (alsocolors: SC) -> (A?, X?) { ; ---- scroll the whole screen 1 character down ; contents of the top row are unchanged, you should refill/clear this yourself ; Carry flag determines if screen color data should be scrolled too - asm { + %asm { bcs + jmp _scroll_screen @@ -693,7 +693,7 @@ _scroll_screen sub byte2decimal (ubyte: A) -> (Y, X, A) { ; ---- A to decimal string in Y/X/A (100s in Y, 10s in X, 1s in A) - asm { + %asm { ldy #$2f ldx #$3a sec @@ -710,7 +710,7 @@ sub byte2decimal (ubyte: A) -> (Y, X, A) { sub byte2hex (ubyte: A) -> (X, Y, A?) { ; ---- A to hex string in XY (first hex char in X, second hex char in Y) - asm { + %asm { pha and #$0f tax @@ -733,7 +733,7 @@ hex_digits .text "0123456789abcdef" ; can probably be reused for other stuff as var .text word2hex_output = "1234" ; 0-terminated, to make printing easier sub word2hex (word: XY) -> (?) { ; ---- convert 16 bit word in X/Y into 4-character hexadecimal string into memory 'word2hex_output' - asm { + %asm { stx c64.SCRATCH_ZP2 tya jsr byte2hex @@ -757,7 +757,7 @@ sub word2bcd (word: XY) -> (A?, X?) { ; into a BCD value that is being doubled on each iteration. As all the ; arithmetic is being done in BCD the result is a binary to decimal ; conversion. - asm { + %asm { stx c64.SCRATCH_ZP1 sty c64.SCRATCH_ZP2 sed ; switch to decimal mode @@ -789,7 +789,7 @@ sub word2bcd (word: XY) -> (A?, X?) { var .array(5) word2decimal_output sub word2decimal (word: XY) -> (?) { ; ---- convert 16 bit word in X/Y into decimal string into memory 'word2decimal_output' - asm { + %asm { jsr word2bcd lda word2bcd_bcdbuff+2 clc @@ -827,7 +827,7 @@ sub print_string (address: XY) -> (A?, Y?) { ; note: the IL65 compiler contains an optimization that will replace ; a call to this subroutine with a string argument of just one char, ; by just one call to c64.CHROUT of that single char. - asm { + %asm { stx c64.SCRATCH_ZP1 sty c64.SCRATCH_ZP2 ldy #0 @@ -843,7 +843,7 @@ sub print_string (address: XY) -> (A?, Y?) { sub print_pstring (address: XY) -> (A?, X?, Y) { ; ---- print pstring (length as first byte) from X/Y, returns str len in Y - asm { + %asm { stx c64.SCRATCH_ZP1 sty c64.SCRATCH_ZP2 ldy #0 @@ -863,7 +863,7 @@ sub print_pstring (address: XY) -> (A?, X?, Y) { sub print_pimmediate () -> () { ; ---- print pstring in memory immediately following the subroutine fast call instruction ; note that the clobbered registers (A,X,Y) are not listed ON PURPOSE - asm { + %asm { tsx lda $102,x tay ; put high byte in y @@ -891,7 +891,7 @@ sub print_pimmediate () -> () { sub print_byte_decimal0 (ubyte: A) -> (?) { ; ---- print the byte in A in decimal form, with left padding 0s (3 positions total) - asm { + %asm { jsr byte2decimal pha tya @@ -906,7 +906,7 @@ sub print_byte_decimal0 (ubyte: A) -> (?) { sub print_byte_decimal (ubyte: A) -> (?) { ; ---- print the byte in A in decimal form, without left padding 0s - asm { + %asm { jsr byte2decimal pha cpy #'0' @@ -927,7 +927,7 @@ _print_tens txa sub print_byte_hex (prefix: SC, ubyte: A) -> (?) { ; ---- print the byte in A in hex form (if Carry is set, a radix prefix '$' is printed as well) - asm { + %asm { bcc + pha lda #'$' @@ -945,7 +945,7 @@ sub print_byte_hex (prefix: SC, ubyte: A) -> (?) { sub print_word_hex (prefix: SC, word: XY) -> (?) { ; ---- print the (unsigned) word in X/Y in hexadecimal form (4 digits) ; (if Carry is set, a radix prefix '$' is printed as well) - asm { + %asm { stx c64.SCRATCH_ZP1 tya jsr print_byte_hex @@ -958,7 +958,7 @@ sub print_word_hex (prefix: SC, word: XY) -> (?) { sub print_word_decimal0 (word: XY) -> (?) { ; ---- print the (unsigned) word in X/Y in decimal form, with left padding 0s (5 positions total) - asm { + %asm { jsr word2decimal lda word2decimal_output jsr c64.CHROUT @@ -976,7 +976,7 @@ sub print_word_decimal0 (word: XY) -> (?) { sub print_word_decimal (word: XY) -> (A?, X? Y?) { ; ---- print the word in X/Y in decimal form, without left padding 0s - asm { + %asm { jsr word2decimal ldy #0 lda word2decimal_output @@ -1011,7 +1011,7 @@ sub input_chars (buffer: AX) -> (A?, Y) { ; ---- Input a string (max. 80 chars) from the keyboard. ; It assumes the keyboard is selected as I/O channel!! - asm { + %asm { sta c64.SCRATCH_ZP1 stx c64.SCRATCH_ZP2 ldy #0 ; char counter = 0 @@ -1043,7 +1043,7 @@ sub input_chars (buffer: AX) -> (A?, Y) { ; ; - moving data to higher addresses works even if areas overlap ; ; - moving data to lower addresses only works if areas do not overlap ; ; @todo fix this -; asm { +; %asm { ; lda #src_start ; sta $5f diff --git a/lib/il65lib.ill b/lib/il65lib.ill index a0f01759b..ba6c8f7aa 100644 --- a/lib/il65lib.ill +++ b/lib/il65lib.ill @@ -9,7 +9,7 @@ ~ il65_lib_zp { ; note: separate block so the 64tass assembler can remove this when no zp restore is required - asm { + %asm { ; ---- store the Zeropage in a backup area save_zeropage @@ -62,7 +62,7 @@ zp_backup .fill 256, 0 memory .word SCRATCH_ZPWORD2 = $fd ; scratch word in ZP ($fd/$fe) - asm { + %asm { ; ---- jmp (indirect) routines for register pairs containing the indirect address jsr_indirect_nozpuse_AX diff --git a/lib/mathlib.ill b/lib/mathlib.ill index ea19a4e11..0aa648297 100644 --- a/lib/mathlib.ill +++ b/lib/mathlib.ill @@ -22,7 +22,7 @@ sub multiply_bytes (byte1: X, byte2: Y) -> (A, X?) { ; ---- multiply 2 bytes, result as byte in A (signed or unsigned) - asm { + %asm { stx SCRATCH_ZP1 sty SCRATCH_ZP2 ldx #8 @@ -40,7 +40,7 @@ sub multiply_bytes (byte1: X, byte2: Y) -> (A, X?) { sub multiply_bytes_16 (byte1: X, byte2: Y) -> (A?, XY) { ; ---- multiply 2 bytes, result as word in X/Y (unsigned) - asm { + %asm { lda #0 _m_with_add stx SCRATCH_ZP1 sty SCRATCH_ZP2 @@ -62,7 +62,7 @@ _m_with_add stx SCRATCH_ZP1 sub multiply_bytes_addA_16 (byte1: X, byte2: Y, add: A) -> (A?, XY) { ; ---- multiply 2 bytes and add A, result as word in X/Y (unsigned) - asm { + %asm { jmp multiply_bytes_16._m_with_add } } @@ -73,7 +73,7 @@ sub multiply_words (number: XY) -> (?) { ; input: X/Y = first 16-bit number, SCRATCH_ZPWORD1 in ZP = second 16-bit number ; output: multiply_words_product 32-bits product, LSB order (low-to-high) - asm { + %asm { stx SCRATCH_ZPWORD2 sty SCRATCH_ZPWORD2+1 @@ -105,7 +105,7 @@ mult16 lda #$00 sub divmod_bytes (number: X, divisor: Y) -> (X, A) { ; ---- divide X by Y, result quotient in X, remainder in A (unsigned) ; division by zero will result in quotient = 255 and remainder = original number - asm { + %asm { stx SCRATCH_ZP1 sty SCRATCH_ZP2 @@ -131,7 +131,7 @@ sub divmod_words (divisor: XY) -> (A?, XY) { ; output: SCRATCH_ZPWORD1 in ZP: 16 bit result, X/Y: 16 bit remainder ; division by zero will result in quotient = 65535 and remainder = divident - asm { + %asm { remainder = SCRATCH_ZP1 stx SCRATCH_ZPWORD2 diff --git a/reference.md b/reference.md index 892a988fb..d9db4eaee 100644 --- a/reference.md +++ b/reference.md @@ -19,7 +19,7 @@ which aims to provide many conveniences over raw assembly code (even when using - automatic variable allocations, automatic string variables and string sharing - automatic type conversions - floating point operations -- automatically preserving and restoring CPU registers state, when calling routines that otherwise would clobber these +- optional automatic preserving and restoring CPU registers state, when calling routines that otherwise would clobber these - abstracting away low level aspects such as zero page handling, program startup, explicit memory addresses - breakpoints, that let the Vice emulator drop into the monitor if execution hits them - source code labels automatically loaded in Vice emulator so it can show them in disassembly @@ -39,7 +39,7 @@ You need [Python 3.5](https://www.python.org/downloads/) or newer to run IL65 it IL65 is mainly targeted at the Commodore-64 machine, but should be mostly system independent. -MEMORY MODEL +Memory Model ------------ Most of the 64 kilobyte address space can be accessed by your program. @@ -123,7 +123,7 @@ and if blocks require it themselves you can't combine various modules anymore on -DATA TYPES +Data Types ---------- IL65 supports the following data types: @@ -168,11 +168,14 @@ unless it is part of a subroutine call statement. For an indirect goto call, the (``jmp`` indirect) and an indirect subroutine call (``jsr`` indirect) is synthesized using a couple of instructions. -PROGRAM STRUCTURE +Program Structure ----------------- -In IL65 every line in the source file can only contain *one* statement or declaration. +In IL65 every line in the source file can only contain *one* statement or definitons. Compilation is done on *one* main source code file, but other files can be imported. +A source file can start with global *directives* (starting with ``%``) and continues +with imports and block definitions. + ### Comments @@ -181,44 +184,69 @@ If the comment is the only thing on the line, it is copied into the resulting as This makes it easier to understand and relate the generated code. -### Output Modes +### Output Mode The default format of the generated program is a "raw" binary where code starts at ``$c000``. -You can generate other types of programs as well, by telling IL65 via an output mode statement +You can generate other types of programs as well, by telling IL65 via an output mode directive at the beginning of your program: -| mode declaration | meaning | -|----------------------|------------------------------------------------------------------------------------| -| ``output raw`` | no load address bytes | -| ``output prg`` | include the first two load address bytes, (default is ``$0801``), no BASIC program | -| ``output prg,basic`` | as 'prg', but include a BASIC start program with SYS call, default code start is immediately after the BASIC program at ``$081d``, or after that. | -| | | -| ``address $0801`` | override program start address (default is set to ``$c000`` for raw mode and ``$0801`` for C-64 prg mode). Cannot be used if output mode is ``prg,basic`` because BASIC programs always have to start at ``$0801``. | +| mode directive | meaning | +|-----------------------|------------------------------------------------------------------------------------| +| ``%output raw`` | no load address bytes | +| ``%output prg`` | include the first two load address bytes, (default is ``$0801``), no BASIC program | +| ``%output prg,basic`` | as 'prg', but include a BASIC start program with SYS call, default code start is immediately after the BASIC program at ``$081d``, or after that. | +| | | +| ``%address $0801`` | override program start address (default is set to ``$c000`` for raw mode and ``$0801`` for C-64 prg mode). Cannot be used if output mode is ``prg,basic`` because BASIC programs always have to start at ``$0801``. | ### ZeroPage Options You can tell the compiler how to treat the *zero page*. Normally it is considered a 'no-go' area except for the frew free locations mentioned under "Memory Model". -However you can specify some options globally in your program to change this behavior: +However you can specify some options globally in your program by using the zp directive +to change this behavior: -- ``zp clobber`` +- ``%zp clobber`` Use the whole zeropage for variables. It is not possible to exit your program correctly back to BASIC, other than resetting the machine. It does make your program smaller and faster because many more variables can be stored in the ZP, which is more efficient. -- ``zp clobber, restore`` +- ``%zp clobber, restore`` Use the whole zeropage, but make a backup copy of the original values at program start. When your program exits, the original ZP is restored (except for the software jiffy clock in ``$a0 - $a2``) and you drop back to the BASIC prompt. Not that the default IRQ routine is *still enabled* when your program is entered! See the paragraph on the zero page for more info about this. -If you use ``zp clobber``, you can no longer use most BASIC or KERNAL routines, +If you use ``%zp clobber``, you can no longer use most BASIC or KERNAL routines, because these depend on most of the locations in the ZP. This includes the floating-point logic and several utility routines that do I/O, such as ``print_string``. +### Importing, Including and Binary-Including other files or raw assembly code + +```%import "filename[.ill]"``` + Must be used *after* any global option directives, and outside of a block, + but otherwise can be placed anywhere in the program. + Reads and compiles the named IL65 file and stores the resulting definitions + as if they were a part of your current program. + +```%asminclude "filename.txt", scopelabel``` + This directive can only be used inside a block. + The assembler will include the file as raw assembly source text at this point, il65 will not process this at all. + The scopelabel will be used as a prefix to access the labels from the included source code, + otherwise you would risk symbol redefinitions or duplications. + +```%asmbinary "filename.bin" [, [, ]]``` + This directive can only be used inside a block. + The assembler will include the file as binary bytes at this point, il65 will not process this at all. + The optional offset and length can be used to select a particular piece of the file. + +```%asm {``` [raw assembly code lines] ``}`` + This directive includes raw unparsed assembly code at that exact spot in the program. + The ``%asm {`` and ``}`` start and end markers each have to be on their own unique line. + + ### Program Entry Point Every program has to have one entry point where code execution begins. @@ -229,40 +257,29 @@ Blocks and other details are described below. ### Blocks -~ blockname [address] { - statements -} +``~ blockname [address] {`` -The blockname "ZP" is reserved and always means the ZeroPage. Its start address is always set to $04, + [directives...] + [statements...] + +``}`` + +Blocks form the separate pieces of code and data of your program. They are combined and +arranged to a single output program. No code or data can occur outside a block. +A block is also a *scope* in your program so the symbols in the block don't clash with +symbols of the same name defined elsewhere. You can refer to the symbols in a particular block +by using a *dotted name*: ``blockname.symbolname``. + +Block names must be unique in your entire program, +except "ZP": the contents of every block with that name are merged into one. +This block name refers to the zero page. Its start address is always set to $04, because $00/$01 are used by the hardware and $02/$03 are reserved as general purpose scratch registers. - -Block names cannot occur more than once, EXCEPT 'ZP' where the contents of every occurrence of it are merged. Block address must be >= $0200 (because $00-$fff is the ZP and $100-$200 is the cpu stack) You can omit the blockname but then you can only refer to the contents of the block via its absolute address, -which is required in this case. If you omit both, the block is ignored altogether (and a warning is displayed). +which is required in this case. If you omit *both*, the block is ignored altogether (and a warning is displayed). This is a way to quickly "comment out" a piece of code that is unfinshed or may contain errors that you -want to work on later, because the contents of the ignored block are not syntactically parsed. - - -### Importing, Including and Binary-Including Files - -import "filename[.ill]" - Must be used *after* any global option statements such as ``output``, - and can only be used outside of a block. But can otherwise occur between any blocks. - Reads everything from the named IL65 file at this point and compile it as a normal part of the program. - -asminclude "filename.txt", scopelabel - Can only be used in a block. - The assembler will include the file as asm source text at this point, il65 will not process this at all. - The scopelabel will be used as a prefix to access the labels from the included source code, - otherwise you would risk symbol redefinitions or duplications. - -asmbinary "filename.bin" [, [, ]] - Can only be used in a block. - The assembler will include the file as binary bytes at this point, il65 will not process this at all. - The optional offset and length can be used to select a particular piece of the file. - +want to work on later, because the contents of the ignored block are not fully parsed. ### Assignments @@ -282,7 +299,6 @@ A special assignment is the *augmented assignment* where the value is modified i Several assignment operators are available: ``+=``, ``-=``, ``&=``, ``|=``, ``^=``, ``<<=``, ``>>=`` - ### Expressions In most places where a number or other value is expected, you can use just the number, or a full constant expression. @@ -294,7 +310,6 @@ all builtin functions (max, avg, min, sum etc). They can also reference idendifi if this makes sense. - ### Subroutine Definition Subroutines are parts of the code that can be repeatedly invoked using a subroutine call from elsewhere. @@ -335,25 +350,24 @@ but instead assign a memory address to it: ### Subroutine Calling You call a subroutine like this: - subroutinename_or_address [!] ( [arguments...] ) + + ``subroutinename_or_address (`` [arguments...] ``)`` + +or: + + ``subroutinename_or_address !``[register(s)] ``( [arguments...] )`` If the subroutine returns one or more values as results, you must use an assignment statement to store those values somewhere: - outputvar1, outputvar2 = subroutine ( arg1, arg2, arg3 ) + + ``outputvar1, outputvar2 = subroutine ( arg1, arg2, arg3 )`` The output variables must occur in the correct sequence of return registers as specified in the subroutine's definiton. It is possible to not specify any of them but the compiler will issue a warning then if the result values of a subroutine call are discarded. Even if the subroutine returns something in a register that already is the correct one you want to keep, you'll have to explicitly assign the return value to that register. -If you omit it, no return value is stored at all (well, unless you call the subroutine without -register preserving, see the next paragraph.) - -Normally, the registers are preserved when calling the subroutine and restored on return (except -the ones that explictly take result values from the call). -If you add a '!' after the name, no register preserving is done and the call essentially -is just a single JSR instruction. Arguments should match the subroutine definition. You are allowed to omit the parameter names. If no definition is available (because you're directly calling memory or a label or something else), you can freely add arguments (but in this case they all have to be named). @@ -362,8 +376,40 @@ To jump to a subroutine (without returning), prefix the subroutine call with the Unlike gotos in other languages, here it take arguments as well, because it essentially is the same as calling a subroutine and only doing something different when it's finished. +**Register preserving calls:** use the ``!`` followed by a combination of A, X and Y (or followed +by nothing, which is the same as AXY) to tell the compiler you want to preserve the origial +value of the given registers after the subroutine call. Otherwise, the subroutine may just +as well clobber all three registers. Preserving the original values does result in some +stack manipulation code to be inserted for every call like this, which can be quite slow. + +#### Calling Convention + +Subroutine arguments and results are passed via registers (and sometimes implicitly +via certain memory locations). @todo support call non-register args (variable parameter passing) + +In IL65 the "caller saves" principle applies to registers used in a subroutine. +This means the code that calls a subroutine or performs some function that clobber certain +registers (A, X or Y), is responsible for storing and restoring the original values if +that is required. + +*You should assume that the 3 hardware registers A, X and Y are volatile and their contents +cannot be depended upon, unless you make sure otherwise*. + +Normally, the registers are NOT preserved when calling a subroutine or when a certian +operations are performed. Most calls will be simply a few instructions to load the +values in the registers and then a JSR or JMP. + +By using the ``%preserve_registers`` directive (globally or in a block) you can tell the +compiler to preserve all registers. This does generate a lot of extra code that puts +original values on the stack and gets them off the stack again once the subroutine is done. +In this case however you don't have to worry about A, X and Y losing their original values +and you can essentially treat them as three local variables instead of scratch data. + +You can also use a ``!`` on a single subroutine call to preserve register values, instead of +setting this behavior for the entire block or even program. See the paragraph on Subroutine Calling +for more info. ### Conditional Execution Flow @@ -394,7 +440,7 @@ if you run into this type of assembler error. Debugging (with Vice) --------------------- -The ``breakpoint`` statement is a special statement that instructs the compiler to put +The ``%breakpoint`` directive instructs the compiler to put a *breakpoint* at that position in the code. It's a logical breakpoint instead of a physical BRK instruction because that will usually halt the machine altogether instead of breaking execution. Instead, a NOP instruction is generated and in a special output file the list of breakpoints is written. @@ -409,7 +455,8 @@ so if your program runs and it hits a breakpoint, Vice will halt execution and d -TODOS + +@Todo ----- ### IF_XX: @@ -549,12 +596,6 @@ these should call (or emit inline) optimized pieces of assembly code, so they ru -### Register Preservation Block - -preserve [regs] { .... } adds register preservation around the containing code default = all 3 regs, or specify which. -nopreserve [regs] { .... } removes register preservation on all statements in the block that would otherwise have it. - - ### Bitmap Definition (for Sprites and Characters) to define CHARACTERS (8x8 monochrome or 4x8 multicolor = 8 bytes) @@ -565,7 +606,9 @@ and SPRITES (24x21 monochrome or 12x21 multicolor = 63 bytes) ### More Datatypes -@todo 24 and 32 bits integers, unsigned and signed? +@todo pointers/addresses? (as opposed to normal WORDs) +@todo signed integers (byte and word)? + ### Some support for simple arithmetic @@ -573,6 +616,3 @@ and SPRITES (24x21 monochrome or 12x21 multicolor = 63 bytes) A = X * Y A /= Y A = Y / Y - -@todo multiplication routines (8*8 -> 16, 8*16 -> 16, 16*16->16 (or 32?)) -@todo division routines diff --git a/testsource/calls.ill b/testsource/calls.ill index b945345d3..b3c61d227 100644 --- a/testsource/calls.ill +++ b/testsource/calls.ill @@ -59,7 +59,7 @@ bar goto $c2 goto $c2() - asm { + %asm { nop nop nop @@ -75,6 +75,15 @@ bar sub4! ("hello", other=42) sub4! (string="hello", other = 42) sub4! (string="hello, there", other = 42) + + sub3 (81) + sub3 !(81) + sub3 !A (81) + sub3 !X (81) + sub3 !Y (81) + sub3 !XY (81) + sub3 !AXY (81) + bar!() [XY] ! () [var1] !() @@ -84,7 +93,7 @@ bar $c000!() $c2!() - asm { + %asm { nop nop nop @@ -110,7 +119,7 @@ bar $c2() - asm { + %asm { nop nop nop diff --git a/testsource/conditionals.ill b/testsource/conditionals.ill index 41055c6c8..45789fab1 100644 --- a/testsource/conditionals.ill +++ b/testsource/conditionals.ill @@ -1,6 +1,6 @@ -output prg,basic +%output prg,basic -import "c64lib" +%import "c64lib" ~ main { var .word value diff --git a/testsource/dtypes.ill b/testsource/dtypes.ill index bb5f2abfe..84398466d 100644 --- a/testsource/dtypes.ill +++ b/testsource/dtypes.ill @@ -1,10 +1,10 @@ ; var definitions and immediate primitive data type tests -output raw -zp clobber +%output raw +%zp clobber -import "c64lib" +%import "c64lib" ~ ZP { ; ZeroPage block definition: diff --git a/testsource/floats.ill b/testsource/floats.ill index cecf61451..92780f8e5 100644 --- a/testsource/floats.ill +++ b/testsource/floats.ill @@ -1,8 +1,8 @@ ; floating point tests -output prg, basic +%output prg, basic -import "c64lib" +%import "c64lib" ~ main_testing { start diff --git a/testsource/input.ill b/testsource/input.ill index 31ad7a866..c92ad2e96 100644 --- a/testsource/input.ill +++ b/testsource/input.ill @@ -1,14 +1,15 @@ -output prg,basic +%output prg,basic -import "c64lib" +%import "c64lib" ~ main { var .text name = "?"*80 + var .word orig_irq start c64.init_system() - XY = c64.CINV + orig_irq = c64.CINV SI = 1 c64.CINV = #irq_handler SI = 0 @@ -19,27 +20,28 @@ start c64.CHROUT('\n') blop - breakpoint ; yeah! + %breakpoint ; yeah! c64scr.print_string("thank you, mr or mrs: ") c64scr.print_string(name) c64.CHROUT('\n') SI = 1 - c64.CINV = XY + c64.CINV = orig_irq SI = 0 return irq_handler - asm { - lda $cb - cmp #$40 + %asm { + lda c64.SFDX + cmp #$40 ; nothing pressed? beq + - inc c64.EXTCOL + inc c64.EXTCOL ; otherwise change color + jmp c64.IRQDFRT } + } \ No newline at end of file diff --git a/testsource/large.ill b/testsource/large.ill index cea963d38..5f7144da3 100644 --- a/testsource/large.ill +++ b/testsource/large.ill @@ -1,9 +1,9 @@ ; var definitions and immediate primitive data type tests -output prg, basic -zp clobber +%output prg, basic +%zp clobber -import "c64lib" +%import "c64lib" ~ main { start @@ -343,7 +343,7 @@ bar goto $c2 goto $c2() - asm { + %asm { nop nop nop @@ -368,7 +368,7 @@ bar $c000!() $c2!() - asm { + %asm { nop nop nop @@ -394,7 +394,7 @@ bar $c2() - asm { + %asm { nop nop nop @@ -696,7 +696,7 @@ bar goto $c2 goto $c2() - asm { + %asm { nop nop nop @@ -721,7 +721,7 @@ bar $c000!() $c2!() - asm { + %asm { nop nop nop @@ -747,7 +747,7 @@ bar $c2() - asm { + %asm { nop nop nop @@ -1049,7 +1049,7 @@ bar goto $c2 goto $c2() - asm { + %asm { nop nop nop @@ -1074,7 +1074,7 @@ bar $c000!() $c2!() - asm { + %asm { nop nop nop @@ -1100,7 +1100,7 @@ bar $c2() - asm { + %asm { nop nop nop @@ -1402,7 +1402,7 @@ bar goto $c2 goto $c2() - asm { + %asm { nop nop nop @@ -1427,7 +1427,7 @@ bar $c000!() $c2!() - asm { + %asm { nop nop nop @@ -1453,7 +1453,7 @@ bar $c2() - asm { + %asm { nop nop nop @@ -1755,7 +1755,7 @@ bar goto $c2 goto $c2() - asm { + %asm { nop nop nop @@ -1780,7 +1780,7 @@ bar $c000!() $c2!() - asm { + %asm { nop nop nop @@ -1806,7 +1806,7 @@ bar $c2() - asm { + %asm { nop nop nop @@ -2108,7 +2108,7 @@ bar goto $c2 goto $c2() - asm { + %asm { nop nop nop @@ -2133,7 +2133,7 @@ bar $c000!() $c2!() - asm { + %asm { nop nop nop @@ -2159,7 +2159,7 @@ bar $c2() - asm { + %asm { nop nop nop @@ -2461,7 +2461,7 @@ bar goto $c2 goto $c2() - asm { + %asm { nop nop nop @@ -2486,7 +2486,7 @@ bar $c000!() $c2!() - asm { + %asm { nop nop nop @@ -2512,7 +2512,7 @@ bar $c2() - asm { + %asm { nop nop nop @@ -2814,7 +2814,7 @@ bar goto $c2 goto $c2() - asm { + %asm { nop nop nop @@ -2839,7 +2839,7 @@ bar $c000!() $c2!() - asm { + %asm { nop nop nop @@ -2865,7 +2865,7 @@ bar $c2() - asm { + %asm { nop nop nop @@ -3167,7 +3167,7 @@ bar goto $c2 goto $c2() - asm { + %asm { nop nop nop @@ -3192,7 +3192,7 @@ bar $c000!() $c2!() - asm { + %asm { nop nop nop @@ -3218,7 +3218,7 @@ bar $c2() - asm { + %asm { nop nop nop @@ -3520,7 +3520,7 @@ bar goto $c2 goto $c2() - asm { + %asm { nop nop nop @@ -3545,7 +3545,7 @@ bar $c000!() $c2!() - asm { + %asm { nop nop nop @@ -3571,7 +3571,7 @@ bar $c2() - asm { + %asm { nop nop nop @@ -3873,7 +3873,7 @@ bar goto $c2 goto $c2() - asm { + %asm { nop nop nop @@ -3898,7 +3898,7 @@ bar $c000!() $c2!() - asm { + %asm { nop nop nop @@ -3924,7 +3924,7 @@ bar $c2() - asm { + %asm { nop nop nop @@ -4226,7 +4226,7 @@ bar goto $c2 goto $c2() - asm { + %asm { nop nop nop @@ -4251,7 +4251,7 @@ bar $c000!() $c2!() - asm { + %asm { nop nop nop @@ -4277,7 +4277,7 @@ bar $c2() - asm { + %asm { nop nop nop @@ -4579,7 +4579,7 @@ bar goto $c2 goto $c2() - asm { + %asm { nop nop nop @@ -4604,7 +4604,7 @@ bar $c000!() $c2!() - asm { + %asm { nop nop nop @@ -4630,7 +4630,7 @@ bar $c2() - asm { + %asm { nop nop nop @@ -4932,7 +4932,7 @@ bar goto $c2 goto $c2() - asm { + %asm { nop nop nop @@ -4957,7 +4957,7 @@ bar $c000!() $c2!() - asm { + %asm { nop nop nop @@ -4983,7 +4983,7 @@ bar $c2() - asm { + %asm { nop nop nop @@ -5285,7 +5285,7 @@ bar goto $c2 goto $c2() - asm { + %asm { nop nop nop @@ -5310,7 +5310,7 @@ bar $c000!() $c2!() - asm { + %asm { nop nop nop @@ -5336,7 +5336,7 @@ bar $c2() - asm { + %asm { nop nop nop @@ -5638,7 +5638,7 @@ bar goto $c2 goto $c2() - asm { + %asm { nop nop nop @@ -5663,7 +5663,7 @@ bar $c000!() $c2!() - asm { + %asm { nop nop nop @@ -5689,7 +5689,7 @@ bar $c2() - asm { + %asm { nop nop nop @@ -5991,7 +5991,7 @@ bar goto $c2 goto $c2() - asm { + %asm { nop nop nop @@ -6016,7 +6016,7 @@ bar $c000!() $c2!() - asm { + %asm { nop nop nop @@ -6042,7 +6042,7 @@ bar $c2() - asm { + %asm { nop nop nop @@ -6344,7 +6344,7 @@ bar goto $c2 goto $c2() - asm { + %asm { nop nop nop @@ -6369,7 +6369,7 @@ bar $c000!() $c2!() - asm { + %asm { nop nop nop @@ -6395,7 +6395,7 @@ bar $c2() - asm { + %asm { nop nop nop @@ -6697,7 +6697,7 @@ bar goto $c2 goto $c2() - asm { + %asm { nop nop nop @@ -6722,7 +6722,7 @@ bar $c000!() $c2!() - asm { + %asm { nop nop nop @@ -6748,7 +6748,7 @@ bar $c2() - asm { + %asm { nop nop nop @@ -7050,7 +7050,7 @@ bar goto $c2 goto $c2() - asm { + %asm { nop nop nop @@ -7075,7 +7075,7 @@ bar $c000!() $c2!() - asm { + %asm { nop nop nop @@ -7101,7 +7101,7 @@ bar $c2() - asm { + %asm { nop nop nop diff --git a/testsource/numbergame.ill b/testsource/numbergame.ill index 17c6374fc..2a1ab1b9c 100644 --- a/testsource/numbergame.ill +++ b/testsource/numbergame.ill @@ -1,8 +1,5 @@ -output prg,basic -;reg_preserve off ; @todo global option off/on default off? - - -import "c64lib" +%output prg,basic +%import "c64lib" ~ main { diff --git a/testsource/source1.ill b/testsource/source1.ill index 85885bf5e..58a65f701 100644 --- a/testsource/source1.ill +++ b/testsource/source1.ill @@ -4,8 +4,8 @@ ; line 3 comment -output basic , prg ; create a c-64 program with basic SYS call to launch it -zp restore , clobber ; clobber over the zp memory normally used by basic/kernel rom, frees up more zp +%output basic , prg ; create a c-64 program with basic SYS call to launch it +%zp restore , clobber ; clobber over the zp memory normally used by basic/kernel rom, frees up more zp ~main $0a00 @@ -88,7 +88,7 @@ sub memsub () -> () = $fff2 sub customsub (Y)->() { - asm { + %asm { nop nop lda #99 @@ -120,7 +120,7 @@ somelabel1222 ~ block3 { somelabel1 - asm { + %asm { nop nop nop diff --git a/testsource/source2.ill b/testsource/source2.ill index d979ebd83..d543b3fb5 100644 --- a/testsource/source2.ill +++ b/testsource/source2.ill @@ -1,71 +1,24 @@ -; source IL file - ; these are comments +%output prg +%address $c000 -output prg, basic ; create a c-64 program with basic SYS call to launch it -zp clobber,restore ; clobber over the zp memory normally used by basic/kernel rom, frees up more zp - -~main -{ - memory borderb1 = $d020 - memory .byte borderb2 = $d020 - memory .word screencolors = $d020 - memory .array($10) screenblock1 = $0400 ; 10 bytes - memory .array($0100) screenblock2 = $0500 ; 256 bytes - memory .array($0234) screenblock3 = $2000 ; some weird size - memory .wordarray($20) screenblock4 = $0600 ; 32 words - memory .matrix(40,25) charscreen = $0400 - var .array($10) var1 = $aa - var .wordarray($10) var2 = $9988 - var .array($10) var3 = $11 - var var4 - var .matrix(40,25) vcharscreen ; init with 0 - var .matrix(40,25) vcharscreen2 = 111 ; init with 111 - -start - ; set some colors - Y = 0 - borderb1 = Y - X=Y=A = 0 - X = 1 - screencolors = X - borderb1 = 0 - borderb2 = 1 - screencolors = $0 - screencolors = $0207 - - ; fill block of memory with a single value (byte or word) - ;screenblock1,* = A - ;screenblock1,* = X - ;screenblock1,* = Y - ;screenblock1,* = 2 - ;screenblock1,* = $aaa - ;screenblock2,* = A - ;screenblock2,* = X - ;screenblock2,* = Y - ;screenblock2,* = 2 - ;screenblock2,* = $aaa - ;screenblock3,* = A - ;screenblock3,* = X - ;screenblock3,* = Y - ;screenblock3,* = 2 - ;screenblock3,* = $aaa - ;screenblock4,* = A - ;screenblock4,* = X - ;screenblock4,* = Y - ;screenblock4,* = 2 - ;screenblock4,* = $aaa - - ; swap bytes - ;A = var1[0] - ;X = var1[1] - ;var1[0] = X - ;var1[1] = A - - ; swap words - ;A = var2[0] ; must crash, cannot put word in register - ;X = var2[1] ;crash - ;var2[0] = X ; ok, 00-padded - ;var2[1] = A ; ok, 00-padded - - return +~ test { + var .byte localvar = 33 + return +} + +~ main { +start + A=0 + [$d020]=A + X=false + Y=true + return + + ;included_assembly + %asminclude "included.source" test_include + + ;included_binary + %asmbinary "included.binary" + %asmbinary "included.binary" $40 + %asmbinary "included.binary" $40 $200 } diff --git a/testsource/source3.ill b/testsource/source3.ill index 42d848181..d81093cd0 100644 --- a/testsource/source3.ill +++ b/testsource/source3.ill @@ -1,8 +1,8 @@ -output prg,basic ; create a c-64 program with basic SYS call to launch it +%output prg,basic ; create a c-64 program with basic SYS call to launch it -import "c64lib" ; searched in several locations and with .ill file extension added +%import "c64lib" ; searched in several locations and with .ill file extension added ~ main { diff --git a/testsource/source4.ill b/testsource/source4.ill index 985fe23f6..73a048272 100644 --- a/testsource/source4.ill +++ b/testsource/source4.ill @@ -1,6 +1,6 @@ -output prg,basic ; create a c-64 program with basic SYS to() launch it +%output prg,basic ; create a c-64 program with basic SYS to() launch it - import "c64lib.ill" + %import "c64lib.ill" ~ main { @@ -9,21 +9,21 @@ output prg,basic ; create a c-64 program with basic SYS to() launch it const .word BORDER = $d020 start - c64scr.print_pimmediate ! () ; this prints the pstring immediately following it - asm { + c64scr.print_pimmediate() ; this prints the pstring immediately following it + %asm { .ptext "hello-pimmediate!{cr}" } - c64scr.print_byte_decimal0 ! (19) - c64.CHROUT ! (13) - c64scr.print_byte_decimal ! (19) - c64.CHROUT ! (13) + c64scr.print_byte_decimal0 (19) + c64.CHROUT (13) + c64scr.print_byte_decimal (19) + c64.CHROUT (13) - c64scr.print_word_decimal0 ! ($0102) - c64.CHROUT ! (13) - c64scr.print_word_decimal ! ($0102) - c64.CHROUT ! (13) + c64scr.print_word_decimal0 ($0102) + c64.CHROUT (13) + c64scr.print_word_decimal ($0102) + c64.CHROUT (13) return start2 diff --git a/testsource/source5.ill b/testsource/source5.ill deleted file mode 100644 index b5d28cb81..000000000 --- a/testsource/source5.ill +++ /dev/null @@ -1,24 +0,0 @@ -output prg -address $c000 - -~ test { - var .byte localvar = 33 - return -} - -~ main { -start - A=0 - [$d020]=A - X=false - Y=true - return - - ;included_assembly - asminclude "included.source" test_include - - ;included_binary - asmbinary "included.binary" - asmbinary "included.binary" $40 - asmbinary "included.binary" $40 $200 -} diff --git a/todo.ill b/todo.ill index 06d8a5dcc..66730b5b8 100644 --- a/todo.ill +++ b/todo.ill @@ -1,10 +1,12 @@ -output prg,basic +%output prg,basic +%preserve_registers no -;reg_preserve off ; @todo global option off/on default off? NOT AN OPTION -> change the default to OFF - -import "c64lib" +%import "c64lib" ~ main { + %preserve_registers false + + const num = 2 var var1 =2 var .word wvar1 = 2 @@ -41,7 +43,7 @@ start ;XY <<= 2 - asm { + %asm { ldy #200 - lda #81 sta c64.Screen+39-40,y @@ -92,10 +94,13 @@ loop X = $22 Y = $33 - c64scr.clear_screen !(81, 5) ; @todo new syntax to specify registers to save (! = all three A,X,Y) - ;c64scr.clear_screen !A (81, 5) ; @todo new syntax to specify registers to save (only A) - ;c64scr.clear_screen !AX (81, 5) - ;c64scr.clear_screen !AXY (81, 5) + c64scr.clear_screen (81, 5) + c64scr.clear_screen !(81, 5) + c64scr.clear_screen !A (81, 5) + c64scr.clear_screen !X (81, 5) + c64scr.clear_screen !Y (81, 5) + c64scr.clear_screen !XY (81, 5) + c64scr.clear_screen !AXY (81, 5) c64scr.print_byte_hex(1,A) c64.CHROUT(' ')