diff --git a/docs/library/windowpaint.md b/docs/library/windowpaint.md index 15c21bb9e..f23108bfb 100644 --- a/docs/library/windowpaint.md +++ b/docs/library/windowpaint.md @@ -30,7 +30,7 @@ Where ``` SUB windowPaint(x as uByte,y as uByte, width as uByte, height as uByte, inkCol as ubyte, paperCol as uByte, isBright as uByte, isFlash as uByte) - paint(x,y,width,height,(isFlash<<7+isBright<<6+paperCol<<3+inkCol)) + paint(x,y,width,height,(isFlash<<7) bOR (isBright<<6) bOR (paperCol<<3) bOR inkCol) END SUB diff --git a/src/arch/z80/optimizer/__init__.py b/src/arch/z80/optimizer/__init__.py index 4f9c989f1..3667ff846 100644 --- a/src/arch/z80/optimizer/__init__.py +++ b/src/arch/z80/optimizer/__init__.py @@ -1,243 +1 @@ -# -*- coding: utf-8 -*- - -from src.api.config import OPTIONS -from src.api.debug import __DEBUG__ -from src.api.utils import flatten_list - -from ..peephole import engine -from . import basicblock -from .basicblock import DummyBasicBlock -from .common import JUMP_LABELS, LABELS, MEMORY -from .helpers import ALL_REGS, END_PROGRAM_LABEL -from .labelinfo import LabelInfo -from .patterns import RE_LABEL, RE_PRAGMA - - -def init(): - LABELS.clear() - JUMP_LABELS.clear() - - LABELS["*START*"] = LabelInfo("*START*", 0, DummyBasicBlock(ALL_REGS, ALL_REGS)) # Special START BLOCK - LABELS["*__END_PROGRAM*"] = LabelInfo("__END_PROGRAM", 0, DummyBasicBlock(ALL_REGS, list("bc"))) - - # SOME Global modules initialization - LABELS["__ADDF"] = LabelInfo("__ADDF", 0, DummyBasicBlock(ALL_REGS, list("aedbc"))) - LABELS["__SUBF"] = LabelInfo("__SUBF", 0, DummyBasicBlock(ALL_REGS, list("aedbc"))) - LABELS["__DIVF"] = LabelInfo("__DIVF", 0, DummyBasicBlock(ALL_REGS, list("aedbc"))) - LABELS["__MULF"] = LabelInfo("__MULF", 0, DummyBasicBlock(ALL_REGS, list("aedbc"))) - LABELS["__GEF"] = LabelInfo("__GEF", 0, DummyBasicBlock(ALL_REGS, list("aedbc"))) - LABELS["__GTF"] = LabelInfo("__GTF", 0, DummyBasicBlock(ALL_REGS, list("aedbc"))) - LABELS["__EQF"] = LabelInfo("__EQF", 0, DummyBasicBlock(ALL_REGS, list("aedbc"))) - LABELS["__STOREF"] = LabelInfo("__STOREF", 0, DummyBasicBlock(ALL_REGS, list("hlaedbc"))) - LABELS["PRINT_AT"] = LabelInfo("PRINT_AT", 0, DummyBasicBlock(ALL_REGS, list("a"))) - LABELS["INK"] = LabelInfo("INK", 0, DummyBasicBlock(ALL_REGS, list("a"))) - LABELS["INK_TMP"] = LabelInfo("INK_TMP", 0, DummyBasicBlock(ALL_REGS, list("a"))) - LABELS["PAPER"] = LabelInfo("PAPER", 0, DummyBasicBlock(ALL_REGS, list("a"))) - LABELS["PAPER_TMP"] = LabelInfo("PAPER_TMP", 0, DummyBasicBlock(ALL_REGS, list("a"))) - LABELS["RND"] = LabelInfo("RND", 0, DummyBasicBlock(ALL_REGS, [])) - LABELS["INKEY"] = LabelInfo("INKEY", 0, DummyBasicBlock(ALL_REGS, [])) - LABELS["PLOT"] = LabelInfo("PLOT", 0, DummyBasicBlock(ALL_REGS, ["a"])) - LABELS["DRAW"] = LabelInfo("DRAW", 0, DummyBasicBlock(ALL_REGS, ["h", "l"])) - LABELS["DRAW3"] = LabelInfo("DRAW3", 0, DummyBasicBlock(ALL_REGS, list("abcde"))) - LABELS["__ARRAY"] = LabelInfo("__ARRAY", 0, DummyBasicBlock(ALL_REGS, ["h", "l"])) - LABELS["__MEMCPY"] = LabelInfo("__MEMCPY", 0, DummyBasicBlock(list("bcdefhl"), list("bcdehl"))) - LABELS["__PLOADF"] = LabelInfo("__PLOADF", 0, DummyBasicBlock(ALL_REGS, ALL_REGS)) # Special START BLOCK - LABELS["__PSTOREF"] = LabelInfo("__PSTOREF", 0, DummyBasicBlock(ALL_REGS, ALL_REGS)) # Special START BLOCK - - -def cleanupmem(initial_memory): - """Cleans up initial memory. Each label must be - ALONE. Each instruction must have an space, etc... - """ - i = 0 - while i < len(initial_memory): - tmp = initial_memory[i] - match = RE_LABEL.match(tmp) - if not match: - i += 1 - continue - - if tmp.rstrip() == match.group(): - i += 1 - continue - - initial_memory[i] = tmp[match.end() :] - initial_memory.insert(i, match.group()) - i += 1 - - -def cleanup_local_labels(block): - """Traverses memory, to make any local label a unique - global one. At this point there's only a single code - block - """ - global PROC_COUNTER - - stack = [[]] - hashes = [{}] - stackprc = [PROC_COUNTER] - used = [{}] # List of hashes of unresolved labels per scope - - MEMORY = block.mem - - for cell in MEMORY: - if cell.inst.upper() == "PROC": - stack += [[]] - hashes += [{}] - stackprc += [PROC_COUNTER] - used += [{}] - PROC_COUNTER += 1 - continue - - if cell.inst.upper() == "ENDP": - if len(stack) > 1: # There might be unbalanced stack due to syntax errors - for label in used[-1].keys(): - if label in stack[-1]: - newlabel = hashes[-1][label] - for cell in used[-1][label]: - cell.replace_label(label, newlabel) - - stack.pop() - hashes.pop() - stackprc.pop() - used.pop() - continue - - tmp = cell.asm.asm - if tmp.upper()[:5] == "LOCAL": - tmp = tmp[5:].split(",") - for lbl in tmp: - lbl = lbl.strip() - if lbl in stack[-1]: - continue - stack[-1] += [lbl] - hashes[-1][lbl] = "PROC%i." % stackprc[-1] + lbl - if used[-1].get(lbl, None) is None: - used[-1][lbl] = [] - - cell.asm = ";" + cell.asm # Remove it - continue - - if cell.is_label: - label = cell.inst - for i in range(len(stack) - 1, -1, -1): - if label in stack[i]: - label = hashes[i][label] - cell.asm = label + ":" - break - continue - - for label in cell.used_labels: - labelUsed = False - for i in range(len(stack) - 1, -1, -1): - if label in stack[i]: - newlabel = hashes[i][label] - cell.replace_label(label, newlabel) - labelUsed = True - break - - if not labelUsed: - if used[-1].get(label, None) is None: - used[-1][label] = [] - - used[-1][label] += [cell] - - for i in range(len(MEMORY) - 1, -1, -1): - if MEMORY[i].asm.asm[0] == ";": - MEMORY.pop(i) - - block.mem = MEMORY - block.asm = [x.asm for x in MEMORY if len(x.asm)] - - -def get_labels(basic_block): - """Traverses memory, to annotate all the labels in the global - LABELS table - """ - for i, cell in enumerate(basic_block): - if cell.is_label: - label = cell.inst - LABELS[label] = LabelInfo(label, cell.addr, basic_block, i) # Stores it globally - - -def initialize_memory(basic_block): - """Initializes global memory array with the one in the main (initial) basic_block""" - global MEMORY - - init() - MEMORY = basic_block.mem - get_labels(basic_block) - - -def optimize(initial_memory: list[str]) -> str: - """This will remove useless instructions""" - global BLOCKS - global PROC_COUNTER - - del MEMORY[:] - PROC_COUNTER = 0 - - cleanupmem(initial_memory) - if OPTIONS.optimization_level <= 2: # if -O2 or lower, do nothing and return - return "\n".join(x for x in initial_memory if not RE_PRAGMA.match(x)) - - basicblock.BasicBlock.clean_asm_args = OPTIONS.optimization_level > 3 - bb = basicblock.BasicBlock(initial_memory) - cleanup_local_labels(bb) - initialize_memory(bb) - - BLOCKS = basic_blocks = basicblock.get_basic_blocks(bb) # 1st partition the Basic Blocks - - for b in basic_blocks: - __DEBUG__("--- BASIC BLOCK: {} ---".format(b.id), 1) - __DEBUG__("Code:\n" + "\n".join(" {}".format(x) for x in b.code), 1) - __DEBUG__("Requires: {}".format(b.requires()), 1) - __DEBUG__("Destroys: {}".format(b.destroys()), 1) - __DEBUG__("Label goes: {}".format(b.label_goes), 1) - __DEBUG__("Comes from: {}".format([x.id for x in b.comes_from]), 1) - __DEBUG__("Goes to: {}".format([x.id for x in b.goes_to]), 1) - __DEBUG__("Next: {}".format(b.next.id if b.next is not None else None), 1) - __DEBUG__("Size: {} Time: {}".format(b.sizeof, b.max_tstates), 1) - __DEBUG__("--- END ---", 1) - - LABELS["*START*"].basic_block.add_goes_to(basic_blocks[0]) - LABELS["*START*"].basic_block.next = basic_blocks[0] - - basic_blocks[0].prev = LABELS["*START*"].basic_block - if END_PROGRAM_LABEL in LABELS: - LABELS[END_PROGRAM_LABEL].basic_block.add_goes_to(LABELS["*__END_PROGRAM*"].basic_block) - - # In O3 we simplify the graph by reducing jumps over jumps - for label in JUMP_LABELS: - block = LABELS[label].basic_block - if isinstance(block, DummyBasicBlock): - continue - - # The instruction that starts this block must be one of jr / jp - first = block.get_next_exec_instruction() - if first is None or first.inst not in ("jp", "jr"): - continue - - for blk in list(LABELS[label].used_by): - if not first.condition_flag or blk[-1].condition_flag == first.condition_flag: - new_label = first.opers[0] - blk[-1].asm = blk[-1].code.replace(label, new_label) - block.delete_comes_from(blk) - LABELS[label].used_by.remove(blk) - LABELS[new_label].used_by.add(blk) - blk.add_goes_to(LABELS[new_label].basic_block) - - for x in basic_blocks: - x.compute_cpu_state() - - filtered_patterns_list = [p for p in engine.PATTERNS if OPTIONS.optimization_level >= p.level >= 3] - for x in basic_blocks: - x.optimize(filtered_patterns_list) - - for x in basic_blocks: - if x.comes_from == [] and len([y for y in JUMP_LABELS if x is LABELS[y].basic_block]): - x.ignored = True - - return "\n".join( - [y for y in flatten_list([x.code for x in basic_blocks if not x.ignored]) if not RE_PRAGMA.match(y)] - ) +from .main import init, optimize diff --git a/src/arch/z80/optimizer/basicblock.py b/src/arch/z80/optimizer/basicblock.py index de17139e3..94809cb33 100644 --- a/src/arch/z80/optimizer/basicblock.py +++ b/src/arch/z80/optimizer/basicblock.py @@ -16,6 +16,8 @@ from src.arch.z80.optimizer.patterns import RE_ID_OR_NUMBER from src.arch.z80.peephole import evaluator +__all__ = "BasicBlock", "DummyBasicBlock" + class BasicBlock(Iterable[MemCell]): """A Class describing a basic block""" @@ -480,140 +482,3 @@ def requires(self, i: int = 0, end_=None) -> set[str]: def is_used(self, regs: Iterable[str], i: int, top: int | None = None) -> bool: return len([x for x in regs if x in self.__requires]) > 0 - - -def split_block(block: BasicBlock, start_of_new_block: int) -> tuple[BasicBlock, BasicBlock]: - assert 0 <= start_of_new_block < len(block), f"Invalid split pos: {start_of_new_block}" - new_block = BasicBlock([]) - new_block.mem = block.mem[start_of_new_block:] - block.mem = block.mem[:start_of_new_block] - - new_block.next = block.next - block.next = new_block - new_block.prev = block - - if new_block.next is not None: - new_block.next.prev = new_block - - for blk in list(block.goes_to): - block.delete_goes_to(blk) - new_block.add_goes_to(blk) - - block.add_goes_to(new_block) - - for i, mem in enumerate(new_block): - if mem.is_label and mem.inst in LABELS: - LABELS[mem.inst].basic_block = new_block - LABELS[mem.inst].position = i - - if block[-1].is_ender: - if not block[-1].condition_flag: # If it's an unconditional jp, jr, call, ret - block.delete_goes_to(block.next) - - return block, new_block - - -def compute_calls(basic_blocks: list[BasicBlock], jump_labels: set[str]) -> None: - calling_blocks: dict[BasicBlock, BasicBlock] = {} - - # Compute which blocks use jump labels - for bb in basic_blocks: - if bb[-1].is_ender and (op := bb[-1].branch_arg) in LABELS: - LABELS[op].used_by.add(bb) - - # For these blocks, add the referenced block in the goes_to - for label in jump_labels: - for bb in LABELS[label].used_by: - bb.add_goes_to(LABELS[label].basic_block) - - # Annotate which blocks uses call (which should be the last instruction) - for bb in basic_blocks: - if bb[-1].inst != "call": - continue - - op = bb[-1].branch_arg - if op in LABELS: - LABELS[op].basic_block.called_by.add(bb) - calling_blocks[bb] = LABELS[op].basic_block - - # For the annotated blocks, trace their goes_to, and their goes_to from - # their goes_to and so on, until ret (unconditional or not) is found, and - # save that block in a set for later - visited: set[tuple[BasicBlock, BasicBlock]] = set() - pending: set[tuple[BasicBlock, BasicBlock]] = set(calling_blocks.items()) - - while pending: - caller, bb = pending.pop() - if (caller, bb) in visited: - continue - - visited.add((caller, bb)) - - if not bb[-1].is_ender: # if it does not branch, search in the next block - pending.add((caller, bb.next)) - continue - - if bb[-1].inst in {"ret", "reti", "retn"}: - if bb[-1].condition_flag: - pending.add((caller, bb.next)) - - bb.add_goes_to(caller.next) - continue - - if bb[-1].inst in {"call", "rst"}: # A call from this block - if bb[-1].condition_flag: # if it has conditions, it can return from the next block - pending.add((caller, bb.next)) - - -def get_jump_labels(main_basic_block: BasicBlock) -> set[str]: - """Given the main basic block (which contain the entire program), populate - the global JUMP_LABEL set with LABELS used by CALL, JR, JP (i.e JP LABEL0) - Also updates the global LABELS index with the pertinent information. - - Any BasicBlock containing a JUMP_LABEL in any position which is not the initial - one (0 position) must be split at that point into two basic blocks. - """ - jump_labels: set[str] = set() - - for i, mem in enumerate(main_basic_block): - if mem.is_label: - LABELS.pop(mem.inst) - LABELS[mem.inst] = LabelInfo( - label=mem.inst, addr=i, basic_block=main_basic_block, position=i # Unknown yet - ) - continue - - if not mem.is_ender: - continue - - lbl = mem.branch_arg - if lbl is None: - continue - - jump_labels.add(lbl) - - if lbl not in LABELS: - __DEBUG__(f"INFO: {lbl} is not defined. No optimization is done.", 2) - LABELS[lbl] = LabelInfo(lbl, 0, DummyBasicBlock(ALL_REGS, ALL_REGS)) - - return jump_labels - - -def get_basic_blocks(block: BasicBlock) -> list[BasicBlock]: - """If a block is not partitionable, returns a list with the same block. - Otherwise, returns a list with the resulting blocks. - """ - result: list[BasicBlock] = [block] - JUMP_LABELS.clear() - JUMP_LABELS.update(get_jump_labels(block)) - - # Split basic blocks per label or branch instruction - split_pos = block.get_first_partition_idx() - while split_pos is not None: - _, block = split_block(block, split_pos) - result.append(block) - split_pos = block.get_first_partition_idx() - - compute_calls(result, JUMP_LABELS) - - return result diff --git a/src/arch/z80/optimizer/common.py b/src/arch/z80/optimizer/common.py index e98428b6c..1c599f90f 100644 --- a/src/arch/z80/optimizer/common.py +++ b/src/arch/z80/optimizer/common.py @@ -1,9 +1,10 @@ # -*- config: utf-8 -*- from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Final if TYPE_CHECKING: + from .basicblock import BasicBlock from .labelinfo import LabelInfo # counter for generating unique random fake values @@ -12,10 +13,10 @@ # Labels which must start a basic block, because they're used in a JP/CALL LABELS: dict[str, LabelInfo] = {} # Label -> LabelInfo object -JUMP_LABELS: set[str] = set([]) -MEMORY = [] # Instructions emitted by the backend +JUMP_LABELS: Final[set[str]] = set() +MEMORY: Final[list[str]] = [] # Instructions emitted by the backend # PROC labels name space counter PROC_COUNTER = 0 -BLOCKS = [] # Memory blocks +BLOCKS: Final[list[BasicBlock]] = [] # Memory blocks diff --git a/src/arch/z80/optimizer/flow_graph.py b/src/arch/z80/optimizer/flow_graph.py new file mode 100644 index 000000000..738c501ce --- /dev/null +++ b/src/arch/z80/optimizer/flow_graph.py @@ -0,0 +1,145 @@ +from src.api.debug import __DEBUG__ + +from .basicblock import BasicBlock, DummyBasicBlock +from .common import JUMP_LABELS, LABELS +from .helpers import ALL_REGS +from .labelinfo import LabelInfo + +__all__ = ("get_basic_blocks",) + + +def split_block(block: BasicBlock, start_of_new_block: int) -> tuple[BasicBlock, BasicBlock]: + assert 0 <= start_of_new_block < len(block), f"Invalid split pos: {start_of_new_block}" + new_block = BasicBlock([]) + new_block.mem = block.mem[start_of_new_block:] + block.mem = block.mem[:start_of_new_block] + + new_block.next = block.next + block.next = new_block + new_block.prev = block + + if new_block.next is not None: + new_block.next.prev = new_block + + for blk in list(block.goes_to): + block.delete_goes_to(blk) + new_block.add_goes_to(blk) + + block.add_goes_to(new_block) + + for i, mem in enumerate(new_block): + if mem.is_label and mem.inst in LABELS: + LABELS[mem.inst].basic_block = new_block + LABELS[mem.inst].position = i + + if block[-1].is_ender: + if not block[-1].condition_flag: # If it's an unconditional jp, jr, call, ret + block.delete_goes_to(block.next) + + return block, new_block + + +def compute_calls(basic_blocks: list[BasicBlock], jump_labels: set[str]) -> None: + calling_blocks: dict[BasicBlock, BasicBlock] = {} + + # Compute which blocks use jump labels + for bb in basic_blocks: + if bb[-1].is_ender and (op := bb[-1].branch_arg) in LABELS: + LABELS[op].used_by.add(bb) + + # For these blocks, add the referenced block in the goes_to + for label in jump_labels: + for bb in LABELS[label].used_by: + bb.add_goes_to(LABELS[label].basic_block) + + # Annotate which blocks uses call (which should be the last instruction) + for bb in basic_blocks: + if bb[-1].inst != "call": + continue + + op = bb[-1].branch_arg + if op in LABELS: + LABELS[op].basic_block.called_by.add(bb) + calling_blocks[bb] = LABELS[op].basic_block + + # For the annotated blocks, trace their goes_to, and their goes_to from + # their goes_to and so on, until ret (unconditional or not) is found, and + # save that block in a set for later + visited: set[tuple[BasicBlock, BasicBlock]] = set() + pending: set[tuple[BasicBlock, BasicBlock]] = set(calling_blocks.items()) + + while pending: + caller, bb = pending.pop() + if (caller, bb) in visited: + continue + + visited.add((caller, bb)) + + if not bb[-1].is_ender: # if it does not branch, search in the next block + pending.add((caller, bb.next)) + continue + + if bb[-1].inst in {"ret", "reti", "retn"}: + if bb[-1].condition_flag: + pending.add((caller, bb.next)) + + bb.add_goes_to(caller.next) + continue + + if bb[-1].inst in {"call", "rst"}: # A call from this block + if bb[-1].condition_flag: # if it has conditions, it can return from the next block + pending.add((caller, bb.next)) + + +def get_jump_labels(main_basic_block: BasicBlock) -> set[str]: + """Given the main basic block (which contain the entire program), populate + the global JUMP_LABEL set with LABELS used by CALL, JR, JP (i.e JP LABEL0) + Also updates the global LABELS index with the pertinent information. + + Any BasicBlock containing a JUMP_LABEL in any position which is not the initial + one (0 position) must be split at that point into two basic blocks. + """ + jump_labels: set[str] = set() + + for i, mem in enumerate(main_basic_block): + if mem.is_label: + LABELS.pop(mem.inst) + LABELS[mem.inst] = LabelInfo( + label=mem.inst, addr=i, basic_block=main_basic_block, position=i # Unknown yet + ) + continue + + if not mem.is_ender: + continue + + lbl = mem.branch_arg + if lbl is None: + continue + + jump_labels.add(lbl) + + if lbl not in LABELS: + __DEBUG__(f"INFO: {lbl} is not defined. No optimization is done.", 2) + LABELS[lbl] = LabelInfo(lbl, 0, DummyBasicBlock(ALL_REGS, ALL_REGS)) + + return jump_labels + + +def get_basic_blocks(block: BasicBlock) -> list[BasicBlock]: + """If a block is not partitionable, returns a list with the same block. + Otherwise, returns a list with the resulting blocks. + """ + result: list[BasicBlock] = [block] + JUMP_LABELS.clear() + JUMP_LABELS.update(get_jump_labels(block)) + + # Split basic blocks per label or branch instruction + split_pos = block.get_first_partition_idx() + while split_pos is not None: + _, block = split_block(block, split_pos) + result.append(block) + split_pos = block.get_first_partition_idx() + + compute_calls(result, JUMP_LABELS) + + return result diff --git a/src/arch/z80/optimizer/main.py b/src/arch/z80/optimizer/main.py new file mode 100644 index 000000000..aabb9f105 --- /dev/null +++ b/src/arch/z80/optimizer/main.py @@ -0,0 +1,239 @@ +from src.api.config import OPTIONS +from src.api.debug import __DEBUG__ +from src.api.utils import flatten_list +from src.arch.z80.peephole import engine + +from . import flow_graph +from .basicblock import BasicBlock, DummyBasicBlock +from .common import JUMP_LABELS, LABELS, MEMORY +from .helpers import ALL_REGS, END_PROGRAM_LABEL +from .labelinfo import LabelInfo +from .patterns import RE_LABEL, RE_PRAGMA + +__all__ = "init", "optimize" + + +def init(): + LABELS.clear() + JUMP_LABELS.clear() + + LABELS["*START*"] = LabelInfo("*START*", 0, DummyBasicBlock(ALL_REGS, ALL_REGS)) # Special START BLOCK + LABELS["*__END_PROGRAM*"] = LabelInfo("__END_PROGRAM", 0, DummyBasicBlock(ALL_REGS, list("bc"))) + + # SOME Global modules initialization + LABELS["__ADDF"] = LabelInfo("__ADDF", 0, DummyBasicBlock(ALL_REGS, list("aedbc"))) + LABELS["__SUBF"] = LabelInfo("__SUBF", 0, DummyBasicBlock(ALL_REGS, list("aedbc"))) + LABELS["__DIVF"] = LabelInfo("__DIVF", 0, DummyBasicBlock(ALL_REGS, list("aedbc"))) + LABELS["__MULF"] = LabelInfo("__MULF", 0, DummyBasicBlock(ALL_REGS, list("aedbc"))) + LABELS["__GEF"] = LabelInfo("__GEF", 0, DummyBasicBlock(ALL_REGS, list("aedbc"))) + LABELS["__GTF"] = LabelInfo("__GTF", 0, DummyBasicBlock(ALL_REGS, list("aedbc"))) + LABELS["__EQF"] = LabelInfo("__EQF", 0, DummyBasicBlock(ALL_REGS, list("aedbc"))) + LABELS["__STOREF"] = LabelInfo("__STOREF", 0, DummyBasicBlock(ALL_REGS, list("hlaedbc"))) + LABELS["PRINT_AT"] = LabelInfo("PRINT_AT", 0, DummyBasicBlock(ALL_REGS, list("a"))) + LABELS["INK"] = LabelInfo("INK", 0, DummyBasicBlock(ALL_REGS, list("a"))) + LABELS["INK_TMP"] = LabelInfo("INK_TMP", 0, DummyBasicBlock(ALL_REGS, list("a"))) + LABELS["PAPER"] = LabelInfo("PAPER", 0, DummyBasicBlock(ALL_REGS, list("a"))) + LABELS["PAPER_TMP"] = LabelInfo("PAPER_TMP", 0, DummyBasicBlock(ALL_REGS, list("a"))) + LABELS["RND"] = LabelInfo("RND", 0, DummyBasicBlock(ALL_REGS, [])) + LABELS["INKEY"] = LabelInfo("INKEY", 0, DummyBasicBlock(ALL_REGS, [])) + LABELS["PLOT"] = LabelInfo("PLOT", 0, DummyBasicBlock(ALL_REGS, ["a"])) + LABELS["DRAW"] = LabelInfo("DRAW", 0, DummyBasicBlock(ALL_REGS, ["h", "l"])) + LABELS["DRAW3"] = LabelInfo("DRAW3", 0, DummyBasicBlock(ALL_REGS, list("abcde"))) + LABELS["__ARRAY"] = LabelInfo("__ARRAY", 0, DummyBasicBlock(ALL_REGS, ["h", "l"])) + LABELS["__MEMCPY"] = LabelInfo("__MEMCPY", 0, DummyBasicBlock(list("bcdefhl"), list("bcdehl"))) + LABELS["__PLOADF"] = LabelInfo("__PLOADF", 0, DummyBasicBlock(ALL_REGS, ALL_REGS)) # Special START BLOCK + LABELS["__PSTOREF"] = LabelInfo("__PSTOREF", 0, DummyBasicBlock(ALL_REGS, ALL_REGS)) # Special START BLOCK + + +def _cleanup_mem(initial_memory: list[str]) -> None: + """Cleans up initial memory. Each label must be + ALONE. Each instruction must have an space, etc... + """ + i = 0 + while i < len(initial_memory): + tmp = initial_memory[i] + match = RE_LABEL.match(tmp) + if not match: + i += 1 + continue + + if tmp.rstrip() == match.group(): + i += 1 + continue + + initial_memory[i] = tmp[match.end() :] + initial_memory.insert(i, match.group()) + i += 1 + + +def cleanup_local_labels(block: BasicBlock) -> None: + """Traverses memory, to make any local label a unique + global one. At this point there's only a single code + block + """ + global PROC_COUNTER + + stack = [[]] + hashes = [{}] + stackprc = [PROC_COUNTER] + used = [{}] # List of hashes of unresolved labels per scope + + MEMORY[:] = block.mem[:] + + for cell in MEMORY: + if cell.inst.upper() == "PROC": + stack += [[]] + hashes += [{}] + stackprc += [PROC_COUNTER] + used += [{}] + PROC_COUNTER += 1 + continue + + if cell.inst.upper() == "ENDP": + if len(stack) > 1: # There might be unbalanced stack due to syntax errors + for label in used[-1].keys(): + if label in stack[-1]: + newlabel = hashes[-1][label] + for cell in used[-1][label]: + cell.replace_label(label, newlabel) + + stack.pop() + hashes.pop() + stackprc.pop() + used.pop() + continue + + tmp = cell.asm.asm + if tmp.upper()[:5] == "LOCAL": + tmp = tmp[5:].split(",") + for lbl in tmp: + lbl = lbl.strip() + if lbl in stack[-1]: + continue + stack[-1] += [lbl] + hashes[-1][lbl] = "PROC%i." % stackprc[-1] + lbl + if used[-1].get(lbl, None) is None: + used[-1][lbl] = [] + + cell.asm = ";" + cell.asm # Remove it + continue + + if cell.is_label: + label = cell.inst + for i in range(len(stack) - 1, -1, -1): + if label in stack[i]: + label = hashes[i][label] + cell.asm = label + ":" + break + continue + + for label in cell.used_labels: + labelUsed = False + for i in range(len(stack) - 1, -1, -1): + if label in stack[i]: + newlabel = hashes[i][label] + cell.replace_label(label, newlabel) + labelUsed = True + break + + if not labelUsed: + if used[-1].get(label, None) is None: + used[-1][label] = [] + + used[-1][label] += [cell] + + for i in range(len(MEMORY) - 1, -1, -1): + if MEMORY[i].asm.asm[0] == ";": + MEMORY.pop(i) + + block.mem = MEMORY + block.asm = [x.asm for x in MEMORY if len(x.asm)] + + +def get_labels(basic_block: BasicBlock) -> None: + """Traverses memory, to annotate all the labels in the global + LABELS table + """ + for i, cell in enumerate(basic_block): + if cell.is_label: + label = cell.inst + LABELS[label] = LabelInfo(label, cell.addr, basic_block, i) # Stores it globally + + +def initialize_memory(basic_block: BasicBlock) -> None: + """Initializes global memory array with the one in the main (initial) basic_block""" + init() + MEMORY[:] = basic_block.mem[:] + get_labels(basic_block) + + +def optimize(initial_memory: list[str]) -> str: + """This will remove useless instructions""" + global BLOCKS + global PROC_COUNTER + + MEMORY.clear() + PROC_COUNTER = 0 + + _cleanup_mem(initial_memory) + if OPTIONS.optimization_level <= 2: # if -O2 or lower, do nothing and return + return "\n".join(x for x in initial_memory if not RE_PRAGMA.match(x)) + + BasicBlock.clean_asm_args = OPTIONS.optimization_level > 3 + bb = BasicBlock(initial_memory) + cleanup_local_labels(bb) + initialize_memory(bb) + + BLOCKS = basic_blocks = flow_graph.get_basic_blocks(bb) # 1st partition the Basic Blocks + + for b in basic_blocks: + __DEBUG__("--- BASIC BLOCK: {} ---".format(b.id), 1) + __DEBUG__("Code:\n" + "\n".join(" {}".format(x) for x in b.code), 1) + __DEBUG__("Requires: {}".format(b.requires()), 1) + __DEBUG__("Destroys: {}".format(b.destroys()), 1) + __DEBUG__("Label goes: {}".format(b.label_goes), 1) + __DEBUG__("Comes from: {}".format([x.id for x in b.comes_from]), 1) + __DEBUG__("Goes to: {}".format([x.id for x in b.goes_to]), 1) + __DEBUG__("Next: {}".format(b.next.id if b.next is not None else None), 1) + __DEBUG__("Size: {} Time: {}".format(b.sizeof, b.max_tstates), 1) + __DEBUG__("--- END ---", 1) + + LABELS["*START*"].basic_block.add_goes_to(basic_blocks[0]) + LABELS["*START*"].basic_block.next = basic_blocks[0] + + basic_blocks[0].prev = LABELS["*START*"].basic_block + if END_PROGRAM_LABEL in LABELS: + LABELS[END_PROGRAM_LABEL].basic_block.add_goes_to(LABELS["*__END_PROGRAM*"].basic_block) + + # In O3 we simplify the graph by reducing jumps over jumps + for label in JUMP_LABELS: + block = LABELS[label].basic_block + if isinstance(block, DummyBasicBlock): + continue + + # The instruction that starts this block must be one of jr / jp + first = block.get_next_exec_instruction() + if first is None or first.inst not in ("jp", "jr"): + continue + + for blk in list(LABELS[label].used_by): + if not first.condition_flag or blk[-1].condition_flag == first.condition_flag: + new_label = first.opers[0] + blk[-1].asm = blk[-1].code.replace(label, new_label) + block.delete_comes_from(blk) + LABELS[label].used_by.remove(blk) + LABELS[new_label].used_by.add(blk) + blk.add_goes_to(LABELS[new_label].basic_block) + + for x in basic_blocks: + x.compute_cpu_state() + + filtered_patterns_list = [p for p in engine.PATTERNS if OPTIONS.optimization_level >= p.level >= 3] + for x in basic_blocks: + x.optimize(filtered_patterns_list) + + for x in basic_blocks: + if x.comes_from == [] and len([y for y in JUMP_LABELS if x is LABELS[y].basic_block]): + x.ignored = True + + return "\n".join(y for y in flatten_list(x.code for x in basic_blocks if not x.ignored) if not RE_PRAGMA.match(y)) diff --git a/tests/arch/zx48k/optimizer/test_basicblock.py b/tests/arch/zx48k/optimizer/test_basicblock.py index 001072b72..92c4ee21a 100644 --- a/tests/arch/zx48k/optimizer/test_basicblock.py +++ b/tests/arch/zx48k/optimizer/test_basicblock.py @@ -2,7 +2,8 @@ import unittest -from src.arch.z80 import optimizer +import src.arch.z80.optimizer.flow_graph +import src.arch.z80.optimizer.main from src.arch.z80.optimizer import basicblock from src.arch.z80.peephole import evaluator @@ -23,7 +24,7 @@ def test_block_partition_jp(self): nop """ self.blk.code = [x for x in code.split("\n") if x.strip()] - blks = basicblock.get_basic_blocks(self.blk) + blks = src.arch.z80.optimizer.flow_graph.get_basic_blocks(self.blk) self.assertEqual(len(blks), 2) self.assertEqual(blks[0].code, ["nop", "jp __UNKNOWN"]) self.assertEqual(blks[1].code, ["nop"]) @@ -37,7 +38,7 @@ def test_block_partition_call(self): nop """ self.blk.code = [x for x in code.split("\n") if x.strip()] - blks = basicblock.get_basic_blocks(self.blk) + blks = src.arch.z80.optimizer.flow_graph.get_basic_blocks(self.blk) self.assertEqual(len(blks), 2) self.assertEqual(blks[0].code, ["nop", "call __UNKNOWN"]) self.assertEqual(blks[1].code, ["nop"]) @@ -51,7 +52,7 @@ def test_block_partition_jp_flag(self): nop """ self.blk.code = [x for x in code.split("\n") if x.strip()] - blks = basicblock.get_basic_blocks(self.blk) + blks = src.arch.z80.optimizer.flow_graph.get_basic_blocks(self.blk) self.assertEqual(len(blks), 2) self.assertEqual(blks[0].code, ["nop", "jp z, __UNKNOWN"]) self.assertEqual(blks[1].code, ["nop"]) @@ -65,7 +66,7 @@ def test_block_partition_call_flag(self): nop """ self.blk.code = [x for x in code.split("\n") if x.strip()] - blks = basicblock.get_basic_blocks(self.blk) + blks = src.arch.z80.optimizer.flow_graph.get_basic_blocks(self.blk) self.assertEqual(len(blks), 2) self.assertEqual(blks[0].code, ["nop", "call z, __UNKNOWN"]) self.assertEqual(blks[1].code, ["nop"]) @@ -85,8 +86,8 @@ def test_call_part(self): ld a, 2 """ self.blk.code = [x for x in code.split("\n") if x.strip()] - optimizer.initialize_memory(self.blk) - blks = basicblock.get_basic_blocks(self.blk) + src.arch.z80.optimizer.main.initialize_memory(self.blk) + blks = src.arch.z80.optimizer.flow_graph.get_basic_blocks(self.blk) self.assertEqual(len(blks), 3) self.assertEqual(blks[0].code, ["my_block:", "ld a, 3", "ret"]) self.assertEqual(blks[1].code, ["ld a, 1", "call my_block"]) @@ -107,8 +108,8 @@ def test_call_ret2(self): ret """ self.blk.code = [x for x in code.split("\n") if x.strip()] - optimizer.initialize_memory(self.blk) - blks = basicblock.get_basic_blocks(self.blk) + src.arch.z80.optimizer.main.initialize_memory(self.blk) + blks = src.arch.z80.optimizer.flow_graph.get_basic_blocks(self.blk) self.assertEqual(len(blks), 3) self.assertEqual(blks[0].code, ["ld a, 1", "call my_block"]) self.assertEqual(blks[1].code, ["ld a, 2", "ret"]) @@ -134,8 +135,8 @@ def test_long_block(self): ld a, 5 """ self.blk.code = [x for x in code.split("\n") if x.strip()] - optimizer.initialize_memory(self.blk) - blks = basicblock.get_basic_blocks(self.blk) + src.arch.z80.optimizer.main.initialize_memory(self.blk) + blks = src.arch.z80.optimizer.flow_graph.get_basic_blocks(self.blk) self.assertEqual(len(blks), 6) self.assertEqual(blks[0].code, ["ld a, 0", "jp __LABEL2"]) self.assertEqual(blks[1].code, ["__LABEL0:", "ld a, 1", "jp z, __LABEL1"]) @@ -208,8 +209,8 @@ def test_loop_goes_and_comes(self): ld (ix-1), a """ self.blk.code = [x for x in code.split("\n") if x.strip()] - optimizer.initialize_memory(self.blk) - blks = basicblock.get_basic_blocks(self.blk) + src.arch.z80.optimizer.main.initialize_memory(self.blk) + blks = src.arch.z80.optimizer.flow_graph.get_basic_blocks(self.blk) assert len(blks) == 3 b1, b2, b3 = blks assert b1.goes_to == b2.goes_to == {b2} diff --git a/tests/arch/zx48k/optimizer/test_optimizer.py b/tests/arch/zx48k/optimizer/test_optimizer.py index c364751f5..d8a873cad 100644 --- a/tests/arch/zx48k/optimizer/test_optimizer.py +++ b/tests/arch/zx48k/optimizer/test_optimizer.py @@ -1,18 +1,19 @@ from contextlib import contextmanager +from src.api.config import OPTIONS from src.arch.z80 import optimizer from src.arch.z80.peephole import engine @contextmanager def mock_options_level(level: int): - initial_level = optimizer.OPTIONS.optimization_level + initial_level = OPTIONS.optimization_level try: - optimizer.OPTIONS.optimization_level = level + OPTIONS.optimization_level = level yield finally: - optimizer.OPTIONS.optimization_level = initial_level + OPTIONS.optimization_level = initial_level class TestOptimizer: diff --git a/tools/scrview.py b/tools/scrview.py index 3ad49b3a7..9c7fa1fa4 100755 --- a/tools/scrview.py +++ b/tools/scrview.py @@ -2,9 +2,11 @@ # -*- coding: utf-8 -*- import sys -import pygame from typing import List, Tuple +import pygame + +import src.arch.z80.optimizer.main WIDTH = 256 # ZX Spectrum screen width in pixels HEIGHT = 192 # ZX Spectrum screen height in pixels @@ -93,7 +95,7 @@ def plot_byte(screen, data: List[int], offset: int): def paint(data: List[int]): - pygame.init() + src.arch.z80.optimizer.main.init() screen = pygame.display.set_mode([WIDTH * SCALE, HEIGHT * SCALE]) for i in range(SCREEN_AREA_SIZE):