diff --git a/Project-02-03-04-05/Source.gv.png b/Project-02-03-04-05/Source.gv.png new file mode 100644 index 0000000..bd73e4a Binary files /dev/null and b/Project-02-03-04-05/Source.gv.png differ diff --git a/Project-02-03-04-05/cfa/__init__.py b/Project-02-03-04-05/cfa/__init__.py new file mode 100644 index 0000000..978054b --- /dev/null +++ b/Project-02-03-04-05/cfa/__init__.py @@ -0,0 +1,9 @@ +from .live_variables import LiveVariablesAnalysis, Var +from .reached_uses import ReachedUsesAnalysis, UseFact + +__all__ = [ + "Var", + "UseFact", + "LiveVariablesAnalysis", + "ReachedUsesAnalysis", +] diff --git a/Project-02-03-04-05/cfa/analysis_dot.py b/Project-02-03-04-05/cfa/analysis_dot.py new file mode 100644 index 0000000..d38b91a --- /dev/null +++ b/Project-02-03-04-05/cfa/analysis_dot.py @@ -0,0 +1,192 @@ +import syntax +import colorsys +from cfg.CFG_Node import CFG_DIAMOND + + +def _expr_used_names(expr) -> set[str]: + """Collect variable names (syntax.ID) used inside an expression subtree.""" + used: set[str] = set() + + def visit(node): + if node is None: + return + if isinstance(node, syntax.ID): + used.add(node.name) + return + if isinstance(node, syntax.EXPRESSION): + for _, child in node.children(): + visit(child) + + visit(expr) + return used + + +def _show_analysis_on_node(node) -> bool: + """Return True if analysis annotations should be displayed for this node.""" + ast = node.ast_node + if isinstance(node, CFG_DIAMOND): + return False + if ast is None: + return False + return isinstance( + ast, + ( + syntax.ASSIGN, + syntax.CALL, + syntax.IF, + syntax.WHILE, + syntax.DECL, + syntax.LET, + syntax.SEQ, + syntax.COMP, + syntax.EQOP, + syntax.LOP, + ), + ) + + +def _lv_in_for_display(node, analysis): + """Display-level IN set for LV.""" + in_set = set(analysis.in_sets.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.""" + # 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) + fill_rgb = colorsys.hsv_to_rgb(hue, 0.28, 0.97) + + def to_hex(rgb): + r, g, b = (int(round(c * 255)) for c in rgb) + return f"#{r:02x}{g:02x}{b:02x}" + + return to_hex(edge_rgb), to_hex(fill_rgb) + + +def run_all_analyses(cfg): + """Run Live Variables and Reached Uses on *cfg*. + + 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.live_variables import LiveVariablesAnalysis + from cfa.reached_uses import ReachedUsesAnalysis + + lv = LiveVariablesAnalysis(cfg) + ru = ReachedUsesAnalysis(cfg) + + all_ids = set(lv.in_sets.keys()) | set(lv.out_sets.keys()) + annotations = { + nid: ( + "LivingVariables\\n" + f"In := {sorted(_lv_in_for_display(node_by_id[nid], lv))}\\n" + f"Out := {sorted(lv.out_sets.get(nid, set()))}" + ) + for nid in all_ids + if lv.in_sets.get(nid, set()) or lv.out_sets.get(nid, set()) + if nid in node_by_id and _show_analysis_on_node(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.""" + 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} + + def emit(node): + base_label = node.dot_label() or "" + shape = node.dot_shape + style = node.dot_style + style_str = f", {style}" if style else "" + lines.append(f' n{node.id} [label="{base_label}", shape={shape}{style_str}];') + + if node.id in annotations: + ann_id = f"a{node.id}" + ann_label = annotations[node.id].replace('"', '\\"') + edge_color, fill_color = node_colors.get(node.id, ("#1f77b4", "#d9ecff")) + lines.append( + f' {ann_id} [label="{ann_label}", shape=note, ' + f'style="filled", fillcolor="{fill_color}", color="{edge_color}", ' + f'fontcolor="{edge_color}"];' + ) + lines.append( + f' {ann_id} -> n{node.id} [style=dotted, arrowhead=none, ' + f'color="{edge_color}"];' + ) + + for i, child in enumerate(sorted(node.children, key=lambda n: n.id)): + edge_label = "" + if isinstance(node, CFG_DIAMOND): + if i == 0: + edge_label = ' [label="T"]' + elif i == 1: + edge_label = ' [label="F"]' + lines.append(f" n{node.id} -> n{child.id}{edge_label};") + + cfg.traverse(emit, start=cfg.START) + + if ru_edges: + for idx, def_id in enumerate(sorted(ru_edges)): + use_ids = sorted(set(ru_edges[def_id])) + if not use_ids: + continue + + # One routing hub per definition node to mimic UML-like + # "out to the side, then down/across to targets" connectors. + side = "e" if idx % 2 == 0 else "w" + source_port = "se" if side == "e" else "sw" + hub_id = f"rh{def_id}" + edge_color, fill_color = node_colors.get(def_id, ("#1f77b4", "#d9ecff")) + + lines.append( + f' {hub_id} [shape=point, width=0.05, height=0.05, ' + f'color="{edge_color}", fillcolor="{edge_color}", style=filled];' + ) + lines.append(f" {{ rank=same; n{def_id}; {hub_id}; }}") + lines.append( + f' n{def_id}:{source_port} -> {hub_id} [color="{edge_color}", ' + f'style=dashed, penwidth=1.2, arrowhead=none, constraint=false, ' + f'tailclip=true, headclip=true];' + ) + + for use_id in use_ids: + if side == "e": + target_port = "ne" if (use_id % 2 == 0) else "se" + else: + target_port = "nw" if (use_id % 2 == 0) else "sw" + lines.append( + f' {hub_id} -> n{use_id}:{target_port} [color="{edge_color}", ' + f'fontcolor="{edge_color}", fontsize=8, style=dashed, ' + f'penwidth=1.0, arrowsize=0.6, constraint=false, ' + f'tailclip=true, headclip=true];' + ) + + lines.append("}") + return "\n".join(lines) diff --git a/Project-02-03-04-05/cfa/live_variables.py b/Project-02-03-04-05/cfa/live_variables.py new file mode 100644 index 0000000..db2f75d --- /dev/null +++ b/Project-02-03-04-05/cfa/live_variables.py @@ -0,0 +1,351 @@ +""" +live_variables.py — Live Variables backward dataflow analysis for TRIPLA CFGs. + +A variable v is *live* at the entry of node n if there exists a path +n → … → use(v) where v is not redefined along the way. + +Data structures +--------------- +gen dict[int, set[Var]] — GEN(n) = variables *used* at n +kill dict[int, set[Var]] — KILL(n) = variables *defined* at n +in_sets dict[int, set[Var]] — live variables at node *entry* +out_sets dict[int, set[Var]] — live variables at node *exit* + +Transfer equations (backward): + OUT(n) = ∪ IN(s) for all successors s + IN(n) = (OUT(n) − KILL(n)) ∪ GEN(n) + +Variables are represented in scoped form ``(scope, name)``, e.g. ``("f","x")``. +This avoids collisions between equal variable names in different functions. + +This module also exports ``_BackwardAnalysisBase``, the shared base class +that ``ReachedUsesAnalysis`` in reached_uses.py inherits from. The base +provides: + • AST traversal to collect function-nesting and parameter metadata + • Lexical variable resolution (parameter shadowing handled correctly) + • BFS-based CFG-node → owning-function assignment + • Unified uses / defs extraction for all node types + +Var = tuple[str, str] +""" +from __future__ import annotations + +from collections import deque +from typing import TYPE_CHECKING + +import cfg_build +import syntax +from cfg.CFG_Node import CFG_START + +if TYPE_CHECKING: + from cfg.CFG import CFG + +# --------------------------------------------------------------------------- +# Public type alias (imported by reached_uses.py) +# --------------------------------------------------------------------------- + +GLOBAL_SCOPE = "" +Var = tuple[str, str] # (function_name|GLOBAL_SCOPE, variable_name) + + +# ============================================================================ +# Shared base: function metadata, scope assignment, uses/defs extraction +# ============================================================================ + +class _BackwardAnalysisBase: + """Infrastructure shared by LiveVariablesAnalysis and ReachedUsesAnalysis. + + Calling ``super().__init__(cfg)`` from a subclass: + 1. Snapshots cfg_build.FUNCTIONS. + 2. Collects AST-level function-nesting and parameter metadata. + 3. BFS-assigns every CFG node to its owning function. + 4. Extracts uses and defs for every CFG node. + + After __init__ the following attributes are available to subclasses: + + self.cfg — the CFG object + self._functions — dict[str, tuple]: snapshot of cfg_build.FUNCTIONS + self._func_parent — dict[str, str|None]: lexical parent per function + self._func_params — dict[str, tuple[str,...]]: params per function + self._func_scope — dict[int, str]: node-id → owning function name + self.uses — dict[int, set[Var]]: variables used at each node + self.defs — dict[int, set[Var]]: variables defined at each node + """ + + def __init__(self, cfg: "CFG") -> None: + self.cfg = cfg + # Snapshot FUNCTIONS so later global-state resets do not affect us. + self._functions: dict[str, tuple] = dict(cfg_build.FUNCTIONS) + + self.uses: dict[int, set[Var]] = {} + self.defs: dict[int, set[Var]] = {} + + self._func_parent, self._func_params = self._collect_function_metadata() + self._func_scope: dict[int, str] = self._compute_function_scope() + self._extract_uses_defs() + + # ------------------------------------------------------------------ + # Step 1a — Walk AST to collect lexical nesting + parameter lists + # ------------------------------------------------------------------ + + def _collect_function_metadata( + self, + ) -> tuple[dict[str, str | None], dict[str, tuple[str, ...]]]: + """Walk the AST and collect function-parent and parameter information. + + Returns + ------- + func_parent : dict[str, str | None] + func_parent[f] is the name of the immediately enclosing function + (or None for top-level functions). + func_params : dict[str, tuple[str, ...]] + func_params[f] is the ordered tuple of formal parameter names of f. + """ + func_parent: dict[str, str | None] = {} + func_params: dict[str, tuple[str, ...]] = {} + + def visit(expr: syntax.EXPRESSION | None, current_func: str | None) -> None: + if expr is None: + return + if isinstance(expr, syntax.LET): + decls = expr.decl if isinstance(expr.decl, list) else [expr.decl] + # Register metadata for each declared function. + for d in decls: + if isinstance(d, syntax.DECL): + # Use assignment (last-seen wins) to stay consistent + # with cfg_build.FUNCTIONS, which also overwrites on + # duplicate names. setdefault (first-seen wins) would + # disagree when a nested function shadows a top-level + # one with the same name, causing wrong scope resolution. + func_parent[d.f_name] = current_func + func_params[d.f_name] = tuple(d.params) + # Recurse into function bodies and the in-expression. + for d in decls: + if isinstance(d, syntax.DECL): + visit(d.body, d.f_name) + else: + visit(d, current_func) + visit(expr.body, current_func) + return + for _, child in expr.children(): + visit(child, current_func) + + visit(self.cfg.ast, None) + return func_parent, func_params + + # ------------------------------------------------------------------ + # Step 1b — Resolve a variable name through the lexical scope chain + # ------------------------------------------------------------------ + + def _resolve_var(self, func: str | None, name: str) -> Var: + """Resolve a variable name via lexical scope chain.""" + if func is None: + return (GLOBAL_SCOPE, name) + + cur: str | None = func + seen: set[str] = set() + while cur is not None and cur not in seen: + seen.add(cur) + if name in self._func_params.get(cur, ()): + return (cur, name) + cur = self._func_parent.get(cur) + + # Fallback: local variable in current function scope. + return (func, name) + + # ------------------------------------------------------------------ + # Step 2 — BFS-assign every CFG node to its owning function + # ------------------------------------------------------------------ + + def _compute_function_scope(self) -> dict[int, str]: + """BFS from each function's START node; return node-id → function-name. + + Two stopping conditions keep attribution strictly inside each function: + + 1. Do not follow into a *different* function's CFG_START (prevents + attributing callee body nodes to the caller, and vice-versa). + 2. Do not follow *past* the function's own CFG_END (prevents + following CFG_END → CFG_RETURN → continuation nodes that belong + to the *caller* context, which caused variables used there to be + resolved in the wrong scope). + + The first function whose BFS claims a node wins. + """ + functions = self._functions + func_scope: dict[int, str] = {} + all_f_start_ids: set[int] = {fs.id for _, (fs, _) in functions.items()} + + for f_name, (f_start, f_end) in functions.items(): + queue: deque = deque([f_start]) + while queue: + node = queue.popleft() + if node.id in func_scope: + continue # already claimed by an earlier function + func_scope[node.id] = f_name + # Stop here — do not follow CFG_END into caller context. + if node.id == f_end.id: + continue + for child in node.children: + # Do not follow into a different function's START. + if ( + isinstance(child, CFG_START) + and child.id in all_f_start_ids + and child.id != f_start.id + ): + continue + queue.append(child) + + return func_scope + + # ------------------------------------------------------------------ + # Step 3 — Extract uses / defs for every CFG node + # ------------------------------------------------------------------ + + def _extract_uses_defs(self) -> None: + """Populate ``self.uses`` and ``self.defs`` for every node in the CFG. + + Extraction rules: + • CFG_START(DECL f(p1,…,pk)) → defs = {(f,p1), …, (f,pk)} + • Node wrapping ID(x) → uses = {lexical_resolve(func, x)} + • Node wrapping ASSIGN(x = e) → defs = {lexical_resolve(func, x)} + • Everything else → uses = {}, defs = {} + + Sub-expressions already have their own CFG nodes and are not + re-inspected here; each node is responsible only for its own ast_node. + """ + for node in self.cfg.nodes(): + nid = node.id + func = self._func_scope.get(nid) # None → outer / global scope + ast = node.ast_node + + uses: set[Var] = set() + defs: set[Var] = set() + + if isinstance(node, CFG_START) and isinstance(ast, syntax.DECL): + # Function entry defines each formal parameter. + for param in ast.params: + defs.add((ast.f_name, param)) + elif ast is not None: + if isinstance(ast, syntax.ID): + resolved = self._resolve_var(func, ast.name) + uses.add(resolved) + elif isinstance(ast, syntax.ASSIGN): + resolved = self._resolve_var(func, ast.var.name) + defs.add(resolved) + + self.uses[nid] = uses + self.defs[nid] = defs + + +# ============================================================================ +# Live Variables Analysis +# ============================================================================ + +class LiveVariablesAnalysis(_BackwardAnalysisBase): + """Backward dataflow analysis: Live Variables. + + A variable (f, x) is *live* at the entry of node n if there is a path + from n to some use of (f, x) along which (f, x) is not redefined. + + This is the simpler predecessor to ReachedUsesAnalysis (reached_uses.py): + it tracks which variables are live, not *where* they are used. + + Attributes + ---------- + gen dict[int, set[Var]] GEN(n) = uses(n) — vars used at n + kill dict[int, set[Var]] KILL(n) = defs(n) — vars defined at n + in_sets dict[int, set[Var]] live variables at n's *entry* + out_sets dict[int, set[Var]] live variables at n's *exit* + + (uses and defs are identical to gen / kill and are inherited from the + base class.) + + Transfer equations (backward): + OUT(n) = ∪ IN(s) for all successors s + IN(n) = (OUT(n) − KILL(n)) ∪ GEN(n) + """ + + def __init__(self, cfg: "CFG") -> None: + # Base populates uses, defs, _func_scope, etc. + super().__init__(cfg) + + self.gen: dict[int, set[Var]] = {} + self.kill: dict[int, set[Var]] = {} + self.in_sets: dict[int, set[Var]] = {} + self.out_sets: dict[int, set[Var]] = {} + + self._build_gen_kill() + self.solve() + + # ------------------------------------------------------------------ + # Build gen / kill; initialise in / out to ∅ + # ------------------------------------------------------------------ + + def _build_gen_kill(self) -> None: + """GEN(n) = uses(n), KILL(n) = defs(n); initialise in/out sets.""" + for node in self.cfg.nodes(): + nid = node.id + self.gen[nid] = set(self.uses[nid]) + self.kill[nid] = set(self.defs[nid]) + self.in_sets[nid] = set() + self.out_sets[nid] = set() + + # ------------------------------------------------------------------ + # Backward worklist fixpoint + # ------------------------------------------------------------------ + + def solve(self) -> None: + """Backward worklist until fixpoint. + + Transfer: + OUT(n) = ∪ IN(s) for all successors s + IN(n) = (OUT(n) − KILL(n)) ∪ GEN(n) + + Only nodes reachable from cfg.START are processed (guard against + propagate=False parent references from CFG.__remove_and_rewire). + """ + nodes = list(self.cfg.nodes()) + known: set[int] = set(self.gen.keys()) + id_to_node = {n.id: n for n in nodes} + worklist: deque = deque(nodes) + + # Build predecessor relation from children edges. This is more reliable + # than node.parents because CFG rewiring may add edges with + # propagate=False, leaving parent links stale. + preds: dict[int, set[int]] = {nid: set() for nid in known} + for node in nodes: + for child in node.children: + if child.id in known: + preds[child.id].add(node.id) + + while worklist: + node = worklist.popleft() + nid = node.id + + new_out: set[Var] = set() + for child in node.children: + if child.id in known: + new_out |= self.in_sets[child.id] + + new_in: set[Var] = (new_out - self.kill[nid]) | self.gen[nid] + + if new_out != self.out_sets[nid] or new_in != self.in_sets[nid]: + self.out_sets[nid] = new_out + self.in_sets[nid] = new_in + for pred_id in preds[nid]: + worklist.append(id_to_node[pred_id]) + + # ------------------------------------------------------------------ + # Result + # ------------------------------------------------------------------ + + def live_vars_by_node(self) -> dict[int, set[Var]]: + """Return the live-variable set at the *entry* of each node. + + Returns + ------- + dict[int, set[Var]] + Keys: CFG node ids whose in_set is non-empty. + Values: copy of the live-variable set at that node's entry. + """ + return {nid: set(vs) for nid, vs in self.in_sets.items() if vs} diff --git a/Project-02-03-04-05/cfa/reached_uses.py b/Project-02-03-04-05/cfa/reached_uses.py new file mode 100644 index 0000000..d4a3a58 --- /dev/null +++ b/Project-02-03-04-05/cfa/reached_uses.py @@ -0,0 +1,203 @@ +""" +reached_uses.py — Reached-Uses backward dataflow analysis for TRIPLA CFGs. + +Extends ``_BackwardAnalysisBase`` from live_variables.py, which provides the +shared function-scope resolution and uses/defs extraction machinery. The Live +Variables analysis (LiveVariablesAnalysis) in that module is the simpler +predecessor of this analysis (tip from the course notes: implement LV first, +then extend to RU). + +How ReachedUsesAnalysis extends LiveVariablesAnalysis +------------------------------------------------------ +Live Variables tracks *which* variables are live at each node (set[Var]). +Reached Uses additionally tracks *where* each variable is used by attaching +the use-node id to every fact, giving set[UseFact] = set[tuple[int, Var]]. + +The transfer function changes accordingly: + LV: IN(n) = (OUT(n) − KILL_LV(n)) ∪ GEN_LV(n) [sets of Var] + RU: IN(n) = (OUT(n) − KILL_RU(n)) ∪ GEN_RU(n) [sets of UseFact] + + GEN_LV(n) = uses(n) — set[Var] + GEN_RU(n) = { (n.id, var) | var ∈ uses(n) } — set[UseFact] + + KILL_LV(n) = defs(n) — set[Var] + KILL_RU(n) = { (uid, var) | var ∈ defs(n), — set[UseFact] + (uid, var) ∈ all_uses_by_var[var] } + + The set-difference in both cases removes exactly the facts for variables + that are defined at n — equivalent to the ⊖ operator from the lecture + slides (M ⊖ K = {(p,id) ∈ M | id ∉ K}). + +Type aliases +------------ + Var = tuple[str, str] # (scope, variable_name) + UseFact = tuple[int, Var] # (use_node_id, scoped_var) + +Analysis attributes (all populated after construction) +------------------------------------------------------ + uses dict[int, set[Var]] + defs dict[int, set[Var]] + gen dict[int, set[UseFact]] + kill dict[int, set[UseFact]] + in_sets dict[int, set[UseFact]] + out_sets dict[int, set[UseFact]] + all_uses_by_var dict[Var, set[UseFact]] + +Final result +------------ + reached_uses_by_node() → dict[int, list[int]] + Keys: defining-node ids + Values: sorted, deduplicated list of use-node ids reached by the def +""" +from __future__ import annotations + +from collections import deque +from typing import TYPE_CHECKING + +# Import the shared base class (and Var) from the Live Variables module. +from cfa.live_variables import _BackwardAnalysisBase, Var + +if TYPE_CHECKING: + from cfg.CFG import CFG + +# --------------------------------------------------------------------------- +# Public type aliases (re-exported so tests/reached_uses_stub.py can pick up +# ReachedUsesAnalysis without needing to know about live_variables.py) +# --------------------------------------------------------------------------- + +UseFact = tuple[int, Var] # (use_node_id, scoped_var) + + +# ============================================================================ +# Reached-Uses Analysis +# ============================================================================ + +class ReachedUsesAnalysis(_BackwardAnalysisBase): + """Backward dataflow analysis: Reached Uses. + + Inherits uses/defs extraction and function-scope resolution from + _BackwardAnalysisBase (live_variables.py). Extends it with use-fact + tracking: each fact carries the id of the node where the variable is used, + enabling def-use pairs to be recovered from the fixpoint solution. + + Transfer equations (backward): + OUT(n) = ∪ IN(s) for all successors s + IN(n) = GEN(n) ∪ (OUT(n) − KILL(n)) + + GEN(n) = { (n.id, var) | var ∈ uses(n) } + KILL(n) = { (uid, var) | var ∈ defs(n), + (uid, var) ∈ all_uses_by_var[var] } + """ + + def __init__(self, cfg: "CFG") -> None: + # Base populates: uses, defs, _func_scope, _func_parent, _func_params. + super().__init__(cfg) + + self.gen: dict[int, set[UseFact]] = {} + self.kill: dict[int, set[UseFact]] = {} + self.in_sets: dict[int, set[UseFact]] = {} + self.out_sets: dict[int, set[UseFact]] = {} + self.all_uses_by_var: dict[Var, set[UseFact]] = {} + + self._build_gen_kill() + self.solve() + + # ------------------------------------------------------------------ + # Step 1 — Build gen, kill, all_uses_by_var; initialise in/out + # ------------------------------------------------------------------ + + def _build_gen_kill(self) -> None: + """Compute gen and kill sets; populate all_uses_by_var.""" + # GEN[n] = { (n.id, var) | var ∈ uses[n] } + for node in self.cfg.nodes(): + nid = node.id + self.gen[nid] = {(nid, var) for var in self.uses[nid]} + self.in_sets[nid] = set() + self.out_sets[nid] = set() + + # all_uses_by_var: index all use-facts by their variable. + for nid, facts in self.gen.items(): + for (uid, var) in facts: + self.all_uses_by_var.setdefault(var, set()).add((uid, var)) + + # KILL[n] = all use-facts for variables defined at n. + for node in self.cfg.nodes(): + nid = node.id + kill_n: set[UseFact] = set() + for var in self.defs[nid]: + if var in self.all_uses_by_var: + kill_n |= self.all_uses_by_var[var] + self.kill[nid] = kill_n + + # ------------------------------------------------------------------ + # Step 2 — Backward worklist fixpoint + # ------------------------------------------------------------------ + + def solve(self) -> None: + """Backward worklist until fixpoint. + + Transfer: + OUT(n) = ∪ IN(s) for all successors s + IN(n) = GEN(n) ∪ (OUT(n) − KILL(n)) + + Only nodes reachable from cfg.START are processed (guard against + propagate=False parent references from CFG.__remove_and_rewire). + """ + nodes = list(self.cfg.nodes()) + known: set[int] = set(self.gen.keys()) # ids of cfg.nodes() + id_to_node = {n.id: n for n in nodes} + worklist: deque = deque(nodes) + + # Build predecessor relation from children edges. CFG rewiring may + # create edges with propagate=False, so node.parents can be stale. + preds: dict[int, set[int]] = {nid: set() for nid in known} + for node in nodes: + for child in node.children: + if child.id in known: + preds[child.id].add(node.id) + + while worklist: + node = worklist.popleft() + nid = node.id + + new_out: set[UseFact] = set() + for child in node.children: + if child.id in known: + new_out |= self.in_sets[child.id] + + new_in: set[UseFact] = self.gen[nid] | (new_out - self.kill[nid]) + + if new_out != self.out_sets[nid] or new_in != self.in_sets[nid]: + self.out_sets[nid] = new_out + self.in_sets[nid] = new_in + for pred_id in preds[nid]: + worklist.append(id_to_node[pred_id]) + + # ------------------------------------------------------------------ + # Public result + # ------------------------------------------------------------------ + + def reached_uses_by_node(self) -> dict[int, list[int]]: + """Return the final reached-uses result. + + For each defining node d: + result[d.id] = sorted list of use-node ids u such that + (u, var) ∈ OUT[d] for some var ∈ defs[d]. + + Semantics: the definition at d of variable var reaches the use at u + if there is a CFG path d → … → u along which var is not redefined. + + Only nodes with at least one definition appear as keys. + """ + result: dict[int, list[int]] = {} + for node in self.cfg.nodes(): + nid = node.id + defs_n = self.defs[nid] + if not defs_n: + continue + reached: set[int] = set() + for (uid, var) in self.out_sets[nid]: + if var in defs_n: + reached.add(uid) + result[nid] = sorted(reached) + return result diff --git a/Project-02-03-04-05/cfadots/dataflow_analysis.dot b/Project-02-03-04-05/cfadots/dataflow_analysis.dot new file mode 100644 index 0000000..c078ba3 --- /dev/null +++ b/Project-02-03-04-05/cfadots/dataflow_analysis.dot @@ -0,0 +1,183 @@ +digraph CFG { + // Analysis: Live Variables + Reached Uses + graph [splines=ortho, overlap=false, ranksep=0.7, nodesep=0.45]; + node [fontname="Helvetica"]; + n1 [label="START", shape=ellipse, style=filled, color=gray]; + n1 -> n3; + n3 [label="33", shape=box]; + n3 -> n4; + n4 [label="s = 33", shape=box]; + a4 [label="LivingVariables\nIn := []\nOut := [('', 's')]", shape=note, style="filled", fillcolor="#b2f7ec", color="#3fd1b9", fontcolor="#3fd1b9"]; + a4 -> n4 [style=dotted, arrowhead=none, color="#3fd1b9"]; + n4 -> n5; + n5 [label="0", shape=box]; + n5 -> n6; + n6 [label="i = 0", shape=box]; + a6 [label="LivingVariables\nIn := [('', 's')]\nOut := [('', 'i'), ('', 's')]", shape=note, style="filled", fillcolor="#c3b2f7", color="#633fd1", fontcolor="#633fd1"]; + a6 -> n6 [style=dotted, arrowhead=none, color="#633fd1"]; + n6 -> n7; + n7 [label="0", shape=box]; + n7 -> n8; + n8 [label="j = 0", shape=box]; + a8 [label="LivingVariables\nIn := [('', 'i'), ('', 's')]\nOut := [('', 'i'), ('', 'j'), ('', 's')]", shape=note, style="filled", fillcolor="#f7b2c9", color="#d13f70", fontcolor="#d13f70"]; + a8 -> n8 [style=dotted, arrowhead=none, color="#d13f70"]; + n8 -> n9; + n9 [label="i", shape=box]; + n9 -> n10; + n10 [label="j", shape=box]; + n10 -> n11; + n11 [label="i + j", shape=box]; + n11 -> n12; + n12 [label="t = (i + j)", shape=box]; + a12 [label="LivingVariables\nIn := [('', 'i'), ('', 'j'), ('', 's')]\nOut := [('', 'i'), ('', 'j'), ('', 's')]", shape=note, style="filled", fillcolor="#b2f7d5", color="#3fd188", fontcolor="#3fd188"]; + a12 -> n12 [style=dotted, arrowhead=none, color="#3fd188"]; + n12 -> n13; + n13 [label="6", shape=box]; + n13 -> n14; + n14 [label="f = 6", shape=box]; + a14 [label="LivingVariables\nIn := [('', 'i'), ('', 'j'), ('', 's')]\nOut := [('', 'f'), ('', 'i'), ('', 'j'), ('', 's')]", shape=note, style="filled", fillcolor="#b2b8f7", color="#3f4bd1", fontcolor="#3f4bd1"]; + a14 -> n14 [style=dotted, arrowhead=none, color="#3f4bd1"]; + n14 -> n16; + n16 [label="s", shape=box]; + n16 -> n17; + n17 [label="42", shape=box]; + n17 -> n18; + n18 [label="s < 42", shape=box]; + a18 [label="LivingVariables\nIn := [('', 'f'), ('', 'i'), ('', 'j'), ('', 's')]\nOut := [('', 'f'), ('', 'i'), ('', 'j'), ('', 's')]", shape=note, style="filled", fillcolor="#f7e6b2", color="#d1ac3f", fontcolor="#d1ac3f"]; + a18 -> n18 [style=dotted, arrowhead=none, color="#d1ac3f"]; + n18 -> n19; + n19 [label="", shape=diamond]; + n19 -> n2 [label="T"]; + n19 -> n21 [label="F"]; + n2 [label="END", shape=ellipse, style=filled, color=gray]; + n21 [label="s", shape=box]; + n21 -> n22; + n22 [label="1", shape=box]; + n22 -> n23; + n23 [label="s - 1", shape=box]; + n23 -> n24; + n24 [label="a = (s - 1)", shape=box]; + a24 [label="LivingVariables\nIn := [('', 'f'), ('', 'i'), ('', 'j'), ('', 's')]\nOut := [('', 'f'), ('', 'i'), ('', 'j'), ('', 's')]", shape=note, style="filled", fillcolor="#f7b2f7", color="#d13fd1", fontcolor="#d13fd1"]; + a24 -> n24 [style=dotted, arrowhead=none, color="#d13fd1"]; + n24 -> n25; + n25 [label="i", shape=box]; + n25 -> n26; + n26 [label="j", shape=box]; + n26 -> n27; + n27 [label="i + j", shape=box]; + n27 -> n28; + n28 [label="f", shape=box]; + n28 -> n29; + n29 [label="1", shape=box]; + n29 -> n30; + n30 [label="f + 1", shape=box]; + n30 -> n31; + n31 [label="(i + j) * (f + 1)", shape=box]; + n31 -> n32; + n32 [label="c = ((i + j) * (f + 1))", shape=box]; + a32 [label="LivingVariables\nIn := [('', 'f'), ('', 'i'), ('', 'j'), ('', 's')]\nOut := [('', 'c'), ('', 'f'), ('', 'j'), ('', 's')]", shape=note, style="filled", fillcolor="#e0b2f7", color="#a03fd1", fontcolor="#a03fd1"]; + a32 -> n32 [style=dotted, arrowhead=none, color="#a03fd1"]; + n32 -> n33; + n33 [label="c", shape=box]; + n33 -> n34; + n34 [label="0", shape=box]; + n34 -> n35; + n35 [label="c > 0", shape=box]; + a35 [label="LivingVariables\nIn := [('', 'c'), ('', 'f'), ('', 'j'), ('', 's')]\nOut := [('', 'c'), ('', 'f'), ('', 'j'), ('', 's')]", shape=note, style="filled", fillcolor="#b2c1f7", color="#3f5ed1", fontcolor="#3f5ed1"]; + a35 -> n35 [style=dotted, arrowhead=none, color="#3f5ed1"]; + n35 -> n36; + n36 [label="", shape=diamond]; + n36 -> n40 [label="T"]; + n36 -> n44 [label="F"]; + n40 [label="j", shape=box]; + n40 -> n41; + n41 [label="c", shape=box]; + n41 -> n42; + n42 [label="j / c", shape=box]; + n42 -> n43; + n43 [label="j = (j / c)", shape=box]; + a43 [label="LivingVariables\nIn := [('', 'c'), ('', 'f'), ('', 'j'), ('', 's')]\nOut := [('', 'f'), ('', 'j'), ('', 's')]", shape=note, style="filled", fillcolor="#b2d8f7", color="#3f8fd1", fontcolor="#3f8fd1"]; + a43 -> n43 [style=dotted, arrowhead=none, color="#3f8fd1"]; + n43 -> n48; + n48 [label="j", shape=box]; + n48 -> n49; + n49 [label="2", shape=box]; + n49 -> n50; + n50 [label="j * 2", shape=box]; + n50 -> n51; + n51 [label="f", shape=box]; + n51 -> n52; + n52 [label="(j * 2) / f", shape=box]; + n52 -> n53; + n53 [label="i = ((j * 2) / f)", shape=box]; + a53 [label="LivingVariables\nIn := [('', 'f'), ('', 'j'), ('', 's')]\nOut := [('', 'f'), ('', 'i'), ('', 'j'), ('', 's')]", shape=note, style="filled", fillcolor="#d7b2f7", color="#8d3fd1", fontcolor="#8d3fd1"]; + a53 -> n53 [style=dotted, arrowhead=none, color="#8d3fd1"]; + n53 -> n54; + n54 [label="s", shape=box]; + n54 -> n55; + n55 [label="1", shape=box]; + n55 -> n56; + n56 [label="s + 1", shape=box]; + n56 -> n57; + n57 [label="s = (s + 1)", shape=box]; + a57 [label="LivingVariables\nIn := [('', 'f'), ('', 'i'), ('', 'j'), ('', 's')]\nOut := [('', 'f'), ('', 'i'), ('', 'j'), ('', 's')]", shape=note, style="filled", fillcolor="#def7b2", color="#9bd13f", fontcolor="#9bd13f"]; + a57 -> n57 [style=dotted, arrowhead=none, color="#9bd13f"]; + n57 -> n16; + n44 [label="j", shape=box]; + n44 -> n45; + n45 [label="c", shape=box]; + n45 -> n46; + n46 [label="j * c", shape=box]; + n46 -> n47; + n47 [label="i = (j * c)", shape=box]; + a47 [label="LivingVariables\nIn := [('', 'c'), ('', 'f'), ('', 'j'), ('', 's')]\nOut := [('', 'f'), ('', 'j'), ('', 's')]", shape=note, style="filled", fillcolor="#f7c6b2", color="#d1693f", fontcolor="#d1693f"]; + a47 -> n47 [style=dotted, arrowhead=none, color="#d1693f"]; + n47 -> n48; + rh4 [shape=point, width=0.05, height=0.05, color="#3fd1b9", fillcolor="#3fd1b9", style=filled]; + { rank=same; n4; rh4; } + n4:se -> rh4 [color="#3fd1b9", style=dashed, penwidth=1.2, arrowhead=none, constraint=false, tailclip=true, headclip=true]; + rh4 -> n16:ne [color="#3fd1b9", fontcolor="#3fd1b9", fontsize=8, style=dashed, penwidth=1.0, arrowsize=0.6, constraint=false, tailclip=true, headclip=true]; + rh4 -> n21:se [color="#3fd1b9", fontcolor="#3fd1b9", fontsize=8, style=dashed, penwidth=1.0, arrowsize=0.6, constraint=false, tailclip=true, headclip=true]; + rh4 -> n54:ne [color="#3fd1b9", fontcolor="#3fd1b9", fontsize=8, style=dashed, penwidth=1.0, arrowsize=0.6, constraint=false, tailclip=true, headclip=true]; + rh6 [shape=point, width=0.05, height=0.05, color="#633fd1", fillcolor="#633fd1", style=filled]; + { rank=same; n6; rh6; } + n6:sw -> rh6 [color="#633fd1", style=dashed, penwidth=1.2, arrowhead=none, constraint=false, tailclip=true, headclip=true]; + rh6 -> n9:sw [color="#633fd1", fontcolor="#633fd1", fontsize=8, style=dashed, penwidth=1.0, arrowsize=0.6, constraint=false, tailclip=true, headclip=true]; + rh6 -> n25:sw [color="#633fd1", fontcolor="#633fd1", fontsize=8, style=dashed, penwidth=1.0, arrowsize=0.6, constraint=false, tailclip=true, headclip=true]; + rh8 [shape=point, width=0.05, height=0.05, color="#d13f70", fillcolor="#d13f70", style=filled]; + { rank=same; n8; rh8; } + n8:se -> rh8 [color="#d13f70", style=dashed, penwidth=1.2, arrowhead=none, constraint=false, tailclip=true, headclip=true]; + rh8 -> n10:ne [color="#d13f70", fontcolor="#d13f70", fontsize=8, style=dashed, penwidth=1.0, arrowsize=0.6, constraint=false, tailclip=true, headclip=true]; + rh8 -> n26:ne [color="#d13f70", fontcolor="#d13f70", fontsize=8, style=dashed, penwidth=1.0, arrowsize=0.6, constraint=false, tailclip=true, headclip=true]; + rh8 -> n40:ne [color="#d13f70", fontcolor="#d13f70", fontsize=8, style=dashed, penwidth=1.0, arrowsize=0.6, constraint=false, tailclip=true, headclip=true]; + rh8 -> n44:ne [color="#d13f70", fontcolor="#d13f70", fontsize=8, style=dashed, penwidth=1.0, arrowsize=0.6, constraint=false, tailclip=true, headclip=true]; + rh8 -> n48:ne [color="#d13f70", fontcolor="#d13f70", fontsize=8, style=dashed, penwidth=1.0, arrowsize=0.6, constraint=false, tailclip=true, headclip=true]; + rh14 [shape=point, width=0.05, height=0.05, color="#3f4bd1", fillcolor="#3f4bd1", style=filled]; + { rank=same; n14; rh14; } + n14:se -> rh14 [color="#3f4bd1", style=dashed, penwidth=1.2, arrowhead=none, constraint=false, tailclip=true, headclip=true]; + rh14 -> n28:ne [color="#3f4bd1", fontcolor="#3f4bd1", fontsize=8, style=dashed, penwidth=1.0, arrowsize=0.6, constraint=false, tailclip=true, headclip=true]; + rh14 -> n51:se [color="#3f4bd1", fontcolor="#3f4bd1", fontsize=8, style=dashed, penwidth=1.0, arrowsize=0.6, constraint=false, tailclip=true, headclip=true]; + rh32 [shape=point, width=0.05, height=0.05, color="#a03fd1", fillcolor="#a03fd1", style=filled]; + { rank=same; n32; rh32; } + n32:se -> rh32 [color="#a03fd1", style=dashed, penwidth=1.2, arrowhead=none, constraint=false, tailclip=true, headclip=true]; + rh32 -> n33:se [color="#a03fd1", fontcolor="#a03fd1", fontsize=8, style=dashed, penwidth=1.0, arrowsize=0.6, constraint=false, tailclip=true, headclip=true]; + rh32 -> n41:se [color="#a03fd1", fontcolor="#a03fd1", fontsize=8, style=dashed, penwidth=1.0, arrowsize=0.6, constraint=false, tailclip=true, headclip=true]; + rh32 -> n45:se [color="#a03fd1", fontcolor="#a03fd1", fontsize=8, style=dashed, penwidth=1.0, arrowsize=0.6, constraint=false, tailclip=true, headclip=true]; + rh43 [shape=point, width=0.05, height=0.05, color="#3f8fd1", fillcolor="#3f8fd1", style=filled]; + { rank=same; n43; rh43; } + n43:sw -> rh43 [color="#3f8fd1", style=dashed, penwidth=1.2, arrowhead=none, constraint=false, tailclip=true, headclip=true]; + rh43 -> n26:nw [color="#3f8fd1", fontcolor="#3f8fd1", fontsize=8, style=dashed, penwidth=1.0, arrowsize=0.6, constraint=false, tailclip=true, headclip=true]; + rh43 -> n40:nw [color="#3f8fd1", fontcolor="#3f8fd1", fontsize=8, style=dashed, penwidth=1.0, arrowsize=0.6, constraint=false, tailclip=true, headclip=true]; + rh43 -> n44:nw [color="#3f8fd1", fontcolor="#3f8fd1", fontsize=8, style=dashed, penwidth=1.0, arrowsize=0.6, constraint=false, tailclip=true, headclip=true]; + rh43 -> n48:nw [color="#3f8fd1", fontcolor="#3f8fd1", fontsize=8, style=dashed, penwidth=1.0, arrowsize=0.6, constraint=false, tailclip=true, headclip=true]; + rh53 [shape=point, width=0.05, height=0.05, color="#8d3fd1", fillcolor="#8d3fd1", style=filled]; + { rank=same; n53; rh53; } + n53:sw -> rh53 [color="#8d3fd1", style=dashed, penwidth=1.2, arrowhead=none, constraint=false, tailclip=true, headclip=true]; + rh53 -> n25:sw [color="#8d3fd1", fontcolor="#8d3fd1", fontsize=8, style=dashed, penwidth=1.0, arrowsize=0.6, constraint=false, tailclip=true, headclip=true]; + rh57 [shape=point, width=0.05, height=0.05, color="#9bd13f", fillcolor="#9bd13f", style=filled]; + { rank=same; n57; rh57; } + n57:se -> rh57 [color="#9bd13f", style=dashed, penwidth=1.2, arrowhead=none, constraint=false, tailclip=true, headclip=true]; + rh57 -> n16:ne [color="#9bd13f", fontcolor="#9bd13f", fontsize=8, style=dashed, penwidth=1.0, arrowsize=0.6, constraint=false, tailclip=true, headclip=true]; + rh57 -> n21:se [color="#9bd13f", fontcolor="#9bd13f", fontsize=8, style=dashed, penwidth=1.0, arrowsize=0.6, constraint=false, tailclip=true, headclip=true]; + rh57 -> n54:ne [color="#9bd13f", fontcolor="#9bd13f", fontsize=8, style=dashed, penwidth=1.0, arrowsize=0.6, constraint=false, tailclip=true, headclip=true]; +} \ No newline at end of file diff --git a/Project-02-03-04-05/cfg_build.py b/Project-02-03-04-05/cfg_build.py index cc36206..425cdb8 100644 --- a/Project-02-03-04-05/cfg_build.py +++ b/Project-02-03-04-05/cfg_build.py @@ -148,11 +148,15 @@ class IF(compiler.IF): class WHILE(compiler.WHILE): def cfa(self, pred, end = None): + # Dedicated loop-head so every iteration re-evaluates the full condition. + cond_entry = CFG_Node() + pred.add_child(cond_entry) + if hasattr(self.cond, 'arg1') and hasattr(self.cond, 'arg2'): # This is a comparison operation (e.g., a > b) # Create the condition evaluation nodes - left_node = self.cond.arg1.cfa(pred) + left_node = self.cond.arg1.cfa(cond_entry) right_node = self.cond.arg2.cfa(left_node) # Create the comparison node and attach @@ -161,7 +165,7 @@ class WHILE(compiler.WHILE): right_node.add_child(comp_node) else: # This is a simple condition (e.g., constant true/false) - cond_node = self.cond.cfa(pred) + cond_node = self.cond.cfa(cond_entry) comp_node = cond_node # Attach junction node @@ -175,11 +179,8 @@ class WHILE(compiler.WHILE): # The body should connect back to the start of condition evaluation body_end = self.body.cfa(body_entry) if body_end is not None: - # Connect the body end back to the condition evaluation - if hasattr(self.cond, 'arg1') and hasattr(self.cond, 'arg2'): - body_end.add_child(left_node) - else: - body_end.add_child(pred) + # Connect loop body back to full condition evaluation. + body_end.add_child(cond_entry) # Attach joining node after = CFG_Node() diff --git a/Project-02-03-04-05/cfgdots/smoke_test_simple_dfa_reached_uses.dot b/Project-02-03-04-05/cfgdots/smoke_test_simple_dfa_reached_uses.dot new file mode 100644 index 0000000..51d27e2 --- /dev/null +++ b/Project-02-03-04-05/cfgdots/smoke_test_simple_dfa_reached_uses.dot @@ -0,0 +1,24 @@ +digraph CFG { + // Analysis: Reached Uses + node [fontname="Helvetica"]; + n1 [label="START", shape=ellipse, style=filled, color=gray]; + n1 -> n9; + n9 [label="2", shape=box]; + n9 -> n10; + n10 [label="CALL g", shape=box, style=filled, color=orange]; + n10 -> n3; + n10 -> n12; + n3 [label="START g(x, y)\nRU: [8]", shape=ellipse, style=filled, color=green]; + n3 -> n6; + n6 [label="3", shape=box]; + n6 -> n7; + n7 [label="y = 3\nRU: []", shape=box]; + n7 -> n8; + n8 [label="x", shape=box]; + n8 -> n4; + n4 [label="END g(x, y)", shape=ellipse, style=filled, color=green]; + n4 -> n12; + n12 [label="RET g", shape=box, style=filled, color=orange]; + n12 -> n2; + n2 [label="END", shape=ellipse, style=filled, color=gray]; +} \ No newline at end of file diff --git a/Project-02-03-04-05/main.py b/Project-02-03-04-05/main.py index 77de897..3655dbc 100644 --- a/Project-02-03-04-05/main.py +++ b/Project-02-03-04-05/main.py @@ -7,9 +7,11 @@ import matplotlib.image as mpimg import matplotlib.pyplot as plt from graphviz import Source +import cfg_build import lib.console as cnsl import syntax import triplayacc as yacc +from cfa.analysis_dot import analysis_to_dot, run_all_analyses from cfg.CFG import CFG from vistram.tram import * from vistram.vistram import MachineUI @@ -63,13 +65,54 @@ def pretty_print(node, indent=0): 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.""" + lv = analyses["lv"] + ru = analyses["ru"] + _ = ru + node_by_id = {n.id: n for n in cfg.nodes()} + + def node_text(nid: int) -> str: + node = node_by_id.get(nid) + if node is None: + return "" + lbl = node.dot_label() + return str(lbl) if lbl is not None else "" + + print("\nLive Variables Report") + print("---------------------") + node_ids = sorted(set(lv.in_sets.keys()) | set(lv.out_sets.keys())) + for nid in node_ids: + in_set = sorted(lv.in_sets.get(nid, set())) + out_set = sorted(lv.out_sets.get(nid, set())) + if not in_set and not out_set: + continue + print(f"n{nid} [{node_text(nid)}]: In={in_set} Out={out_set}") + + print("\nReached Uses Report") + print("-------------------") + has_ru = False + for def_id in sorted(ru_edges): + uses = sorted(set(ru_edges[def_id])) + if not uses: + continue + has_ru = True + use_desc = [f"n{uid} [{node_text(uid)}]" for uid in uses] + print(f"n{def_id} [{node_text(def_id)}] -> {use_desc}") + if not has_ru: + print("(no reached uses)") + + if __name__ == "__main__": print("\nTRIPLA Parser and TRIPLA to TRAM Compiler") while True: - mode = cnsl.prompt_choice("\nSelect action:", ["Parse .tripla", "Compile .tripla", "CFG for .tripla", "Exit"]) + mode = cnsl.prompt_choice( + "\nSelect action:", + ["Parse .tripla", "Compile .tripla", "CFG for .tripla", "Analyze .tripla", "Exit"], + ) - if mode == 3: + if mode == 4: print("\nBye Bye.") break @@ -157,6 +200,7 @@ if __name__ == "__main__": filename = default out_path = Path(__file__).parent / 'cfgdots' / filename + out_path.parent.mkdir(exist_ok=True) with open(out_path, "w") as f: f.write(dot_str) @@ -166,6 +210,46 @@ if __name__ == "__main__": render_diagram(dot_str) print("Rendered CFG diagram.") + elif mode == 3: + # Reset 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" + print(f"\nRunning {analysis_name} …") + try: + _analyses, annotations, ru_edges = run_all_analyses(cfg) + 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) + + if cnsl.prompt_confirmation("\nExport annotated CFG as .dot file?"): + default = f"{path.stem}_analysis.dot" + filename = input(f"Filename [{default}]: ").strip() + if not filename: + filename = default + + out_dir = Path(__file__).parent / "cfadots" + out_dir.mkdir(exist_ok=True) + out_path = out_dir / filename + try: + with open(out_path, "w") as f: + f.write(dot_str) + print(f"Saved annotated DOT file as: {out_path}") + except Exception as exc: + print(f"Could not save DOT file: {exc}") + + if cnsl.prompt_confirmation("Display annotated CFG diagram?"): + render_diagram(dot_str) + print("Rendered annotated CFG diagram.") diff --git a/Project-02-03-04-05/triplaprograms/dataflow.tripla b/Project-02-03-04-05/triplaprograms/dataflow.tripla new file mode 100644 index 0000000..82cdf2d --- /dev/null +++ b/Project-02-03-04-05/triplaprograms/dataflow.tripla @@ -0,0 +1,12 @@ +s = 33; +i = 0; +j = 0; +t = i + j; +f = 6; +while (s < 42) do { + a = s - 1; + c = (i + j) * (f + 1); + if (c > 0) then j = j / c else i = j * c; + i = (j * 2) / f; + s = s + 1 +} diff --git a/Project-02-03-04-05/triplaprograms/reached.tripla b/Project-02-03-04-05/triplaprograms/reached.tripla new file mode 100644 index 0000000..643294d --- /dev/null +++ b/Project-02-03-04-05/triplaprograms/reached.tripla @@ -0,0 +1,9 @@ +let f(x,y,z) { + y = 2; + z = 3; + let g(x) { + x = 7; + if (y > 0) then g(y) else x = 8; + x + } in g(x) + x +} in f(2,3)