Sign in

    qbe

    Download zip
    Author
    Version
    0.6.0
    License
    Apache-2.0
    Last updated
    last month
    Downloads
    61

    #azhzx/qbe

    A MoonBit rewrite of QBE

    #Project Documentation

    #Project Overview

    qbe.mbt aims to port the core backend capabilities of Quick Backend (QBE) to the MoonBit ecosystem.

    Provides a lightweight compiler backend.

    Provides SSA intermediate representation, IL text parsing and output, instruction selection, register allocation, ABI processing.

    #Core Feature Scope

    Provides QBE-style SSA intermediate representation model, supporting functions, basic blocks, temporary variables, instructions, jumps, phi nodes, data segments, and type systems;

    Supports QBE IL text format parsing, output, and pretty printing for exchanging intermediate representations with upstream QBE toolchain or custom frontends;

    Provides unified compilation entry point

    Supports amd64 (System V, GAS output, Linux/macOS two styles)

    Supports WebAssembly (wasm32, WAT text output)

    Supports RISC-V 64 (rv64) and LoongArch 64 (la64, LP64D) GAS output, plus direct SSA interpretation

    Supports basic backend pipeline

    Supports common IL instructions

    Provides debugging auxiliary modules

    Provides unified compilation entry point @qbe.compile / @qbe.compile_debug, covering IL parsing, SSA construction, register allocation, and assembly output;

    Provides WebAssembly compilation entry point @qbe.compile_wasm / @qbe.compile_wasm_debug, covering IL parsing, SSA construction, and WAT text output;

    Provides RISC-V compilation entry point @qbe.compile_rv64 / @qbe.compile_rv64_debug, covering IL parsing, SSA construction, RISC-V register allocation, and assembly output; Provides LoongArch64 compilation entry point @qbe.compile_la64 / @qbe.compile_la64_debug (LP64D ABI, data sections and float constant pool emitted); Provides the SSA interpreter entry @qbe.interpret (async) - executes pre-isel IR directly with a built-in portable runtime (putchar/puts/printf/malloc/free/exit) and an injectable external-symbol hook;

    Provides MoonBit unit/blackbox/whitebox tests, maintaining core regression tests (.ssa differential regression + moon test);

    Provides README examples covering IL parsing, SSA construction, register allocation, assembly output, and target architecture selection.

    #Quick Start

    @qbe.compile compiles an IL text to amd64 GAS assembly; @qbe.compile_debug returns dumps from each stage:

    ///|
    test {
    let src =
    #|export function w $add(w %a, w %b) {
    #|@start
    #| %s =w add %a, %b
    #| ret %s
    #|}
    #|
    match @qbe.compile(src) {
    Ok(assembly) => {
    assert_true(assembly.contains("addl"))
    assert_true(assembly.contains("add:"))
    }
    Err(_) => fail("compile failed")
    }
    match @qbe.compile_debug(src, "P") {
    Ok(dump) => assert_true(dump.contains("After parsing"))
    Err(_) => fail("compile failed")
    }
    }

    @qbe.compile_wasm compiles an IL text to WAT (WebAssembly Text) format:

    ///|
    test {
    let src =
    #|export function w $add(w %a, w %b) {
    #|@start
    #| %s =w add %a, %b
    #| ret %s
    #|}
    #|
    match @qbe.compile_wasm(src) {
    Ok(wat) => {
    assert_true(wat.contains("(func $add"))
    assert_true(wat.contains("i32.add"))
    }
    Err(_) => fail("wasm compile failed")
    }
    }

    @qbe.compile_rv64 compiles an IL text to RISC-V 64 GAS assembly:

    ///|
    test {
    let src =
    #|export function w $add(w %a, w %b) {
    #|@start
    #| %s =w add %a, %b
    #| ret %s
    #|}
    #|
    match @qbe.compile_rv64(src) {
    Ok(assembly) => {
    assert_true(assembly.contains("add:"))
    assert_true(assembly.contains("addw a0, a0, a1"))
    }
    Err(_) => fail("rv64 compile failed")
    }
    }

    @qbe.compile_la64 compiles an IL text to LoongArch64 GAS assembly:

    ///|
    test {
    let src =
    #|export function w $add(w %a, w %b) {
    #|@start
    #| %s =w add %a, %b
    #| ret %s
    #|
    #|}
    match @qbe.compile_la64(src) {
    Ok(assembly) => {
    assert_true(assembly.contains("add:"))
    assert_true(assembly.contains("add.w"))
    }
    Err(_) => fail("la64 compile failed")
    }
    }

    @qbe.interpret executes the SSA directly (async, like lli for LLVM IR):

    ///|
    async test {
    let src =
    #|export function l $square(l %x) {
    #|@start
    #| %r =l mul %x, %x
    #| ret %r
    #|
    #|}
    match @qbe.interpret(src, entry="square", args=[@interp.VInt(7)]) {
    Ok((@interp.VInt(v), _out)) => assert_eq(v, 49L)
    Ok(_) => fail("unexpected result kind")
    Err(@util.QbeError::CompileError(m)) => fail("ce: \{m}")
    Err(@util.QbeError::Ice(m)) => fail("ice: \{m}")
    Err(@util.QbeError::ParseError(_, _, m)) => fail("parse: \{m}")
    }
    }

    #Technical Details

    #Package Structure and Compilation Pipeline

    MoonBit packages organized by compilation pipeline stages (see doc/):

    PhasePackageDescription
    Data StructurestypesSSA IR: Fn/Blk/Ins/Phi/Jump/Con/Tmp/Dat etc., shared by all backend packages
    UtilitiesutilError types, string interning (Interner), output, sorting
    LexinglexerIL text → token sequence, errors collected to err_msgs instead of exceptions
    ParsingparserToken sequence → Fn/Dat/Typ, supports type/data/function three top-level definitions
    CFG AnalysiscfgReverse postorder, predecessors, dominator tree, dominance frontiers, loop depth, alias analysis, jump simplification
    SSA ConstructionssaUse chains, memopt, phi insertion, block renaming, loadopt, copy propagation, validity checking
    Constant FoldingfoldDirectly evaluates instructions whose operands are all constants and replaces with references
    Wasm ABIabi_wasmWasm calling convention: Par/Arg→Nop, Call simplification
    Wasm Instruction Selectionisel_wasmWasm op mapping, address mode decomposition, CFG→structured control flow
    Wasm Assembly Outputemit_wasmWAT text format output
    ABI ProcessingabiSystem V AMD64 calling convention: parameter/return registers, stack spilling, vararg
    Instruction Selectioniselamd64 instruction patterns: immediates, address modes, division magic numbers, conditional jumps
    Liveness AnalysisliveBackward data flow to compute in/out, block boundary statistics nlive_w/nlive_d
    Register SpillingspillCost-based and loop-weighted spilling point selection, iterates to convergence
    Register AllocationregaBuilds interference graph from live sets, greedy coloring
    Assembly OutputemitRenders GAS assembly (Linux .L/macOS L, _ prefix)
    RISC-V ABIabi_rv64rv64 calling convention: A0–A7 / FA0–FA7 parameters and returns, aggregate type splitting
    RISC-V Instruction Selectionisel_rv64rv64 instruction mapping, compare+branch merging
    RISC-V Assembly Outputemit_rv64RISC-V GAS text output
    LoongArch ABIabi_la64la64 (LP64D) calling convention: A0-A7 / FA0-FA7 parameters and returns
    LoongArch Instruction Selectionisel_la64la64 instruction mapping, comparisons lowered to slt/sltu
    LoongArch Assembly Outputemit_la64LoongArch GAS text output (data + float pool)
    SSA Interpreterinterpdirect pre-isel IR execution with built-in runtime
    CLI Entrycmd/mainArgument parsing and file I/O (thin shell, calls @qbe facade, -t selects target)
    Library Entry.Unified compilation API compile / compile_debug and IR type re-exports

    Complete pipeline (run_passes in pipeline.mbt, encapsulated for library users in @qbe.compile):

    parse → fillrpo → fillpreds → filluse → memopt → filldom → fillfron → filllive(false) → phiins → renblk → filluse → ssacheck → fillloop → fillalias → loadopt → filluse → ssacheck → copy → filluse → fold → abi → fillpreds → filluse → isel → fillrpo → filllive → fillcost → spill → rega → fillrpo → simpljmp → fillrpo → fillpreds → emitfn

    Wasm pipeline (run_passes_wasm, encapsulated for library users in @qbe.compile_wasm):

    parse → fillrpo → fillpreds → filluse → memopt → filldom → fillfron → filllive(false) → phiins → renblk → filluse → ssacheck → fillloop → fillalias → loadopt → filluse → ssacheck → copy → filluse → fold → abi_wasm → fillpreds → filluse → isel_wasm → [skip spill/rega — wasm has no physical registers] → emit_wasm

    RISC-V pipeline (run_passes_rv64, encapsulated for library users in @qbe.compile_rv64):

    parse → fillrpo → fillpreds → filluse → memopt → filldom → fillfron → filllive(false) → phiins → renblk → filluse → ssacheck → fillloop → fillalias → loadopt → filluse → ssacheck → copy → filluse → fold → abi_rv64 → fillpreds → filluse → isel_rv64 → init_rv64_target() ← switch TargetCfg (register layout) → fillrpo → filllive → fillcost → spill → rega → fillrpo → simpljmp → fillrpo → fillpreds → emit_rv64 LoongArch pipeline (`run_passes_la64`, encapsulated for library users in `@qbe.compile_la64`):
    parse → cfg/ssa/live/fold passes → abi_la64 → fillpreds → filluse → isel_la64 → init_la64_target() ← switch TargetCfg (register layout) → fillrpo → filllive → fillcost → spill → rega → simpljmp → emit_la64 (+ data sections + float constant pool)
    Interpreter path (`@qbe.interpret`):
    parse → lay out data segment → bind args → interpret pre-isel IR (phi, calls, memory, builtins) → Result[InterpValue, QbeError]

    #Intermediate Representation Design

    • SSA IR: Functions (Fn), basic blocks (Blk), temporary variables (Tmp), instructions (Ins) are all mutable structs, modified in place without producing copies; supports phi nodes and multiple jump forms (unconditional jump, conditional jump, integer/float conditional jump, 5 return types).
    • Opcodes: Op enum covers all 100+ QBE instructions (arithmetic, bitwise, shifts, comparisons, load/store, extensions/conversions, alloc, vararg, call and internal instructions Nop/Addr/Swap/Xcmp etc.), with OpInfo carrying operand properties and foldable markers.
    • Reference Types: Ref is an operand reference, unifying temporary variables (RTmp), constants (RCon), types (RType), stack slots (RSlot), call points (RCall), memory (RMem).
    • Bit Sets BSet: Compact bit sets implemented with Array[UInt64], used for liveness variable sets and register masks.
    • Register Numbers: RAX=1..RSP=16, XMM0=17..XMM15=32, RXX=0 means "no register".

    #Key Algorithms

    • SSA Construction: Based on dominance frontiers (fillfron) inserts phi nodes, block and variable renaming establishes SSA form, ssacheck performs validity checking.
    • Liveness Analysis: Backward data flow iterates to fixed point; gen_set built once and reused, only recomputing in/out.
    • Register Allocation: Spill first by cost (use/definition point count + 10^loop_depth loop weighting, word/double channels evaluated separately by NGPS=9/NFPS=15), then in rega builds interference graph from live sets and does greedy coloring; inconsistent registers at block boundaries get copy inserted for synchronization.
    • Instruction Selection: Does semantics-preserving strength reduction — folding immediates into instructions, combining add chains into [base + index*scale + offset] addressing, converting constant divisor division to magic number multiply-add-shift, converting comparison + jnz patterns to amd64 conditional jumps.
    • Memory Optimization: memopt eliminates redundant alloc/load/store; loadopt eliminates repeated loads from same address with no intervening store in same block; copy propagation merges equivalent temporary variables.

    #ABI and Target Support

    Provides four targets, selected with -t on command line (amd64_sysv default), with independent library API entry points (plus --run for direct interpretation):

    • amd64_sysv: abi phase replaces abstract Arg/Par/Ret* with concrete register/stack slot references; aggregate types follow System V rules for register vs memory; outputs two GAS styles (Linux .L / macOS L + _ prefix, selected with -G). Has complete 406-case differential regression.
    • wasm: abi_wasm phase replaces Par/Arg instructions with Nop (parameters passed directly via local variables), simplifies Call references; isel_wasm does instruction mapping then skips register allocation (wasm is stack machine, no physical registers), emit_wasm outputs WAT text format. wasm32 pointer width is 32 bits (Km = Kw), no Kl type.
    • rv64: abi_rv64 lowers parameters to A0–A7 / FA0–FA7 per RISC-V calling convention, returns via A0/A1 / FA0/FA1; isel_rv64 maps IL instructions to RISC-V instructions (compare + branch merged directly, no flags, no magic number division, no complex addressing); then runs spill/rega same as amd64 — target differences switched at runtime via types.TargetCfg (init_amd64_target() / init_rv64_target()), emit_rv64 outputs RISC-V GAS assembly (fp/ra frame chain, 16-byte stack alignment).
    • la64: abi_la64 lowers parameters to A0-A7 / FA0-FA7 per the LoongArch LP64D psABI; isel_la64 lowers comparisons to slt/sltu sequences (no flags) and materializes constants; emit_la64 outputs LoongArch GAS assembly with data sections and the floating-point constant pool. Reuses spill/rega via init_la64_target().
    • interp: @qbe.interpret executes pre-isel IR directly - flat little-endian memory, data-segment layout with symbol refs, function pointers, recursion, a pure-MoonBit builtin runtime, and an injectable external hook (the analogue of LLVM ORC's symbol resolution).

    Three targets compared:

    amd64_sysvwasmrv64la64interp
    Library entrycompile / compile_debugcompile_wasm / compile_wasm_debugcompile_rv64 / compile_rv64_debugcompile_la64 / compile_la64_debuginterpret (async)
    CLI-t amd64_sysv (default)-t wasm-t rv64-t la64--run FUNC[,ARG]...
    Outputx86-64 GASWATRISC-V GASLoongArch GASinterpreted result
    Register allocationspill + regaskipped (stack machine)spill + rega (TargetCfg switch)spill + rega (TargetCfg switch)none (direct execution)
    Validation strengthDifferential regression byte-by-byteUnit tests + snapshotsUnit tests + e2e snapshots (no reference baseline)Unit tests + e2e snapshots (psABI-verified)Unit tests + e2e tests

    #Debugging and Testing

    • Command-line -d <flags> provides per-stage dumps (-dP parse, -dM memopt, -dN SSA, -dC copy, -dF fold, -dA abi, -dI isel, -dL live, -dS spill, -dR rega), combinable; when debug is enabled, assembly is not output. Library entry compile_debug(text, flags) returns the same dump text.
    • Tests in three layers:
      • Unit/whitebox tests *_wbtest.mbt: Cover all compilation pipeline packages — types (BSet/Con/Ref/Op/Class/Jump etc.), util (Interner/formatting), lexer, parser, cfg (dominator tree/loop/jump simplification), ssa (phi insertion/copy/memopt), fold, live, abi/abi_wasm/abi_rv64/abi_la64, isel/isel_wasm/isel_rv64/isel_la64, spill, rega, emit/emit_wasm/emit_rv64/emit_la64, interp, cmd/main;
      • Blackbox tests qbe_test.mbt + qbe_snapshot_test.mbt: Directly call @qbe.compile / @qbe.compile_debug, covering end-to-end compilation (arithmetic, floating-point, memory, recursion, loop phi) and error paths; qbe_snapshot_test.mbt generated by python tools/gen_snapshot_mbt.py from test/ categories, anchored with inspect snapshots;
      • Differential regression: test/*.ssa (406 cases) compared byte-by-byte with reference qbe binary (tools/qbe-ref pinned snapshot, built with make -C tools/qbe-ref) (python compare.py, can specify other binary with QBE_REF).
    • Run: moon test; update snapshots: moon test --update; coverage: moon coverage analyze.

    #Porting and Attribution Notes

    Original project information Original project name: Quick Backend (QBE)

    Original project link: https://github.com/8l/qbe

    This project license: Apache 2.0

    Original project license: MIT

    Original project license text
    © 2015-2017 Quentin Carbonneaux quentin@c9x.me Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

    Compared to the original project, this project makes the following simplifications and redesigns:

    Rewrites code using MoonBit's modern ML-family language style instead of replicating C's suckless structure;

    Prioritizes implementing core backend capabilities that can run independently in MoonBit

    Rewrites manual memory management from C code to MoonBit's safe data structures and enum types, reducing memory risks;

    #Future Plans

    • ✅ WebAssembly (wasm32) code generation support (WAT text output)
    • ✅ RISC-V 64 (rv64) code generation (GAS output, reusing spill/rega)
    • ✅ LoongArch 64 (la64) code generation (LP64D ABI, data + float pool, reusing spill/rega)
    • ✅ SSA interpreter (interp package, --run CLI flag, builtin runtime + external symbol hook)
    • rv64 backend improvements: data segment and floating-point constant rodata output, differential reference verification
    • Interface with mbtcc to verify full end-to-end feasibility

    BSet

    using @azhzx/qbe/types { type BSet }

    Blk

    using @azhzx/qbe/types { type Blk }

    Class

    using @azhzx/qbe/types { type Class }

    Class - corresponds to enum Class in all.h

    Con

    using @azhzx/qbe/types { type Con }

    Dat

    using @azhzx/qbe/types { type Dat }

    using @azhzx/qbe/types { type Fn }

    Ins

    using @azhzx/qbe/types { type Ins }

    InterpValue

    Jump

    using @azhzx/qbe/types { type Jump }

    using @azhzx/qbe/types { type Op }

    Ref

    using @azhzx/qbe/types { type Ref }

    ADT replacing C bit-field (uint type:3; uint val:29)

    Tmp

    using @azhzx/qbe/types { type Tmp }

    Typ

    using @azhzx/qbe/types { type Typ }

    compile

    fn compile(text : String, gas? : String) -> Result[String,
    QbeError
    ]

    compile_debug

    fn compile_debug(text : String, flags : String) -> Result[String,
    QbeError
    ]

    compile_la64

    fn compile_la64(text : String, gas? : String) -> Result[String,
    QbeError
    ]

    compile_la64_debug

    fn compile_la64_debug(text : String, flags : String) -> Result[String,
    QbeError
    ]

    compile_rv64

    fn compile_rv64(text : String) -> Result[String,
    QbeError
    ]

    compile_rv64_debug

    fn compile_rv64_debug(text : String, flags : String) -> Result[String,
    QbeError
    ]

    compile_wasm

    fn compile_wasm(text : String) -> Result[String,
    QbeError
    ]

    compile_wasm_debug

    fn compile_wasm_debug(text : String, flags : String) -> Result[String,
    QbeError
    ]

    interpret

    fn interpret(src : String, entry? : String, args? : Array[
    InterpValue
    ], max_steps? : Int, max_depth? : Int, hook? : (String, Array[
    InterpValue
    ]) ->
    InterpValue
    ??) -> Result[(
    InterpValue
    , String),
    QbeError
    ]