diff --git a/Project-02-03-04-05/Source.gv.png b/Project-02-03-04-05/Source.gv.png index bd73e4a..9ffc31f 100644 Binary files a/Project-02-03-04-05/Source.gv.png and b/Project-02-03-04-05/Source.gv.png differ diff --git a/Project-02-03-04-05/cfa/BackwardAnalysis.py b/Project-02-03-04-05/cfa/BackwardAnalysis.py index 04918dd..80e5aee 100644 --- a/Project-02-03-04-05/cfa/BackwardAnalysis.py +++ b/Project-02-03-04-05/cfa/BackwardAnalysis.py @@ -25,7 +25,7 @@ class BackwardAnalysis: self.__funcs: dict[str, tuple] = dict(cfg_build.FUNCTIONS) self.__func_parent, self._func_params = self.__collect_function_metadata() - self.__func_scope: dict[int, str] = self.__compute_function_scope() + self.func_scope: dict[int, str] = self.__compute_function_scope() self.__extract_uses_and_defs() # Walk the AST and collect function-parent and parameter information. @@ -90,7 +90,7 @@ class BackwardAnalysis: def __extract_uses_and_defs(self) -> None: for node in self.cfg.nodes(): nid = node.id - func = self.__func_scope.get(nid) + func = self.func_scope.get(nid) ast = node.ast_node uses: set[Var] = set() @@ -102,17 +102,17 @@ class BackwardAnalysis: defs.add((ast.f_name, param)) elif ast is not None: if isinstance(ast, syntax.ID): - resolved = self.__resolve_var(func, ast.name) + resolved = self.resolve_var(func, ast.name) uses.add(resolved) elif isinstance(ast, syntax.ASSIGN): - resolved = self.__resolve_var(func, ast.var.name) + resolved = self.resolve_var(func, ast.var.name) defs.add(resolved) self.uses[nid] = uses self.defs[nid] = defs # Resolve a variables name and scope by walking up the hierarchy - def __resolve_var(self, func: str | None, name: str) -> Var: + def resolve_var(self, func: str | None, name: str) -> Var: if func is None: return GLOBAL_SCOPE, name diff --git a/Project-02-03-04-05/cfa/to_dot.py b/Project-02-03-04-05/cfa/to_dot.py index 899faf8..999fd78 100644 --- a/Project-02-03-04-05/cfa/to_dot.py +++ b/Project-02-03-04-05/cfa/to_dot.py @@ -2,8 +2,38 @@ import syntax import colorsys from cfg.CFG_Node import CFG_DIAMOND +# Builds annotations for the LiveVariables analysis. +def build_lv_annotations(cfg, lv) -> dict[int, str]: + node_by_id = {n.id: n for n in cfg.nodes()} + all_ids = set(lv.incoming.keys()) | set(lv.outgoing.keys()) + return { + nid: ( + "LivingVariables\\n" + f"In := {sorted(__lv_in_set(node_by_id[nid], lv))}\\n" + f"Out := {sorted(lv.outgoing.get(nid, set()))}" + ) + for nid in all_ids + if lv.incoming.get(nid, set()) or lv.outgoing.get(nid, set()) + if nid in node_by_id and __should_display_analysis(node_by_id[nid]) + } + +# For display only: IN(ASSIGN) has GEN = empty, so RHS variables are missing from incoming — they live at their +# own ID nodes. Add them here for a proper DOT annotation. +def __lv_in_set(node, analysis): + in_set = set(analysis.incoming.get(node.id, set())) + ast_node = node.ast_node + if isinstance(ast_node, syntax.ASSIGN): + func = analysis.func_scope.get(node.id) + rhs_vars = { + analysis.resolve_var(func, name) + for name in __expr_used_names(ast_node.expr) + } + in_set |= rhs_vars + return in_set + +# For display only: the right-hand side of an ASSIGN has no dedicated CFG nodes, so LV places uses of "a", "b" +# at their own nodes — not at the ASSIGN. Recover them here to complete the DOT annotation at the ASSIGN node. def __expr_used_names(expr) -> set[str]: - """Collect variable names (syntax.ID) used inside an expression subtree.""" used: set[str] = set() def visit(node): @@ -42,23 +72,8 @@ def __should_display_analysis(node) -> bool: ), ) - -def _lv_in_for_display(node, analysis): - """Display-level IN set for LV.""" - in_set = set(analysis.incoming.get(node.id, set())) - ast_node = node.ast_node - if isinstance(ast_node, syntax.ASSIGN): - func = analysis.__func_scope.get(node.id) - rhs_vars = { - analysis.__resolve_var(func, name) - for name in __expr_used_names(ast_node.expr) - } - in_set |= rhs_vars - return in_set - - -def _node_color(node_id: int) -> tuple[str, str]: - """Return (edge_color, fill_color) deterministically for a node id.""" +# Generates colors for CFG nodes based on their id. +def __node_color(node_id: int) -> tuple[str, str]: # Golden-angle hue distribution gives stable, distinct colors. hue = ((node_id * 0.6180339887498949) % 1.0) edge_rgb = colorsys.hsv_to_rgb(hue, 0.70, 0.82) @@ -70,54 +85,27 @@ def _node_color(node_id: int) -> tuple[str, str]: return to_hex(edge_rgb), to_hex(fill_rgb) +# Return a DOT string for the CFG annotated with analysis results. +def analysis_to_dot(cfg, analyses: dict, analysis_name: str) -> str: + ru = analyses.get("ru") + lv = analyses.get("lv") -def run_all_analyses(cfg): - """Run Live Variables and Reached Uses on *cfg*. + ru_edges = ru.reached_uses_by_node() if ru is not None else None - Returns ``(analyses, annotations, ru_edges)`` where: - • *analyses* is a dict with keys ``"lv"`` and ``"ru"``, - • *annotations* contains only LivingVariables helper-node labels, - • *ru_edges* maps definition-node ids to reached use-node ids. - """ - node_by_id = {n.id: n for n in cfg.nodes()} - - from cfa.LiveVariables import LiveVariablesAnalysis - from cfa.ReachedUses import ReachedUsesAnalysis - - lv = LiveVariablesAnalysis(cfg) - ru = ReachedUsesAnalysis(cfg) - - all_ids = set(lv.incoming.keys()) | set(lv.outgoing.keys()) - annotations = { - nid: ( - "LivingVariables\\n" - f"In := {sorted(_lv_in_for_display(node_by_id[nid], lv))}\\n" - f"Out := {sorted(lv.outgoing.get(nid, set()))}" - ) - for nid in all_ids - if lv.incoming.get(nid, set()) or lv.outgoing.get(nid, set()) - if nid in node_by_id and __should_display_analysis(node_by_id[nid]) - } - - return {"lv": lv, "ru": ru}, annotations, ru.reached_uses_by_node() - - -def analysis_to_dot( - cfg, - annotations: dict[int, str], - analysis_name: str, - ru_edges: dict[int, list[int]] | None = None, -) -> str: - """Return a DOT string for *cfg* annotated with analysis results.""" + # DOT graph header lines = [ "digraph CFG {", f' // Analysis: {analysis_name}', ' graph [splines=ortho, overlap=false, ranksep=0.7, nodesep=0.45];', ' node [fontname="Helvetica"];', ] - color_nodes = set(annotations.keys()) | set((ru_edges or {}).keys()) - node_colors = {nid: _node_color(nid) for nid in color_nodes} + # Build LV annotations and assign a color per annotated node + annotations = build_lv_annotations(cfg, lv) + color_nodes = set(annotations.keys()) | set((ru_edges or {}).keys()) + node_colors = {nid: __node_color(nid) for nid in color_nodes} + + # Emit each CFG node with its label, shape, and optional LV annotation note def emit(node): base_label = node.dot_label() or "" shape = node.dot_shape @@ -150,6 +138,7 @@ def analysis_to_dot( cfg.traverse(emit, start=cfg.START) + # Draw dashed def -> use edges for Reached Uses if ru_edges: for idx, def_id in enumerate(sorted(ru_edges)): use_ids = sorted(set(ru_edges[def_id])) diff --git a/Project-02-03-04-05/main.py b/Project-02-03-04-05/main.py index 60cf63b..a430da7 100644 --- a/Project-02-03-04-05/main.py +++ b/Project-02-03-04-05/main.py @@ -11,7 +11,9 @@ import cfg_build import lib.console as cnsl import syntax import triplayacc as yacc -from cfa.to_dot import analysis_to_dot, run_all_analyses +from cfa.LiveVariables import LiveVariables +from cfa.ReachedUses import ReachedUses +from cfa.to_dot import analysis_to_dot from cfg.CFG import CFG from vistram.tram import * from vistram.vistram import MachineUI @@ -64,12 +66,11 @@ def pretty_print(node, indent=0): else: print(f"{prefix} {key}: {value}") - -def print_analysis_reports(cfg, analyses: dict, ru_edges: dict[int, list[int]]): - """Print compact Live Variables and Reached Uses reports to console.""" +# Print compact Live Variables and Reached Uses reports to console. +def print_analysis_reports(cfg, analyses: dict): lv = analyses["lv"] ru = analyses["ru"] - _ = ru + ru_edges = ru.reached_uses_by_node() node_by_id = {n.id: n for n in cfg.nodes()} def node_text(nid: int) -> str: @@ -102,7 +103,6 @@ def print_analysis_reports(cfg, analyses: dict, ru_edges: dict[int, list[int]]): if not has_ru: print("(no reached uses)") - if __name__ == "__main__": print("\nTRIPLA Parser and TRIPLA to TRAM Compiler") @@ -211,28 +211,25 @@ if __name__ == "__main__": print("Rendered CFG diagram.") elif mode == 3: - # Reset global CFG builder state so each analysis run is clean + # Reset the global CFG builder state so each analysis run is clean cfg_build.FUNCTIONS.clear() cfg_build.CURRENT_FUNCTION = None cfg = make_cfg(ast) - analysis_name = "Live Variables + Reached Uses" + analysis_name = "Live Variables + Reached Uses analyses" print(f"\nRunning {analysis_name} …") try: - _analyses, annotations, ru_edges = run_all_analyses(cfg) + lv = LiveVariables(cfg) + ru = ReachedUses(cfg) + analyses = {"lv": lv, "ru": ru} except Exception as exc: print(f"Analysis failed: {exc}") continue - print( - f"Done. {len(annotations)} LV annotation node(s), " - f"{len(ru_edges)} RU definition node(s)." - ) - - dot_str = analysis_to_dot(cfg, annotations, analysis_name, ru_edges) - if cnsl.prompt_confirmation("\nPrint analysis reports to console?"): - print_analysis_reports(cfg, _analyses, ru_edges) + print_analysis_reports(cfg, analyses) + + dot_str = analysis_to_dot(cfg, analyses, analysis_name) if cnsl.prompt_confirmation("\nExport annotated CFG as .dot file?"): default = f"{path.stem}_analysis.dot"