Yash Nagda

Blog post

Week 10: GSoC Project Progress

Hours this week: 21

Total hours: 178

At the end of the ninth week, I was working on the following tasks:

  • Continue to work upon the optimizations to improve the performance of the mlir backend.
  • Complete the WasmSSA refactoring for scf dialects.
  • Complete the string-to-enum refactor for the LLVM dialect.

This week, I continued that work locally and also opened two PRs for the enum based MLIR API and the tinyC parser fixes found while testing.

The work was divided into three main tasks:

  • moving LLVM operation and type handling from repeated string comparisons to typed enums
  • adding parser and lowering support for cases found while testing the API
  • continuing the direct LLVM dialect to WasmSSA control-flow reconstruction

Task 1: Enum-based MLIR API

PR #228: Performance: Changing string comparison to use enums

This PR continues the string-to-enum refactoring discussed in the Week 7 and Week 8 blogs. Earlier, LLVM passes and target backends repeatedly compared names such as "llvm.load", "llvm.store", or textual type strings while walking the IR. The new design keeps strings at the parsing and printing boundaries and uses typed values inside the compiler.

The API now uses MLIR_OpType for operation identity and MLIR_LLVM_TypeKind for type identity. The native implementation stores these values directly, while the upstream implementation converts real upstream MLIR operations and types to the same API representation. The parser converts text to enums, and the table inside mlir_op_names.cpp converts enums back to text when an operation must be printed or constructed.

As an initial migration, four LLVM optimization passes and the x86 backend now use the typed API. This gives a small path for reviewing the design before converting every backend. The PR also adds support for structured access to integer and floating-point literals, including values wider than 64 bits.

While testing the API, an address space bug was also found in llvm.mlir.addressof. tinyC previously emitted the default !llvm.ptr type even when a global used a non zero address space. The new API preserves the global’s address space and creates the correct pointer type, such as !llvm.ptr<3>.

This PR is used to make comparisions faster, by replacing repeated strcmp checks against operation and type names with enum comparisons. This also helps us to make both the native and upstream MLIR use the same typed interface. This helps the downstream passes and platform backends to have a common API type for codes lowered from both the native and upstream MLIR. This makes this stage independent of the higher level MLIR implementation.

Task 2: Parser and operation support found during testing

PR #229: Add parser support for a few operations

The second PR contains smaller fixes found while testing more programs through the MLIR API. It adds support for:

  • registered arith.extf and arith.truncf operations for floating-point higher and lower precision and truncation
  • function-pointer and pointer-to-pointer fields during struct copies and field loads
  • inline function-pointer fields, including void returning functions.
  • enum constants inside global array initializers, inferring the length of global arrays from their initializer values

For example, operations such as void (*notify)(const char *) can now be parsed inside a struct and called indirectly. Also, an array such as int colors[] = { RED, GREEN, BLUE } receives the correct length and initializer data. Tests were added for the all the available backends.

Task 3: Current local LLVM to WasmSSA lowering work

The local work continues the direct lowering design described in the Week 9 blog:

flowchart TB
  A["SCF / CF"] --> B["common LLVM lowering"]
  B --> C["flat LLVM CFG"]
  C --> D["CFG snapshot and normalization"]
  D --> E["structured WasmSSA plan"]
  E --> F["wasmssa.block / wasmssa.loop / wasmssa.if"]

The local implementation follows the same phase breakdown as mlir_lift_cf_to_scf.c and upstream CFGToSCF.cpp. The phases remove one structural obstacle that prevents a flat LLVM CFG from being expressed as nested WasmSSA control flow.

Step-1: We scan each llvm.func region and classify terminators (llvm.br, llvm.cond_br, llvm.switch, llvm.return). We also record successor slots, edge operands, and nested regions. From that walk, the compiler builds an immutable CFGInfo graph with dense block indices, explicit edge identity, reachability checks, and a block-handle hash map for O(1) lookup. All later passes read this snapshot, so the source LLVM MLIR is never mutated.

Step-2: These are the shared graph information used by the later normalization passes: directed edges keyed by (source block, successor slot), and synthetic mux blocks that combine several incoming edges into one structured entry or exit. These do not run as standalone passes in the Wasm path, but every M7 entry mux and M8 continuation mux depends on them.

Step-3: Multiple compatible llvm.return sites are grouped into equivalence classes and redirected through shared synthetic return blocks. Each class records the return operand types once. Every site in the class becomes a virtual branch to that shared exit. This gives the graph a common return shape before loop and branch structuring begin. The resulting M5ExitCombiner is kept around because M7 loop-exit normalization reuses the same exit machinery.

Step-4: This is a separate LCSSA-like repair pass (transform_to_reduce_loop) that fixes values defined inside a loop SCC and used outside it. In the Wasm path this logic is applied into M7 step: the value-use index is built once from CFGInfo, additional live-outs are discovered per SCC, and m7_apply_reduce_interface records the init, header, back-edge, and exit value slots that WasmSSA emission will need later.

Step-5: Tarjan SCC analysis finds cyclic components. Entry edges, back edges, and exit edges are classified per successor slot, not just per block pair. Multi-entry loops get an entry multiplexer. Multiple backedges and exits are routed through a single latch and loop-exit dispatch. The pass also records iteration roles, live-out replacements, and per-loop NormGraphView subgraphs so nested loop bodies can be processed recursively without treating SCF regions as the unit of structure.

Step-6: This is the dominance-based pass that turns acyclic llvm.cond_br and llvm.switch regions into structured if/switch plans. It computes dominated successor arms, finds or synthesizes continuations, inserts exit muxes when arms target different merge blocks, and makes each arm single-exit before recursion. M8 also owns the metadata (M8EmissionMetadata) that will later map normalized block arguments to WasmSSA if results. This phase is designed but not yet complete locally.

With that pipeline in mind, the current local commits implement the following parts:

  • commit-1: added walkers for discovering LLVM branches, loops, and nested regions
  • commit-2: added an immutable CFG snapshot and a mutable NormalizedCFG layer, with dense block indices, explicit edge identity, reachability checks, analysis storage, and the generation-tracked cache described above
  • commit-3: implemented M5 return normalization, which groups compatible llvm.return sites behind shared synthetic return blocks
  • commit-4: implemented M7 cycle normalization using SCC, reverse postorder, and dominance analysis, then introduced entry and latch multiplexers, loop-exit dispatch, live-out metadata, and the embedded M6 reduce-form repair

M5 and M7 now run entirely on NormalizedCFG so that the original LLVM blocks and operations remain immutable. M8 branch normalization and the structured-plan emission that follows it are the remaining local work, so this path is not yet part of the two opened PRs.

Outcome

By the end of this week:

  • the enum-based MLIR API is open for review in PR #228
  • parser and operation gaps found during testing are covered by PR #229
  • the LLVM-to-WasmSSA path now has an immutable CFG snapshot and a separate normalized graph
  • return and cycle normalization are implemented locally as the first major structurization stages

Apart from these tasks, I also reviewed the following PRs:

Next steps

The next step for the LLVM-to-Wasm work is to finish M8 branch normalization and connect the normalized graph to complete wasmssa.if, wasmssa.loop, and wasmssa.block emission. The new output will then be compared with the existing SCF path for behavior and control-flow shape.

For the MLIR API, I will prepare a representative C++ benchmark from the lfortran/parser_3.cpp tests. The benchmark and compiler will be built using an optimized Clang configuration so that debug-build overhead does not hide the cost of the API itself. This benchmark will be used to identify important performance bottlenecks for analysis further.

Acknowledgments

A huge thanks to my mentors, Ondřej Čertík, Pranav Goswami, Harshita Kalani & other contributors for helping me to work on this project.