[Debug] Add scope op (#6454)

Add the `dbg.scope` operation to the debug dialect. The op creates an
additional level of hierarchy in the DI, a "scope", which can be used to
group variables and other scopes.

Operations such as `hw.module` introduce an implicit scope. All debug
operations within a module are added to that implicit scope, unless they
have an explicit `scope` operand. Providing an explicit scope can be
used to represent inlined modules.

Scopes in DI do not necessarily have to correspond to levels of a module
hierarchy. They can also be used to model things like control flow
scopes, call stacks, and other source-language concepts.

This commit also introduces an optional `scope` operand on
`dbg.variable`. The `DebugInfo` analysis, which traverses the IR and
builds up a canonical representation of the DI, honors this operand and
adds the variables to the corresponding scope.
This commit is contained in:
Fabian Schuiki 2023-12-08 09:38:46 -08:00 committed by GitHub
parent fd9392d31e
commit 594b8f65df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 160 additions and 6 deletions

View File

@ -140,6 +140,45 @@ still expose the constant values under the name `Depth` and `Width` in the debug
info.
## Tracking Inlined Modules
The `dbg.scope` op can be used to track debug information about inlined modules.
By default, operations such as `hw.module` in conjunction with `hw.instance`
introduce an implicit module scope. All debug operations within a module are
added to that implicit scope, unless they have an explicit `scope` operand. This
explicit scope operand can be used to group the DI of an inlined module.
Consider the following modules:
```
hw.module @Foo(in %a: i42) {
dbg.variable "a", %a : i42
hw.instance "bar" @Bar(x: %a: i42)
}
hw.module @Bar(in %x: i42) {
dbg.variable "x", %x : i42
%0 = comb.mul %x, %x : i42
dbg.variable "squared", %0 : i42
}
```
If we inline module `Bar`, we can introduce a `dbg.scope` operation to represent
the original instance, and group all debug variables in `Bar` under this
explicit scope:
```
hw.module @Foo(in %a: i42) {
dbg.variable "a", %a : i42
%0 = dbg.scope "bar", "Bar"
dbg.variable "x", %a scope %0 : i42
%1 = comb.mul %a, %a : i42
dbg.variable "squared", %1 scope %0 : i42
}
```
Despite the fact that the instance op no longer exists, the explicit `dbg.scope`
op models the additional levle of hierarchy that used to exist in the input.
## Types

View File

@ -34,6 +34,8 @@ struct DIModule {
SmallVector<DIVariable *, 0> variables;
/// If this is an extern declaration.
bool isExtern = false;
/// If this is an inline scope created by a `dbg.scope` operation.
bool isInline = false;
};
struct DIInstance {

View File

@ -19,6 +19,37 @@ class DebugOp<string mnemonic, list<Trait> traits = []> :
Op<DebugDialect, mnemonic, traits>;
def ScopeOp : DebugOp<"scope"> {
let summary = "Define a scope for debug values";
let description = [{
Creates an additional level of hierarchy in the DI, a "scope", which can be
used to group variables and other scopes.
Operations such as `hw.module` introduce an implicit scope. All debug
operations within a module are added to that implicit scope, unless they
have an explicit `scope` operand. Providing an explicit scope can be used to
represent inlined modules.
Scopes in DI do not necessarily have to correspond to levels of a module
hierarchy. They can also be used to model things like control flow scopes,
call stacks, and other source-language concepts.
The `scope` operand of any debug dialect operation must be defined locally
by a `dbg.scope` operation. It cannot be a block argument. (This is intended
as a temporary restriction, to be lifted in the future.)
}];
let arguments = (ins
StrAttr:$instanceName,
StrAttr:$moduleName,
Optional<ScopeType>:$scope
);
let results = (outs ScopeType:$result);
let assemblyFormat = [{
$instanceName `,` $moduleName (`scope` $scope^)? attr-dict
}];
}
def VariableOp : DebugOp<"variable"> {
let summary = "A named value to be captured in debug info";
let description = [{
@ -34,10 +65,17 @@ def VariableOp : DebugOp<"variable"> {
these source language values can be reconstituted from the actual IR values
present at the end of compilation.
See the rationale for examples and details.
See the rationale for examples and details. See the `dbg.scope` operation
for additional details on how to use the `scope` operand.
}];
let arguments = (ins
StrAttr:$name,
AnyType:$value,
Optional<ScopeType>:$scope
);
let assemblyFormat = [{
$name `,` $value (`scope` $scope^)? attr-dict `:` type($value)
}];
let arguments = (ins StrAttr:$name, AnyType:$value);
let assemblyFormat = [{ $name `,` $value attr-dict `:` type($value) }];
}

View File

@ -14,6 +14,12 @@ include "mlir/IR/AttrTypeBase.td"
class DebugTypeDef<string name> : TypeDef<DebugDialect, name> { }
def ScopeType : DebugTypeDef<"Scope"> {
let mnemonic = "scope";
let summary = "debug scope";
let description = "The result of a `dbg.scope` operation.";
}
def StructType : DebugTypeDef<"Struct"> {
let mnemonic = "struct";
let summary = "debug struct aggregate";

View File

@ -88,14 +88,34 @@ void DebugInfoBuilder::visitModule(hw::HWModuleOp moduleOp, DIModule &module) {
// return. Otherwise collect ports, instances, and variables as a
// fallback.
// Check what kind of DI is present in the module.
// Check what kind of DI is present in the module. Also create additional
// `DIModule` hierarchy levels for each explicit scope op in the module.
SmallDenseMap<debug::ScopeOp, DIModule *> scopes;
bool hasVariables = false;
bool hasInstances = false;
moduleOp.walk([&](Operation *op) {
if (isa<debug::VariableOp>(op))
hasVariables = true;
if (auto scopeOp = dyn_cast<debug::ScopeOp>(op)) {
auto *node = createModule();
node->isInline = true;
node->name = scopeOp.getModuleNameAttr();
node->op = scopeOp;
scopes.insert({scopeOp, node});
}
});
// Helper function to resolve a `scope` operand on a variable to the
// `DIModule` into which the variable should be collected. If the `scope` is
// not set, or it isn't a valid `dbg.scope` op, returns the `module` argument
// of this function.
auto getScope = [&](Value scopeValue) -> DIModule & {
if (scopeValue)
if (auto scopeOp = scopeValue.getDefiningOp<debug::ScopeOp>())
return *scopes.lookup(scopeOp);
return module;
};
// If the module has no DI for variables, add variables for each of the ports
// as a fallback.
if (!hasVariables) {
@ -119,10 +139,18 @@ void DebugInfoBuilder::visitModule(hw::HWModuleOp moduleOp, DIModule &module) {
var->name = varOp.getNameAttr();
var->loc = varOp.getLoc();
var->value = varOp.getValue();
module.variables.push_back(var);
getScope(varOp.getScope()).variables.push_back(var);
return;
}
if (auto scopeOp = dyn_cast<debug::ScopeOp>(op)) {
auto *instance = createInstance();
instance->name = scopeOp.getInstanceNameAttr();
instance->op = scopeOp;
instance->module = scopes.lookup(scopeOp);
getScope(scopeOp.getScope()).instances.push_back(instance);
}
// Fallback if the module has no DI for its instances.
if (!hasInstances) {
if (auto instOp = dyn_cast<hw::InstanceOp>(op)) {

View File

@ -57,7 +57,8 @@ void MaterializeDebugInfoPass::materializeVariable(OpBuilder &builder,
if (name.getValue().starts_with("_"))
return;
if (auto dbgValue = convertToDebugAggregates(builder, value))
builder.create<debug::VariableOp>(value.getLoc(), name, dbgValue);
builder.create<debug::VariableOp>(value.getLoc(), name, dbgValue,
/*scope=*/Value{});
}
/// Unpack all aggregates in a FIRRTL value and repack them as debug aggregates.

View File

@ -13,6 +13,8 @@
using namespace mlir;
using namespace circt;
static void dump(DIModule &module, raw_indented_ostream &os);
static void dump(DIVariable &variable, raw_indented_ostream &os) {
os << "Variable " << variable.name;
if (variable.loc)
@ -38,6 +40,11 @@ static void dump(DIInstance &instance, raw_indented_ostream &os) {
if (instance.op)
os << " for " << instance.op->getName() << " at " << instance.op->getLoc();
os << "\n";
if (instance.module->isInline) {
os.indent();
dump(*instance.module, os);
os.unindent();
}
}
static void dump(DIModule &module, raw_indented_ostream &os) {

View File

@ -48,3 +48,29 @@ hw.module @Aggregates(in %data_a: i32, in %data_b: index, in %data_c_0: i17, in
%1 = dbg.struct {"a": %data_a, "b": %data_b, "c": %0} : i32, index, !dbg.array<i17>
dbg.variable "data", %1 : !dbg.struct<i32, index, !dbg.array<i17>>
}
// CHECK-LABEL: Module "InlineScopes" for hw.module
// CHECK: Variable "a"
// CHECK-NEXT: Arg 0 of hw.module of type i42
// CHECK: Instance "inner" of "InnerModule" for dbg.scope
// CHECK: Module "InnerModule" for dbg.scope
// CHECK: Variable "b"
// CHECK-NEXT: Result 0 of comb.mul of type i42
hw.module @InlineScopes(in %a: i42) {
dbg.variable "a", %a : i42
%0 = comb.mul %a, %a : i42
%1 = dbg.scope "inner", "InnerModule"
dbg.variable "b", %0 scope %1 : i42
}
// CHECK-LABEL: Module "NestedScopes" for hw.module
// CHECK: Instance "foo" of "Foo" for dbg.scope
// CHECK: Module "Foo" for dbg.scope
// CHECK: Instance "bar" of "Bar" for dbg.scope
// CHECK: Module "Bar" for dbg.scope
// CHECK: Variable "a"
hw.module @NestedScopes(in %a: i42) {
%0 = dbg.scope "foo", "Foo"
%1 = dbg.scope "bar", "Bar" scope %0
dbg.variable "a", %a scope %1 : i42
}

View File

@ -19,6 +19,13 @@ func.func @Foo(%arg0: i32, %arg1: index, %arg2: f64) {
%1 = dbg.array [%arg1, %arg1] : index
dbg.variable "megabar", %1 : !dbg.array
// CHECK-NEXT: [[TMP:%.+]] = dbg.scope "inlined", "Bar"
// CHECK-NEXT: dbg.variable {{.+}} scope [[TMP]]
// CHECK-NEXT: dbg.scope {{.+}} scope [[TMP]]
%2 = dbg.scope "inlined", "Bar"
dbg.variable "x", %arg0 scope %2 : i32
dbg.scope "y", "Baz" scope %2
return
}