Yash Nagda

Blog post

Week 12: GSoC Project Progress

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.constant in mem2reg — Mem2reg still inserted arith.constant for few SSA values. Example: promoted locals in self-host TUs produced mixed arith/llvm IR that aarch64 and wasmssa backends reject. Correct: all constants in the LLVM dialect as llvm.mlir.constant. Fix: switched the mem2reg emission site to llvm.mlir.constant (666d0df).

  • Mach-O lazy stack didn’t update states correctly at control-flow joins — After continue lowered to an unconditional wasm br, the Mach-O lifter restored logical stack depth at end/else but left mem_count at 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: call LAZY_SYNC() after end and on else entry in mlir_wasm_to_macho.c (c44a4d2).

  • Long decimal literals overflow in lex — PI/DPI globals parsed with a wrapped mantissa. Example: 3.14159265358979323846 became ~5.6e-03, so PI_D/DPI_D in corec/base/math.c were wrong in emitted LLVM. Correct: full double-precision literal value. Fix: promote to wide double accumulation when the digit run no longer fits in uint64_t (5557b07).

  • sizeof(char) fixed to 1 byte size — Char types use TY_I32 with int_bits = 8, but type_size returned the i32 width. Example: sizeof(char) * n for arena allocation lowered to 4 * n instead of 1 * n. Correct: sizeof(char) and sizeof(unsigned char) are 1. Fix: added c_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: strcmp on bytes 0xFF vs 0 returned a negative difference instead of 255, which made wrong calculations. Correct: unsigned byte loads zero-extend to i32 before subtraction. Fix: propagate int_unsigned through pointer EX_CAST in tinyC (4214659). Another lowering bug here was that llvm.zext i8 to i32 treated as no-op in WasmSSA lowering. It skipped zero-extensions on the i32 register path. Example: LLVM from base_strncmp kept sign-extended char bytes, so subtraction results were wrong even after the above fix. Correct: llvm.zext i8 to i32 applies i32.and 255. Fix: stop treating i8 zext as identity in llvm to wasmssa lowering (79fc6a8).

  • Integer casts to unsigned char in strncmp — (unsigned char)s[i] was not zero-extended before subtraction. Example: base_strncmp printed -1 instead of 255 for 0xFF - 0. Correct: truncate to i8, then use extui to i32 for unsigned targets. Fix: emit explicit i8 trunc + signed/unsigned extend in tinyC EX_CAST (79fc6a8).

  • Operation llvm.intr.wasm.memory.size is 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.tc failed 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.grow is unsupported — tinyc used the grow intrinsic for __builtin_wasm_memory_grow, but aarch64, x64 ELF, and native LLVM-IR paths only handled memory.size. Example: cross-backend wasm_memory_grow.tc failed to lower intrinsic under llc. Correct: load old page count, add delta with cap, store back, return previous count or -1. Fix: shared __wasm_mem_pages load/store lowering in aarch64, x64, and LLVM-IR lowering (0d00131).

  • File-scope const globals were emitted as mutable variables — static const float variable was always created as a mutable llvm.global. Example: math constants like math_s0 were showed as is_const = false in WasmSSA codes. Correct: immutable globals carry the LLVM constant attribute. Fix: track const on parsed global types and pass is_constant to MLIR_CreateLLVMGlobal (2f31868).

LLVM-WasmSSA Bugs

  • Loop continue skipped latch dispatch — M8 emitted transfers to a loop header as a direct br to the loop label, bypassing the latch exit dispatch. Example: a continue in 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 through m8_emit_loop_latch() instead of branching straight to the header (4d1ef96).

  • Block arguments printed as invalid %N values — Generic WasmSSA printing assigned register numbers to block args that have no SSA name. Example: wasmssa.binop(%arg0: i32, ...) appeared as unrelated %42 in dumps, making IR comparisons harder to debug. Correct: unnamed block args print as %arg0, %arg1, … matching the block header. Fix: format_value_name lowers back to %arg{index} for BLOCK_ARG values (87c3951).

  • Empty llvm.switch omitted 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 through m8_emit_find_label / m8_emit_transfer before value binding (4d7e044).

  • Linked WasmSSA modules skipped at wasmssa linking — Many Operations parsed from --from-wasm path arrived as OP_TYPE_UNREGISTERED with canonical wasmssa.* names; lowering matched enum tags only. Example: linked self-host modules retained ~0 wasmssa.func bodies instead of 2,425 definitions. Correct: textual wasmssa.* ops resolve to OP_TYPE_WASMSSA_* and lower normally. Fix: added wasmssa_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.