253 lines
8.4 KiB
Python
253 lines
8.4 KiB
Python
import os
|
|
import tkinter as tk
|
|
from pathlib import Path
|
|
|
|
import matplotlib
|
|
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.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
|
|
|
|
matplotlib.use("TkAgg")
|
|
|
|
# Assembles the AST into TRAM code
|
|
def assemble(ast):
|
|
code = ast.code({}, 0)
|
|
return code + [halt()]
|
|
|
|
def make_cfg(ast):
|
|
return CFG(ast)
|
|
|
|
# Renders a diagram of the AST
|
|
def render_diagram(dot_string: str):
|
|
# Set DPI for PNG
|
|
os.environ["GV_FILE_DPI"] = "300"
|
|
|
|
src = Source(dot_string, format="png", engine="dot")
|
|
png_path = src.render(cleanup=True)
|
|
|
|
img = mpimg.imread(png_path)
|
|
|
|
fig = plt.figure(figsize=(12, 12))
|
|
fig.canvas.manager.window.wm_title("TRIPLA AST Viewer")
|
|
|
|
plt.imshow(img)
|
|
plt.axis("off")
|
|
plt.show()
|
|
|
|
# Pretty prints the AST
|
|
def pretty_print(node, indent=0):
|
|
prefix = " " * indent
|
|
print(f"{prefix}{type(node).__name__}:")
|
|
|
|
for key, value in node.__dict__.items():
|
|
if isinstance(value, syntax.EXPRESSION):
|
|
pretty_print(value, indent + 4)
|
|
|
|
elif isinstance(value, list):
|
|
print(f"{prefix} {key}: [")
|
|
for element in value:
|
|
if isinstance(element, syntax.EXPRESSION):
|
|
pretty_print(element, indent + 4)
|
|
else:
|
|
print(" " * (indent + 4) + str(element))
|
|
print(f"{prefix} ]")
|
|
|
|
else:
|
|
print(f"{prefix} {key}: {value}")
|
|
|
|
# Print compact Live Variables and Reached Uses reports to console.
|
|
def print_analysis_reports(cfg, analyses: dict):
|
|
lv = analyses["lv"]
|
|
ru = analyses["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:
|
|
node = node_by_id.get(nid)
|
|
if node is None:
|
|
return "<unknown>"
|
|
lbl = node.dot_label()
|
|
return str(lbl) if lbl is not None else "<no-label>"
|
|
|
|
print("\nLive Variables Report")
|
|
print("---------------------")
|
|
node_ids = sorted(set(lv.incoming.keys()) | set(lv.outgoing.keys()))
|
|
for nid in node_ids:
|
|
in_set = sorted(lv.incoming.get(nid, set()))
|
|
out_set = sorted(lv.outgoing.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", "Analyze .tripla", "Exit"],
|
|
)
|
|
|
|
if mode == 4:
|
|
print("\nBye Bye.")
|
|
break
|
|
|
|
base = Path(__file__).parent / "triplaprograms"
|
|
programs = sorted([f for f in base.glob("*.tripla")])
|
|
if not programs:
|
|
print("\nNo .tripla files found.")
|
|
continue
|
|
|
|
idx = cnsl.prompt_choice("\nSelect TRIPLA program:", [p.name for p in programs])
|
|
path = programs[idx]
|
|
|
|
source = path.read_text()
|
|
ast = yacc.parser.parse(source)
|
|
|
|
if mode == 0:
|
|
# Pretty print
|
|
if cnsl.prompt_confirmation("\nPretty-print AST?"):
|
|
print("")
|
|
pretty_print(ast)
|
|
|
|
# Export DOT
|
|
dot_str = ast.to_dot()
|
|
if cnsl.prompt_confirmation("Export AST as .dot file?"):
|
|
default = f"{path.stem}.dot"
|
|
filename = input(f"Filename [{default}]: ").strip()
|
|
if not filename:
|
|
filename = default
|
|
try:
|
|
base_dir = Path(__file__).parent
|
|
out_path = base_dir / filename
|
|
with open(out_path, "w") as f:
|
|
f.write(dot_str)
|
|
print(f"Saved DOT file as: {out_path}")
|
|
except Exception as e:
|
|
print(f"Could not save DOT file: {e}")
|
|
|
|
# Display AST diagram
|
|
if cnsl.prompt_confirmation("Display AST diagram?"):
|
|
render_diagram(dot_str)
|
|
print("Rendered AST diagram.")
|
|
|
|
elif mode == 1:
|
|
tram_code = assemble(ast)
|
|
|
|
# Print TRAM code
|
|
if cnsl.prompt_confirmation("\nPrint TRAM code to console?"):
|
|
print("\nGenerated TRAM code:\n")
|
|
for instr in tram_code:
|
|
print(instr.toString())
|
|
|
|
# Save TRAM code
|
|
if cnsl.prompt_confirmation("Save TRAM code as .tram file?"):
|
|
base_dir = Path(__file__).parent / "tramcodes"
|
|
base_dir.mkdir(exist_ok=True)
|
|
|
|
default = f"{path.stem}.tram"
|
|
filename = input(f"Filename [{default}]: ").strip()
|
|
if not filename:
|
|
filename = default
|
|
|
|
out_path = base_dir / filename
|
|
with open(out_path, "w") as f:
|
|
for instr in tram_code:
|
|
f.write(instr.toString() + "\n")
|
|
|
|
print(f"Saved TRAM code to: {out_path}")
|
|
|
|
# Display TRAM code in Visual TRAM UI
|
|
if cnsl.prompt_confirmation("Display TRAM code in Visual TRAM UI?"):
|
|
root = tk.Tk()
|
|
ui = MachineUI(root)
|
|
ui.machine.initial_program = tram_code
|
|
ui.machine.reset()
|
|
root.mainloop()
|
|
|
|
elif mode == 2:
|
|
cfg = make_cfg(ast)
|
|
|
|
dot_str = cfg.to_dot()
|
|
if cnsl.prompt_confirmation("\nExport CFG as .dot file?"):
|
|
default = f"{path.stem}_cfg.dot"
|
|
filename = input(f"Filename [{default}]: ").strip()
|
|
if not filename:
|
|
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)
|
|
|
|
print(f"Saved CFG DOT file as: {out_path}")
|
|
|
|
if cnsl.prompt_confirmation("Display CFG diagram?"):
|
|
render_diagram(dot_str)
|
|
print("Rendered CFG diagram.")
|
|
|
|
elif mode == 3:
|
|
# 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 analyses"
|
|
print(f"\nRunning {analysis_name} …")
|
|
try:
|
|
lv = LiveVariables(cfg)
|
|
ru = ReachedUses(cfg)
|
|
analyses = {"lv": lv, "ru": ru}
|
|
except Exception as exc:
|
|
print(f"Analysis failed: {exc}")
|
|
continue
|
|
|
|
if cnsl.prompt_confirmation("\nPrint analysis reports to console?"):
|
|
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"
|
|
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.")
|