From a0232289e818830fcac9da868f63190f4d71f5ca Mon Sep 17 00:00:00 2001
From: Mark Koch <mark.koch@quantinuum.com>
Date: Fri, 26 Jul 2024 10:02:44 +0100
Subject: [PATCH] feat: Add very basic debug info

---
 guppylang/compiler/func_compiler.py |  2 +-
 guppylang/definition/custom.py      |  7 +++--
 guppylang/definition/function.py    |  2 +-
 guppylang/hugr_builder/hugr.py      | 44 ++++++++++++++++++++++++-----
 guppylang/module.py                 |  8 ++++--
 5 files changed, 50 insertions(+), 13 deletions(-)

diff --git a/guppylang/compiler/func_compiler.py b/guppylang/compiler/func_compiler.py
index 30cb66ba..c07dac29 100644
--- a/guppylang/compiler/func_compiler.py
+++ b/guppylang/compiler/func_compiler.py
@@ -47,7 +47,7 @@ def compile_local_func_def(
         [v.name for v, _ in captured] + list(func.ty.input_names),
     )
 
-    def_node = graph.add_def(closure_ty, dfg.node, func.name)
+    def_node = graph.add_def(closure_ty, dfg.node, func.name, func)
     def_input, input_ports = graph.add_input_with_ports(
         list(closure_ty.inputs), def_node
     )
diff --git a/guppylang/definition/custom.py b/guppylang/definition/custom.py
index 83a452d6..acd08685 100644
--- a/guppylang/definition/custom.py
+++ b/guppylang/definition/custom.py
@@ -157,7 +157,7 @@ def load_with_args(
         # we explicitly monomorphise here and invoke the call compiler with the
         # inferred type args.
         fun_ty = self.ty.instantiate(type_args)
-        def_node = graph.add_def(fun_ty, dfg.node, self.name)
+        def_node = graph.add_def(fun_ty, dfg.node, self.name, self.defined_at)
         with graph.parent(def_node):
             _, inp_ports = graph.add_input_with_ports(list(fun_ty.inputs))
             returns = self.compile_call(
@@ -279,7 +279,10 @@ def __init__(self, op: ops.OpType) -> None:
 
     def compile(self, args: list[OutPortV]) -> list[OutPortV]:
         node = self.graph.add_node(
-            self.op.model_copy(deep=True), inputs=args, parent=self.dfg.node
+            self.op.model_copy(deep=True),
+            inputs=args,
+            parent=self.dfg.node,
+            debug=self.node,
         )
         return_ty = get_type(self.node)
         return [node.add_out_port(ty) for ty in type_to_row(return_ty)]
diff --git a/guppylang/definition/function.py b/guppylang/definition/function.py
index 3c3c4b6d..f0b91e2a 100644
--- a/guppylang/definition/function.py
+++ b/guppylang/definition/function.py
@@ -112,7 +112,7 @@ def compile_outer(self, graph: Hugr, parent: Node) -> "CompiledFunctionDef":
         access to the other compiled functions yet. The body is compiled later in
         `CompiledFunctionDef.compile_inner()`.
         """
-        def_node = graph.add_def(self.ty, parent, self.name)
+        def_node = graph.add_def(self.ty, parent, self.name, self.defined_at)
         return CompiledFunctionDef(
             self.id,
             self.name,
diff --git a/guppylang/hugr_builder/hugr.py b/guppylang/hugr_builder/hugr.py
index 3803471c..f164c18f 100644
--- a/guppylang/hugr_builder/hugr.py
+++ b/guppylang/hugr_builder/hugr.py
@@ -10,6 +10,7 @@
 from hugr.serialization import serial_hugr as raw
 from hugr.serialization.ops import OpType
 
+from guppylang.ast_util import AstNode, get_file, get_line_offset, line_col
 from guppylang.tys.subst import Inst
 from guppylang.tys.ty import (
     FunctionType,
@@ -298,15 +299,18 @@ class Hugr:
     _children: dict[NodeIdx, list[Node]]
     _default_parent: Node | None
 
-    def __init__(self, name: str | None = None) -> None:
+    def __init__(self, name: str | None = None, file: str | None = None) -> None:
         """Creates a new Hugr."""
         self.name = name or "Unnamed"
         self._default_parent = None
         self._graph = nx.MultiDiGraph()
         self._children = {-1: []}
+        meta_data: dict[str, Any] = {"name": name}
+        if file is not None:
+            meta_data["di"] = {"file": file}
         self.root = self.add_node(
             op=ops.OpType(ops.Module(parent=UNDEFINED)),
-            meta_data={"name": name},
+            meta_data=meta_data,
             parent=None,
         )
 
@@ -334,6 +338,7 @@ def add_node(
         output_types: TypeList | None = None,
         parent: Node | None = None,
         inputs: list[OutPortV] | None = None,
+        debug: AstNode | None = None,
         meta_data: dict[str, Any] | None = None,
     ) -> VNode:
         """Helper method to add a generic value node to the graph."""
@@ -346,7 +351,7 @@ def add_node(
             parent=parent,
             in_port_types=[p.ty for p in inputs] if inputs is not None else input_types,
             out_port_types=output_types,
-            meta_data=meta_data or {},
+            meta_data=(meta_data or {}) | debug_metadata(debug),
         )
         self._insert_node(node, inputs)
         return node
@@ -358,6 +363,7 @@ def _add_dfg_node(
         output_types: TypeList | None = None,
         parent: Node | None = None,
         inputs: list[OutPortV] | None = None,
+        debug: AstNode | None = None,
         meta_data: dict[str, Any] | None = None,
     ) -> DFContainingVNode:
         """Helper method to add a generic dataflow containing value node to the
@@ -371,7 +377,7 @@ def _add_dfg_node(
             parent=parent,
             in_port_types=[p.ty for p in inputs] if inputs is not None else input_types,
             out_port_types=output_types,
-            meta_data=meta_data or {},
+            meta_data=(meta_data or {}) | debug_metadata(debug),
         )
         self._insert_node(node, inputs)
         return node
@@ -624,11 +630,15 @@ def add_partial(
         )
 
     def add_def(
-        self, fun_ty: FunctionType, parent: Node | None, name: str
+        self,
+        fun_ty: FunctionType,
+        parent: Node | None,
+        name: str,
+        debug: AstNode | None = None,
     ) -> DFContainingVNode:
         """Adds a `FucnDefn` node to the graph."""
         op = ops.FuncDefn(name=name, signature=fun_ty.to_hugr_poly(), parent=UNDEFINED)
-        return self._add_dfg_node(ops.OpType(op), [], [fun_ty], parent, None)
+        return self._add_dfg_node(ops.OpType(op), [], [fun_ty], parent, None, debug)
 
     def add_declare(self, fun_ty: FunctionType, parent: Node, name: str) -> VNode:
         """Adds a `FuncDecl` node to the graph."""
@@ -837,6 +847,9 @@ def to_raw(self) -> raw.SerialHugr:
         nodes: list[ops.OpType] = [
             ops.OpType(ops.Module(parent=UNDEFINED))
         ] * self._graph.number_of_nodes()
+        metadata: list[dict[str, Any]] = [
+            {} for _ in range(self._graph.number_of_nodes())
+        ]
         for n in self.nodes():
             idx = raw_index[n.idx]
             # Nodes without parent have themselves as parent in the serialised format
@@ -844,6 +857,7 @@ def to_raw(self) -> raw.SerialHugr:
             n.update_op()
             n.op.root.parent = raw_index[parent.idx]
             nodes[idx] = n.op
+            metadata[idx] = n.meta_data
 
         edges: list[raw.Edge] = []
         for src, tgt in self.edges():
@@ -857,8 +871,24 @@ def to_raw(self) -> raw.SerialHugr:
         for src, tgt in self.order_edges():
             edges.append(((raw_index[src.idx], None), (raw_index[tgt.idx], None)))
 
-        return raw.SerialHugr(nodes=nodes, edges=edges)
+        return raw.SerialHugr(nodes=nodes, edges=edges, metadata=metadata)
 
     def serialize(self) -> str:
         """Serialize this Hugr in JSON format."""
         return self.to_raw().to_json()
+
+
+def debug_metadata(debug: AstNode | None) -> dict[str, Any]:
+    if debug is not None:
+        file = get_file(debug)
+        line, col = line_col(debug)
+        line_offset = get_line_offset(debug)
+        if file is not None and line_offset is not None:
+            return {
+                "di": {
+                    "file": get_file(debug),
+                    "line": line + line_offset - 1,
+                    "col": col,
+                }
+            }
+    return {}
diff --git a/guppylang/module.py b/guppylang/module.py
index 28f16cd0..ce9169dc 100644
--- a/guppylang/module.py
+++ b/guppylang/module.py
@@ -33,6 +33,7 @@ class GuppyModule:
     """A Guppy module that may contain function and type definitions."""
 
     name: str
+    file: str | None
 
     # Whether the module has already been compiled
     _compiled: bool
@@ -59,8 +60,11 @@ class GuppyModule:
     # `_register_buffered_instance_funcs` is called. This way, we can associate
     _instance_func_buffer: dict[str, RawDef] | None
 
-    def __init__(self, name: str, import_builtins: bool = True):
+    def __init__(
+        self, name: str, file: str | None = None, import_builtins: bool = True
+    ):
         self.name = name
+        self.file = file
         self._globals = Globals({}, {}, {}, {})
         self._compiled_globals = {}
         self._imported_globals = Globals.default()
@@ -197,7 +201,7 @@ def compile(self) -> Hugr:
         self._globals = self._globals.update_defs(other_defs)
 
         # Prepare Hugr for this module
-        graph = Hugr(self.name)
+        graph = Hugr(self.name, self.file)
         module_node = graph.set_root_name(self.name)
 
         # Compile definitions to Hugr