<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: KATO Kanryu</title>
    <description>The latest articles on DEV Community by KATO Kanryu (@kanryu).</description>
    <link>https://dev.to/kanryu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4104912%2F97cb0313-5686-4836-910e-3279297d3da6.png</url>
      <title>DEV Community: KATO Kanryu</title>
      <link>https://dev.to/kanryu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kanryu"/>
    <language>en</language>
    <item>
      <title>Why I designed my compiler to emit only LLVM IR instead of binaries: Hitchhiking Clang and zero-allocation iterators</title>
      <dc:creator>KATO Kanryu</dc:creator>
      <pubDate>Tue, 01 Sep 2026 18:44:15 +0000</pubDate>
      <link>https://dev.to/kanryu/why-i-designed-my-compiler-to-emit-only-llvm-ir-instead-of-binaries-hitchhiking-clang-and-783</link>
      <guid>https://dev.to/kanryu/why-i-designed-my-compiler-to-emit-only-llvm-ir-instead-of-binaries-hitchhiking-clang-and-783</guid>
      <description>&lt;p&gt;Hi everyone,&lt;/p&gt;

&lt;p&gt;Over the past few weeks, I’ve been building a compiled systems programming language inspired by Go's ergonomics, but designed with zero runtime overhead, no GC, and strict C-ABI compatibility.&lt;/p&gt;

&lt;p&gt;Rather than building a full end-to-end compiler that outputs machine code or PE/ELF binaries directly, I made several deliberate architectural decisions to keep the compiler frontend minimal while maximizing flexibility. I wanted to share a few design patterns and lessons learned that proved effective.&lt;/p&gt;




&lt;h3&gt;
  
  
  1. The "Hitchhiking" Strategy: Stop at LLVM IR
&lt;/h3&gt;

&lt;p&gt;A common temptation when building a compiler is trying to handle binary emission, object packing, and linking within the compiler itself. However, doing so immediately drags you into platform-specific rabbit holes (PE/COFF vs. ELF vs. Mach-O, debug formats, resource embedding, import libraries).&lt;/p&gt;

&lt;p&gt;Instead, the compiler (&lt;code&gt;hikec&lt;/code&gt;) is strictly scoped: it parses source files and emits a single, clean LLVM IR file (&lt;code&gt;.ll&lt;/code&gt;). &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this unlocked:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero-cost toolchain integration:&lt;/strong&gt; On Windows, generating a DLL with an import library (&lt;code&gt;.dll.a&lt;/code&gt;) for C++ clients required nothing more than passing &lt;code&gt;-Wl,--out-implib&lt;/code&gt; directly to Clang.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-language LTO &amp;amp; Inlining:&lt;/strong&gt; When compiling C++ code alongside the emitted &lt;code&gt;.ll&lt;/code&gt; with &lt;code&gt;clang++ -flto&lt;/code&gt;, LLVM optimizes across the language boundary as a single unified IR graph, inlining language functions directly into C++ caller sites.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WASM without WASI-SDK:&lt;/strong&gt; Emitting &lt;code&gt;wasm32&lt;/code&gt; IR allows direct compilation via &lt;code&gt;clang --target=wasm32 -nostdlib&lt;/code&gt; into standalone &lt;code&gt;.wasm&lt;/code&gt; without requiring heavy Emscripten toolchains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User-controlled linking:&lt;/strong&gt; Complex tasks like binding Windows &lt;code&gt;.rc&lt;/code&gt; resources, application manifests, or custom linker scripts remain completely under the user's control in their regular build pipelines.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  2. 2-Pass Stack Iterators for Zero-Allocation &lt;code&gt;for-range&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Without a garbage collector, running &lt;code&gt;for-range&lt;/code&gt; loops over custom dictionary/map types usually presents a dilemma: dynamic heap allocation for iterator state, or restricting traversal syntax.&lt;/p&gt;

&lt;p&gt;To solve this, I implemented a 2-Pass stack iterator protocol resolved at compile time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pass 1 (Size Probe):&lt;/strong&gt; The compiler issues a probe call &lt;code&gt;InitIterator(nil)&lt;/code&gt; to retrieve the byte size of the iterator state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stack Allocation:&lt;/strong&gt; The compiler allocates the exact buffer on the caller's stack frame using &lt;code&gt;alloca&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pass 2 (Initialization):&lt;/strong&gt; &lt;code&gt;InitIterator(buf)&lt;/code&gt; initializes the state in place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iteration:&lt;/strong&gt; &lt;code&gt;Next(buf)&lt;/code&gt; yields key/value pointers until exhaustion.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This provides expressive Go-like &lt;code&gt;for k, v := range map&lt;/code&gt; loops with guaranteed zero heap allocations.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Unified Fat-Pointer ABI for Functions &amp;amp; Closures
&lt;/h3&gt;

&lt;p&gt;To support first-class functions, anonymous functions, and lexical closures with C-ABI compatibility, all callable values are compiled into a unified 2-word fat pointer:&lt;/p&gt;

&lt;p&gt;$$\text{FuncValue} \implies { \text{i8* fn_ptr},\, \text{i8* env_ptr} }$$&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Top-level and stateless functions simply carry a &lt;code&gt;null&lt;/code&gt; environment pointer.&lt;/li&gt;
&lt;li&gt;Closures carry a pointer to a captured environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Escape Analysis:&lt;/strong&gt; The semantic analyzer tracks whether captured variables outlive their enclosing stack frame. Only variables that actually escape are promoted to the heap (&lt;code&gt;malloc&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;At the call site, the runtime invokes &lt;code&gt;fn_ptr&lt;/code&gt; passing &lt;code&gt;env_ptr&lt;/code&gt; as an implicit first argument. Stateless calls invoke zero-overhead dispatch thunks.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Takeaways
&lt;/h3&gt;

&lt;p&gt;Decoupling the frontend (AST, Sema, Monomorphization, IR gen) from the backend optimization/linking pipeline drastically reduced implementation complexity while offering more flexibility to the end-user.&lt;/p&gt;

&lt;p&gt;For those who have built custom languages or LLVM frontends: where do you typically draw the boundary between your compiler and existing toolchains?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;For those interested in the implementation details, the codebase and examples are available on GitHub:&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/kanryu/hike-lang" rel="noopener noreferrer"&gt;https://github.com/kanryu/hike-lang&lt;/a&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>go</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
