diff --git a/guppylang-internals/src/guppylang_internals/compiler/core.py b/guppylang-internals/src/guppylang_internals/compiler/core.py index cb8e1ca94..1b6d47218 100644 --- a/guppylang-internals/src/guppylang_internals/compiler/core.py +++ b/guppylang-internals/src/guppylang_internals/compiler/core.py @@ -40,6 +40,7 @@ from guppylang_internals.diagnostic import Error from guppylang_internals.engine import DEF_STORE, ENGINE from guppylang_internals.error import GuppyError, InternalGuppyError +from guppylang_internals.metadata.debug_info import StringTable from guppylang_internals.std._internal.compiler.tket_exts import GUPPY_EXTENSION from guppylang_internals.tys.arg import ConstArg, TypeArg from guppylang_internals.tys.builtin import nat_type @@ -151,9 +152,12 @@ class CompilerContext(ToHugrContext): checked_globals: Globals + metadata_file_table: StringTable + def __init__( self, module: DefinitionBuilder[ops.Module], + file_table: StringTable | None = None, ) -> None: self.module = module self.worklist = {} @@ -161,6 +165,9 @@ def __init__( self.global_funcs = {} self.checked_globals = Globals(None) self.current_mono_args = None + self.metadata_file_table = ( + file_table if file_table is not None else StringTable([]) + ) @contextmanager def set_monomorphized_args( diff --git a/guppylang-internals/src/guppylang_internals/compiler/expr_compiler.py b/guppylang-internals/src/guppylang_internals/compiler/expr_compiler.py index bf02898c3..a88d8d9d5 100644 --- a/guppylang-internals/src/guppylang_internals/compiler/expr_compiler.py +++ b/guppylang-internals/src/guppylang_internals/compiler/expr_compiler.py @@ -9,14 +9,15 @@ import hugr.std.int import hugr.std.logic import hugr.std.prelude -from hugr import Wire, ops +from hugr import Node, Wire, ops from hugr import tys as ht from hugr import val as hv from hugr.build import function as hf from hugr.build.cond_loop import Conditional from hugr.build.dfg import DP, DfBase +from hugr.metadata import HugrDebugInfo -from guppylang_internals.ast_util import AstNode, AstVisitor, get_type +from guppylang_internals.ast_util import AstNode, AstVisitor, get_file, get_type from guppylang_internals.cfg.builder import tmp_vars from guppylang_internals.checker.core import Variable, contains_subscript from guppylang_internals.checker.errors.generic import UnsupportedError @@ -28,6 +29,7 @@ GlobalConstId, ) from guppylang_internals.compiler.hugr_extension import PartialOp +from guppylang_internals.debug_mode import debug_mode_enabled from guppylang_internals.definition.custom import CustomFunctionDef from guppylang_internals.definition.value import ( CallableDef, @@ -37,6 +39,7 @@ ) from guppylang_internals.engine import ENGINE from guppylang_internals.error import GuppyError, InternalGuppyError +from guppylang_internals.metadata.debug_info import make_location_record from guppylang_internals.nodes import ( AbortExpr, AbortKind, @@ -129,6 +132,12 @@ def compile_row(self, expr: ast.expr, dfg: DFContainer) -> list[Wire]: """ return [self.compile(e, dfg) for e in expr_to_row(expr)] + def add_op( + self, op: ops.DataflowOp, /, *args: Wire, ast_node: AstNode | None = None + ) -> Node: + """Adds an op to the builder, with optional debug info.""" + return add_op(self.builder, op, *args, ast_node=ast_node) + @property def builder(self) -> DfBase[ops.DfParentOp]: """The current Hugr dataflow graph builder.""" @@ -214,7 +223,7 @@ def _if_else( cond_wire = self.visit(cond) cond_ty = self.builder.hugr.port_type(cond_wire.out_port()) if cond_ty == OpaqueBool: - cond_wire = self.builder.add_op(read_bool(), cond_wire) + cond_wire = self.add_op(read_bool(), cond_wire) conditional = self.builder.add_conditional( cond_wire, *(self.visit(inp) for inp in inputs) ) @@ -286,8 +295,8 @@ def visit_GenericParamValue(self, node: GenericParamValue) -> Wire: load_nat = hugr.std.PRELUDE.get_op("load_nat").instantiate( [arg], ht.FunctionType([], [ht.USize()]) ) - usize = self.builder.add_op(load_nat) - return self.builder.add_op(convert_ifromusize(), usize) + usize = self.add_op(load_nat, ast_node=node) + return self.add_op(convert_ifromusize(), usize, ast_node=node) case ty: # Look up monomorphization match self.ctx.current_mono_args[node.param.idx]: @@ -316,12 +325,12 @@ def visit_List(self, node: ast.List) -> Wire: def _unpack_tuple(self, wire: Wire, types: Sequence[Type]) -> Sequence[Wire]: """Add a tuple unpack operation to the graph""" types = [t.to_hugr(self.ctx) for t in types] - return list(self.builder.add_op(ops.UnpackTuple(types), wire)) + return list(self.add_op(ops.UnpackTuple(types), wire)) def _pack_tuple(self, wires: Sequence[Wire], types: Sequence[Type]) -> Wire: """Add a tuple pack operation to the graph""" types = [t.to_hugr(self.ctx) for t in types] - return self.builder.add_op(ops.MakeTuple(types), *wires) + return self.add_op(ops.MakeTuple(types), *wires) def _pack_returns(self, returns: Sequence[Wire], return_ty: Type) -> Wire: """Groups function return values into a tuple""" @@ -363,8 +372,8 @@ def visit_LocalCall(self, node: LocalCall) -> Wire: num_returns = len(type_to_row(func_ty.output)) args = self._compile_call_args(node.args, func_ty) - call = self.builder.add_op( - ops.CallIndirect(func_ty.to_hugr(self.ctx)), func, *args + call = self.add_op( + ops.CallIndirect(func_ty.to_hugr(self.ctx)), func, *args, ast_node=node ) regular_returns = list(call[:num_returns]) inout_returns = call[num_returns:] @@ -420,7 +429,7 @@ def _compile_tensor_with_leftovers( num_returns = len(type_to_row(func_ty.output)) 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( + call = self.add_op( ops.CallIndirect(func_ty.to_hugr(self.ctx)), func, *consumed_wires ) regular_returns: list[Wire] = list(call[:num_returns]) @@ -472,8 +481,11 @@ def visit_PartialApply(self, node: PartialApply) -> Wire: func_ty.to_hugr(self.ctx), [get_type(arg).to_hugr(self.ctx) for arg in node.args], ) - return self.builder.add_op( - op, self.visit(node.func), *(self.visit(arg) for arg in node.args) + return self.add_op( + op, + self.visit(node.func), + *(self.visit(arg) for arg in node.args), + ast_node=node, ) def visit_TypeApply(self, node: TypeApply) -> Wire: @@ -503,7 +515,7 @@ def visit_UnaryOp(self, node: ast.UnaryOp) -> Wire: # since it is not implemented via a dunder method if isinstance(node.op, ast.Not): arg = self.visit(node.operand) - return self.builder.add_op(not_op(), arg) + return self.add_op(not_op(), arg, ast_node=node) raise InternalGuppyError("Node should have been removed during type checking.") @@ -561,9 +573,9 @@ def _visit_result_tag(self, tag: Const, loc: ast.expr) -> str: def visit_AbortExpr(self, node: AbortExpr) -> Wire: signal = self.visit(node.signal) - signal_usize = self.builder.add_op(convert_itousize(), signal) + signal_usize = self.add_op(convert_itousize(), signal, ast_node=node) msg = self.visit(node.msg) - err = self.builder.add_op(make_error(), signal_usize, msg) + err = self.add_op(make_error(), signal_usize, msg, ast_node=node) in_tys = [get_type(e).to_hugr(self.ctx) for e in node.values] out_tys = [ty.to_hugr(self.ctx) for ty in type_to_row(get_type(node))] args = [self.visit(e) for e in node.values] @@ -572,7 +584,7 @@ def visit_AbortExpr(self, node: AbortExpr) -> Wire: h_node = build_panic(self.builder, in_tys, out_tys, err, *args) case AbortKind.ExitShot: op = panic(in_tys, out_tys, AbortKind.ExitShot) - h_node = self.builder.add_op(op, err, *args) + h_node = self.add_op(op, err, *args, ast_node=node) return self._pack_returns(list(h_node.outputs()), get_type(node)) def visit_BarrierExpr(self, node: BarrierExpr) -> Wire: @@ -582,7 +594,7 @@ def visit_BarrierExpr(self, node: BarrierExpr) -> Wire: ht.FunctionType.endo(hugr_tys), ) - barrier_n = self.builder.add_op(op, *(self.visit(e) for e in node.args)) + barrier_n = self.add_op(op, *(self.visit(e) for e in node.args), ast_node=node) self._update_inout_ports(node.args, iter(barrier_n), node.func_ty) return self._pack_returns([], NoneType()) @@ -605,27 +617,37 @@ def visit_StateResultExpr(self, node: StateResultExpr) -> Wire: if not node.array_len: # If the input is a sequence of qubits, we pack them into an array. qubits_in = [self.visit(e) for e in node.args[1:]] - qubit_arr_in = self.builder.add_op( - array_new(ht.Qubit, len(node.args) - 1), *qubits_in + qubit_arr_in = self.add_op( + array_new(ht.Qubit, len(node.args) - 1), *qubits_in, ast_node=node ) # 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 + qubit_arr_in = self.add_op( + array_to_std_array(ht.Qubit, num_qubits_arg), + qubit_arr_in, + ast_node=node, ) - qubit_arr_out = self.builder.add_op(op, qubit_arr_in) + qubit_arr_out = self.add_op(op, qubit_arr_in, ast_node=node) - qubit_arr_out = self.builder.add_op( - std_array_to_array(ht.Qubit, num_qubits_arg), qubit_arr_out + qubit_arr_out = self.add_op( + std_array_to_array(ht.Qubit, num_qubits_arg), + qubit_arr_out, + ast_node=node, ) - qubits_out = unpack_array(self.builder, qubit_arr_out) + qubits_out = unpack_array(self.builder, qubit_arr_out, ast_node=node) else: # If the input is an array of qubits, we need to convert to a standard # array. 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], + ast_node=node, ) ] @@ -655,8 +677,10 @@ def visit_DesugaredArrayComp(self, node: DesugaredArrayComp) -> Wire: count_var = Variable(next(tmp_vars), int_type(), node) 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)) + self.dfg[array_var] = self.add_op( + 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) @@ -664,7 +688,7 @@ def visit_DesugaredArrayComp(self, node: DesugaredArrayComp) -> Wire: with self._build_generators([node.generator], [array_var, count_var]): elt = self.visit(node.elt) array, count = self.dfg[array_var], self.dfg[count_var] - idx = self.builder.add_op(convert_itousize(), count) + idx = self.add_op(convert_itousize(), count) self.dfg[array_var] = self.builder.add_op( barray_return(hugr_elt_ty, node.length.to_arg().to_hugr(self.ctx)), array, @@ -748,6 +772,23 @@ def visit_Compare(self, node: ast.Compare) -> Wire: raise InternalGuppyError("Node should have been removed during type checking.") +P = TypeVar("P", bound=ops.DfParentOp) + + +def add_op( + builder: DfBase[P], + op: ops.DataflowOp, + /, + *args: Wire, + ast_node: AstNode | None = None, +) -> Node: + """Adds an op to the builder, with optional debug info.""" + op_node = builder.add_op(op, *args) + if debug_mode_enabled() and ast_node is not None and get_file(ast_node) is not None: + op_node.metadata[HugrDebugInfo] = make_location_record(ast_node) + return op_node + + def expr_to_row(expr: ast.expr) -> list[ast.expr]: """Turns an expression into a row expressions by unpacking top-level tuples.""" return expr.elts if isinstance(expr, ast.Tuple) else [expr] @@ -758,13 +799,14 @@ def pack_returns( return_ty: Type, builder: DfBase[ops.DfParentOp], ctx: CompilerContext, + ast_node: AstNode | None = None, ) -> Wire: """Groups function return values into a tuple""" if isinstance(return_ty, TupleType | NoneType) and not return_ty.preserve: types = type_to_row(return_ty) assert len(returns) == len(types) hugr_tys = [t.to_hugr(ctx) for t in types] - return builder.add_op(ops.MakeTuple(hugr_tys), *returns) + return add_op(builder, ops.MakeTuple(hugr_tys), *returns, ast_node=ast_node) assert len(returns) == 1, ( f"Expected a single return value. Got {returns}. return type {return_ty}" ) @@ -772,13 +814,21 @@ def pack_returns( def unpack_wire( - wire: Wire, return_ty: Type, builder: DfBase[ops.DfParentOp], ctx: CompilerContext + wire: Wire, + return_ty: Type, + builder: DfBase[ops.DfParentOp], + ctx: CompilerContext, + ast_node: AstNode | None = None, ) -> list[Wire]: """The inverse of `pack_returns`""" if isinstance(return_ty, TupleType | NoneType) and not return_ty.preserve: types = type_to_row(return_ty) hugr_tys = [t.to_hugr(ctx) for t in types] - return list(builder.add_op(ops.UnpackTuple(hugr_tys), wire).outputs()) + return list( + add_op( + builder, ops.UnpackTuple(hugr_tys), wire, ast_node=ast_node + ).outputs() + ) return [wire] @@ -885,6 +935,7 @@ def apply_array_op_with_conversions( size_arg: ht.TypeArg, input_array: Wire, convert_bool: bool = False, + ast_node: AstNode | None = None, ) -> Wire: """Applies common transformations to a Guppy array input before it can be passed to a Hugr op operating on a standard Hugr array, and then reverses them again on the @@ -898,20 +949,28 @@ def apply_array_op_with_conversions( array_read = array_read_bool(ctx) array_read = builder.load_function(array_read) map_op = array_map(OpaqueBool, size_arg, ht.Bool) - input_array = builder.add_op(map_op, input_array, array_read) + input_array = add_op( + builder, map_op, input_array, array_read, ast_node=ast_node + ) elem_ty = ht.Bool - input_array = builder.add_op(array_to_std_array(elem_ty, size_arg), input_array) + input_array = add_op( + builder, array_to_std_array(elem_ty, size_arg), input_array, ast_node=ast_node + ) - result_array = builder.add_op(op, input_array) + result_array = add_op(builder, op, input_array, ast_node=ast_node) - result_array = builder.add_op(std_array_to_array(elem_ty, size_arg), result_array) + result_array = add_op( + builder, std_array_to_array(elem_ty, size_arg), result_array, ast_node=ast_node + ) if convert_bool: array_make_opaque = array_make_opaque_bool(ctx) array_make_opaque = builder.load_function(array_make_opaque) map_op = array_map(ht.Bool, size_arg, OpaqueBool) - result_array = builder.add_op(map_op, result_array, array_make_opaque) + result_array = add_op( + builder, map_op, result_array, array_make_opaque, ast_node=ast_node + ) elem_ty = OpaqueBool return result_array diff --git a/guppylang-internals/src/guppylang_internals/compiler/modifier_compiler.py b/guppylang-internals/src/guppylang_internals/compiler/modifier_compiler.py index b64578267..a0413464e 100644 --- a/guppylang-internals/src/guppylang_internals/compiler/modifier_compiler.py +++ b/guppylang-internals/src/guppylang_internals/compiler/modifier_compiler.py @@ -8,7 +8,7 @@ from guppylang_internals.compiler.cfg_compiler import compile_cfg from guppylang_internals.compiler.core import CompilerContext, DFContainer from guppylang_internals.compiler.expr_compiler import ExprCompiler -from guppylang_internals.definition.metadata import add_metadata +from guppylang_internals.metadata.common import add_metadata from guppylang_internals.nodes import CheckedModifiedBlock, PlaceNode from guppylang_internals.std._internal.compiler.array import ( array_new, diff --git a/guppylang-internals/src/guppylang_internals/debug_mode.py b/guppylang-internals/src/guppylang_internals/debug_mode.py new file mode 100644 index 000000000..6b35fd8de --- /dev/null +++ b/guppylang-internals/src/guppylang_internals/debug_mode.py @@ -0,0 +1,18 @@ +"""Global state for determining whether to attach debug information to Hugr nodes +during compilation.""" + +DEBUG_MODE_ENABLED = False + + +def turn_on_debug_mode() -> None: + global DEBUG_MODE_ENABLED + DEBUG_MODE_ENABLED = True + + +def turn_off_debug_mode() -> None: + global DEBUG_MODE_ENABLED + DEBUG_MODE_ENABLED = False + + +def debug_mode_enabled() -> bool: + return DEBUG_MODE_ENABLED diff --git a/guppylang-internals/src/guppylang_internals/definition/custom.py b/guppylang-internals/src/guppylang_internals/definition/custom.py index 89380d1d5..d9238b0c6 100644 --- a/guppylang-internals/src/guppylang_internals/definition/custom.py +++ b/guppylang-internals/src/guppylang_internals/definition/custom.py @@ -4,7 +4,7 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, ClassVar -from hugr import Wire, ops +from hugr import Node, Wire, ops from hugr import tys as ht from hugr.build.dfg import DfBase from hugr.std.collections.borrow_array import EXTENSION as BORROW_ARRAY_EXTENSION @@ -378,6 +378,13 @@ def builder(self) -> DfBase[ops.DfParentOp]: """The hugr dataflow builder.""" return self.dfg.builder + def add_op(self, op: ops.DataflowOp, *args: Wire) -> Node: + """Adds an op to the current builder ensuring debug information is added if + available.""" + from guppylang_internals.compiler.expr_compiler import add_op + + return add_op(self.builder, op, *args, ast_node=self.node) + class CustomCallCompiler(CustomInoutCallCompiler, ABC): """Abstract base class for custom function call compilers with only owned args.""" @@ -386,7 +393,11 @@ class CustomCallCompiler(CustomInoutCallCompiler, ABC): def compile(self, args: list[Wire]) -> list[Wire]: """Compiles a custom function call and returns the resulting ports. - Use the provided `self.builder` to add nodes to the Hugr graph. + Use `self.add_op` to add nodes to the Hugr graph. + + If you want to add a different builder than `self.builder`while still ensuring + debug information is attached during compilation, import `add_op` from + `expr_compiler` and pass `self.node` to it. """ def compile_with_inouts(self, args: list[Wire]) -> CallReturnWires: @@ -436,7 +447,7 @@ def __init__( def compile_with_inouts(self, args: list[Wire]) -> CallReturnWires: op = self.op(self.ty, self.type_args, self.ctx) - node = self.builder.add_op(op, *args) + node = self.add_op(op, *args) num_returns = ( len(type_to_row(self.func.ty.output)) if self.func else len(self.ty.output) ) @@ -471,15 +482,15 @@ def compile_with_inouts(self, args: list[Wire]) -> CallReturnWires: hugr_op_ty = ht.FunctionType(converted_in, converted_out) op = self.op(hugr_op_ty, self.type_args, self.ctx) converted_args = [ - self.builder.add_op(read_bool(), arg) + self.add_op(read_bool(), arg) if self.builder.hugr.port_type(arg.out_port()) == OpaqueBool else arg for arg in args ] - node = self.builder.add_op(op, *converted_args) + node = self.add_op(op, *converted_args) result = list(node.outputs()) converted_result = [ - self.builder.add_op(make_opaque(), res) + self.add_op(make_opaque(), res) if self.builder.hugr.port_type(res.out_port()) == ht.Bool else res for res in result @@ -528,7 +539,7 @@ def _handle_affine_type(self, ty: ht.Type, arg: Wire) -> list[Wire]: type_args, ht.FunctionType(self.ty.input, self.ty.output), ) - return list(self.builder.add_op(clone_op, arg)) + return list(self.add_op(clone_op, arg)) case _: pass raise InternalGuppyError( diff --git a/guppylang-internals/src/guppylang_internals/definition/declaration.py b/guppylang-internals/src/guppylang_internals/definition/declaration.py index d594147bc..a26c46b65 100644 --- a/guppylang-internals/src/guppylang_internals/definition/declaration.py +++ b/guppylang-internals/src/guppylang_internals/definition/declaration.py @@ -5,8 +5,14 @@ from hugr import Node, Wire from hugr.build import function as hf from hugr.build.dfg import DefinitionBuilder, OpVar +from hugr.metadata import HugrDebugInfo -from guppylang_internals.ast_util import AstNode, has_empty_body, with_loc, with_type +from guppylang_internals.ast_util import ( + AstNode, + has_empty_body, + with_loc, + with_type, +) from guppylang_internals.checker.core import Context, Globals from guppylang_internals.checker.expr_checker import check_call, synthesize_call from guppylang_internals.checker.func_checker import check_signature @@ -15,6 +21,7 @@ DFContainer, require_monomorphization, ) +from guppylang_internals.debug_mode import debug_mode_enabled from guppylang_internals.definition.common import ( CompilableDef, ParsableDef, @@ -25,6 +32,7 @@ compile_call, default_func_link_name, load_with_args, + make_subprogram_record, parse_py_func, ) from guppylang_internals.definition.value import ( @@ -151,6 +159,10 @@ def compile_outer( module: hf.Module = module node = module.declare_function(self.link_name, self.ty.to_hugr_poly(ctx)) + if debug_mode_enabled(): + node.metadata[HugrDebugInfo] = make_subprogram_record( + self.defined_at, ctx, is_decl=True + ) return CompiledFunctionDecl( self.id, self.name, @@ -207,4 +219,4 @@ def compile_call( ) -> CallReturnWires: """Compiles a call to the function.""" # Use implementation from function definition. - return compile_call(args, type_args, dfg, self.ty, self.declaration) + return compile_call(args, type_args, dfg, self.ty, self.declaration, node) diff --git a/guppylang-internals/src/guppylang_internals/definition/function.py b/guppylang-internals/src/guppylang_internals/definition/function.py index 4e85f3c23..c4dd30118 100644 --- a/guppylang-internals/src/guppylang_internals/definition/function.py +++ b/guppylang-internals/src/guppylang_internals/definition/function.py @@ -8,11 +8,14 @@ import hugr.tys as ht from hugr import Node, Wire from hugr.build.dfg import DefinitionBuilder, OpVar +from hugr.debug_info import DISubprogram from hugr.hugr.node_port import ToNode +from hugr.metadata import HugrDebugInfo from guppylang_internals.ast_util import ( AstNode, annotate_location, + get_file, parse_source, with_loc, with_type, @@ -32,6 +35,7 @@ PartiallyMonomorphizedArgs, ) from guppylang_internals.compiler.func_compiler import compile_global_func_def +from guppylang_internals.debug_mode import debug_mode_enabled from guppylang_internals.definition.common import ( CheckableDef, MonomorphizableDef, @@ -41,7 +45,6 @@ UserProvidedLinkName, ) from guppylang_internals.definition.enum import ParsedEnumDef -from guppylang_internals.definition.metadata import GuppyMetadata, add_metadata from guppylang_internals.definition.struct import ParsedStructDef from guppylang_internals.definition.value import ( CallableDef, @@ -51,8 +54,10 @@ ) from guppylang_internals.engine import DEF_STORE, ENGINE from guppylang_internals.error import GuppyError +from guppylang_internals.metadata.common import FunctionMetadata, add_metadata +from guppylang_internals.metadata.debug_info import make_location_record from guppylang_internals.nodes import GlobalCall -from guppylang_internals.span import SourceMap +from guppylang_internals.span import SourceMap, to_span from guppylang_internals.tys.subst import Inst, Subst from guppylang_internals.tys.ty import FunctionType, Type, UnitaryFlags, type_to_row @@ -96,7 +101,7 @@ class RawFunctionDef(ParsableDef, UserProvidedLinkName): unitary_flags: UnitaryFlags = field(default=UnitaryFlags.NoFlags, kw_only=True) - metadata: GuppyMetadata | None = field(default=None, kw_only=True) + metadata: FunctionMetadata | None = field(default=None, kw_only=True) def parse(self, globals: Globals, sources: SourceMap) -> "ParsedFunctionDef": """Parses and checks the user-provided signature of the function.""" @@ -142,7 +147,7 @@ class ParsedFunctionDef(CheckableDef, CallableDef): description: str = field(default="function", init=False) - metadata: GuppyMetadata | None = field(default=None, kw_only=True) + metadata: FunctionMetadata | None = field(default=None, kw_only=True) def check(self, globals: Globals) -> "CheckedFunctionDef": """Type checks the body of the function.""" @@ -222,6 +227,9 @@ def monomorphize( func_def = module.module_root_builder().define_function( self.link_name, hugr_ty.body.input, hugr_ty.body.output, hugr_ty.params ) + if debug_mode_enabled(): + assert self.metadata is not None + self.metadata.set_debug_info(make_subprogram_record(self.defined_at, ctx)) add_metadata( func_def, self.metadata, @@ -287,7 +295,7 @@ def compile_call( node: AstNode, ) -> CallReturnWires: """Compiles a call to the function.""" - return compile_call(args, type_args, dfg, self.ty, self.func_def) + return compile_call(args, type_args, dfg, self.ty, self.func_def, node) def compile_inner(self, globals: CompilerContext) -> None: """Compiles the body of the function.""" @@ -312,12 +320,15 @@ def compile_call( dfg: DFContainer, ty: FunctionType, func: ToNode, + call_ast: AstNode, ) -> CallReturnWires: """Compiles a call to the function.""" func_ty: ht.FunctionType = ty.instantiate(type_args).to_hugr(dfg.ctx) type_args = [arg.to_hugr(dfg.ctx) for arg in type_args] num_returns = len(type_to_row(ty.output)) call = dfg.builder.call(func, *args, instantiation=func_ty, type_args=type_args) + if debug_mode_enabled(): + call.metadata[HugrDebugInfo] = make_location_record(call_ast) return CallReturnWires( regular_returns=list(call[:num_returns]), inout_returns=list(call[num_returns:]), @@ -335,3 +346,26 @@ def parse_py_func(f: PyFunc, sources: SourceMap) -> tuple[ast.FunctionDef, str | if not isinstance(func_ast, ast.FunctionDef): raise GuppyError(ExpectedError(func_ast, "a function definition")) return parse_function_with_docstring(func_ast) + + +# Note: Defined here as opposed to in `metadata.debug_info` to avoid circular imports +# due to using `CompilerContext` (not an issue for `make_location_record`). +def make_subprogram_record( + node: ast.FunctionDef, ctx: CompilerContext, is_decl: bool = False +) -> DISubprogram: + """Create a DISubprogram debug record for `node`, which should be a function + definition or declaration.""" + filename = get_file(node) + # If we can't fine a file for a node, we default to 0 which corresponds to the + # entrypoint file. + file_idx = ctx.metadata_file_table.get_index(filename) if filename else 0 + if is_decl or not node.body: + return DISubprogram( + file=file_idx, line_no=to_span(node).start.line, scope_line=None + ) + else: + return DISubprogram( + file=file_idx, + line_no=to_span(node).start.line, + scope_line=to_span(node.body[0]).start.line, + ) diff --git a/guppylang-internals/src/guppylang_internals/definition/metadata.py b/guppylang-internals/src/guppylang_internals/definition/metadata.py deleted file mode 100644 index 4d0280575..000000000 --- a/guppylang-internals/src/guppylang_internals/definition/metadata.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Metadata attached to objects within the Guppy compiler, both for internal use and to -attach to HUGR nodes for lower-level processing.""" - -from abc import ABC -from dataclasses import dataclass, field, fields -from typing import Any, ClassVar, Generic, TypeVar - -from hugr.hugr.node_port import ToNode - -from guppylang_internals.diagnostic import Fatal -from guppylang_internals.error import GuppyError - -T = TypeVar("T") - - -@dataclass(init=True, kw_only=True) -class GuppyMetadataValue(ABC, Generic[T]): - """A template class for a metadata value within the scope of the Guppy compiler. - Implementations should provide the `key` in reverse-URL format.""" - - key: ClassVar[str] - value: T | None = None - - -class MetadataMaxQubits(GuppyMetadataValue[int]): - key = "tket.hint.max_qubits" - - -@dataclass(frozen=True, init=True, kw_only=True) -class GuppyMetadata: - """DTO for metadata within the scope of the guppy compiler for attachment to HUGR - nodes. See `add_metadata`.""" - - max_qubits: MetadataMaxQubits = field(default_factory=MetadataMaxQubits, init=False) - - @classmethod - def reserved_keys(cls) -> set[str]: - return {f.type.key for f in fields(GuppyMetadata)} # type: ignore[union-attr] - - -@dataclass(frozen=True) -class MetadataAlreadySetError(Fatal): - title: ClassVar[str] = "Metadata key already set" - message: ClassVar[str] = "Received two values for the metadata key `{key}`" - key: str - - -@dataclass(frozen=True) -class ReservedMetadataKeysError(Fatal): - title: ClassVar[str] = "Metadata key is reserved" - message: ClassVar[str] = ( - "The following metadata keys are reserved by Guppy but also provided in " - "additional metadata: `{keys}`" - ) - keys: set[str] - - -def add_metadata( - node: ToNode, - metadata: GuppyMetadata | None = None, - *, - additional_metadata: dict[str, Any] | None = None, -) -> None: - """Adds metadata to the given node using the keys defined through inheritors of - `GuppyMetadataValue` defined in the `GuppyMetadata` class. - - Additional metadata is forwarded as is, although the given dictionary may not - contain any keys already reserved by fields in `GuppyMetadata`. - """ - if metadata is not None: - for f in fields(GuppyMetadata): - data: GuppyMetadataValue[Any] = getattr(metadata, f.name) - if data.key in node.metadata: - raise GuppyError(MetadataAlreadySetError(None, data.key)) - if data.value is not None: - node.metadata[data.key] = data.value - - if additional_metadata is not None: - reserved_keys = GuppyMetadata.reserved_keys() - used_reserved_keys = reserved_keys.intersection(additional_metadata.keys()) - if len(used_reserved_keys) > 0: - raise GuppyError(ReservedMetadataKeysError(None, keys=used_reserved_keys)) - - for key, value in additional_metadata.items(): - if key in node.metadata: - raise GuppyError(MetadataAlreadySetError(None, key)) - node.metadata[key] = value diff --git a/guppylang-internals/src/guppylang_internals/definition/pytket_circuits.py b/guppylang-internals/src/guppylang_internals/definition/pytket_circuits.py index 704ad5d07..4355232e3 100644 --- a/guppylang-internals/src/guppylang_internals/definition/pytket_circuits.py +++ b/guppylang-internals/src/guppylang_internals/definition/pytket_circuits.py @@ -7,7 +7,9 @@ from hugr import Node, Wire, envelope, ops, val from hugr import tys as ht from hugr.build.dfg import DefinitionBuilder, OpVar +from hugr.debug_info import DILocation, DISubprogram from hugr.envelope import EnvelopeConfig +from hugr.metadata import HugrDebugInfo from hugr.std.float import FLOAT_T from pytket.circuit import Circuit from tket.circuit import Tk2Circuit @@ -20,6 +22,7 @@ check_signature, ) from guppylang_internals.compiler.core import CompilerContext, DFContainer +from guppylang_internals.debug_mode import debug_mode_enabled from guppylang_internals.definition.common import ( CompilableDef, ParsableDef, @@ -29,6 +32,7 @@ PyFunc, compile_call, load_with_args, + make_subprogram_record, parse_py_func, ) from guppylang_internals.definition.ty import TypeDef @@ -40,6 +44,7 @@ ) from guppylang_internals.engine import ENGINE from guppylang_internals.error import GuppyError, InternalGuppyError +from guppylang_internals.metadata.debug_info import make_location_record from guppylang_internals.nodes import GlobalCall from guppylang_internals.span import SourceMap, Span, ToSpan from guppylang_internals.std._internal.compiler.array import ( @@ -99,7 +104,13 @@ def parse(self, globals: Globals, sources: SourceMap) -> "ParsedPytketDef": ) raise GuppyError(err) return ParsedPytketDef( - self.id, self.name, func_ast, stub_signature, self.input_circuit, False + self.id, + self.name, + func_ast, + stub_signature, + self.input_circuit, + False, + None, ) @@ -135,6 +146,7 @@ def parse(self, globals: Globals, sources: SourceMap) -> "ParsedPytketDef": circuit_signature, self.input_circuit, self.use_arrays, + self.source_span, ) @@ -155,6 +167,8 @@ class ParsedPytketDef(CallableDef, CompilableDef): input_circuit: Circuit use_arrays: bool + source_span: Span | None # Only set for load_pytket for debug purposes. + description: str = field(default="pytket circuit", init=False) def compile_outer( @@ -173,6 +187,22 @@ def compile_outer( outer_func = module.module_root_builder().define_function( self.name, func_type.body.input, func_type.body.output ) + if debug_mode_enabled(): + # Function stub case. + if self.defined_at is not None: + assert isinstance(self.defined_at, ast.FunctionDef) + func_metadata = make_subprogram_record( + self.defined_at, ctx, is_decl=True + ) + # Load pytket case, + elif self.source_span is not None: + file_idx = ctx.metadata_file_table.get_index(self.source_span.file) + func_metadata = DISubprogram( + file=file_idx, + line_no=self.source_span.start.line, + scope_line=None, + ) + outer_func.metadata[HugrDebugInfo] = func_metadata # Number of qubit inputs in the outer function. offset = ( @@ -234,6 +264,16 @@ def compile_outer( # Pass all arguments to call node. call_node = outer_func.call(hugr_func, *(input_list + bool_wires + param_wires)) + if debug_mode_enabled(): + if self.defined_at is not None: + call_node.metadata[HugrDebugInfo] = make_location_record( + self.defined_at + ) + elif self.source_span is not None: + call_node.metadata[HugrDebugInfo] = DILocation( + column=self.source_span.start.column, + line_no=self.source_span.start.line, + ) # Pytket circuit hugr has qubit and bool wires in the opposite # order to Guppy output wires. @@ -282,6 +322,7 @@ def compile_outer( self.ty, self.input_circuit, self.use_arrays, + self.source_span, outer_func, ) @@ -346,7 +387,7 @@ def compile_call( ) -> CallReturnWires: """Compiles a call to the function.""" # Use implementation from function definition. - return compile_call(args, type_args, dfg, self.ty, self.func_def) + return compile_call(args, type_args, dfg, self.ty, self.func_def, node) def _signature_from_circuit( diff --git a/guppylang-internals/src/guppylang_internals/definition/struct.py b/guppylang-internals/src/guppylang_internals/definition/struct.py index 6310dde95..94cfcf672 100644 --- a/guppylang-internals/src/guppylang_internals/definition/struct.py +++ b/guppylang-internals/src/guppylang_internals/definition/struct.py @@ -213,7 +213,7 @@ class ConstructorCompiler(CustomCallCompiler): """Compiler for the `__new__` constructor method of a struct.""" def compile(self, args: list[Wire]) -> list[Wire]: - return list(self.builder.add(ops.MakeTuple()(*args))) + return list(self.add_op(ops.MakeTuple(), *args)) constructor_sig = FunctionType( inputs=[ diff --git a/guppylang-internals/src/guppylang_internals/definition/traced.py b/guppylang-internals/src/guppylang_internals/definition/traced.py index 002405682..41b9eac4e 100644 --- a/guppylang-internals/src/guppylang_internals/definition/traced.py +++ b/guppylang-internals/src/guppylang_internals/definition/traced.py @@ -7,6 +7,7 @@ import hugr.tys as ht from hugr import Node, Wire from hugr.build.dfg import DefinitionBuilder, OpVar +from hugr.metadata import HugrDebugInfo from guppylang_internals.ast_util import AstNode, with_loc from guppylang_internals.checker.core import Context, Globals @@ -19,11 +20,15 @@ check_signature, ) from guppylang_internals.compiler.core import CompilerContext, DFContainer +from guppylang_internals.debug_mode import debug_mode_enabled from guppylang_internals.definition.common import ( CompilableDef, ParsableDef, ) -from guppylang_internals.definition.function import parse_py_func +from guppylang_internals.definition.function import ( + make_subprogram_record, + parse_py_func, +) from guppylang_internals.definition.value import ( CallableDef, CallReturnWires, @@ -31,6 +36,7 @@ CompiledHugrNodeDef, ) from guppylang_internals.error import GuppyError +from guppylang_internals.metadata.debug_info import make_location_record from guppylang_internals.nodes import GlobalCall from guppylang_internals.span import SourceMap from guppylang_internals.tys.subst import Inst, Subst @@ -91,6 +97,10 @@ def compile_outer( func_def = module.module_root_builder().define_function( self.name, func_type.body.input, func_type.body.output, func_type.params ) + if debug_mode_enabled(): + func_def.metadata[HugrDebugInfo] = make_subprogram_record( + self.defined_at, ctx + ) return CompiledTracedFunctionDef( self.id, self.name, @@ -139,6 +149,8 @@ def compile_call( call = dfg.builder.call( self.func_def, *args, instantiation=func_ty, type_args=type_args ) + if debug_mode_enabled(): + call.metadata[HugrDebugInfo] = make_location_record(node) return CallReturnWires( regular_returns=list(call[:num_returns]), inout_returns=list(call[num_returns:]), diff --git a/guppylang-internals/src/guppylang_internals/engine.py b/guppylang-internals/src/guppylang_internals/engine.py index e2c2bb8af..f925cafd1 100644 --- a/guppylang-internals/src/guppylang_internals/engine.py +++ b/guppylang-internals/src/guppylang_internals/engine.py @@ -1,17 +1,20 @@ from collections import defaultdict +from pathlib import Path from types import FrameType from typing import TYPE_CHECKING import hugr import hugr.build.function as hf from hugr import ops +from hugr.debug_info import DICompileUnit from hugr.envelope import ExtensionDesc, GeneratorDesc from hugr.ext import Extension, ExtensionRegistry -from hugr.metadata import HugrGenerator, HugrUsedExtensions +from hugr.metadata import HugrDebugInfo, HugrGenerator, HugrUsedExtensions from hugr.package import ModulePointer, Package from semver import Version import guppylang_internals +from guppylang_internals.debug_mode import debug_mode_enabled from guppylang_internals.definition.common import ( CheckableDef, CheckedDef, @@ -27,7 +30,11 @@ CompiledHugrNodeDef, ) from guppylang_internals.error import pretty_errors +from guppylang_internals.metadata.debug_info import ( + StringTable, +) from guppylang_internals.span import SourceMap +from guppylang_internals.tracing.util import get_calling_frame from guppylang_internals.tys.builtin import ( array_type_def, bool_type_def, @@ -285,7 +292,14 @@ def compile(self, id: DefId) -> ModulePointer: # Lower definitions to Hugr from guppylang_internals.compiler.core import CompilerContext - ctx = CompilerContext(graph) + # Set up string tables for metadata serialization. We know that the first entry + # in the table is always the file containing the Hugr entrypoint. + frame = get_calling_frame() + assert frame is not None + filename = frame.f_code.co_filename + file_table = StringTable([filename]) + + ctx = CompilerContext(graph, file_table) compiled_def = ctx.compile(self.checked[id]) self.compiled = ctx.compiled @@ -298,6 +312,16 @@ def compile(self, id: DefId) -> ModulePointer: # loosened after https://github.com/quantinuum/hugr/issues/2501 is fixed graph.hugr.entrypoint = compiled_def.hugr_node + # Add debug info about the module to the root node + if debug_mode_enabled(): + module_info = DICompileUnit( + directory=Path.cwd().as_uri(), + # We know this file is always the first entry in the file table. + filename=ctx.metadata_file_table.get_index(filename), + file_table=ctx.metadata_file_table.table, + ) + graph.hugr.module_root.metadata[HugrDebugInfo] = module_info + # Use cached base extensions and registry, only add additional extensions base_extensions = self._get_base_packaged_extensions() packaged_extensions = [*base_extensions, *self.additional_extensions] diff --git a/guppylang-internals/src/guppylang_internals/metadata/__init__.py b/guppylang-internals/src/guppylang_internals/metadata/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/guppylang-internals/src/guppylang_internals/metadata/common.py b/guppylang-internals/src/guppylang_internals/metadata/common.py new file mode 100644 index 000000000..b4831e2a6 --- /dev/null +++ b/guppylang-internals/src/guppylang_internals/metadata/common.py @@ -0,0 +1,89 @@ +from dataclasses import dataclass, field +from typing import Any, ClassVar + +from hugr.debug_info import DebugRecord +from hugr.hugr.node_port import ToNode +from hugr.metadata import HugrDebugInfo +from hugr.utils import JsonType + +from guppylang_internals.diagnostic import Fatal +from guppylang_internals.error import GuppyError +from guppylang_internals.metadata.max_qubits import MetadataMaxQubits + + +@dataclass(frozen=True) +class MetadataAlreadySetError(Fatal): + title: ClassVar[str] = "Metadata key already set" + message: ClassVar[str] = "Received two values for the metadata key `{key}`" + key: str + + +@dataclass(frozen=True) +class ReservedMetadataKeysError(Fatal): + title: ClassVar[str] = "Metadata key is reserved" + message: ClassVar[str] = ( + "The following metadata keys are reserved by Guppy but also provided in " + "additional metadata: `{keys}`" + ) + keys: set[str] + + +@dataclass +class FunctionMetadata: + """Class for storing metadata to be attached to Hugr nodes during compilation.""" + + _node_metadata: dict[str, JsonType] = field(default_factory=dict) + _RESERVED_KEYS: ClassVar[set[str]] = { + HugrDebugInfo.KEY, + MetadataMaxQubits.KEY, + } + + def as_dict(self) -> dict[str, JsonType]: + return self._node_metadata + + def set_debug_info(self, debug_info: DebugRecord) -> None: + self._node_metadata[HugrDebugInfo.KEY] = debug_info.to_json() + + def set_max_qubits(self, max_qubits: int) -> None: + self._node_metadata[MetadataMaxQubits.KEY] = max_qubits + + def get_debug_info(self) -> DebugRecord | None: + if HugrDebugInfo.KEY not in self._node_metadata: + return None + return DebugRecord.from_json(self._node_metadata.get(HugrDebugInfo.KEY)) + + def get_max_qubits(self) -> int | None: + qubits = self._node_metadata.get(MetadataMaxQubits.KEY) + assert qubits is None or isinstance(qubits, int) + return qubits + + @classmethod + def reserved_keys(cls) -> set[str]: + return cls._RESERVED_KEYS + + +def add_metadata( + node: ToNode, + metadata: FunctionMetadata | None = None, + *, + additional_metadata: dict[str, Any] | None = None, +) -> None: + """Extends metadata of a node, ensuring reserved keys aren't overwritten.""" + if metadata is not None: + metadata_dict = metadata.as_dict() + for key in metadata_dict: + if key in node.metadata: + raise GuppyError(MetadataAlreadySetError(None, key)) + if metadata_dict[key] is not None: + node.metadata[key] = metadata_dict[key] + + if additional_metadata is not None: + reserved_keys = FunctionMetadata.reserved_keys() + used_reserved_keys = reserved_keys.intersection(additional_metadata.keys()) + if len(used_reserved_keys) > 0: + raise GuppyError(ReservedMetadataKeysError(None, keys=used_reserved_keys)) + + for key, value in additional_metadata.items(): + if key in node.metadata: + raise GuppyError(MetadataAlreadySetError(None, key)) + node.metadata[key] = value diff --git a/guppylang-internals/src/guppylang_internals/metadata/debug_info.py b/guppylang-internals/src/guppylang_internals/metadata/debug_info.py new file mode 100644 index 000000000..f87a0bbc2 --- /dev/null +++ b/guppylang-internals/src/guppylang_internals/metadata/debug_info.py @@ -0,0 +1,35 @@ +import ast +from dataclasses import dataclass + +from hugr.debug_info import DILocation + +from guppylang_internals.span import to_span + + +def make_location_record(node: ast.AST) -> DILocation: + """Creates a DILocation metadata record for `node`.""" + return DILocation( + line_no=to_span(node).start.line, column=to_span(node).start.column + ) + + +@dataclass +class StringTable: + """Utility class for managing a string table for debug info serialization.""" + + table: list[str] + + def get_index(self, s: str) -> int: + """Returns the index of `s` in the string table, adding it if it's not already + present.""" + for idx, entry in enumerate(self.table): + if entry == s: + return idx + else: + idx = len(self.table) + self.table.append(s) + return idx + + def get_string(self, idx: int) -> str: + """Returns the string corresponding to `idx` in the string table.""" + return self.table[idx] diff --git a/guppylang-internals/src/guppylang_internals/metadata/max_qubits.py b/guppylang-internals/src/guppylang_internals/metadata/max_qubits.py new file mode 100644 index 000000000..06ae61934 --- /dev/null +++ b/guppylang-internals/src/guppylang_internals/metadata/max_qubits.py @@ -0,0 +1,20 @@ +from dataclasses import dataclass + +from hugr.metadata import Metadata +from hugr.utils import JsonType + + +@dataclass(frozen=True) +class MetadataMaxQubits(Metadata[int]): + KEY = "tket.hint.max_qubits" + + @classmethod + def to_json(cls, value: int) -> JsonType: + return value + + @classmethod + def from_json(cls, value: JsonType) -> int: + if not isinstance(value, int): + msg = f"Expected an integer for MetadataMaxQubits, but got {type(value)}" + raise TypeError(msg) + return value diff --git a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/array.py b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/array.py index 0b577e58b..f3b8b3315 100644 --- a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/array.py +++ b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/array.py @@ -20,6 +20,8 @@ if TYPE_CHECKING: from hugr.build.dfg import DfBase + from guppylang_internals.ast_util import AstNode + # ------------------------------------------------------ # --------------- std.array operations ----------------- @@ -246,13 +248,19 @@ def array_swap(elem_ty: ht.Type, length: ht.TypeArg) -> ops.ExtOp: P = TypeVar("P", bound=ops.DfParentOp) -def unpack_array(builder: DfBase[P], array: Wire) -> list[Wire]: +def unpack_array( + builder: DfBase[P], array: Wire, ast_node: AstNode | None = None +) -> list[Wire]: """Unpacks a fixed length array into its elements.""" + from guppylang_internals.compiler.expr_compiler import add_op + array_ty = builder.hugr.port_type(array.out_port()) assert isinstance(array_ty, ht.ExtType) match array_ty.args: case [ht.BoundedNatArg(length), ht.TypeTypeArg(elem_ty)]: - res = builder.add_op(array_unpack(elem_ty, length), array) + res = add_op( + builder, array_unpack(elem_ty, length), array, ast_node=ast_node + ) return [res[i] for i in range(length)] case _: raise InternalGuppyError("Invalid array type args") @@ -290,7 +298,7 @@ def build_classical_array(self, elems: list[Wire]) -> Wire: def build_linear_array(self, elems: list[Wire]) -> Wire: """Lowers a call to `array.__new__` for linear arrays.""" - return self.builder.add_op(array_new(self.elem_ty, len(elems)), *elems) + return self.add_op(array_new(self.elem_ty, len(elems)), *elems) def compile(self, args: list[Wire]) -> list[Wire]: if self.elem_ty.type_bound() == ht.TypeBound.Linear: @@ -304,9 +312,9 @@ class ArrayGetitemCompiler(ArrayCompiler): def _build_classical_getitem(self, array: Wire, idx: Wire) -> CallReturnWires: """Constructs `__getitem__` for classical arrays.""" - idx = self.builder.add_op(convert_itousize(), idx) + idx = self.add_op(convert_itousize(), idx) - opt_elem, arr = self.builder.add_op( + opt_elem, arr = self.add_op( array_get(self.elem_ty, self.length), array, idx, @@ -319,8 +327,8 @@ def _build_classical_getitem(self, array: Wire, idx: Wire) -> CallReturnWires: def _build_linear_getitem(self, array: Wire, idx: Wire) -> CallReturnWires: """Constructs `array.__getitem__` for linear arrays.""" - idx = self.builder.add_op(convert_itousize(), idx) - arr, elem = self.builder.add_op( + idx = self.add_op(convert_itousize(), idx) + arr, elem = self.add_op( barray_borrow(self.elem_ty, self.length), array, idx, @@ -358,8 +366,8 @@ def _build_classical_setitem( self, array: Wire, idx: Wire, elem: Wire ) -> CallReturnWires: """Constructs `__setitem__` for classical arrays.""" - idx = self.builder.add_op(convert_itousize(), idx) - result = self.builder.add_op( + idx = self.add_op(convert_itousize(), idx) + result = self.add_op( array_set(self.elem_ty, self.length), array, idx, @@ -376,8 +384,8 @@ def _build_linear_setitem( self, array: Wire, idx: Wire, elem: Wire ) -> CallReturnWires: """Constructs `array.__setitem__` for linear arrays.""" - idx = self.builder.add_op(convert_itousize(), idx) - arr = self.builder.add_op( + idx = self.add_op(convert_itousize(), idx) + arr = self.add_op( barray_return(self.elem_ty, self.length), array, idx, @@ -408,7 +416,7 @@ class ArrayDiscardAllUsedCompiler(ArrayCompiler): def compile(self, args: list[Wire]) -> list[Wire]: if self.elem_ty.type_bound() == ht.TypeBound.Linear: [arr] = args - self.builder.add_op( + self.add_op( barray_discard_all_borrowed(self.elem_ty, self.length), arr, ) @@ -420,11 +428,11 @@ class ArrayIsBorrowedCompiler(ArrayCompiler): def compile_with_inouts(self, args: list[Wire]) -> CallReturnWires: [array, idx] = args - idx = self.builder.add_op(convert_itousize(), idx) - array, b = self.builder.add_op( + idx = self.add_op(convert_itousize(), idx) + array, b = self.add_op( barray_is_borrowed(self.elem_ty, self.length), array, idx ) - b = self.builder.add_op(make_opaque(), b) + b = self.add_op(make_opaque(), b) return CallReturnWires(regular_returns=[b], inout_returns=[array]) def compile(self, args: list[Wire]) -> list[Wire]: @@ -437,12 +445,12 @@ class ArraySwapCompiler(ArrayCompiler): def compile_with_inouts(self, args: list[Wire]) -> CallReturnWires: [array, idx1, idx2] = args - idx1 = self.builder.add_op(convert_itousize(), idx1) - idx2 = self.builder.add_op(convert_itousize(), idx2) + idx1 = self.add_op(convert_itousize(), idx1) + idx2 = self.add_op(convert_itousize(), idx2) # Swap returns Either(left=array, right=array) # Left (case 0) is failure, right (case 1) is success - either_result = self.builder.add_op( + either_result = self.add_op( array_swap(self.elem_ty, self.length), array, idx1, diff --git a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/either.py b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/either.py index 976db1b8f..6ac4fffeb 100644 --- a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/either.py +++ b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/either.py @@ -74,8 +74,8 @@ def compile(self, args: list[Wire]) -> list[Wire]: assert isinstance(inp_arg, TypeArg) [inp] = args # Unpack the single input into a row - inp_row = unpack_wire(inp, inp_arg.ty, self.builder, self.ctx) - return [self.builder.add_op(ops.Tag(self.tag, ty), *inp_row)] + inp_row = unpack_wire(inp, inp_arg.ty, self.builder, self.ctx, self.node) + return [self.add_op(ops.Tag(self.tag, ty), *inp_row)] class EitherTestCompiler(EitherCompiler): @@ -110,7 +110,8 @@ def compile(self, args: list[Wire]) -> list[Wire]: with cond.add_case(i) as case: if i == self.tag: out = case.add_op( - ops.Tag(1, ht.Option(*target_tys)), *case.inputs() + ops.Tag(1, ht.Option(*target_tys)), + *case.inputs(), ) else: out = case.add_op(ops.Tag(0, ht.Option(*target_tys))) @@ -135,4 +136,4 @@ def compile(self, args: list[Wire]) -> list[Wire]: # Pack outputs into a single wire. We're not allowed to return a row since the # signature has a generic return type (also see `TupleType.preserve`) return_ty = get_type(self.node) - return [pack_returns(list(out), return_ty, self.builder, self.ctx)] + return [pack_returns(list(out), return_ty, self.builder, self.ctx, self.node)] diff --git a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/list.py b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/list.py index 389d6faad..ce9eb82d6 100644 --- a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/list.py +++ b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/list.py @@ -112,8 +112,8 @@ def build_classical_getitem( elem_ty: ht.Type, ) -> CallReturnWires: """Lowers a call to `list.__getitem__` for classical lists.""" - idx = self.builder.add_op(convert_itousize(), idx) - result = self.builder.add_op(list_get(elem_ty), list_wire, idx) + idx = self.add_op(convert_itousize(), idx) + result = self.add_op(list_get(elem_ty), list_wire, idx) elem = build_unwrap(self.builder, result, "List index out of bounds") return CallReturnWires(regular_returns=[elem], inout_returns=[list_wire]) @@ -128,11 +128,9 @@ def build_linear_getitem( # implementation of the list type ensures that linear element types are turned # into optionals. elem_opt_ty = ht.Option(elem_ty) - none = self.builder.add_op(ops.Tag(0, elem_opt_ty)) - idx = self.builder.add_op(convert_itousize(), idx) - list_wire, result = self.builder.add_op( - list_set(elem_opt_ty), list_wire, idx, none - ) + none = self.add_op(ops.Tag(0, elem_opt_ty)) + idx = self.add_op(convert_itousize(), idx) + list_wire, result = self.add_op(list_set(elem_opt_ty), list_wire, idx, none) elem_opt = build_unwrap_right(self.builder, result, "List index out of bounds") elem = build_unwrap( self.builder, elem_opt, "Linear list element has already been used" @@ -167,8 +165,8 @@ def build_classical_setitem( elem_ty: ht.Type, ) -> CallReturnWires: """Lowers a call to `list.__setitem__` for classical lists.""" - idx = self.builder.add_op(convert_itousize(), idx) - list_wire, result = self.builder.add_op(list_set(elem_ty), list_wire, idx, elem) + idx = self.add_op(convert_itousize(), idx) + list_wire, result = self.add_op(list_set(elem_ty), list_wire, idx, elem) # Unwrap the result, but we don't have to hold onto the returned old value build_unwrap_right(self.builder, result, "List index out of bounds") return CallReturnWires(regular_returns=[], inout_returns=[list_wire]) @@ -183,11 +181,9 @@ def build_linear_setitem( """Lowers a call to `array.__setitem__` for linear arrays.""" # Embed the element into an optional elem_opt_ty = ht.Option(elem_ty) - elem = self.builder.add_op(ops.Some(elem_ty), elem) - idx = self.builder.add_op(convert_itousize(), idx) - list_wire, result = self.builder.add_op( - list_set(elem_opt_ty), list_wire, idx, elem - ) + elem = self.add_op(ops.Some(elem_ty), elem) + idx = self.add_op(convert_itousize(), idx) + list_wire, result = self.add_op(list_set(elem_opt_ty), list_wire, idx, elem) old_elem_opt = build_unwrap_right( self.builder, result, "List index out of bounds" ) @@ -223,7 +219,7 @@ def build_classical_pop( elem_ty: ht.Type, ) -> CallReturnWires: """Lowers a call to `list.pop` for classical lists.""" - list_wire, result = self.builder.add_op(list_pop(elem_ty), list_wire) + list_wire, result = self.add_op(list_pop(elem_ty), list_wire) elem = build_unwrap(self.builder, result, "List index out of bounds") return CallReturnWires(regular_returns=[elem], inout_returns=[list_wire]) @@ -234,7 +230,7 @@ def build_linear_pop( ) -> CallReturnWires: """Lowers a call to `list.pop` for linear lists.""" elem_opt_ty = ht.Option(elem_ty) - list_wire, result = self.builder.add_op(list_pop(elem_opt_ty), list_wire) + list_wire, result = self.add_op(list_pop(elem_opt_ty), list_wire) elem_opt = build_unwrap(self.builder, result, "List index out of bounds") elem = build_unwrap( self.builder, elem_opt, "Linear list element has already been used" @@ -264,7 +260,7 @@ def build_classical_push( elem_ty: ht.Type, ) -> CallReturnWires: """Lowers a call to `list.push` for classical lists.""" - list_wire = self.builder.add_op(list_push(elem_ty), list_wire, elem) + list_wire = self.add_op(list_push(elem_ty), list_wire, elem) return CallReturnWires(regular_returns=[], inout_returns=[list_wire]) def build_linear_push( @@ -276,8 +272,8 @@ def build_linear_push( """Lowers a call to `list.push` for linear lists.""" # Wrap element into an optional elem_opt_ty = ht.Option(elem_ty) - elem_opt = self.builder.add_op(ops.Some(elem_ty), elem) - list_wire = self.builder.add_op(list_push(elem_opt_ty), list_wire, elem_opt) + elem_opt = self.add_op(ops.Some(elem_ty), elem) + list_wire = self.add_op(list_push(elem_opt_ty), list_wire, elem_opt) return CallReturnWires(regular_returns=[], inout_returns=[list_wire]) def compile_with_inouts(self, args: list[Wire]) -> CallReturnWires: @@ -307,8 +303,8 @@ def compile_with_inouts(self, args: list[Wire]) -> CallReturnWires: elem_ty = elem_ty_arg.ty.to_hugr(self.ctx) if elem_ty_arg.ty.linear: elem_ty = ht.Option(elem_ty) - list_wire, length = self.builder.add_op(list_length(elem_ty), list_wire) - length = self.builder.add_op(convert_ifromusize(), length) + list_wire, length = self.add_op(list_length(elem_ty), list_wire) + length = self.add_op(convert_ifromusize(), length) return CallReturnWires(regular_returns=[length], inout_returns=[list_wire]) def compile(self, args: list[Wire]) -> list[Wire]: diff --git a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/option.py b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/option.py index 988a3d8c8..d5a6bf48c 100644 --- a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/option.py +++ b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/option.py @@ -39,7 +39,7 @@ def __init__(self, tag: int): self.tag = tag def compile(self, args: list[Wire]) -> list[Wire]: - return [self.builder.add_op(ops.Tag(self.tag, self.option_ty), *args)] + return [self.add_op(ops.Tag(self.tag, self.option_ty), *args)] class OptionTestCompiler(OptionCompiler): diff --git a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/platform.py b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/platform.py index 46703cfe2..958838d7d 100644 --- a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/platform.py +++ b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/platform.py @@ -66,11 +66,11 @@ def compile(self, args: list[Wire]) -> list[Wire]: args.append(tys.BoundedNatArg(NumericType.INT_WIDTH)) # Bool results need an extra conversion into regular hugr bools if is_bool_type(ty): - value = self.builder.add_op(read_bool(), value) + value = self.add_op(read_bool(), value) hugr_ty = tys.Bool op = RESULT_EXTENSION.get_op(self.op_name) sig = tys.FunctionType(input=[hugr_ty], output=[]) - self.builder.add_op(op.instantiate(args, sig), value) + self.add_op(op.instantiate(args, sig), value) return [] @@ -97,19 +97,17 @@ def compile_with_inouts(self, args: list[Wire]) -> CallReturnWires: # argument). hugr_elem_ty = elem_ty.to_hugr(self.ctx) hugr_size = size_arg.to_hugr(self.ctx) - arr, out_arr = self.builder.add_op(array_clone(hugr_elem_ty, hugr_size), arr) + arr, out_arr = self.add_op(array_clone(hugr_elem_ty, hugr_size), arr) # For bool arrays, we furthermore need to coerce a read on all the array # elements if is_bool_type(elem_ty): array_read = array_read_bool(self.ctx) array_read = self.builder.load_function(array_read) map_op = array_map(OpaqueBool, hugr_size, tys.Bool) - arr = self.builder.add_op(map_op, arr, array_read).out(0) + arr = self.add_op(map_op, arr, array_read).out(0) hugr_elem_ty = tys.Bool # Turn `borrow_array` into regular `array` - arr = self.builder.add_op(array_to_std_array(hugr_elem_ty, hugr_size), arr).out( - 0 - ) + arr = self.add_op(array_to_std_array(hugr_elem_ty, hugr_size), arr).out(0) hugr_ty = hugr.std.collections.array.Array(hugr_elem_ty, hugr_size) sig = tys.FunctionType(input=[hugr_ty], output=[]) @@ -117,7 +115,7 @@ def compile_with_inouts(self, args: list[Wire]) -> CallReturnWires: if self.with_int_width: args.append(tys.BoundedNatArg(NumericType.INT_WIDTH)) op = ops.ExtOp(RESULT_EXTENSION.get_op(self.op_name), signature=sig, args=args) - self.builder.add_op(op, arr) + self.add_op(op, arr) return CallReturnWires([], [out_arr]) diff --git a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/prelude.py b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/prelude.py index 90149333a..5304c3d93 100644 --- a/guppylang-internals/src/guppylang_internals/std/_internal/compiler/prelude.py +++ b/guppylang-internals/src/guppylang_internals/std/_internal/compiler/prelude.py @@ -259,7 +259,7 @@ def compile_with_inouts(self, args: list[Wire]) -> CallReturnWires: output=[ht.Either([error_type()], self.ty.output)], ) op = self.op(opt_func_type, self.type_args, self.ctx) - either = self.builder.add_op(op, *args) + either = self.add_op(op, *args) result = unwrap_result(self.builder, self.ctx, either) return CallReturnWires(regular_returns=[result], inout_returns=[]) @@ -274,7 +274,7 @@ def compile_with_inouts(self, args: list[Wire]) -> CallReturnWires: [ht.ListArg([ht.TypeTypeArg(ty) for ty in tys])] ) - barrier_n = self.builder.add_op(op, *args) + barrier_n = self.add_op(op, *args) return CallReturnWires( regular_returns=[], inout_returns=[barrier_n[i] for i in range(len(tys))] diff --git a/guppylang/src/guppylang/decorator.py b/guppylang/src/guppylang/decorator.py index 7c2ced442..ee2de0b0a 100644 --- a/guppylang/src/guppylang/decorator.py +++ b/guppylang/src/guppylang/decorator.py @@ -27,7 +27,6 @@ from guppylang_internals.definition.function import ( RawFunctionDef, ) -from guppylang_internals.definition.metadata import GuppyMetadata from guppylang_internals.definition.overloaded import OverloadedFunctionDef from guppylang_internals.definition.parameter import ( ConstVarDef, @@ -43,6 +42,7 @@ from guppylang_internals.definition.ty import TypeDef from guppylang_internals.dummy_decorator import _DummyGuppy, sphinx_running from guppylang_internals.engine import DEF_STORE +from guppylang_internals.metadata.common import FunctionMetadata from guppylang_internals.span import Loc, SourceMap, Span from guppylang_internals.tracing.util import hide_trace from guppylang_internals.tys.arg import Argument @@ -725,7 +725,7 @@ def _with_optional_kwargs( class ParsedGuppyKwargs(NamedTuple): flags: UnitaryFlags - metadata: GuppyMetadata + metadata: FunctionMetadata link_name: str | None @@ -744,8 +744,9 @@ def _parse_kwargs(kwargs: GuppyKwargs) -> ParsedGuppyKwargs: if kwargs.pop("power", False): flags |= UnitaryFlags.Power - metadata = GuppyMetadata() - metadata.max_qubits.value = kwargs.pop("max_qubits", None) + metadata = FunctionMetadata() + if "max_qubits" in kwargs: + metadata.set_max_qubits(kwargs.pop("max_qubits")) link_name = kwargs.pop("link_name", None) diff --git a/guppylang/src/guppylang/defs.py b/guppylang/src/guppylang/defs.py index 4dc10ccc8..e0368d9de 100644 --- a/guppylang/src/guppylang/defs.py +++ b/guppylang/src/guppylang/defs.py @@ -121,7 +121,7 @@ def emulator( isinstance(self.wrapped, RawFunctionDef) and self.wrapped.metadata is not None ): - hinted_qubits = self.wrapped.metadata.max_qubits.value + hinted_qubits = self.wrapped.metadata.get_max_qubits() if qubits is None: qubits = hinted_qubits elif hinted_qubits is not None and qubits < hinted_qubits: diff --git a/pyproject.toml b/pyproject.toml index 2e8556c11..4b80f2ba7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ guppylang = { workspace = true } guppylang-internals = { workspace = true } miette-py = { workspace = true } # Uncomment these to test the latest dependency version during development -# hugr = { git = "https://github.com/quantinuum/hugr", subdirectory = "hugr-py", rev = "191c473" } +hugr = { git = "https://github.com/quantinuum/hugr", subdirectory = "hugr-py", rev = "6b0358e" } [build-system] requires = ["hatchling"] diff --git a/tests/definition/__init__.py b/tests/definition/__init__.py deleted file mode 100644 index c922290ad..000000000 --- a/tests/definition/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Unit tests for guppylang_internals.definition diff --git a/tests/metadata/__init__.py b/tests/metadata/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/metadata/test_debug_info.py b/tests/metadata/test_debug_info.py new file mode 100644 index 000000000..7501e3c82 --- /dev/null +++ b/tests/metadata/test_debug_info.py @@ -0,0 +1,182 @@ +from guppylang.std import array +from guppylang.std.debug import state_result +from guppylang.std.quantum import discard, discard_array, qubit +from guppylang_internals.debug_mode import turn_off_debug_mode, turn_on_debug_mode +from hugr.debug_info import DICompileUnit, DILocation, DISubprogram +from hugr.metadata import HugrDebugInfo +from hugr.ops import Call, ExtOp, FuncDecl, FuncDefn, MakeTuple + +from guppylang import guppy +from tests.resources.metadata_example import ( + bar, + baz, + comptime_bar, + pytket_bar_load, + pytket_bar_stub, +) + +turn_on_debug_mode() + + +def get_last_uri_part(uri: str) -> str: + return uri.split("/")[-1] + + +def get_last_name_part(file_name: str) -> str: + return file_name.split(".")[-1] + + +def test_compile_unit(): + @guppy + def foo() -> None: + pass + + hugr = foo.compile().modules[0] + meta = hugr.module_root.metadata + assert HugrDebugInfo in meta + debug_info = DICompileUnit.from_json(meta[HugrDebugInfo.KEY]) + assert get_last_uri_part(debug_info.directory) == "guppylang" + assert get_last_uri_part(debug_info.file_table[0]) == "test_debug_info.py" + assert debug_info.filename == 0 + + +def test_subprogram(): + @guppy + def foo() -> None: + bar() + baz() + comptime_bar() + q = qubit() + pytket_bar_load(q) + pytket_bar_stub(q) + discard(q) + + hugr = foo.compile().modules[0] + meta = hugr.module_root.metadata + assert HugrDebugInfo in meta + debug_info = DICompileUnit.from_json(meta[HugrDebugInfo.KEY]) + assert [get_last_uri_part(uri) for uri in debug_info.file_table] == [ + "test_debug_info.py", + "metadata_example.py", + ] + funcs = hugr.children(hugr.module_root) + for func in funcs: + op = hugr[func].op + assert isinstance(op, FuncDefn | FuncDecl) + match get_last_name_part(op.f_name): + case "foo": + assert HugrDebugInfo in func.metadata + debug_info = DISubprogram.from_json(func.metadata[HugrDebugInfo.KEY]) + assert debug_info.file == 0 + assert debug_info.line_no == 45 + assert debug_info.scope_line == 46 + case "bar": + assert HugrDebugInfo in func.metadata + debug_info = DISubprogram.from_json(func.metadata[HugrDebugInfo.KEY]) + assert debug_info.file == 1 + assert debug_info.line_no == 10 + assert debug_info.scope_line == 13 + case "baz": + assert HugrDebugInfo in func.metadata + debug_info = DISubprogram.from_json(func.metadata[HugrDebugInfo.KEY]) + assert debug_info.file == 1 + assert debug_info.line_no == 17 + assert debug_info.scope_line is None + case "comptime_bar": + assert HugrDebugInfo in func.metadata + debug_info = DISubprogram.from_json(func.metadata[HugrDebugInfo.KEY]) + assert debug_info.file == 1 + assert debug_info.line_no == 21 + assert debug_info.scope_line == 22 + case "pytket_bar_load": + assert HugrDebugInfo in func.metadata + debug_info = DISubprogram.from_json(func.metadata[HugrDebugInfo.KEY]) + assert debug_info.file == 1 + assert debug_info.line_no == 28 + assert debug_info.scope_line is None + case "pytket_bar_stub": + assert HugrDebugInfo in func.metadata + debug_info = DISubprogram.from_json(func.metadata[HugrDebugInfo.KEY]) + assert debug_info.file == 1 + assert debug_info.line_no == 32 + assert debug_info.scope_line is None + case "": + # No metadata on the inner circuit function. + assert HugrDebugInfo not in func.metadata + case _: + raise AssertionError(f"Unexpected function name {op.f_name}") + + +def test_call_location(): + @guppy + def foo() -> None: + bar() # call 1 + comptime_bar() # call 2 + q = qubit() # compiles to extension op (see test below) + pytket_bar_load(q) # call 3 + inner circuit function call 4 + discard(q) # compiles to extension op (see test below) + + hugr = foo.compile().modules[0] + calls = [node for node, node_data in hugr.nodes() if isinstance(node_data.op, Call)] + assert len(calls) == 4 + lines = [] + for call in calls: + assert HugrDebugInfo in call.metadata + debug_info = DILocation.from_json(call.metadata[HugrDebugInfo.KEY]) + if debug_info.line_no == 28: + assert debug_info.column == 0 + else: + assert debug_info.column == 8 + lines.append(debug_info.line_no) + assert lines == [113, 114, 28, 116] + + +# TODO: Improve this test. +def test_ext_op_location(): + @guppy.struct + class MyStruct: + x: int + + @guppy + def foo() -> None: + MyStruct(1) # Defined through `custom_function` (`MakeTuple` node) + q = qubit() # Defined through `hugr_op` + arr = array(q) # Forces the use of various array extension ops + state_result("tag", arr) # Defined through `custom_function` (custom node) + discard_array(arr) # Defined through `hugr_op` + + hugr = foo.compile().modules[0] + # TODO: Figure out how to attach metadata to these nodes. + # TODO: Find other such limitations and add tests for them. + known_limitations = [ + "tket.bool.read", + "prelude.panic<[Type(Tuple(int<6>, Tuple(int<6>, int<6>, int<6>)))], []>", + "prelude.panic<[], [Type(Tuple(int<6>, Tuple(int<6>, int<6>, int<6>)))]>", + ] + found_annotated_tuples = [] + for node, node_data in hugr.nodes(): + if ( + isinstance(node_data.op, ExtOp) + and node_data.op.name() not in known_limitations + ): + assert HugrDebugInfo in node.metadata + debug_info = DILocation.from_json(node.metadata[HugrDebugInfo.KEY]) + # Check constructor is annotated. + if isinstance(node_data.op, MakeTuple) and HugrDebugInfo in node.metadata: + debug_info = DILocation.from_json(node.metadata[HugrDebugInfo.KEY]) + found_annotated_tuples.append(debug_info.line_no) + assert 142 in found_annotated_tuples + + +def test_turn_off_debug_mode(): + turn_off_debug_mode() + + @guppy + def foo() -> None: + q = qubit() + state_result("tag", q) + discard(q) + + hugr = foo.compile().modules[0] + for node, _ in hugr.nodes(): + assert HugrDebugInfo not in node.metadata diff --git a/tests/definition/test_metadata.py b/tests/metadata/test_metadata.py similarity index 65% rename from tests/definition/test_metadata.py rename to tests/metadata/test_metadata.py index a3fc5e479..36438251d 100644 --- a/tests/definition/test_metadata.py +++ b/tests/metadata/test_metadata.py @@ -3,24 +3,25 @@ from unittest.mock import Mock import pytest -from guppylang_internals.definition.metadata import ( - GuppyMetadata, +from guppylang_internals.error import GuppyError +from guppylang_internals.metadata.common import ( + FunctionMetadata, MetadataAlreadySetError, ReservedMetadataKeysError, add_metadata, ) -from guppylang_internals.error import GuppyError +from hugr.metadata import NodeMetadata def test_add_metadata(): mock_hugr_node = Mock() - mock_hugr_node.metadata = {"some-key": "some-value"} + mock_hugr_node.metadata = NodeMetadata({"some-key": "some-value"}) - guppy_metadata = GuppyMetadata() - guppy_metadata.max_qubits.value = 5 + guppy_metadata = FunctionMetadata() + guppy_metadata.set_max_qubits(5) add_metadata(mock_hugr_node, guppy_metadata) - assert mock_hugr_node.metadata == { + assert mock_hugr_node.metadata.as_dict() == { "some-key": "some-value", "tket.hint.max_qubits": 5, } @@ -28,11 +29,11 @@ def test_add_metadata(): def test_add_additional_metadata(): mock_hugr_node = Mock() - mock_hugr_node.metadata = {"some-key": "some-value"} + mock_hugr_node.metadata = NodeMetadata({"some-key": "some-value"}) add_metadata(mock_hugr_node, additional_metadata={"more-key": "more-value"}) - assert mock_hugr_node.metadata == { + assert mock_hugr_node.metadata.as_dict() == { "some-key": "some-value", "more-key": "more-value", } @@ -40,7 +41,7 @@ def test_add_additional_metadata(): def test_add_metadata_no_reserved_metadata(): mock_hugr_node = Mock() - mock_hugr_node.metadata = {} + mock_hugr_node.metadata = NodeMetadata({}) with pytest.raises( GuppyError, @@ -54,13 +55,15 @@ def test_add_metadata_no_reserved_metadata(): def test_add_metadata_metadata_already_set(): mock_hugr_node = Mock() - mock_hugr_node.metadata = { - "tket.hint.max_qubits": 1, - "preset-key": "preset-value", - } - - guppy_metadata = GuppyMetadata() - guppy_metadata.max_qubits.value = 5 + mock_hugr_node.metadata = NodeMetadata( + { + "tket.hint.max_qubits": 1, + "preset-key": "preset-value", + } + ) + + guppy_metadata = FunctionMetadata() + guppy_metadata.set_max_qubits(5) with pytest.raises( GuppyError, check=lambda e: ( @@ -81,10 +84,10 @@ def test_add_metadata_metadata_already_set(): def test_add_metadata_property_max_qubits(): mock_hugr_node = Mock() - mock_hugr_node.metadata = {} + mock_hugr_node.metadata = NodeMetadata({}) - guppy_metadata = GuppyMetadata() - guppy_metadata.max_qubits.value = 5 + guppy_metadata = FunctionMetadata() + guppy_metadata.set_max_qubits(5) add_metadata(mock_hugr_node, guppy_metadata) - assert mock_hugr_node.metadata == {"tket.hint.max_qubits": 5} + assert mock_hugr_node.metadata.as_dict() == {"tket.hint.max_qubits": 5} diff --git a/tests/test_version_metadata.py b/tests/metadata/test_version_metadata.py similarity index 100% rename from tests/test_version_metadata.py rename to tests/metadata/test_version_metadata.py diff --git a/tests/resources/metadata_example.py b/tests/resources/metadata_example.py new file mode 100644 index 000000000..619dbc0cb --- /dev/null +++ b/tests/resources/metadata_example.py @@ -0,0 +1,32 @@ +"""File used to test the filename table in debug info metadata.""" + +from guppylang.std.quantum import qubit +from pytket import Circuit + +from guppylang import guppy + + +@guppy +def bar() -> None: + # Leave white space to check scope_line is set correctly. + + pass + + +@guppy.declare +def baz() -> None: ... + + +@guppy.comptime +def comptime_bar() -> None: + pass + + +circ = Circuit(1) +circ.H(0) + +pytket_bar_load = guppy.load_pytket("pytket_bar_load", circ, use_arrays=False) + + +@guppy.pytket(circ) +def pytket_bar_stub(q1: qubit) -> None: ... diff --git a/uv.lock b/uv.lock index 439b1e8a3..db3a6ebc4 100644 --- a/uv.lock +++ b/uv.lock @@ -974,7 +974,7 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "hugr", specifier = "~=0.15.4" }, + { name = "hugr", git = "https://github.com/quantinuum/hugr?subdirectory=hugr-py&rev=6b0358e" }, { name = "pytket", specifier = ">=1.34" }, { name = "tket", specifier = ">=0.12.7" }, { name = "tket-exts", specifier = "~=0.12.0" }, @@ -985,8 +985,8 @@ provides-extras = ["pytket"] [[package]] name = "hugr" -version = "0.15.4" -source = { registry = "https://pypi.org/simple" } +version = "0.15.5" +source = { git = "https://github.com/quantinuum/hugr?subdirectory=hugr-py&rev=6b0358e#6b0358ee34258040c410b0446cec9a45e709c55f" } dependencies = [ { name = "graphviz" }, { name = "pydantic" }, @@ -994,35 +994,6 @@ dependencies = [ { name = "semver" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f2/fe/676058e746b7509d2c80123c22444d81e5f470b7bdcd2c1159185b9a4749/hugr-0.15.4.tar.gz", hash = "sha256:0a0d72daa37854dd933fcea7c4ee0c715c21efdf2365700762f9c6f57afc0c50", size = 1050567, upload-time = "2026-02-20T14:11:24.043Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/f1/fb73bdf8d8da5c01338785163b3de5331c8bc31f5f4a4410eabd1d1ea7c9/hugr-0.15.4-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:9002ce346931e20240c14d2dccde3e6f7e51ec77834b444a9b1d8c70fd954415", size = 3716743, upload-time = "2026-02-20T14:11:20.679Z" }, - { url = "https://files.pythonhosted.org/packages/e8/f2/a368acdebfec252c1301327fa50faf7306110437a5f8c5a73332141b83d9/hugr-0.15.4-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:203f733efe157c9c43d0314e5e7afe723416a1ac9088b53ac4b907785219fc5a", size = 3313534, upload-time = "2026-02-20T14:11:17.379Z" }, - { url = "https://files.pythonhosted.org/packages/49/f8/4efcca2432ce8dbb00471790967ae17aaad9af438bd162750edaca909a44/hugr-0.15.4-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bf973c130aff874008b912e17683e5cbfffdc9db2793515945c43aedbee41b1", size = 3642898, upload-time = "2026-02-20T14:10:34.487Z" }, - { url = "https://files.pythonhosted.org/packages/ff/11/8f52c403f85e13330d6adec274b1b3ad0f41a91542cf1d8364029997643e/hugr-0.15.4-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e8d962f2bdad0fd265ae21470b28f595e652dafde60fb50af0d0c565cf5b3b3b", size = 3643046, upload-time = "2026-02-20T14:10:38.462Z" }, - { url = "https://files.pythonhosted.org/packages/81/b9/4d1bce1a9525428b51a4f41cb48d257bfff811488756a90026e0a4e18f73/hugr-0.15.4-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:85d489ada2727c2cfcfccfd03d6b2dc007c76b2655425d30d6004d26abf2223c", size = 3909770, upload-time = "2026-02-20T14:10:49.763Z" }, - { url = "https://files.pythonhosted.org/packages/35/6d/eaef430e984f0ef715b5857e85ec1a347f837850a03a9c34f5ff08740cd3/hugr-0.15.4-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7614c5a66969ebea0c2eb5098f8dc985fbe7b9edc81e70cce98326bf0bf18c67", size = 4097902, upload-time = "2026-02-20T14:10:41.868Z" }, - { url = "https://files.pythonhosted.org/packages/df/e4/8f383056983052f0d729455bad4595c35014ce6903bfe9895693f0efc4ac/hugr-0.15.4-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6252da7f1dddc2d854420700fd1a5dd67f215c8c5182d469eedc023aa320484c", size = 4179798, upload-time = "2026-02-20T14:10:45.588Z" }, - { url = "https://files.pythonhosted.org/packages/91/a8/b420dcbf6902637f68a5210635fc0cab3505605739c635ecf0cb60025098/hugr-0.15.4-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39c025244e0e66ef7b18735ae31f2909d0553843375922ffe2afb4231f3271da", size = 3983149, upload-time = "2026-02-20T14:10:53.369Z" }, - { url = "https://files.pythonhosted.org/packages/e8/3a/1e5af2a8a9521c3e5813f2269c088bbd0b2ca90b9db0ed8374f36c1dd0f4/hugr-0.15.4-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9cb1402a8d363c81f0f1d0831bb1951f534a666b79fa55b72be5416de9b34acc", size = 3853876, upload-time = "2026-02-20T14:10:56.595Z" }, - { url = "https://files.pythonhosted.org/packages/b0/0e/d7b3954c306d38cc86ca9af8f0962c3fe63ed38cbf9e56ba7a5075ebbdc1/hugr-0.15.4-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:392c7c316a521129ff38414ed779a9b129517b10c733e8936f167901fce43f0f", size = 3921103, upload-time = "2026-02-20T14:10:59.967Z" }, - { url = "https://files.pythonhosted.org/packages/c6/9f/3c66b77328dc46cb4c3d3df0f4cbfb7733346774e7a125525f23d8fddfbc/hugr-0.15.4-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:bccfd6293928924dbcd65793f72f958b1db73785924fdf921dcd48632efe4345", size = 3996237, upload-time = "2026-02-20T14:11:03.751Z" }, - { url = "https://files.pythonhosted.org/packages/f8/3a/0cddb1f0d5ccefa2130f1aa03f592c4f65fd65d5bf0ae4268a007fbfeba9/hugr-0.15.4-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8c4e6d9ef297849c46fd9827db48b3e768b041c96e4ccb4ed036cdfa3b69a055", size = 4218977, upload-time = "2026-02-20T14:11:10.917Z" }, - { url = "https://files.pythonhosted.org/packages/e3/3a/abaca253c27a7fda9078002917649a2b431114b83d7a07c37b3bd737e12e/hugr-0.15.4-cp310-abi3-win32.whl", hash = "sha256:98a633f30cb3334786ad847465670df44c27bd5c8b16d81f45d7ba7e9925ea59", size = 3252870, upload-time = "2026-02-20T14:11:15.821Z" }, - { url = "https://files.pythonhosted.org/packages/f7/46/f67200d556b36f321e240cd82cf79ac27e8208698d2aef192dbc376c08b5/hugr-0.15.4-cp310-abi3-win_amd64.whl", hash = "sha256:ec416a0bf673a67efe52d2b8e7912921839cffb797f11ac79ab360cac4bee2ce", size = 3553381, upload-time = "2026-02-20T14:11:14.216Z" }, - { url = "https://files.pythonhosted.org/packages/b5/91/3f24f7d9af4ac945ba96ec4fa0174891d220a14de0f01dec25b52617ee0f/hugr-0.15.4-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:efdc2521d62854cb1b410fc58fd0cdcdb4b7a0fc0ad6541402aee129792eea37", size = 3715189, upload-time = "2026-02-20T14:11:22.574Z" }, - { url = "https://files.pythonhosted.org/packages/56/6c/f7d6be5911299f20c26a3bab5489fdf60cd2249e754a8b18e3d8955d0a83/hugr-0.15.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:00c79ba03ebd93cd2930452adfe096396849f54ecb7f249dd3da6e75bf5593b6", size = 3309749, upload-time = "2026-02-20T14:11:19.07Z" }, - { url = "https://files.pythonhosted.org/packages/a3/1a/72050d4744ba97ddeaa5b073eb9489d680b9838667696f7f83e4c11196fe/hugr-0.15.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:685c2ca64b2a1dad94dc025501bb93a9742cc4dc97d1886fa65e0eed33c8c607", size = 3643638, upload-time = "2026-02-20T14:10:36.639Z" }, - { url = "https://files.pythonhosted.org/packages/8b/c7/db23b9364c84f72d9d950afaaabe319b5eb5ed3fe36ca6b66b7a7531c0cd/hugr-0.15.4-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9e57da8071a987be37fbe294a8049b8ce7d61494c4e1b7ec8cfafd5f6894896", size = 3644120, upload-time = "2026-02-20T14:10:40.076Z" }, - { url = "https://files.pythonhosted.org/packages/92/71/3ded41c860f90b00353894330e196c1b9fc4a89a5625cc538ae0ed5bcb7e/hugr-0.15.4-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:731e4405832aa4f5be8dd5e30a853ebbd88d9ac9e50f6ca9e2a40690d771ed7a", size = 3909806, upload-time = "2026-02-20T14:10:51.786Z" }, - { url = "https://files.pythonhosted.org/packages/a3/38/cb749ba447e790b5e2afb96e6a66689c8a427c95f5cb9dcbf5ca056f99b2/hugr-0.15.4-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8056372332789d1328c25ba493b756dc6918c7ff93677637c1be0affb8c4675", size = 4096032, upload-time = "2026-02-20T14:10:43.358Z" }, - { url = "https://files.pythonhosted.org/packages/c2/de/96a83a31973027e1bf5f4ae51a9d20d0ec18644285375a1fc8160430bb2e/hugr-0.15.4-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f60bccf1d8a240d4e47337495e1c3796f7b4427598f327e9ebba823864933cb4", size = 4180673, upload-time = "2026-02-20T14:10:47.559Z" }, - { url = "https://files.pythonhosted.org/packages/a0/ca/5888c6a6a3b1a67343bb863741e8f9f7faf82bc5b04a8e54afc8a99366c4/hugr-0.15.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6547b072bfb3fdc892992969669b9a1af7526365f9ba067d085a17bca4b9056b", size = 3982740, upload-time = "2026-02-20T14:10:54.914Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e2/c790d50e5bc909444ae372c80a788df817a91aa783851b7b95d475ebcc04/hugr-0.15.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e695834e99ddcf98465a5d5907c5877edd9bcd1c68f60372b770a75b838022bd", size = 3853146, upload-time = "2026-02-20T14:10:58.198Z" }, - { url = "https://files.pythonhosted.org/packages/87/a3/f6799c8380c495af3ebd1341becf53feb11721807fafa602ec4d848d24e4/hugr-0.15.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fde6826753c7b3e9581b92231890706a084f9be1d063620aa68a4869408b0df8", size = 3923421, upload-time = "2026-02-20T14:11:01.809Z" }, - { url = "https://files.pythonhosted.org/packages/1f/23/79429d327aca17b1074c394c082e18d0860e633d7d360a64e6a02f179273/hugr-0.15.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7f52df31ecab7e8aaaa28be47671e2fb9ebeaf2ea978665fb851e64e4460a1b7", size = 3995649, upload-time = "2026-02-20T14:11:07.565Z" }, - { url = "https://files.pythonhosted.org/packages/10/fe/83616826ab058c80d02c9e69b9330156713f4642403d83123d237e90d464/hugr-0.15.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a81eda22b9315906ef90fa73a9c4193fb5fd3485ea7a879f3c61a77e33c85450", size = 4217283, upload-time = "2026-02-20T14:11:12.667Z" }, -] [[package]] name = "identify"