Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
057507d
Add draft metadata specification
tatiana-s Feb 23, 2026
a140a66
Merge remote-tracking branch 'origin/main' into ts/debug-info
tatiana-s Feb 24, 2026
a1a5145
Add module info
tatiana-s Feb 24, 2026
83bd3d0
Refactor existing metadata code
tatiana-s Mar 2, 2026
ffa288d
Attach subprogram info
tatiana-s Mar 3, 2026
791885d
Add metadata to decls
tatiana-s Mar 4, 2026
38f6f05
Merge remote-tracking branch 'origin/main' into ts/debug-info
tatiana-s Mar 4, 2026
dd9b68d
Add location info to calls
tatiana-s Mar 4, 2026
ffbcf54
Add proper tests + fix line_no bug
tatiana-s Mar 4, 2026
f11d7e1
Merge remote-tracking branch 'origin/main' into ts/debug-info
tatiana-s Mar 4, 2026
79202f3
Add location info in custom func calls
tatiana-s Mar 4, 2026
29c69a7
Attach location data to (most) extension ops
tatiana-s Mar 5, 2026
d088630
Add global flag for making debug metadata optional
tatiana-s Mar 5, 2026
e034a9b
Extend tests + various special case fixes
tatiana-s Mar 9, 2026
c2ba07a
Fix typing
tatiana-s Mar 9, 2026
ee6eea6
Formatting
tatiana-s Mar 9, 2026
04acb65
Merge remote-tracking branch 'origin/main' into ts/debug-info
tatiana-s Mar 9, 2026
ab80d96
Fix comment
tatiana-s Mar 9, 2026
ea73ac4
Try fixing imports
tatiana-s Mar 9, 2026
8cb8c58
One more import error fix
tatiana-s Mar 9, 2026
9afae78
Merge remote-tracking branch 'origin/main' into ts/debug-info
tatiana-s Mar 20, 2026
c772396
Pin to hugr branch with moved specification
tatiana-s Mar 20, 2026
ae9e9bf
Merge remote-tracking branch 'origin/main' into ts/debug-info
tatiana-s Mar 24, 2026
79d3e1f
Update to newer hugr branch pin and fix imports
tatiana-s Mar 24, 2026
5f01d37
Minor fixes based on comments
tatiana-s Mar 24, 2026
6a4168f
Refactor `add_op` mechanism
tatiana-s Mar 25, 2026
d811114
Fix unwrap issue
tatiana-s Mar 26, 2026
d3e6cf7
Merge remote-tracking branch 'origin/main' into ts/debug-info
tatiana-s Mar 26, 2026
a96a5a5
Reduce raw_builder use
tatiana-s Mar 26, 2026
7f070c1
Fix tests and some clean up
tatiana-s Mar 26, 2026
0755cd7
More clean up
tatiana-s Mar 26, 2026
29e022e
Fixes
tatiana-s Mar 30, 2026
20d790a
Remove `add_op_to` and other small fixes
tatiana-s Mar 30, 2026
064b670
Merge remote-tracking branch 'origin/main' into ts/debug-info
tatiana-s Mar 30, 2026
8e1aab7
Merge remote-tracking branch 'origin/main' into ts/debug-info
tatiana-s Apr 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,13 @@ def compile_bb(
# Convert the bool predicate into a sum for branching.
pred_ty = builder.hugr.port_type(branch_port.out_port())
assert pred_ty == OpaqueBool
branch_port = dfg.builder.add_op(read_bool(), branch_port)
branch_port = dfg.builder.add_op(read_bool(), branch_port, set_debug_info=False)
branch_port = cast("Wire", branch_port)
else:
# Even if we don't branch, we still have to add a `Sum(())` predicates
branch_port = dfg.builder.add_op(ops.Tag(0, ht.UnitSum(1)))
branch_port = dfg.builder.add_op(
ops.Tag(0, ht.UnitSum(1)), set_debug_info=False
)

# Finally, we have to add the block output.
outputs: Sequence[Place]
Expand Down
124 changes: 117 additions & 7 deletions guppylang-internals/src/guppylang_internals/compiler/core.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import itertools
from abc import ABC
from collections.abc import Iterator
from collections.abc import Iterator, Sequence
from contextlib import contextmanager
from dataclasses import dataclass, field
from typing import Any, cast
from typing import Any, Generic, cast

import tket_exts
from hugr import Hugr, Node, Wire, ops
from hugr import Hugr, Node, Wire, ops, val
from hugr import tys as ht
from hugr.build import Conditional, TailLoop
from hugr.build import function as hf
from hugr.build.dfg import DP, DefinitionBuilder, DfBase
from hugr.hugr.base import OpVarCov
from hugr.hugr.node_port import ToNode
from hugr.metadata import NodeMetadata
from hugr.metadata import HugrDebugInfo, NodeMetadata
from hugr.std import PRELUDE
from hugr.std.collections.array import EXTENSION as ARRAY_EXTENSION
from hugr.std.collections.borrow_array import EXTENSION as BORROW_ARRAY_EXTENSION

from guppylang_internals.ast_util import AstNode
from guppylang_internals.checker.core import (
FieldAccess,
Place,
Expand All @@ -35,6 +37,11 @@
from guppylang_internals.definition.value import CompiledCallableDef
from guppylang_internals.engine import DEF_STORE, ENGINE, MonoDefId
from guppylang_internals.error import InternalGuppyError
from guppylang_internals.metadata.debug_info_util import (
StringTable,
debug_conditions_fulfilled,
make_location_record,
)
from guppylang_internals.std._internal.compiler.tket_exts import GUPPY_EXTENSION
from guppylang_internals.tys.common import ToHugrContext
from guppylang_internals.tys.subst import Inst
Expand Down Expand Up @@ -93,16 +100,22 @@ class CompilerContext(ToHugrContext):
#: functions that are part of its public interface.
exported_defs: set[DefId]

metadata_file_table: StringTable

def __init__(
self,
module: DefinitionBuilder[ops.Module],
exported_defs: set[DefId],
file_table: StringTable | None = None,
) -> None:
self.module = module
self.worklist = {}
self.compiled = {}
self.global_funcs = {}
self.exported_defs: set[DefId] = exported_defs
self.metadata_file_table = (
file_table if file_table is not None else StringTable([])
)

def build_compiled_def(self, def_id: DefId, type_args: Inst | None) -> CompiledDef:
"""Returns the compiled definitions corresponding to the given ID.
Expand Down Expand Up @@ -184,7 +197,7 @@ class DFContainer:
current compilation state.
"""

builder: DfBase[ops.DfParentOp]
builder: "DFBuilder[ops.DfParentOp]"
ctx: CompilerContext
locals: CompiledLocals = field(default_factory=dict)

Expand All @@ -197,7 +210,7 @@ def __init__(
generic_builder = cast("DfBase[ops.DfParentOp]", builder)
if locals is None:
locals = {}
self.builder = generic_builder
self.builder = DFBuilder(generic_builder)
self.ctx = ctx
self.locals = locals

Expand Down Expand Up @@ -262,7 +275,104 @@ def __contains__(self, place: Place) -> bool:
def __copy__(self) -> "DFContainer":
# Make a copy of the var map so that mutating the copy doesn't
# mutate our variable mapping
return DFContainer(self.builder, self.ctx, self.locals.copy())
return DFContainer(self.builder.raw_builder, self.ctx, self.locals.copy())


@dataclass
class DFBuilder(Generic[DP]):
"""A wrapper around a dataflow graph builder which ensures compiler-specific
additional actions can be performed every time an operation is added to the graph.

Manages attaching debug information, which requires keeping track of the most
relevant AST node for each operation being compiled with `current_ast_node`.

The underlying builder can still be accessed through `raw_builder`.
"""

raw_builder: DfBase[DP]
current_ast_node: AstNode | None = None

@contextmanager
def set_ast_context(self, ast_node: AstNode) -> Iterator[None]:
"""Context manager to set the current AST node context for debug information
attachment - within the context of this manager the given `ast_node` will be
considered the most relevant AST node for any operation added, temporarily
overriding the previous `current_ast_node`.
"""
prev_node = self.current_ast_node
self.current_ast_node = ast_node
try:
yield
finally:
self.current_ast_node = prev_node

def add_op(
self,
op: ops.DataflowOp,
/,
*args: Wire,
set_debug_info: bool = True,
) -> Node:
"""Adds an op to the dataflow graph builder. Set `set_debug_info=False` to
avoid automatic debug information attachment.
"""
op_node = self.raw_builder.add_op(op, *args)
if set_debug_info and debug_conditions_fulfilled(self.current_ast_node):
assert self.current_ast_node is not None # for type-checker
op_node.metadata[HugrDebugInfo] = make_location_record(
self.current_ast_node
)
return op_node

def call(
self,
func: ToNode,
*args: Wire,
instantiation: ht.FunctionType | None = None,
type_args: Sequence[ht.TypeArg] | None = None,
set_debug_info: bool = True,
) -> Node:
"""Calls a static function in the graph. Set `set_debug_info=False` to
avoid automatic debug information attachment.
"""
call = self.raw_builder.call(
func, *args, instantiation=instantiation, type_args=type_args
)
if set_debug_info and debug_conditions_fulfilled(self.current_ast_node):
assert self.current_ast_node is not None # for type-checker
call.metadata[HugrDebugInfo] = make_location_record(self.current_ast_node)
return call

# Other frequently used operations for which we want to avoid having to use
# `raw_builder` every time for convenience, even though we aren't setting any debug
# information in them (yet).

def get_wire_type(self, wire: Wire) -> ht.Type | None:
return self.raw_builder.hugr.port_type(wire.out_port())

def add_conditional(self, cond_wire: Wire, *args: Wire) -> Conditional:
return self.raw_builder.add_conditional(cond_wire, *args)

def add_tail_loop(
self, just_inputs: Sequence[Wire], rest: Sequence[Wire]
) -> TailLoop:
return self.raw_builder.add_tail_loop(just_inputs, rest)

def load(
self, const: ToNode | val.Value, const_parent: ToNode | None = None
) -> Node:
return self.raw_builder.load(const, const_parent)

def load_function(
self,
func: ToNode,
instantiation: ht.FunctionType | None = None,
type_args: Sequence[ht.TypeArg] | None = None,
) -> Node:
return self.raw_builder.load_function(func, instantiation, type_args)

def add_const(self, value: val.Value, parent: ToNode | None = None) -> Node:
return self.raw_builder.add_const(value, parent)


class CompilerBase(ABC):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
DEBUG_EXTENSION,
CompilerBase,
CompilerContext,
DFBuilder,
DFContainer,
GlobalConstId,
)
Expand Down Expand Up @@ -112,6 +113,13 @@ class ExprCompiler(CompilerBase, AstVisitor[Wire]):

dfg: DFContainer

def visit(self, node: Any, *args: Any, **kwargs: Any) -> Wire:
"""Overrides `visit` to set the current AST node context for debug information
attachment.
"""
with self.builder.set_ast_context(node):
return super().visit(node, *args, **kwargs)

def compile(self, expr: ast.expr, dfg: DFContainer) -> Wire:
"""Compiles an expression and returns a single wire holding the output value."""
self.dfg = dfg
Expand All @@ -127,7 +135,7 @@ def compile_row(self, expr: ast.expr, dfg: DFContainer) -> list[Wire]:
return [self.compile(e, dfg) for e in expr_to_row(expr)]

@property
def builder(self) -> DfBase[ops.DfParentOp]:
def builder(self) -> DFBuilder[ops.DfParentOp]:
"""The current Hugr dataflow graph builder."""
return self.dfg.builder

Expand Down Expand Up @@ -209,7 +217,7 @@ def _if_else(
`False` branch.
"""
cond_wire = self.visit(cond)
cond_ty = self.builder.hugr.port_type(cond_wire.out_port())
cond_ty = self.builder.get_wire_type(cond_wire)
if cond_ty == OpaqueBool:
cond_wire = self.builder.add_op(read_bool(), cond_wire)
conditional = self.builder.add_conditional(
Expand Down Expand Up @@ -343,7 +351,9 @@ def visit_LocalCall(self, node: LocalCall) -> Wire:

args = self._compile_call_args(node.args, func_ty)
call = self.builder.add_op(
ops.CallIndirect(func_ty.to_hugr(self.ctx)), func, *args
ops.CallIndirect(func_ty.to_hugr(self.ctx)),
func,
*args,
)
regular_returns = list(call[:num_returns])
inout_returns = call[num_returns:]
Expand Down Expand Up @@ -400,7 +410,9 @@ def _compile_tensor_with_leftovers(
consumed_args, other_args = args[0:input_len], args[input_len:]
consumed_wires = self._compile_call_args(consumed_args, func_ty)
call = self.builder.add_op(
ops.CallIndirect(func_ty.to_hugr(self.ctx)), func, *consumed_wires
ops.CallIndirect(func_ty.to_hugr(self.ctx)),
func,
*consumed_wires,
)
regular_returns: list[Wire] = list(call[:num_returns])
inout_returns = call[num_returns:]
Expand Down Expand Up @@ -564,13 +576,15 @@ def visit_StateResultExpr(self, node: StateResultExpr) -> Wire:
)
# Turn into standard array from borrow array.
qubit_arr_in = self.builder.add_op(
array_to_std_array(ht.Qubit, num_qubits_arg), qubit_arr_in
array_to_std_array(ht.Qubit, num_qubits_arg),
qubit_arr_in,
)

qubit_arr_out = self.builder.add_op(op, qubit_arr_in)

qubit_arr_out = self.builder.add_op(
std_array_to_array(ht.Qubit, num_qubits_arg), qubit_arr_out
std_array_to_array(ht.Qubit, num_qubits_arg),
qubit_arr_out,
)
qubits_out = unpack_array(self.builder, qubit_arr_out)
else:
Expand All @@ -579,7 +593,12 @@ def visit_StateResultExpr(self, node: StateResultExpr) -> Wire:
qubits_in = [self.visit(node.args[1])]
qubits_out = [
apply_array_op_with_conversions(
self.ctx, self.builder, op, ht.Qubit, num_qubits_arg, qubits_in[0]
self.ctx,
self.builder,
op,
ht.Qubit,
num_qubits_arg,
qubits_in[0],
)
]

Expand Down Expand Up @@ -610,7 +629,9 @@ def visit_DesugaredArrayComp(self, node: DesugaredArrayComp) -> Wire:
hugr_elt_ty = node.elt_ty.to_hugr(self.ctx)
# Initialise empty array.
self.dfg[array_var] = self.builder.add_op(
barray_new_all_borrowed(hugr_elt_ty, node.length.to_arg().to_hugr(self.ctx))
barray_new_all_borrowed(
hugr_elt_ty, node.length.to_arg().to_hugr(self.ctx)
),
)
self.dfg[count_var] = self.builder.load(
hugr.std.int.IntVal(0, width=NumericType.INT_WIDTH)
Expand Down Expand Up @@ -675,17 +696,17 @@ def _build_generators(
# In the "no" case, we set the break predicate to true
break_pred_hugr_ty = ht.Either([iter_ty.to_hugr(self.ctx)], [])
with stop_case:
self.dfg[break_pred.place] = self.dfg.builder.add_op(
self.dfg[break_pred.place] = self.builder.add_op(
ops.Tag(1, break_pred_hugr_ty)
)
# Otherwise, we continue, set the break predicate to false, and insert
# the iterator for the next loop iteration
stack.enter_context(hasnext_case)
next_wire = self.dfg[next_var.place]
elt, it = self.dfg.builder.add_op(ops.UnpackTuple(), next_wire)
elt, it = self.builder.add_op(ops.UnpackTuple(), next_wire)
compiler.dfg = self.dfg
compiler._assign(gen.target, elt)
self.dfg[break_pred.place] = self.dfg.builder.add_op(
self.dfg[break_pred.place] = self.builder.add_op(
ops.Tag(0, break_pred_hugr_ty), it
)
# Enter nested conditionals for each if guard on the generator
Expand All @@ -709,7 +730,7 @@ def expr_to_row(expr: ast.expr) -> list[ast.expr]:
def pack_returns(
returns: Sequence[Wire],
return_ty: Type,
builder: DfBase[ops.DfParentOp],
builder: DFBuilder[ops.DfParentOp],
ctx: CompilerContext,
) -> Wire:
"""Groups function return values into a tuple"""
Expand All @@ -725,7 +746,11 @@ def pack_returns(


def unpack_wire(
wire: Wire, return_ty: Type, builder: DfBase[ops.DfParentOp], ctx: CompilerContext
wire: Wire,
return_ty: Type,
builder: DFBuilder[ops.DfParentOp],
ctx: CompilerContext,
ast_node: AstNode | None = None,
) -> list[Wire]:
"""The inverse of `pack_returns`"""
if isinstance(return_ty, TupleType | NoneType):
Expand Down Expand Up @@ -779,9 +804,6 @@ def python_value_to_hugr(v: Any, exp_ty: Type, ctx: CompilerContext) -> hv.Value
return None


ARRAY_UNWRAP_ELEM: Final[GlobalConstId] = GlobalConstId.fresh("array.__unwrap_elem")
ARRAY_WRAP_ELEM: Final[GlobalConstId] = GlobalConstId.fresh("array.__wrap_elem")

ARRAY_READ_BOOL: Final[GlobalConstId] = GlobalConstId.fresh("array.__read_bool")
ARRAY_MAKE_OPAQUE_BOOL: Final[GlobalConstId] = GlobalConstId.fresh(
"array.__make_opaque_bool"
Expand Down Expand Up @@ -824,7 +846,7 @@ def doesnt_contain_none(xs: list[T | None]) -> TypeGuard[list[T]]:

def apply_array_op_with_conversions(
ctx: CompilerContext,
builder: DfBase[ops.DfParentOp],
builder: DFBuilder[ops.DfParentOp],
op: ops.DataflowOp,
elem_ty: ht.Type,
size_arg: ht.TypeArg,
Expand All @@ -846,7 +868,10 @@ def apply_array_op_with_conversions(
input_array = builder.add_op(map_op, input_array, array_read)
elem_ty = ht.Bool

input_array = builder.add_op(array_to_std_array(elem_ty, size_arg), input_array)
input_array = builder.add_op(
array_to_std_array(elem_ty, size_arg),
input_array,
)

result_array = builder.add_op(op, input_array)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def compile_local_func_def(
# Prepend captured variables to the function arguments
func_ty = func.ty.to_hugr(ctx)
closure_ty = ht.FunctionType([*captured_types, *func_ty.input], func_ty.output)
func_builder = dfg.builder.module_root_builder().define_function(
func_builder = dfg.builder.raw_builder.module_root_builder().define_function(
func.name, closure_ty.input, closure_ty.output
)

Expand Down
Loading
Loading