Blog post
Week 12: GSoC Project Progress
14 Aug 2026
Hours this week: 16
Total hours: 216
At the end of the eleventh week, I had the following tasks to complete:
- 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.
This week, continuing on the first task, I majorly focused on resolving the bugs in the LLVM-to-WasmSSA work. Last week, this PR failed for the Mach-O backend, where it did not self-compile and probably ran into an infinite loop. That made debugging the exact reason for this failure difficult. So, I focused majorly on the correctness part of the LLVM-to-WasmSSA work. In this process, I identified a few bugs in LLVM lowering as well, which are part of the commits in the SCF-lowering work. Once we finish this task, I shall proceed to create separate PRs for these bugs as well; for now, I am focusing on the completion of the major task.
The process used to identify the bugs is as follows:
flowchart TB
A["Each self-host file"] --> B["Generate LLVM code"]
A --> C["Generate WasmSSA code"]
A --> D["Generate Mach-O code"]
B --> E["Compare outputs against original C"]
C --> E
D --> E
E --> F["Identify differences and isolate bugs"]
In this process, bugs identified and their fixes are briefly as follows:
LLVM Lowering Bugs
-
arith.constantin mem2reg — Mem2reg still insertedarith.constantfor few SSA values. Example: promoted locals in self-host TUs produced mixedarith/llvmIR that aarch64 and wasmssa backends reject. Correct: all constants in the LLVM dialect asllvm.mlir.constant. Fix: switched the mem2reg emission site tollvm.mlir.constant(666d0df). -
Mach-O lazy stack didn’t update states correctly at control-flow joins — After
continuelowered to an unconditional wasmbr, the Mach-O lifter restored logical stack depth atend/elsebut leftmem_countat the pre-branch spill state. Example: reachable else-arm instructions could skip spills and decode with a wrong operand stack. Correct: spill state matches validated stack depth before the next instruction. Fix: callLAZY_SYNC()afterendand onelseentry inmlir_wasm_to_macho.c(c44a4d2). -
Long decimal literals overflow in lex — PI/DPI globals parsed with a wrapped mantissa. Example:
3.14159265358979323846became ~5.6e-03, soPI_D/DPI_Dincorec/base/math.cwere wrong in emitted LLVM. Correct: full double-precision literal value. Fix: promote to wide double accumulation when the digit run no longer fits inuint64_t(5557b07). -
sizeof(char)fixed to 1 byte size — Char types useTY_I32withint_bits = 8, buttype_sizereturned the i32 width. Example:sizeof(char) * nfor arena allocation lowered to4 * ninstead of1 * n. Correct:sizeof(char)andsizeof(unsigned char)are 1. Fix: addedc_sizeof_type()for 8-bit integer types at constant-creation time (83ffcb7). -
Unsigned-char pointer casts are sign-extended — Casts to
(const unsigned char *)did not mark the pointer unsigned, so byte loads used sign extension. Example:strcmpon bytes0xFFvs0returned a negative difference instead of 255, which made wrong calculations. Correct: unsigned byte loads zero-extend to i32 before subtraction. Fix: propagateint_unsignedthrough pointerEX_CASTin tinyC (4214659). Another lowering bug here was thatllvm.zext i8 to i32treated as no-op in WasmSSA lowering. It skipped zero-extensions on the i32 register path. Example: LLVM frombase_strncmpkept sign-extended char bytes, so subtraction results were wrong even after the above fix. Correct:llvm.zext i8to i32 appliesi32.and 255. Fix: stop treating i8 zext as identity in llvm to wasmssa lowering (79fc6a8). -
Integer casts to
unsigned charinstrncmp—(unsigned char)s[i]was not zero-extended before subtraction. Example:base_strncmpprinted-1instead of255for0xFF - 0. Correct: truncate to i8, then useextuito i32 for unsigned targets. Fix: emit explicit i8 trunc + signed/unsigned extend in tinyCEX_CAST(79fc6a8). -
Operation
llvm.intr.wasm.memory.sizeis unsupported on aarch64 — Operations using__builtin_wasm_memory_size(0)threw errors in llvm to aarch64, as this operation is not supported in aarch64. Example:wasm_memory_aarch64.tcfailed to lower. Correct: read page count from__wasm_mem_pages(or 0 if absent). Fix: added aarch64 lowering branch mirroring wasmssa to llvm lowering semantics for this operation (ae2b57d). -
Operation
llvm.intr.wasm.memory.growis unsupported — tinyc used the grow intrinsic for__builtin_wasm_memory_grow, but aarch64, x64 ELF, and native LLVM-IR paths only handledmemory.size. Example: cross-backendwasm_memory_grow.tcfailed to lower intrinsic underllc. Correct: load old page count, add delta with cap, store back, return previous count or-1. Fix: shared__wasm_mem_pagesload/store lowering in aarch64, x64, and LLVM-IR lowering (0d00131). -
File-scope
constglobals were emitted as mutable variables —static const floatvariable was always created as a mutablellvm.global. Example: math constants likemath_s0were showed asis_const = falsein WasmSSA codes. Correct: immutable globals carry the LLVM constant attribute. Fix: trackconston parsed global types and passis_constanttoMLIR_CreateLLVMGlobal(2f31868).
LLVM-WasmSSA Bugs
-
Loop
continueskipped latch dispatch — M8 emitted transfers to a loop header as a directbrto the loop label, bypassing the latch exit dispatch. Example: acontinuein a counting loop could reenter the header without rechecking the exit condition. Correct: back-edges go through the latch so the loop condition is evaluated before reentry. Fix: route continue/back-edge transfers throughm8_emit_loop_latch()instead of branching straight to the header (4d1ef96). -
Block arguments printed as invalid
%Nvalues — Generic WasmSSA printing assigned register numbers to block args that have no SSA name. Example:wasmssa.binop(%arg0: i32, ...)appeared as unrelated%42in dumps, making IR comparisons harder to debug. Correct: unnamed block args print as%arg0,%arg1, … matching the block header. Fix:format_value_namelowers back to%arg{index}forBLOCK_ARGvalues (87c3951). -
Empty
llvm.switchomitted default-edge branch — M8 treated a zero-case switch as a pure value bind on the default edge. Example: when the default target was a loop latch or structured merge label, no branch was emitted and control fell through without any tracking. Correct: default edge to a latch or label emits the same transfer as other M8 control-flow edges. Fix: route zero-case defaults throughm8_emit_find_label/m8_emit_transferbefore value binding (4d7e044). -
Linked WasmSSA modules skipped at wasmssa linking — Many Operations parsed from
--from-wasmpath arrived asOP_TYPE_UNREGISTEREDwith canonicalwasmssa.*names; lowering matched enum tags only. Example: linked self-host modules retained ~0wasmssa.funcbodies instead of 2,425 definitions. Correct: textualwasmssa.*ops resolve toOP_TYPE_WASMSSA_*and lower normally. Fix: addedwasmssa_op_type()name lookup scoped to the wasmssa enum range (4d7e044).
All of these fixes are currently part of commits under the PR: LLVM-to-WasmSSA Work. I am working on identifying more bugs and fixes on these lines, to make the code logically correct. Once the work is passing the CI, I shall create separate PRs for various classes of bugs identified.
This process is still ongoing, finding more bugs and fixing them. Once this task is complete, we shall have a good groundwork, where we have a lowering which follows all the necessary rules, ie not having any other dialect other than LLVM dialect in the platform lowering. Also, we shall have resolved many bugs related to lowering of other dialects as well. So, it is an important step.
This marks the end of my GSoC internship. Of course, since the project has just started, there is still a lot of work to be done to first make the lowering strong, and then iterate on performance to make it faster, before we can integrate LFortran with this backend. But this internship has enabled me to make progress with starting the work, understanding the bits, implementing and iterating on a few designs, and having the groundwork understood for my next steps. I shall be continuing my work on this project, and hope to make more progress in the coming months.
This work has taught me a lot of interesting concepts related to compiler optimizations, compiler internals, the MLIR ecosystem, lowering to machine code, and generating and optimizing it. At times, progress was slow; understanding the concepts and making sure to study various parts of the optimizations like register allocation and instruction selection took more time than expected. The critical feedback received from the mentors, especially from Ondřej Čertík, has been very helpful in understanding the nuances of the work. I was able to learn a lot from this experience, and hope to apply it in my future work.
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.