gdb

Line stepping through MLIR with a debugger!

February 10, 2026 C/C++ development and debugging. , , , , ,

gdb session

I’ve added an alternate input source for the silly compiler.  As well as the .silly files that it previously accepted, it now also accepts .mlir (silly-dialect) files as input.

This means that if there’s an experimental language feature that requires new style MLIR, but I don’t want to figure out how to push that all the way through grammar -> parser -> builder -> lowering all at once, I might be able to at least understand the required MLIR patterns by by manually modifying exiting MLIR (generated with ‘silly –emit-mlir’).

For example, I don’t have BREAK support for FOR loops. I can do something simple:

INT64 v;

FOR (INT64 myLoopVar : (1, 5))
{
    PRINT myLoopVar;
    v = myLoopVar + 1;
};

PRINT "after loop: ", v;

The MLIR for this (with location info stripped out), looks like:

fedoravm:/home/peeter/toycalculator/tests/endtoend/for> silly-opt --pretty -s out/for_simplest.mlir 
module {
  func.func @main() -> i32 {
    %c0_i32 = arith.constant 0 : i32
    %c5_i64 = arith.constant 5 : i64
    %c1_i64 = arith.constant 1 : i64
    "silly.scope"() ({
      %0 = "silly.declare"() <{sym_name = "v"}> : () -> !silly.var
      scf.for %arg0 = %c1_i64 to %c5_i64 step %c1_i64  : i64 {
        "silly.print"(%c0_i32, %arg0) : (i32, i64) -> ()
        %3 = "silly.add"(%arg0, %c1_i64) : (i64, i64) -> i64
        silly.assign %0 :  = %3 : i64
      }
      %1 = "silly.string_literal"() <{value = "after loop: "}> : () -> !llvm.ptr
      %2 = silly.load %0 :  : i64
      "silly.print"(%c0_i32, %1, %2) : (i32, !llvm.ptr, i64) -> ()
      "silly.return"(%c0_i32) : (i32) -> ()
    }) : () -> ()
    "silly.yield"() : () -> ()
  }
}

If I want to add a BREAK into the mix (which I don’t support in any of grammar or parser or builder right now), something like:

INT64 v; 
FOR (INT64 i : (1, 5)) {
    PRINT i; 
    v = i + 1; 
    IF (i == 3) { BREAK; }; 
};
PRINT "after loop: ", v; 

Then it can be done by replacing the scf.for with scf.while, and putting in additional termination condition logic. Example:

module {
  func.func @main() -> i32 {
    %c0_i32 = arith.constant 0 : i32
    %c1_i64 = arith.constant 1 : i64
    %c3_i64 = arith.constant 3 : i64
    %c5_i64 = arith.constant 5 : i64
    %true = arith.constant true
    %false = arith.constant false

    "silly.scope"() ({
      %0 = "silly.declare"() <{sym_name = "v"}> : () -> !silly.var

      scf.while (%i = %c1_i64, %broke = %false) : (i64, i1) -> (i64, i1) {
        %not_broke = arith.xori %broke, %true : i1
        %in_range = arith.cmpi slt, %i, %c5_i64 : i64
        %continue = arith.andi %in_range, %not_broke : i1
        scf.condition(%continue) %i, %broke : i64, i1
      } do {
      ^bb0(%loop_var: i64, %break_flag: i1):
        "silly.print"(%c0_i32, %loop_var) : (i32, i64) -> ()
        %2 = "silly.add"(%loop_var, %c1_i64) : (i64, i64) -> i64
        silly.assign %0 :  = %2 : i64

        %is_three = arith.cmpi eq, %loop_var, %c3_i64 : i64
        %should_break = arith.ori %break_flag, %is_three : i1

        %next = arith.addi %loop_var, %c1_i64 : i64
        scf.yield %next, %should_break : i64, i1
      }

      %lit = "silly.string_literal"() <{value = "after loop: "}> : () -> !llvm.ptr
      %p = silly.load %0 :  : i64
      "silly.print"(%c0_i32, %lit, %p) : (i32, !llvm.ptr, i64) -> ()

      "silly.return"(%c0_i32) : (i32) -> ()
    }) : () -> ()
    "silly.yield"() : () -> ()
  }
}

Now, here’s where things get cool.  I noticed something curious when I looked at the .mlir dump from the MLIR parser (which I dumped to verify I was getting the expected round trip output before lowering). The MLIR parser, given only MLIR source, and no other location tagging, goes off and tags everything with location info for the MLIR source itself.  Example:

#loc15 = loc("forbreak.mlsilly":27:12)
#loc16 = loc("forbreak.mlsilly":27:28)
module {
  func.func @main() -> i32 {
    %c0_i32 = arith.constant 0 : i32 loc(#loc2)
    %c1_i64 = arith.constant 1 : i64 loc(#loc3)
    %c3_i64 = arith.constant 3 : i64 loc(#loc4)
    %c5_i64 = arith.constant 5 : i64 loc(#loc5)
    %true = arith.constant true loc(#loc6)
    %false = arith.constant false loc(#loc7)
    "silly.scope"() ({
      %0 = "silly.declare"() <{sym_name = "v"}> : () -> !silly.var loc(#loc9)
      %1:2 = scf.while (%arg0 = %c1_i64, %arg1 = %false) : (i64, i1) -> (i64, i1) {
        %4 = arith.xori %arg1, %true : i1 loc(#loc11)
        %5 = arith.cmpi slt, %arg0, %c5_i64 : i64 loc(#loc12)
        %6 = arith.andi %5, %4 : i1 loc(#loc13)
        scf.condition(%6) %arg0, %arg1 : i64, i1 loc(#loc14)
      } do {
      ^bb0(%arg0: i64 loc("forbreak.mlsilly":27:12), %arg1: i1 loc("forbreak.mlsilly":27:28)):
        "silly.print"(%c0_i32, %arg0) : (i32, i64) -> () loc(#loc17)
        %4 = "silly.add"(%arg0, %c1_i64) : (i64, i64) -> i64 loc(#loc18)
        silly.assign %0 :  = %4 : i64 loc(#loc19)
        %5 = arith.cmpi eq, %arg0, %c3_i64 : i64 loc(#loc20)
        %6 = arith.ori %arg1, %5 : i1 loc(#loc21)
        %7 = arith.addi %arg0, %c1_i64 : i64 loc(#loc22)
        scf.yield %7, %6 : i64, i1 loc(#loc23)
      } loc(#loc10)
      %2 = "silly.string_literal"() <{value = "after loop: "}> : () -> !llvm.ptr loc(#loc24)
      %3 = silly.load %0 :  : i64 loc(#loc25)
      "silly.print"(%c0_i32, %2, %3) : (i32, !llvm.ptr, i64) -> () loc(#loc26)
      "silly.return"(%c0_i32) : (i32) -> () loc(#loc27)
    }) : () -> () loc(#loc8)
    "silly.yield"() : () -> () loc(#loc28)
  } loc(#loc1)
} loc(#loc)
#loc = loc("forbreak.mlsilly":9:1)
#loc1 = loc("forbreak.mlsilly":10:3)
#loc2 = loc("forbreak.mlsilly":11:15)
#loc3 = loc("forbreak.mlsilly":12:15)
#loc4 = loc("forbreak.mlsilly":13:15)
#loc5 = loc("forbreak.mlsilly":14:15)
#loc6 = loc("forbreak.mlsilly":15:13)
#loc7 = loc("forbreak.mlsilly":16:14)
...

My compiler can then turns that location info into dwarf DI, just as it does for regular .silly source file, so I can actually line step through the MLIR itself with any debugger! Here’s an example session:

Breakpoint 1, main () at forbreak.mlsilly:25
25              scf.condition(%continue) %i, %broke : i64, i1
(gdb) l
20            
21            scf.while (%i = %c1_i64, %broke = %false) : (i64, i1) -> (i64, i1) {
22              %not_broke = arith.xori %broke, %true : i1
23              %in_range = arith.cmpi slt, %i, %c5_i64 : i64
24              %continue = arith.andi %in_range, %not_broke : i1
25              scf.condition(%continue) %i, %broke : i64, i1
26            } do {
27            ^bb0(%loop_var: i64, %break_flag: i1):
28              "silly.print"(%c0_i32, %loop_var) : (i32, i64) -> ()
29              %2 = "silly.add"(%loop_var, %c1_i64) : (i64, i64) -> i64
(gdb) l
30              silly.assign %0 :  = %2 : i64
31              
32              %is_three = arith.cmpi eq, %loop_var, %c3_i64 : i64
33              %should_break = arith.ori %break_flag, %is_three : i1
34              
35              %next = arith.addi %loop_var, %c1_i64 : i64
36              scf.yield %next, %should_break : i64, i1
37            }
38
39            %lit = "silly.string_literal"() <{value = "after loop: "}> : () -> !llvm.ptr
(gdb) b 32
Breakpoint 2 at 0x40076c: file forbreak.mlsilly, line 32.
(gdb) c
Continuing.
1

Breakpoint 2, main () at forbreak.mlsilly:32
32              %is_three = arith.cmpi eq, %loop_var, %c3_i64 : i64
(gdb) disassemble
Dump of assembler code for function main:
   0x000000000040072c <+0>:     sub     sp, sp, #0x60
   0x0000000000400730 <+4>:     stp     x30, x21, [sp, #64]
   0x0000000000400734 <+8>:     stp     x20, x19, [sp, #80]
   0x0000000000400738 <+12>:    mov     w19, wzr
   0x000000000040073c <+16>:    mov     w20, #0x1                       // #1
   0x0000000000400740 <+20>:    mov     w21, #0x1                       // #1
   0x0000000000400744 <+24>:    str     xzr, [sp, #8]
   0x0000000000400748 <+28>:    cmp     x21, #0x4
   0x000000000040074c <+32>:    b.gt    0x400784 
   0x0000000000400750 <+36>:    tbnz    w19, #0, 0x400784 
   0x0000000000400754 <+40>:    add     x1, sp, #0x10
   0x0000000000400758 <+44>:    mov     w0, #0x1                        // #1
   0x000000000040075c <+48>:    stp     x21, xzr, [sp, #24]
   0x0000000000400760 <+52>:    str     x20, [sp, #16]
   0x0000000000400764 <+56>:    bl      0x4005b0 <__silly_print@plt>
   0x0000000000400768 <+60>:    add     x21, x21, #0x1
=> 0x000000000040076c <+64>:    cmp     x21, #0x4
   0x0000000000400770 <+68>:    str     x21, [sp, #8]
   0x0000000000400774 <+72>:    cset    w8, eq  // eq = none
   0x0000000000400778 <+76>:    orr     w19, w19, w8
   0x000000000040077c <+80>:    cmp     x21, #0x4
   0x0000000000400780 <+84>:    b.le    0x400750 
   0x0000000000400784 <+88>:    mov     x8, #0x3                        // #3
   0x0000000000400788 <+92>:    ldr     x9, [sp, #8]
   0x000000000040078c <+96>:    mov     w10, #0xc                       // #12
   0x0000000000400790 <+100>:   movk    x8, #0x1, lsl #32
   0x0000000000400794 <+104>:   add     x1, sp, #0x10
   0x0000000000400798 <+108>:   mov     w0, #0x2                        // #2
   0x000000000040079c <+112>:   stp     x8, x10, [sp, #16]
   0x00000000004007a0 <+116>:   adrp    x8, 0x400000
   0x00000000004007a4 <+120>:   add     x8, x8, #0x7f8
   0x00000000004007a8 <+124>:   stp     x9, xzr, [sp, #48]
   0x00000000004007ac <+128>:   mov     w9, #0x1                        // #1
   0x00000000004007b0 <+132>:   stp     x8, x9, [sp, #32]
   0x00000000004007b4 <+136>:   bl      0x4005b0 <__silly_print@plt>
   0x00000000004007b8 <+140>:   ldp     x20, x19, [sp, #80]
   0x00000000004007bc <+144>:   mov     w0, wzr
   0x00000000004007c0 <+148>:   ldp     x30, x21, [sp, #64]
   0x00000000004007c4 <+152>:   add     sp, sp, #0x60
   0x00000000004007c8 <+156>:   ret
End of assembler dump.



(gdb) c
Continuing.
2

Breakpoint 2, main () at forbreak.mlsilly:32
32              %is_three = arith.cmpi eq, %loop_var, %c3_i64 : i64
(gdb) p v
$2 = 2

Having built a compiler for an arbitrary language, and having implemented DWARF instrumentation for that language, I get line support for stepping through the MLIR itself, if I want it.

I can imagine a scenerio where I’ve screwed up the MLIR ops generation in the builder. This lets me set a breakpoint right at the MLIR line in question, and poke around at the disassembly for that point in the code, and see what’s going on. What a cool compiler debugging tool!

Added IF/ELSE support to my toy MLIR/LLVM compiler.

December 16, 2025 clang/llvm , , , , , , , , , , , ,

Screenshot

I dusted off my toy compiler this weekend, and had a bit of fun implementing IF and ELSE support. The MLIR+LLVM compiler infrastructure is impressively versatile, allowing me to implement a feature like this in a handful of hours. It was, however, an invasive feature addition, requiring grammar changes, parser/builder, and lowering adjustments, as well as additional lowering for the SCF dialect ops that were used.

I have an ELIF element in the grammar too, but haven’t done that yet (and haven’t done much testing yet.)  These are the grammar elements I now have defined:

ifelifelse
  : ifStatement
    elifStatement*
    elseStatement?
  ;

ifStatement
  : IF_TOKEN BRACE_START_TOKEN booleanValue BRACE_END_TOKEN SCOPE_START_TOKEN statement* SCOPE_END_TOKEN
  ;

elifStatement
  : ELIF_TOKEN BRACE_START_TOKEN booleanValue BRACE_END_TOKEN SCOPE_START_TOKEN statement* SCOPE_END_TOKEN
  ;

elseStatement
  : ELSE_TOKEN SCOPE_START_TOKEN statement* SCOPE_END_TOKEN
  ;

Previously, I had a single monolithic ifelifelse token, but the generated parser data structure was horrendously complicated:

  class  IfelifelseContext : public antlr4::ParserRuleContext {
  public:
    IfelifelseContext(antlr4::ParserRuleContext *parent, size_t invokingState);
    virtual size_t getRuleIndex() const override;
    antlr4::tree::TerminalNode *IF_TOKEN();
    std::vector<antlr4::tree::TerminalNode *> BRACE_START_TOKEN();
    antlr4::tree::TerminalNode* BRACE_START_TOKEN(size_t i);
    std::vector<BooleanValueContext *> booleanValue();
    BooleanValueContext* booleanValue(size_t i);
    std::vector<antlr4::tree::TerminalNode *> BRACE_END_TOKEN();
    antlr4::tree::TerminalNode* BRACE_END_TOKEN(size_t i);
    std::vector<antlr4::tree::TerminalNode *> SCOPE_START_TOKEN();
    antlr4::tree::TerminalNode* SCOPE_START_TOKEN(size_t i);
    std::vector<antlr4::tree::TerminalNode *> SCOPE_END_TOKEN();
    antlr4::tree::TerminalNode* SCOPE_END_TOKEN(size_t i);
    std::vector<StatementContext *> statement();
    StatementContext* statement(size_t i);
    std::vector<antlr4::tree::TerminalNode *> ELIF_TOKEN();
    antlr4::tree::TerminalNode* ELIF_TOKEN(size_t i);
    antlr4::tree::TerminalNode *ELSE_TOKEN();

    virtual void enterRule(antlr4::tree::ParseTreeListener *listener) override;
    virtual void exitRule(antlr4::tree::ParseTreeListener *listener) override;

  };

In particular, I don’t see a way to determine if a statement should be part of the IF, the ELIF, or the ELSE body. Splitting the grammar into pieces was much better, and leave me with:

  class  IfelifelseContext : public antlr4::ParserRuleContext {
  public:
    IfelifelseContext(antlr4::ParserRuleContext *parent, size_t invokingState);
    virtual size_t getRuleIndex() const override;
    IfStatementContext *ifStatement();
    std::vector<ElifStatementContext *> elifStatement();
    ElifStatementContext* elifStatement(size_t i);
    ElseStatementContext *elseStatement();

    virtual void enterRule(antlr4::tree::ParseTreeListener *listener) override;
    virtual void exitRule(antlr4::tree::ParseTreeListener *listener) override;

  };

which allows me to drill down into each of the lower level elements, and drive the codegen from those. In particular, I can define ANTLR4 parse tree walker callbacks:

        void exitIfStatement(ToyParser::IfStatementContext *ctx) override;
        void exitElseStatement(ToyParser::ElseStatementContext *ctx) override;
        void enterIfelifelse( ToyParser::IfelifelseContext *ctx ) override;

and drive the codegen from there. The choice to use enter/exit callbacks for the various statement objects locks you into a certain mode of programming. In particular, this means that I don’t have functions for parsing a generic statement, so I am forced to emit the if/else body statements indirectly. Here’s an example:

        mlir::Value conditionPredicate = MLIRListener::parsePredicate( loc, booleanValue );

        insertionPointStack.push_back( builder.saveInsertionPoint() );

        // Create the scf.if — it will be inserted at the current IP
        auto ifOp = builder.create<mlir::scf::IfOp>( loc, conditionPredicate );

        mlir::Block &thenBlock = ifOp.getThenRegion().front();
        builder.setInsertionPointToStart( &thenBlock );

Then in the exitIf callback, if there is an else statement to process, I create a block for that, set the insertion point for that, and let the statement processing continue. When that else processing is done, I can restore the insertion point to just after the scf.if, and the rest of the function generation can proceed. Here’s an example of the MLIR dump before any lowering:

module {
  func.func @main() -> i32 {
    "toy.scope"() ({
      "toy.declare"() <{type = i32}> {sym_name = "x"} : () -> ()
      %c3_i64 = arith.constant 3 : i64
      "toy.assign"(%c3_i64) <{var_name = @x}> : (i64) -> ()
      %0 = "toy.load"() <{var_name = @x}> : () -> i32
      %c4_i64 = arith.constant 4 : i64
      %1 = "toy.less"(%0, %c4_i64) : (i32, i64) -> i1
      scf.if %1 {
        %5 = "toy.string_literal"() <{value = "x < 4"}> : () -> !llvm.ptr
        toy.print %5 : !llvm.ptr
        %6 = "toy.string_literal"() <{value = "a second statement"}> : () -> !llvm.ptr
        toy.print %6 : !llvm.ptr
      } else {
        %5 = "toy.string_literal"() <{value = "!(x < 4) -- should be dead code"}> : () -> !llvm.ptr
        toy.print %5 : !llvm.ptr
      }
      %2 = "toy.load"() <{var_name = @x}> : () -> i32
      %c5_i64 = arith.constant 5 : i64
      %3 = "toy.less"(%c5_i64, %2) : (i64, i32) -> i1
      scf.if %3 {
        %5 = "toy.string_literal"() <{value = "x > 5"}> : () -> !llvm.ptr
        toy.print %5 : !llvm.ptr
      } else {
        %5 = "toy.string_literal"() <{value = "!(x > 5) -- should see this"}> : () -> !llvm.ptr
        toy.print %5 : !llvm.ptr
      }
      %4 = "toy.string_literal"() <{value = "Done."}> : () -> !llvm.ptr
      toy.print %4 : !llvm.ptr
      %c0_i32 = arith.constant 0 : i32
      "toy.return"(%c0_i32) : (i32) -> ()
    }) : () -> ()
    "toy.yield"() : () -> ()
  }
}

This is the IR for the following “program”:

INT32 x;

x = 3;

IF ( x < 4 )
{
   PRINT "x < 4";
   PRINT "a second statement";
}
ELSE
{
   PRINT "!(x < 4) -- should be dead code";
};

IF ( x > 5 )
{
   PRINT "x > 5";
}
ELSE
{
   PRINT "!(x > 5) -- should see this";
};

PRINT "Done.";

There are existing mechanisms for lowering the SCF dialect, so it doesn’t take much work. There was one lowering quirk that I didn’t expect to have to deal with. I lower the toy.print Op in two steps, first to toy.call, and then let all the toy.call lowering kick in — however, I was doing that as part of a somewhat hacky toy.scope lowering step. That didn’t work anymore, since I can now have functions that aren’t in the scope, but part of an scf if or else block body. To fix that, I had to switch to a more conventional CallOp lowering class:

    class CallOpLowering : public ConversionPattern {
    public:
      explicit CallOpLowering(MLIRContext* context)
          : ConversionPattern(toy::CallOp::getOperationName(), 1, context) {}

      LogicalResult matchAndRewrite(Operation* op, ArrayRef<Value> operands,
                                    ConversionPatternRewriter& rewriter) const override {
        auto callOp = cast<toy::CallOp>(op);
        auto loc = callOp.getLoc();

        // Get the callee symbol reference (stored as "callee" attribute)
        auto calleeAttr = callOp->getAttrOfType<FlatSymbolRefAttr>("callee");
        if (!calleeAttr)
          return failure();

        // Get result types (empty for void, one type for scalar return)
        TypeRange resultTypes = callOp.getResultTypes();

        auto mlirCall = rewriter.create<mlir::func::CallOp>( loc, resultTypes, 
                                                             calleeAttr, callOp.getOperands() );

        // Replace uses correctly
        if (!resultTypes.empty()) {
          // Non-void: replace the single result
          rewriter.replaceOp(op, mlirCall.getResults());
        } else {
          // Void: erase the op (no result to replace)
          rewriter.eraseOp(op);
        }

        return success();
      }
    };

I lower all calls (print and any other) first to mlir::func::CallOp, and then let existing mlir func lowering kick in and do the rest.

After my “stage I” lowering, this program is mostly translated into LLVM:

module attributes {llvm.ident = "toycalculator V2"} {
  llvm.mlir.global private constant @str_5(dense<[68, 111, 110, 101, 46]> : tensor<5xi8>) {addr_space = 0 : i32} : !llvm.array<5 x i8>
  llvm.mlir.global private constant @str_4(dense<[33, 40, 120, 32, 62, 32, 53, 41, 32, 45, 45, 32, 115, 104, 111, 117, 108, 100, 32, 115, 101, 101, 32, 116, 104, 105, 115]> : tensor<27xi8>) {addr_space = 0 : i32} : !llvm.array<27 x i8>
  llvm.mlir.global private constant @str_3(dense<[120, 32, 62, 32, 53]> : tensor<5xi8>) {addr_space = 0 : i32} : !llvm.array<5 x i8>
  llvm.mlir.global private constant @str_2(dense<[33, 40, 120, 32, 60, 32, 52, 41, 32, 45, 45, 32, 115, 104, 111, 117, 108, 100, 32, 98, 101, 32, 100, 101, 97, 100, 32, 99, 111, 100, 101]> : tensor<31xi8>) {addr_space = 0 : i32} : !llvm.array<31 x i8>
  llvm.mlir.global private constant @str_1(dense<[97, 32, 115, 101, 99, 111, 110, 100, 32, 115, 116, 97, 116, 101, 109, 101, 110, 116]> : tensor<18xi8>) {addr_space = 0 : i32} : !llvm.array<18 x i8>
  func.func private @__toy_print_string(i64, !llvm.ptr)
  llvm.mlir.global private constant @str_0(dense<[120, 32, 60, 32, 52]> : tensor<5xi8>) {addr_space = 0 : i32} : !llvm.array<5 x i8>
  func.func @main() -> i32 attributes {llvm.debug.subprogram = #llvm.di_subprogram, compileUnit = , sourceLanguage = DW_LANG_C, file = <"if.toy" in ".">, producer = "toycalculator", isOptimized = false, emissionKind = Full>, scope = #llvm.di_file<"if.toy" in ".">, name = "main", linkageName = "main", file = <"if.toy" in ".">, line = 1, scopeLine = 1, subprogramFlags = Definition, type = >>} {
    "toy.scope"() ({
      %0 = llvm.mlir.constant(1 : i64) : i64
      %1 = llvm.alloca %0 x i32 {alignment = 4 : i64, bindc_name = "x"} : (i64) -> !llvm.ptr
      llvm.intr.dbg.declare #llvm.di_local_variable, compileUnit = , sourceLanguage = DW_LANG_C, file = <"if.toy" in ".">, producer = "toycalculator", isOptimized = false, emissionKind = Full>, scope = #llvm.di_file<"if.toy" in ".">, name = "main", linkageName = "main", file = <"if.toy" in ".">, line = 1, scopeLine = 1, subprogramFlags = Definition, type = >>, name = "x", file = <"if.toy" in ".">, line = 1, alignInBits = 32, type = #llvm.di_basic_type> = %1 : !llvm.ptr
      %2 = llvm.mlir.constant(3 : i64) : i64
      %3 = llvm.trunc %2 : i64 to i32
      llvm.store %3, %1 {alignment = 4 : i64} : i32, !llvm.ptr
      %4 = llvm.load %1 : !llvm.ptr -> i32
      %5 = llvm.mlir.constant(4 : i64) : i64
      %6 = llvm.sext %4 : i32 to i64
      %7 = llvm.icmp "slt" %6, %5 : i64
      scf.if %7 {
        %15 = llvm.mlir.addressof @str_0 : !llvm.ptr
        %16 = llvm.mlir.constant(5 : i64) : i64
        "toy.call"(%16, %15) <{callee = @__toy_print_string}> : (i64, !llvm.ptr) -> ()
        %17 = llvm.mlir.addressof @str_1 : !llvm.ptr
        %18 = llvm.mlir.constant(18 : i64) : i64
        "toy.call"(%18, %17) <{callee = @__toy_print_string}> : (i64, !llvm.ptr) -> ()
      } else {
        %15 = llvm.mlir.addressof @str_2 : !llvm.ptr
        %16 = llvm.mlir.constant(31 : i64) : i64
        "toy.call"(%16, %15) <{callee = @__toy_print_string}> : (i64, !llvm.ptr) -> ()
      }
      %8 = llvm.load %1 : !llvm.ptr -> i32
      %9 = llvm.mlir.constant(5 : i64) : i64
      %10 = llvm.sext %8 : i32 to i64
      %11 = llvm.icmp "slt" %9, %10 : i64
      scf.if %11 {
        %15 = llvm.mlir.addressof @str_3 : !llvm.ptr
        %16 = llvm.mlir.constant(5 : i64) : i64
        "toy.call"(%16, %15) <{callee = @__toy_print_string}> : (i64, !llvm.ptr) -> ()
      } else {
        %15 = llvm.mlir.addressof @str_4 : !llvm.ptr
        %16 = llvm.mlir.constant(27 : i64) : i64
        "toy.call"(%16, %15) <{callee = @__toy_print_string}> : (i64, !llvm.ptr) -> ()
      }
      %12 = llvm.mlir.addressof @str_5 : !llvm.ptr
      %13 = llvm.mlir.constant(5 : i64) : i64
      "toy.call"(%13, %12) <{callee = @__toy_print_string}> : (i64, !llvm.ptr) -> ()
      %14 = llvm.mlir.constant(0 : i32) : i32
      "toy.return"(%14) : (i32) -> ()
    }) : () -> ()
    "toy.yield"() : () -> ()
  }
}

The only things that are left in my MLIR toy dialect are “toy.scope”, “toy.call”, “toy.yield”, and “toy.return”. After my second lowering pass, everything is in the MLIR LLVM dialect:

llvm.mlir.global private constant @str_5(dense<[68, 111, 110, 101, 46]> : tensor<5xi8>) {addr_space = 0 : i32} : !llvm.array<5 x i8>
llvm.mlir.global private constant @str_4(dense<[33, 40, 120, 32, 62, 32, 53, 41, 32, 45, 45, 32, 115, 104, 111, 117, 108, 100, 32, 115, 101, 101, 32, 116, 104, 105, 115]> : tensor<27xi8>) {addr_space = 0 : i32} : !llvm.array<27 x i8>
llvm.mlir.global private constant @str_3(dense<[120, 32, 62, 32, 53]> : tensor<5xi8>) {addr_space = 0 : i32} : !llvm.array<5 x i8>
llvm.mlir.global private constant @str_2(dense<[33, 40, 120, 32, 60, 32, 52, 41, 32, 45, 45, 32, 115, 104, 111, 117, 108, 100, 32, 98, 101, 32, 100, 101, 97, 100, 32, 99, 111, 100, 101]> : tensor<31xi8>) {addr_space = 0 : i32} : !llvm.array<31 x i8>
llvm.mlir.global private constant @str_1(dense<[97, 32, 115, 101, 99, 111, 110, 100, 32, 115, 116, 97, 116, 101, 109, 101, 110, 116]> : tensor<18xi8>) {addr_space = 0 : i32} : !llvm.array<18 x i8>
func.func private @__toy_print_string(i64, !llvm.ptr)
llvm.mlir.global private constant @str_0(dense<[120, 32, 60, 32, 52]> : tensor<5xi8>) {addr_space = 0 : i32} : !llvm.array<5 x i8>
func.func @main() -> i32 attributes {llvm.debug.subprogram = #llvm.di_subprogram, compileUnit = , sourceLanguage = DW_LANG_C, file = <"if.toy" in ".">, producer = "toycalculator", isOptimized = false, emissionKind = Full>, scope = #llvm.di_file<"if.toy" in ".">, name = "main", linkageName = "main", file = <"if.toy" in ".">, line = 1, scopeLine = 1, subprogramFlags = Definition, type = >>} {
  %0 = llvm.mlir.constant(1 : i64) : i64
  %1 = llvm.alloca %0 x i32 {alignment = 4 : i64, bindc_name = "x"} : (i64) -> !llvm.ptr
  llvm.intr.dbg.declare #llvm.di_local_variable, compileUnit = , sourceLanguage = DW_LANG_C, file = <"if.toy" in ".">, producer = "toycalculator", isOptimized = false, emissionKind = Full>, scope = #llvm.di_file<"if.toy" in ".">, name = "main", linkageName = "main", file = <"if.toy" in ".">, line = 1, scopeLine = 1, subprogramFlags = Definition, type = >>, name = "x", file = <"if.toy" in ".">, line = 1, alignInBits = 32, type = #llvm.di_basic_type> = %1 : !llvm.ptr
  %2 = llvm.mlir.constant(3 : i64) : i64
  %3 = llvm.trunc %2 : i64 to i32
  llvm.store %3, %1 {alignment = 4 : i64} : i32, !llvm.ptr
  %4 = llvm.load %1 : !llvm.ptr -> i32
  %5 = llvm.mlir.constant(4 : i64) : i64
  %6 = llvm.sext %4 : i32 to i64
  %7 = llvm.icmp "slt" %6, %5 : i64
  llvm.cond_br %7, ^bb1, ^bb2
^bb1:  // pred: ^bb0
  %8 = llvm.mlir.addressof @str_0 : !llvm.ptr
  %9 = llvm.mlir.constant(5 : i64) : i64
  call @__toy_print_string(%9, %8) : (i64, !llvm.ptr) -> ()
  %10 = llvm.mlir.addressof @str_1 : !llvm.ptr
  %11 = llvm.mlir.constant(18 : i64) : i64
  call @__toy_print_string(%11, %10) : (i64, !llvm.ptr) -> ()
  llvm.br ^bb3
^bb2:  // pred: ^bb0
  %12 = llvm.mlir.addressof @str_2 : !llvm.ptr
  %13 = llvm.mlir.constant(31 : i64) : i64
  call @__toy_print_string(%13, %12) : (i64, !llvm.ptr) -> ()
  llvm.br ^bb3
^bb3:  // 2 preds: ^bb1, ^bb2
  %14 = llvm.load %1 : !llvm.ptr -> i32
  %15 = llvm.mlir.constant(5 : i64) : i64
  %16 = llvm.sext %14 : i32 to i64
  %17 = llvm.icmp "slt" %15, %16 : i64
  llvm.cond_br %17, ^bb4, ^bb5
^bb4:  // pred: ^bb3
  %18 = llvm.mlir.addressof @str_3 : !llvm.ptr
  %19 = llvm.mlir.constant(5 : i64) : i64
  call @__toy_print_string(%19, %18) : (i64, !llvm.ptr) -> ()
  llvm.br ^bb6
^bb5:  // pred: ^bb3
  %20 = llvm.mlir.addressof @str_4 : !llvm.ptr
  %21 = llvm.mlir.constant(27 : i64) : i64
  call @__toy_print_string(%21, %20) : (i64, !llvm.ptr) -> ()
  llvm.br ^bb6
^bb6:  // 2 preds: ^bb4, ^bb5
  %22 = llvm.mlir.addressof @str_5 : !llvm.ptr
  %23 = llvm.mlir.constant(5 : i64) : i64
  call @__toy_print_string(%23, %22) : (i64, !llvm.ptr) -> ()
  %24 = llvm.mlir.constant(0 : i32) : i32
  return %24 : i32
}

Once assembled, we are left with:

0000000000000000 
: 0: push %rax 1: movl $0x3,0x4(%rsp) 9: xor %eax,%eax b: test %al,%al d: jne 2a f: mov $0x5,%edi 14: mov $0x0,%esi 15: R_X86_64_32 .rodata+0x62 19: call 1e 1a: R_X86_64_PLT32 __toy_print_string-0x4 1e: mov $0x12,%edi 23: mov $0x0,%esi 24: R_X86_64_32 .rodata+0x50 28: jmp 34 2a: mov $0x1f,%edi 2f: mov $0x0,%esi 30: R_X86_64_32 .rodata+0x30 34: call 39 35: R_X86_64_PLT32 __toy_print_string-0x4 39: movslq 0x4(%rsp),%rax 3e: cmp $0x6,%rax 42: jl 50 44: mov $0x5,%edi 49: mov $0x0,%esi 4a: R_X86_64_32 .rodata+0x2b 4e: jmp 5a 50: mov $0x1b,%edi 55: mov $0x0,%esi 56: R_X86_64_32 .rodata+0x10 5a: call 5f 5b: R_X86_64_PLT32 __toy_print_string-0x4 5f: mov $0x5,%edi 64: mov $0x0,%esi 65: R_X86_64_32 .rodata 69: call 6e 6a: R_X86_64_PLT32 __toy_print_string-0x4 6e: xor %eax,%eax 70: pop %rcx 71: ret

Of course, we may enable optimization, and get something much nicer. With -O2, by the time we are done the LLVM lowering, all the constant propagation has kicked in, leaving just:

; ModuleID = 'if.toy'
source_filename = "if.toy"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"

@str_5 = private constant [5 x i8] c"Done."
@str_4 = private constant [27 x i8] c"!(x > 5) -- should see this"
@str_1 = private constant [18 x i8] c"a second statement"
@str_0 = private constant [5 x i8] c"x < 4"

declare void @__toy_print_string(i64, ptr) local_unnamed_addr

define noundef i32 @main() local_unnamed_addr !dbg !4 {
    #dbg_value(i32 3, !8, !DIExpression(), !10)
  tail call void @__toy_print_string(i64 5, ptr nonnull @str_0), !dbg !11
  tail call void @__toy_print_string(i64 18, ptr nonnull @str_1), !dbg !12
  tail call void @__toy_print_string(i64 27, ptr nonnull @str_4), !dbg !13
  tail call void @__toy_print_string(i64 5, ptr nonnull @str_5), !dbg !14
  ret i32 0, !dbg !14
}

Our program is reduced to just a couple of print statements:

0000000000400470 
: 400470: 50 push %rax 400471: bf 05 00 00 00 mov $0x5,%edi 400476: be 12 12 40 00 mov $0x401212,%esi 40047b: e8 f0 fe ff ff call 400370 <__toy_print_string@plt> 400480: bf 12 00 00 00 mov $0x12,%edi 400485: be 00 12 40 00 mov $0x401200,%esi 40048a: e8 e1 fe ff ff call 400370 <__toy_print_string@plt> 40048f: bf 1b 00 00 00 mov $0x1b,%edi 400494: be e0 11 40 00 mov $0x4011e0,%esi 400499: e8 d2 fe ff ff call 400370 <__toy_print_string@plt> 40049e: bf 05 00 00 00 mov $0x5,%edi 4004a3: be d0 11 40 00 mov $0x4011d0,%esi 4004a8: e8 c3 fe ff ff call 400370 <__toy_print_string@plt> 4004ad: 31 c0 xor %eax,%eax 4004af: 59 pop %rcx 4004b0: c3 ret

We've seen how easy it was to implement enough control flow to almost make the toy language useful. Another side effect of this MLIR+LLVM infrastructure, is that our IF/ELSE debugging support comes for free (having paid the cost earlier of having figured out how to emit the dwarf instrumentation). Here's an example:

> gdb -q out/if
Reading symbols from out/if...
(gdb) b main
Breakpoint 1 at 0x400471: file if.toy, line 3.
(gdb) run
Starting program: /home/pjoot/toycalculator/samples/out/if 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

Breakpoint 1, main () at if.toy:3
3       x = 3;
(gdb) l
1       INT32 x;
2
3       x = 3;
4
5       IF ( x < 4 )
6       {
7          PRINT "x < 4";
8          PRINT "a second statement";
9       }
10      ELSE
(gdb) b 7
Breakpoint 2 at 0x40047f: file if.toy, line 7.
(gdb) c
Continuing.

Breakpoint 2, main () at if.toy:7
7          PRINT "x < 4";
(gdb) l
2
3       x = 3;
4
5       IF ( x < 4 )
6       {
7          PRINT "x < 4";
8          PRINT "a second statement";
9       }
10      ELSE
11      {
(gdb) p x
$1 = 3
(gdb) l
12         PRINT "!(x < 4) -- should be dead code";
13      };
14
15      IF ( x > 5 )
16      {
17         PRINT "x > 5";
18      }
19      ELSE
20      {
21         PRINT "!(x > 5) -- should see this";
(gdb) b 21
Breakpoint 3 at 0x4004c0: file if.toy, line 21.
(gdb) c
Continuing.
x < 4
a second statement

Breakpoint 3, main () at if.toy:21
21         PRINT "!(x > 5) -- should see this";

The quest for DWARF instrumentation in a toy MLIR compiler

May 25, 2025 C/C++ development and debugging. , , , , , , ,

I’ve gotten my toy compiler project to the stage where I can generate a program, compile and run it.  Here’s a little example:

Screenshot

This is the MLIR that is generated for this little program:

"builtin.module"() ({
  "toy.program"() ({
    "toy.declare"() <{name = "x", type = f64}> : () -> () loc(#loc)
    %0 = "arith.constant"() <{value = 5 : i64}> : () -> i64 loc(#loc1)
    %1 = "arith.constant"() <{value = 3.140000e+00 : f64}> : () -> f64 loc(#loc1)
    %2 = "toy.add"(%0, %1) : (i64, f64) -> f64 loc(#loc1)
    "toy.assign"(%2) <{name = "x"}> : (f64) -> () loc(#loc1)
    %3 = "toy.load"() <{name = "x"}> : () -> f64 loc(#loc2)
    "toy.print"(%3) : (f64) -> () loc(#loc2)
    "toy.declare"() <{name = "y", type = f64}> : () -> () loc(#loc3)
    %4 = "toy.load"() <{name = "x"}> : () -> f64 loc(#loc4)
    %5 = "arith.constant"() <{value = 2 : i64}> : () -> i64 loc(#loc4)
    %6 = "toy.mul"(%4, %5) : (f64, i64) -> f64 loc(#loc4)
    "toy.assign"(%6) <{name = "y"}> : (f64) -> () loc(#loc4)
    %7 = "toy.load"() <{name = "y"}> : () -> f64 loc(#loc5)
    "toy.print"(%7) : (f64) -> () loc(#loc5)
    "toy.exit"() : () -> () loc(#loc)
  }) : () -> () loc(#loc)
}) : () -> () loc(#loc)
#loc = loc("test.toy":1:1)
#loc1 = loc("test.toy":2:5)
#loc2 = loc("test.toy":3:1)
#loc3 = loc("test.toy":4:1)
#loc4 = loc("test.toy":5:5)
#loc5 = loc("test.toy":6:1)

Notice how the location information is baked into the cake. After lowering of all the MLIR operations to LLVM-IR (except for the top most MLIR module), we have:

"builtin.module"() ({
  "llvm.func"() <{CConv = #llvm.cconv, function_type = !llvm.func, linkage = #llvm.linkage, sym_name = "__toy_print_f64", visibility_ = 0 : i64}> ({
  }) : () -> () loc(#loc)
  "llvm.func"() <{CConv = #llvm.cconv, function_type = !llvm.func, linkage = #llvm.linkage, sym_name = "__toy_print_i64", visibility_ = 0 : i64}> ({
  }) : () -> () loc(#loc)
  "llvm.func"() <{CConv = #llvm.cconv, function_type = !llvm.func, linkage = #llvm.linkage, sym_name = "main", visibility_ = 0 : i64}> ({
    %0 = "llvm.mlir.constant"() <{value = 1 : i64}> : () -> i64 loc(#loc)
    %1 = "llvm.alloca"(%0) <{alignment = 8 : i64, elem_type = f64}> : (i64) -> !llvm.ptr loc(#loc)
    %2 = "llvm.mlir.constant"() <{value = 5 : i64}> : () -> i64 loc(#loc1)
    %3 = "llvm.mlir.constant"() <{value = 3.140000e+00 : f64}> : () -> f64 loc(#loc1)
    %4 = "llvm.sitofp"(%2) : (i64) -> f64 loc(#loc1)
    %5 = "llvm.fadd"(%4, %3) <{fastmathFlags = #llvm.fastmath}> : (f64, f64) -> f64 loc(#loc1)
    "llvm.store"(%5, %1) <{ordering = 0 : i64}> : (f64, !llvm.ptr) -> () loc(#loc1)
    %6 = "llvm.load"(%1) <{ordering = 0 : i64}> : (!llvm.ptr) -> f64 loc(#loc2)
    "llvm.call"(%6) <{CConv = #llvm.cconv, TailCallKind = #llvm.tailcallkind, callee = @__toy_print_f64, fastmathFlags = #llvm.fastmath, op_bundle_sizes = array, operandSegmentSizes = array}> : (f64) -> () loc(#loc2)
    %7 = "llvm.alloca"(%0) <{alignment = 8 : i64, elem_type = f64}> : (i64) -> !llvm.ptr loc(#loc3)
    %8 = "llvm.load"(%1) <{ordering = 0 : i64}> : (!llvm.ptr) -> f64 loc(#loc4)
    %9 = "llvm.mlir.constant"() <{value = 2 : i64}> : () -> i64 loc(#loc4)
    %10 = "llvm.sitofp"(%9) : (i64) -> f64 loc(#loc4)
    %11 = "llvm.fmul"(%8, %10) <{fastmathFlags = #llvm.fastmath}> : (f64, f64) -> f64 loc(#loc4)
    "llvm.store"(%11, %7) <{ordering = 0 : i64}> : (f64, !llvm.ptr) -> () loc(#loc4)
    %12 = "llvm.load"(%7) <{ordering = 0 : i64}> : (!llvm.ptr) -> f64 loc(#loc5)
    "llvm.call"(%12) <{CConv = #llvm.cconv, TailCallKind = #llvm.tailcallkind, callee = @__toy_print_f64, fastmathFlags = #llvm.fastmath, op_bundle_sizes = array, operandSegmentSizes = array}> : (f64) -> () loc(#loc5)
    %13 = "llvm.mlir.constant"() <{value = 0 : i32}> : () -> i32 loc(#loc)
    "llvm.return"(%13) : (i32) -> () loc(#loc)
  }) : () -> () loc(#loc)
}) : () -> () loc(#loc)
#loc = loc("../samples/test.toy":1:1)
#loc1 = loc("../samples/test.toy":2:5)
#loc2 = loc("../samples/test.toy":3:1)
#loc3 = loc("../samples/test.toy":4:1)
#loc4 = loc("../samples/test.toy":5:5)
#loc5 = loc("../samples/test.toy":6:1)

All is still good.  See how all the location information has been retained through the lowering to LLVM-IR transformations. However, as soon as we run mlir::translateModuleToLLVMIR, our final LLVM-IR ends up stripped of all the location information:

; ModuleID = '../samples/test.toy'
source_filename = "../samples/test.toy"

declare void @__toy_print_f64(double)

declare void @__toy_print_i64(i64)

define i32 @main() {
  %1 = alloca double, i64 1, align 8
  store double 8.140000e+00, ptr %1, align 8
  %2 = load double, ptr %1, align 8
  call void @__toy_print_f64(double %2)
  %3 = alloca double, i64 1, align 8
  %4 = load double, ptr %1, align 8
  %5 = fmul double %4, 2.000000e+00
  store double %5, ptr %3, align 8
  %6 = load double, ptr %3, align 8
  call void @__toy_print_f64(double %6)
  ret i32 0
}

!llvm.module.flags = !{!0}

!0 = !{i32 2, !"Debug Info Version", i32 3}

If we build a simple standalone C program with clang, it’s a much different story:

int main() {
    int x = 42;
    return 0;
}

fedora:/home/pjoot/toycalculator/prototypes> clang -g -S -emit-llvm test.c
fedora:/home/pjoot/toycalculator/prototypes> cat test.s
; ModuleID = 'test.c'
source_filename = "test.c"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
target triple = "x86_64-redhat-linux-gnu"

; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @main() #0 !dbg !8 {
  %1 = alloca i32, align 4
  %2 = alloca i32, align 4
  store i32 0, ptr %1, align 4
    #dbg_declare(ptr %2, !13, !DIExpression(), !14)
  store i32 42, ptr %2, align 4, !dbg !14
  ret i32 0, !dbg !15
}

attributes #0 = { noinline nounwind optnone uwtable "frame-pointer"="all" "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }

!llvm.dbg.cu = !{!0}
!llvm.module.flags = !{!2, !3, !4, !5, !6}
!llvm.ident = !{!7}

!0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !1, producer: "clang version 20.1.3 (Fedora 20.1.3-1.fc42)", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug, splitDebugInlining: false, nameTableKind: None)
!1 = !DIFile(filename: "test.c", directory: "/home/pjoot/toycalculator/prototypes", checksumkind: CSK_MD5, checksum: "8d269ab8efc2854a823fb05a50ccf4b0")
!2 = !{i32 7, !"Dwarf Version", i32 5}
!3 = !{i32 2, !"Debug Info Version", i32 3}
!4 = !{i32 1, !"wchar_size", i32 4}
!5 = !{i32 7, !"uwtable", i32 2}
!6 = !{i32 7, !"frame-pointer", i32 2}
!7 = !{!"clang version 20.1.3 (Fedora 20.1.3-1.fc42)"}
!8 = distinct !DISubprogram(name: "main", scope: !1, file: !1, line: 1, type: !9, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !0, retainedNodes: !12)
!9 = !DISubroutineType(types: !10)
!10 = !{!11}
!11 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
!12 = !{}
!13 = !DILocalVariable(name: "x", scope: !8, file: !1, line: 2, type: !11)
!14 = !DILocation(line: 2, column: 9, scope: !8)
!15 = !DILocation(line: 3, column: 5, scope: !8)

This has a number of features that our automatically-lowered LLVM-IR does not have including:

  • !DICompileUnit
  • !DIFile
  • Dwarf Version
  • !DISubprogram
  • !DILocation
  • !llvm.dbg.cu
  • source_filename
  • target datalayout
  • target triple

It doesn’t surprise me that we don’t have some of these (like target triple, …) without doing extra work, but I’d have expected the location info to be converted into !DILocation.

It’s possible to emit DWARF info from an LLVM builder, which is how a non-MLIR project would do it.  I have brutal hack of an example of that in my project — that program is a little standalone beastie that constructs equivalent LLVM-IR for a simulated C program.  After MLIR lowering to LLVM-IR, I’m able to inject all the DI instrumentation (relying on the fact that I know the operations in this program, and the line numbers for those operations).  It would be very difficult to do this in my toy-language compiler driver, since that has to work for all programs.  It is totally and completely obvious that this is the WRONG way to do it.  However, the little program is at least debuggable, and perhaps worth something for that reason.

We see the same sort of DI artifacts if we build a Fortran program with flang (not classic-flang, but MLIR flang):

program main
  implicit none
  integer :: x
  x = 42
  print *, x
end program main

The flang LL has all the expected DI elements:

; ModuleID = 'FIRModule'
source_filename = "FIRModule"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"

$_QQclXc451cd8b1f5eae110b31be7df6c0fcf9 = comdat any

@_QQclXc451cd8b1f5eae110b31be7df6c0fcf9 = linkonce constant [43 x i8] c"/home/pjoot/toycalculator/fortran/test.f90\00", comdat

define void @_QQmain() #0 !dbg !6 {
  %1 = alloca i32, i64 1, align 4, !dbg !9
    #dbg_declare(ptr %1, !10, !DIExpression(), !9)
  store i32 42, ptr %1, align 4, !dbg !12
  %2 = call ptr @_FortranAioBeginExternalListOutput(i32 6, ptr @_QQclXc451cd8b1f5eae110b31be7df6c0fcf9, i32 5), !dbg !13
  %3 = load i32, ptr %1, align 4, !dbg !14
  %4 = call i1 @_FortranAioOutputInteger32(ptr %2, i32 %3), !dbg !14
  %5 = call i32 @_FortranAioEndIoStatement(ptr %2), !dbg !13
  ret void, !dbg !15
}

declare !dbg !16 ptr @_FortranAioBeginExternalListOutput(i32, ptr, i32)

declare !dbg !21 zeroext i1 @_FortranAioOutputInteger32(ptr, i32)

declare !dbg !25 i32 @_FortranAioEndIoStatement(ptr)

declare !dbg !28 void @_FortranAProgramStart(i32, ptr, ptr, ptr)

declare !dbg !31 void @_FortranAProgramEndStatement()

define i32 @main(i32 %0, ptr %1, ptr %2) #0 !dbg !33 {
  call void @_FortranAProgramStart(i32 %0, ptr %1, ptr %2, ptr null), !dbg !36
  call void @_QQmain(), !dbg !36
  call void @_FortranAProgramEndStatement(), !dbg !36
  ret i32 0, !dbg !36
}

attributes #0 = { "frame-pointer"="all" "target-cpu"="x86-64" }

!llvm.module.flags = !{!0, !1, !2}
!llvm.dbg.cu = !{!3}
!llvm.ident = !{!5}

!0 = !{i32 2, !"Debug Info Version", i32 3}
!1 = !{i32 8, !"PIC Level", i32 2}
!2 = !{i32 7, !"PIE Level", i32 2}
!3 = distinct !DICompileUnit(language: DW_LANG_Fortran95, file: !4, producer: "flang version 20.1.5 (https://github.com/llvm/llvm-project.git 7b09d7b446383b71b63d429b21ee45ba389c5134)", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
!4 = !DIFile(filename: "test.f90", directory: "/home/pjoot/toycalculator/fortran")
!5 = !{!"flang version 20.1.5 (https://github.com/llvm/llvm-project.git 7b09d7b446383b71b63d429b21ee45ba389c5134)"}
!6 = distinct !DISubprogram(name: "main", linkageName: "_QQmain", scope: !4, file: !4, line: 1, type: !7, scopeLine: 1, spFlags: DISPFlagDefinition | DISPFlagMainSubprogram, unit: !3)
!7 = !DISubroutineType(cc: DW_CC_program, types: !8)
!8 = !{null}
!9 = !DILocation(line: 3, column: 14, scope: !6)
!10 = !DILocalVariable(name: "x", scope: !6, file: !4, line: 3, type: !11)
!11 = !DIBasicType(name: "integer", size: 32, encoding: DW_ATE_signed)
!12 = !DILocation(line: 4, column: 3, scope: !6)
!13 = !DILocation(line: 5, column: 3, scope: !6)
!14 = !DILocation(line: 5, column: 12, scope: !6)
!15 = !DILocation(line: 6, column: 1, scope: !6)
!16 = !DISubprogram(name: "_FortranAioBeginExternalListOutput", linkageName: "_FortranAioBeginExternalListOutput", scope: !4, file: !4, line: 5, type: !17, scopeLine: 5, spFlags: 0)
!17 = !DISubroutineType(cc: DW_CC_normal, types: !18)
!18 = !{!19, !11, !20, !11}
!19 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !20, size: 64)
!20 = !DIBasicType(name: "integer", size: 8, encoding: DW_ATE_signed)
!21 = !DISubprogram(name: "_FortranAioOutputInteger32", linkageName: "_FortranAioOutputInteger32", scope: !4, file: !4, line: 5, type: !22, scopeLine: 5, spFlags: 0)
!22 = !DISubroutineType(cc: DW_CC_normal, types: !23)
!23 = !{!24, !20, !11}
!24 = !DIBasicType(name: "integer", size: 1, encoding: DW_ATE_signed)
!25 = !DISubprogram(name: "_FortranAioEndIoStatement", linkageName: "_FortranAioEndIoStatement", scope: !4, file: !4, line: 5, type: !26, scopeLine: 5, spFlags: 0)
!26 = !DISubroutineType(cc: DW_CC_normal, types: !27)
!27 = !{!11, !20}
!28 = !DISubprogram(name: "_FortranAProgramStart", linkageName: "_FortranAProgramStart", scope: !4, file: !4, line: 6, type: !29, scopeLine: 6, spFlags: 0)
!29 = !DISubroutineType(cc: DW_CC_normal, types: !30)
!30 = !{null, !11, !11, !11, !11}
!31 = !DISubprogram(name: "_FortranAProgramEndStatement", linkageName: "_FortranAProgramEndStatement", scope: !4, file: !4, line: 6, type: !32, scopeLine: 6, spFlags: 0)
!32 = !DISubroutineType(cc: DW_CC_normal, types: !8)
!33 = distinct !DISubprogram(name: "main", linkageName: "main", scope: !4, file: !4, line: 6, type: !34, scopeLine: 6, spFlags: DISPFlagDefinition, unit: !3)
!34 = !DISubroutineType(cc: DW_CC_normal, types: !35)
!35 = !{!11, !11, !11, !11}
!36 = !DILocation(line: 6, column: 1, scope: !33)

Ignoring all the language bootstrap gorp, this has all the expected DI elements, so we know that there’s a mechanism for flang to emit the DWARF instrumentation.

Looking through the flang source, the only place I found any sort of DI instrumentation was flang/lib/Optimizer/Transforms/AddDebugInfo.cpp, which, in about 600 lines, appears to implement all the DI instrumentation needed for variable and function declarations for the Fortran language (as a MLIR pass.)  There’s nothing in there for location translation that I can see, except for a couple of little sneaky bits

bool debugInfoIsAlreadySet(mlir::Location loc) {
  if (mlir::isa<mlir::FusedLoc>(loc)) {
    if (loc->findInstanceOf<mlir::FusedLocWith<fir::LocationKindAttr>>())
      return false;
    return true;
  }
  return false;
}

...
declOp->setLoc(builder.getFusedLoc({declOp->getLoc()}, localVarAttr));
...
globalOp->setLoc(builder.getFusedLoc({globalOp.getLoc()}, arrayAttr));
...

void AddDebugInfoPass::handleFuncOp(mlir::func::FuncOp funcOp,
                                    mlir::LLVM::DIFileAttr fileAttr,
                                    mlir::LLVM::DICompileUnitAttr cuAttr,
                                    fir::DebugTypeGenerator &typeGen,
                                    mlir::SymbolTable *symbolTable) {
  mlir::Location l = funcOp->getLoc();
  // If fused location has already been created then nothing to do
  // Otherwise, create a fused location.
  if (debugInfoIsAlreadySet(l))
    return;
...
// We have the imported entities now. Generate the final DISubprogramAttr.
  spAttr = mlir::LLVM::DISubprogramAttr::get(
      context, recId, /*isRecSelf=*/false, id2, compilationUnit, Scope,
      funcName, fullName, funcFileAttr, line, line, subprogramFlags,
      subTypeAttr, entities, /*annotations=*/{});
  funcOp->setLoc(builder.getFusedLoc({l}, spAttr));
...

It looks as if a fused location is used to replace any MLIR supplied location for each function, but all the rest of the location info is left as is.  My presumption is that translateModuleToLLVMIR() handles the plain old loc() references to !dbg.  To get an idea what happens in AddDebugInfoPass, let’s look at the MLIR dump at the beginning, and compare to afterwards.

To see this, I used the following gdb script:

set follow-fork-mode child
set detach-on-fork off
set breakpoint pending on
break AddDebugInfoPass::runOnOperation
run

Here’s the debug session to see the module dumps before and after AddDebugInfoPass

fedora:/home/pjoot/toycalculator/fortran> gdb -q -x b --args /usr/local/llvm-20.1.5/bin/flang-new -g -S -emit-llvm test.f90 -o test.ll
...
[Switching to Thread 0x7fffda437640 (LWP 119612)]

Thread 2.1 "flang" hit Breakpoint 1.2, (anonymous namespace)::AddDebugInfoPass::runOnOperation (this=0x6368e0)
    at /home/pjoot/llvm-project/flang/lib/Optimizer/Transforms/AddDebugInfo.cpp:520
520	  mlir::ModuleOp module = getOperation();
(gdb) n
521	  mlir::MLIRContext *context = &getContext();
(gdb) p module->dump()
module attributes {dlti.dl_spec = #dlti.dl_spec : vector<2xi64>, !llvm.ptr<270> = dense<32> : vector<4xi64>, f16 = dense<16> : vector<2xi64>, f64 = dense<64> : vector<2xi64>, i32 = dense<32> : vector<2xi64>, i16 = dense<16> : vector<2xi64>, i8 = dense<8> : vector<2xi64>, i1 = dense<8> : vector<2xi64>, !llvm.ptr = dense<64> : vector<4xi64>, f80 = dense<128> : vector<2xi64>, i128 = dense<128> : vector<2xi64>, i64 = dense<64> : vector<2xi64>, !llvm.ptr<271> = dense<32> : vector<4xi64>, !llvm.ptr<272> = dense<64> : vector<4xi64>, "dlti.endianness" = "little", "dlti.stack_alignment" = 128 : i64>, fir.defaultkind = "a1c4d8i4l4r4", fir.kindmap = "", fir.target_cpu = "x86-64", llvm.data_layout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", llvm.ident = "flang version 20.1.5 (https://github.com/llvm/llvm-project.git 7b09d7b446383b71b63d429b21ee45ba389c5134)", llvm.target_triple = "x86_64-unknown-linux-gnu"} {
  func.func @_QQmain() attributes {fir.bindc_name = "main"} {
    %c5_i32 = arith.constant 5 : i32
    %c6_i32 = arith.constant 6 : i32
    %c42_i32 = arith.constant 42 : i32
    %0 = fir.alloca i32 {bindc_name = "x", uniq_name = "_QFEx"}
    %1 = fircg.ext_declare %0 {uniq_name = "_QFEx"} : (!fir.ref) -> !fir.ref
    fir.store %c42_i32 to %1 : !fir.ref
    %2 = fir.address_of(@_QQclXc451cd8b1f5eae110b31be7df6c0fcf9) : !fir.ref<!fir.char<1,43>>
    %3 = fir.convert %2 : (!fir.ref<!fir.char<1,43>>) -> !fir.ref
    %4 = fir.call @_FortranAioBeginExternalListOutput(%c6_i32, %3, %c5_i32) fastmath : (i32, !fir.ref, i32) -> !fir.ref
    %5 = fir.load %1 : !fir.ref
    %6 = fir.call @_FortranAioOutputInteger32(%4, %5) fastmath : (!fir.ref, i32) -> i1
    %7 = fir.call @_FortranAioEndIoStatement(%4) fastmath : (!fir.ref) -> i32
    return
  }
  func.func private @_FortranAioBeginExternalListOutput(i32, !fir.ref, i32) -> !fir.ref attributes {fir.io, fir.runtime}
  fir.global linkonce @_QQclXc451cd8b1f5eae110b31be7df6c0fcf9 constant : !fir.char<1,43> {
    %0 = fir.string_lit "/home/pjoot/toycalculator/fortran/test.f90\00"(43) : !fir.char<1,43>
    fir.has_value %0 : !fir.char<1,43>
  }
  func.func private @_FortranAioOutputInteger32(!fir.ref, i32) -> i1 attributes {fir.io, fir.runtime}
  func.func private @_FortranAioEndIoStatement(!fir.ref) -> i32 attributes {fir.io, fir.runtime}
  func.func private @_FortranAProgramStart(i32, !llvm.ptr, !llvm.ptr, !llvm.ptr)
  func.func private @_FortranAProgramEndStatement()
  func.func @main(%arg0: i32, %arg1: !llvm.ptr, %arg2: !llvm.ptr) -> i32 {
    %c0_i32 = arith.constant 0 : i32
    %0 = fir.zero_bits !fir.ref<tuple<i32, !fir.ref<!fir.array<0xtuple<!fir.ref, !fir.ref>>>>>
    fir.call @_FortranAProgramStart(%arg0, %arg1, %arg2, %0) fastmath : (i32, !llvm.ptr, !llvm.ptr, !fir.ref<tuple<i32, !fir.ref<!fir.array<0xtuple<!fir.ref, !fir.ref>>>>>) -> ()
    fir.call @_QQmain() fastmath : () -> ()
    fir.call @_FortranAProgramEndStatement() fastmath : () -> ()
    return %c0_i32 : i32
  }
}
(gdb) c
Continuing.

Thread 2.1 "flang" hit Breakpoint 3.2, Fortran::frontend::CodeGenAction::generateLLVMIR (this=0x485530) at /home/pjoot/llvm-project/flang/lib/Frontend/FrontendActions.cpp:889
889	  timingScopeMLIRPasses.stop();

(gdb) mlirModule.op.dump()
module attributes {dlti.dl_spec = #dlti.dl_spec : vector<2xi64>, !llvm.ptr<270> = dense<32> : vector<4xi64>, f16 = dense<16> : vector<2xi64>, f64 = dense<64> : vector<2xi64>, i32 = dense<32> : vector<2xi64>, i16 = dense<16> : vector<2xi64>, i8 = dense<8> : vector<2xi64>, i1 = dense<8> : vector<2xi64>, !llvm.ptr = dense<64> : vector<4xi64>, f80 = dense<128> : vector<2xi64>, i128 = dense<128> : vector<2xi64>, i64 = dense<64> : vector<2xi64>, !llvm.ptr<271> = dense<32> : vector<4xi64>, !llvm.ptr<272> = dense<64> : vector<4xi64>, "dlti.endianness" = "little", "dlti.stack_alignment" = 128 : i64>, fir.defaultkind = "a1c4d8i4l4r4", fir.kindmap = "", fir.target_cpu = "x86-64", llvm.data_layout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", llvm.ident = "flang version 20.1.5 (https://github.com/llvm/llvm-project.git 7b09d7b446383b71b63d429b21ee45ba389c5134)", llvm.target_triple = "x86_64-unknown-linux-gnu"} {
  llvm.func @_QQmain() attributes {fir.bindc_name = "main", frame_pointer = #llvm.framePointerKind, target_cpu = "x86-64"} {
    %0 = llvm.mlir.constant(1 : i64) : i64
    %1 = llvm.alloca %0 x i32 {bindc_name = "x"} : (i64) -> !llvm.ptr
    %2 = llvm.mlir.constant(5 : i32) : i32
    %3 = llvm.mlir.constant(6 : i32) : i32
    %4 = llvm.mlir.constant(42 : i32) : i32
    %5 = llvm.mlir.constant(1 : i64) : i64
    llvm.intr.dbg.declare #llvm.di_local_variable, id = distinct[1]<>, compileUnit = , sourceLanguage = DW_LANG_Fortran95, file = <"test.f90" in "/home/pjoot/toycalculator/fortran">, producer = "flang version 20.1.5 (https://github.com/llvm/llvm-project.git 7b09d7b446383b71b63d429b21ee45ba389c5134)", isOptimized = false, emissionKind = Full>, scope = #llvm.di_file<"test.f90" in "/home/pjoot/toycalculator/fortran">, name = "main", linkageName = "_QQmain", file = <"test.f90" in "/home/pjoot/toycalculator/fortran">, line = 1, scopeLine = 1, subprogramFlags = "Definition|MainSubprogram", type = >, name = "x", file = <"test.f90" in "/home/pjoot/toycalculator/fortran">, line = 3, type = #llvm.di_basic_type> = %1 : !llvm.ptr
    llvm.store %4, %1 : i32, !llvm.ptr
    %6 = llvm.mlir.addressof @_QQclXc451cd8b1f5eae110b31be7df6c0fcf9 : !llvm.ptr
    %7 = llvm.call @_FortranAioBeginExternalListOutput(%3, %6, %2) {fastmathFlags = #llvm.fastmath} : (i32, !llvm.ptr, i32) -> !llvm.ptr
    %8 = llvm.load %1 : !llvm.ptr -> i32
    %9 = llvm.call @_FortranAioOutputInteger32(%7, %8) {fastmathFlags = #llvm.fastmath} : (!llvm.ptr, i32) -> i1
    %10 = llvm.call @_FortranAioEndIoStatement(%7) {fastmathFlags = #llvm.fastmath} : (!llvm.ptr) -> i32
    llvm.return
  }
  llvm.func @_FortranAioBeginExternalListOutput(i32, !llvm.ptr, i32) -> !llvm.ptr attributes {fir.io, fir.runtime, frame_pointer = #llvm.framePointerKind, sym_visibility = "private", target_cpu = "x86-64"}
  llvm.mlir.global linkonce constant @_QQclXc451cd8b1f5eae110b31be7df6c0fcf9() comdat(@__llvm_comdat::@_QQclXc451cd8b1f5eae110b31be7df6c0fcf9) {addr_space = 0 : i32} : !llvm.array<43 x i8> {
    %0 = llvm.mlir.constant("/home/pjoot/toycalculator/fortran/test.f90\00") : !llvm.array<43 x i8>
    llvm.return %0 : !llvm.array<43 x i8>
  }
  llvm.comdat @__llvm_comdat {
    llvm.comdat_selector @_QQclXc451cd8b1f5eae110b31be7df6c0fcf9 any
  }
  llvm.func @_FortranAioOutputInteger32(!llvm.ptr, i32) -> (i1 {llvm.zeroext}) attributes {fir.io, fir.runtime, frame_pointer = #llvm.framePointerKind, sym_visibility = "private", target_cpu = "x86-64"}
  llvm.func @_FortranAioEndIoStatement(!llvm.ptr) -> i32 attributes {fir.io, fir.runtime, frame_pointer = #llvm.framePointerKind, sym_visibility = "private", target_cpu = "x86-64"}
  llvm.func @_FortranAProgramStart(i32, !llvm.ptr, !llvm.ptr, !llvm.ptr) attributes {frame_pointer = #llvm.framePointerKind, sym_visibility = "private", target_cpu = "x86-64"}
  llvm.func @_FortranAProgramEndStatement() attributes {frame_pointer = #llvm.framePointerKind, sym_visibility = "private", target_cpu = "x86-64"}
  llvm.func @main(%arg0: i32, %arg1: !llvm.ptr, %arg2: !llvm.ptr) -> i32 attributes {frame_pointer = #llvm.framePointerKind, target_cpu = "x86-64"} {
    %0 = llvm.mlir.constant(0 : i32) : i32
    %1 = llvm.mlir.zero : !llvm.ptr
    llvm.call @_FortranAProgramStart(%arg0, %arg1, %arg2, %1) {fastmathFlags = #llvm.fastmath} : (i32, !llvm.ptr, !llvm.ptr, !llvm.ptr) -> ()
    llvm.call @_QQmain() {fastmathFlags = #llvm.fastmath} : () -> ()
    llvm.call @_FortranAProgramEndStatement() {fastmathFlags = #llvm.fastmath} : () -> ()
    llvm.return %0 : i32
  }
}

Even before the AddDebugInfoPass we have target_cpu, data_layout, ident, and target_triple set, so AddDebugInfoPass doesn’t add those. We do have all of these, either added by AddDebugInfoPass, or something after it:

 llvm.intr.dbg.declare #llvm.di_local_variable, id = distinct[1]<>, compileUnit = , sourceLanguage = DW_LANG_Fortran95, file = <"test.f90" in "/home/pjoot/toycalculator/fortran">, producer = "flang version 20.1.5 (https://github.com/llvm/llvm-project.git 7b09d7b446383b71b63d429b21ee45ba389c5134)", isOptimized = false, emissionKind = Full>, scope = #llvm.di_file<"test.f90" in "/home/pjoot/toycalculator/fortran">, name = "main", linkageName = "_QQmain", file = <"test.f90" in "/home/pjoot/toycalculator/fortran">, line = 1, scopeLine = 1, subprogramFlags = "Definition|MainSubprogram", type = >, name = "x", file = <"test.f90" in "/home/pjoot/toycalculator/fortran">, line = 3, type = #llvm.di_basic_type> = %1 : !llvm.ptr

So, it looks like we need to add a compileUnit, subprogram, and if we want debug variables, instrumentation for those too.

In some standalone code (prototypes/simplest.cpp) adding those wasn’t enough to ensure that translateModuleToLLVMIR() didn’t strip out the location info.

What did make the difference was adding this bit:

func->setLoc( builder.getFusedLoc( { loc }, subprogramAttr ) );

with that, the generated LLVM-IR, not only doesn’t have the loc() stripped, but everything is properly converted to !dbg:

; ModuleID = 'test'
source_filename = "test"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"

define i32 @main() !dbg !4 {
  %1 = alloca i64, i64 1, align 4, !dbg !8
  store i32 42, ptr %1, align 4, !dbg !8
    #dbg_declare(ptr %1, !9, !DIExpression(), !8)
  ret i32 0, !dbg !10
}

; Function Attrs: nocallback nofree nosync nounwind speculatable willreturn memory(none)
declare void @llvm.dbg.declare(metadata, metadata, metadata) #0

attributes #0 = { nocallback nofree nosync nounwind speculatable willreturn memory(none) }

!llvm.module.flags = !{!0}
!llvm.dbg.cu = !{!1}
!llvm.ident = !{!3}

!0 = !{i32 2, !"Debug Info Version", i32 3}
!1 = distinct !DICompileUnit(language: DW_LANG_C, file: !2, producer: "testcompiler", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
!2 = !DIFile(filename: "test.c", directory: ".")
!3 = !{!"toycompiler 0.0"}
!4 = distinct !DISubprogram(name: "main", linkageName: "main", scope: !2, file: !2, line: 1, type: !5, scopeLine: 1, spFlags: DISPFlagDefinition, unit: !1)
!5 = !DISubroutineType(types: !6)
!6 = !{!7}
!7 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
!8 = !DILocation(line: 2, column: 3, scope: !4)
!9 = !DILocalVariable(name: "x", scope: !4, file: !2, line: 2, type: !7, align: 32)
!10 = !DILocation(line: 3, column: 3, scope: !4)

I’m also able to debug a faked source file corresponding to the LLVM-IR generated by this MWE program:

fedora:/home/pjoot/toycalculator/prototypes> ../build/simplest > output.ll
fedora:/home/pjoot/toycalculator/prototypes> clang -g -o output output.ll -Wno-override-module
fedora:/home/pjoot/toycalculator/prototypes> gdb -q ./output
Reading symbols from ./output...
(gdb) b main
Breakpoint 1 at 0x400450: file test.c, line 2.
(gdb) run
Starting program: /home/pjoot/toycalculator/prototypes/output 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

Breakpoint 1, main () at test.c:2
2           int x = 42;
(gdb) n
3           return 0;
(gdb) p x
$1 = 42
(gdb) rerun
Undefined command: "rerun".  Try "help".
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/pjoot/toycalculator/prototypes/output 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

Breakpoint 1, main () at test.c:2
2           int x = 42;
(gdb) b 3
Breakpoint 2 at 0x400458: file test.c, line 3.
(gdb) c
Continuing.

Breakpoint 2, main () at test.c:3
3           return 0;
(gdb) q
A debugging session is active.

        Inferior 1 [process 119835] will be killed.

Quit anyway? (y or n) y

Now that I have this working in a standalone test case, it’s time to do it in the toy compiler itself, but that’s a task for another day.

Letting a gdb controlled program read from stdin.

December 8, 2023 C/C++ development and debugging. , , ,

I was asked how to let a gdb controlled slave read from stdin, and couldn’t remember how it was done, so I wrote the following little test program, figuring that muscle memory would remind me once I had gdb running.

#include <stdio.h>

int main()
{
   size_t rc = 0;
   char buf[2];

   do {
      rc = fread( buf, 1, 1, stdin );
      if ( rc == 1 ) {
         printf( "%c\n", buf[0] );
      }
   } while ( rc );

   return 0;
}

Turns out the answer is just the gdb run command, which can specify stdin for the program. Here’s a demo session that shows this in action

(gdb) b main
Breakpoint 1 at 0x40061e: file x.c, line 5.
(gdb) run < x.c
Starting program: /home/pjoot/tmp/readit/a.out < x.c

Breakpoint 1, main () at x.c:5
5          size_t rc = 0;
Missing separate debuginfos, use: yum debuginfo-install glibc-2.28-225.0.4.el8_8.6.x86_64
(gdb) n
9             rc = fread( buf, 1, 1, stdin );
(gdb) n
10            if ( rc == 1 ) {
(gdb) p rc
$1 = 1
(gdb) p buf[0]
$2 = 35 '#'
(gdb) n
11               printf( "%c\n", buf[0] );
(gdb) 
#
13         } while ( rc );
(gdb) 
9             rc = fread( buf, 1, 1, stdin );
(gdb) 
10            if ( rc == 1 ) {
(gdb) p rc
$3 = 1
(gdb) p buf[0]
$4 = 105 'i'

The x.c input that I passed to run, was the source of the program that I debugging.

Listing the code pages for gdb ‘set target-charset’

August 21, 2020 C/C++ development and debugging. , , , , , ,

I wanted to display some internal state as an IBM-1141 codepage, but didn’t know the name to use.  I knew that EBCDIC-US could be used for IBM-1047, but gdb didn’t like ibm-1147:

(gdb) set target-charset EBCDIC-US
(gdb) p (char *)0x7ffbb7b58088
$2 = 0x7ffbb7b58088 "{Jim       ;012}", ' ' <repeats 104 times>
(gdb) set target-charset ibm-1141
Undefined item: "ibm-1141".

I’d either didn’t know or had forgotten that we can get a list of the supported codepages. The help shows this:

(gdb) help set target-charset
Set the target character set.
The `target character set' is the one used by the program being debugged.
GDB translates characters and strings between the host and target
character sets as needed.
To see a list of the character sets GDB supports, type `set target-charset'<TAB>

I had to hit tab twice, but after doing so, I see:

(gdb) set target-charset 
Display all 200 possibilities? (y or n)
1026               866                ARABIC7            CP-HU              CP1129             CP1158             CP1371             CP4517             CP856              CP903
1046               866NAV             ARMSCII-8          CP037              CP1130             CP1160             CP1388             CP4899             CP857              CP904
1047               869                ASCII              CP038              CP1132             CP1161             CP1390             CP4909             CP860              CP905
10646-1:1993       874                ASMO-708           CP1004             CP1133             CP1162             CP1399             CP4971             CP861              CP912
10646-1:1993/UCS4  8859_1             ASMO_449           CP1008             CP1137             CP1163             CP273              CP500              CP862              CP915
437                8859_2             BALTIC             CP1025             CP1140             CP1164             CP274              CP5347             CP863              CP916
500                8859_3             BIG-5              CP1026             CP1141             CP1166             CP275              CP737              CP864              CP918
500V1              8859_4             BIG-FIVE           CP1046             CP1142             CP1167             CP278              CP770              CP865              CP920
850                8859_5             BIG5               CP1047             CP1143             CP1250             CP280              CP771              CP866              CP921
851                8859_6             BIG5-HKSCS         CP1070             CP1144             CP1251             CP281              CP772              CP866NAV           CP922
852                8859_7             BIG5HKSCS          CP1079             CP1145             CP1252             CP282              CP773              CP868              CP930
855                8859_8             BIGFIVE            CP1081             CP1146             CP1253             CP284              CP774              CP869              CP932
856                8859_9             BRF                CP1084             CP1147             CP1254             CP285              CP775              CP870              CP933
857                904                BS_4730            CP1089             CP1148             CP1255             CP290              CP803              CP871              CP935
860                ANSI_X3.110        CA                 CP1097             CP1149             CP1256             CP297              CP813              CP874              CP936
861                ANSI_X3.110-1983   CN                 CP1112             CP1153             CP1257             CP367              CP819              CP875              CP937
862                ANSI_X3.4          CN-BIG5            CP1122             CP1154             CP1258             CP420              CP850              CP880              CP939
863                ANSI_X3.4-1968     CN-GB              CP1123             CP1155             CP1282             CP423              CP851              CP891              CP949
864                ANSI_X3.4-1986     CP-AR              CP1124             CP1156             CP1361             CP424              CP852              CP901              CP950
865                ARABIC             CP-GR              CP1125             CP1157             CP1364             CP437              CP855              CP902              auto
*** List may be truncated, max-completions reached. ***

There’s my ibm-1141 in there, but masquerading as CP1141, so I’m able to view my data in that codepage, and lookup the value of characters of interest in 1141:

(gdb) set target-charset CP1141
(gdb) p (char *)0x7ffbb7b58088
$3 = 0x7ffbb7b58088 "äJim       ;012ü", ' ' <repeats 104 times>
(gdb) p /x '{'
$4 = 0x43
(gdb) p /x '}
Unmatched single quote.
(gdb) p /x '}'
$5 = 0xdc
(gdb) p /x *(char *)0x7ffbb7b58088
$6 = 0xc0

I’m able to conclude that the buffer in question appears to be in CP1047, not CP1141 (the first character, which is supposed to be ‘{‘ doesn’t have the CP1141 value of ‘{‘).