Blog post
Week 11: GSoC Project Progress
07 Aug 2026
Hours this week: 22
Total hours: 200
At the end of the tenth week, I had the following tasks to complete:
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.
This week, I worked on these three areas:
- completing the first LLVM-to-WasmSSA lift
- updating the string-to-enum API design based on review comments
- building a simple benchmark for comparing Clang, a small compiler built from the full tinyC compiler pipeline, and the full tinyC compiler
Task 1: Completing the LLVM dialect to WasmSSA lift
PR #232: WIP: Remove SCF Dialect from wasm and aarch lowering
The control-flow reconstruction design was explained in detail in the previous blogs. This week, the work on the local llvm-scf-aarch-4 branch focused on completing that design and making the full path work reliably. The completed pipeline now looks like:
The main commits on this path are:
fd8785econnected the structured control-flow plan to final WasmSSA emission.1e602eatob09b253strengthened the M7 and M8 stages by fixing graph updates, loop exits, latch values, and continuations for control-flow graphs with shared joins.7562a0band747be90improved operand and merge handling, accepted unreachable CFG blocks, and added clearer failure diagnostics.dc174d7and473daa0removed the remaining SCF-dependent lowering path, leaving the backend with the intended LLVM-only input boundary.ff0e564corrected loop back-edge identification, while1eaeb09,dd7c1a5, and9f4bce5fixed issues found during larger tinyC and self-hosting runs, including Wasm object layout, stack handling, and dynamically growing local groups.
With these changes, PR #232 is working in the Linux and Windows CI runs. Mach-O self-hosting is very slow, perhaps broken. So I am working to investigate the issue further here for macOS self-hosting.
Task 2: Revising the MLIR API design
PR #228: Performance: Changing string comparison to use enums
The original aim of this PR was to replace repeated string comparisons with typed enum checks. Review comments showed that the API should also avoid having APIs only specific to the LLVM dialect. The API functions should be general to be used by other dialects as well.
The revised design uses a dialect-first type identity. A type is described by its general type (INT, FLOAT, etc.), kind (pointer, array, structure, or function), together with the dialect (LLVM, SCF, etc.) that owns it. Common queries can ask whether a type is a pointer, array, structure, or function and then use the dialect value to distinguish LLVM types from types owned by another dialect. LLVM-specific entry points remain as compatibility wrappers where required, but the core representation no longer depends on adding a separate API family for every dialect. Further, as per the review comment, these LLVM-specific entry points should also be removed, which will be addressed in next commits.
Another design decision was to keep shared behavior in one common utility file. Integer, floating-point, and string literals needed to have common helper utility functions, whether in the lowering of native API or upstream API. So this design helps us to keep the common helper functions used by both the paths in one place, without deduplication of code. Helpers that only exposed internal details were also removed from the public API.
The corresponding local work is on the llvm-opt-2 branch:
0a70217: moves literal helpers out of the public API headerade33cb: introduces the dialect-first type identitycfce3da: extracts behavior shared by the native and upstream implementations66e89eb: wires the common literal implementation into the different build paths
This keeps the performance motivation of enum-based dispatch while making the interface more general. The API can provide one stable typed boundary to downstream passes and backends, independent of whether the IR comes from native MLIR or upstream MLIR and without designing the entire interface around LLVM alone.
There are additional review comments on PR #228, which will be continued to work upon next week.
Task 3: Building a compiler benchmark
The benchmarking work is implemented here: PR #233: mlir: add profiling benchmarks for parser, driver, and lowering compare. Its purpose is to measure two different kinds of performance: compile-time performance and runtime performance.
The benchmark generates a large workload with repeated functions and loops in both the tinyC-supported form and the equivalent C form used by Clang. This workload is derived from parse3.cpp inside lfortran.
Five compiler configurations are measured:
- default Clang with no optimizations
- Clang with
-O0 - Clang with
-O3 -flto -march=native - a small compiler containing only the required tinyC and lowering pipeline (built using Clang with
-O3 -flto -march=native) - the full optimized tinyC compiler and driver (built using Clang with
-O3 -flto -march=native)
The small compiler allows us to profile without driver and runtime overhead because it only measures the cost of the main parsing, lowering, and native emission pipeline. The full compiler shows the latency for the current tinyC compiler. We measure the compile time and runtime for each of the five configurations.
The current results on the ELF backend are:
| Metric | Clang | Clang -O0 |
Clang -O3 -march=native |
Small compiler | Full compiler |
|---|---|---|---|---|---|
| Compile time (ms) | 5379.919 | 5371.204 | 10046.935 | 2673.354 | 2518.811 |
| Runtime, avg of 3 (ms) | 4.744 | 4.082 | 2.040 | 3.304 | 3.653 |
All values are in milliseconds. For this workload, the small and full compilers complete native executable generation in about half the time of default Clang and Clang -O0. Clang’s optimized build takes longer because the optimization and LTO work move additional cost into compilation and linking, but it produces the fastest executable. The small and full compiler runtimes are close to optimized Clang and much faster than the default Clang result in this run.
Outcome
By the end of this week:
- PR #232 has a first complete LLVM CFG to structured WasmSSA path, with the old SCF-dependent route removed
- PR #228 has moved toward a dialect-independent typed API with shared native and upstream behavior
- PR #233 provides a comparison of compile time and runtime for the five compiler configurations
Next steps
For the LLVM-to-WasmSSA work, I will simplify the current implementation and reduce the large branch difference without weakening the direct LLVM-only design or breaking the passing tests.
For the MLIR API, I will continue addressing review comments and use the benchmark to identify the most important remaining performance costs. The benchmark will also be repeated after API changes so that improvements are measured against the same workload and output requirements.
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.