finish refactoring task 1 and 2

This commit is contained in:
Jan-Niclas Loosen
2026-03-08 17:21:09 +01:00
parent 605eaf3278
commit 438447e6de
4 changed files with 65 additions and 79 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 374 KiB

After

Width:  |  Height:  |  Size: 356 KiB

View File

@@ -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

View File

@@ -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]))

View File

@@ -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"