Repository navigation
Conversation
fa8a7a0 to
2e6eebb
Compare
41849e2 to
fa69209
Compare
There was a problem hiding this comment.
Is it possible to provide a diagram illustrating VIR's design?
There was a problem hiding this comment.
Yes, I plan to write a paper on VIR later. For now, I am revising the VIR documentation to provide a high-level overview.
492a56c to
799fb42
Compare
malloc() compared a freed chunk's recorded size, which includes the chunk header, against the requested size, which does not, so a request in the last header's worth of a freed chunk got a chunk too small for it and wrote into the neighboring mapping. malloc also checked the raw mmap2 result for -1, but the system call reports failure as a negative errno, so a failed mapping was used as a chunk header. Freeing the last chunk now resets the allocator tail even when it has no predecessor. fseek() and ftell() test the raw lseek result for any negative errno, and RV32 passes the high offset word and reads the 64-bit position that _llseek reports.
The RV32 self-hosted image is already past 1 MiB of code, and direct calls, including the startup stub's call to main, used a JAL, which reaches only 1 MiB. Any growth that moved a callee past that limit aborted the stage 1 build. Call through AUIPC and JALR, as calls into the PLT already do, and count the extra instruction in the size estimates.
Compute load segment layout once and route ELF32 and ELF64 program and file headers through shared serializers, and emit ELF32 section headers from initialized structs. Byte layout and offsets are unchanged.
Introduce VIR, the value-oriented IR the compiler will be built on: typed values with reverse uses, block parameters instead of phi instructions, explicit ordered effects for memory and calls, and fixed-prototype call signatures. A verifier checks dominance and use-def consistency, dumps are deterministic, and SCCP, DCE, local CSE, GVN and LICM run on it. Nothing in the compiler uses it yet. Keeping the core standalone lets its invariants be tested by unit tests and a structural fuzzer before a frontend depends on them.
Replace the legacy SSA middle end, its register allocator and its standalone SCCP pass. The parser builds VIR directly through a sealed-block frontend, VIR owns source values, typed memory effects, control flow, calls and target-independent optimization, and a shared machine lowering turns it into allocated machine IR that all four backends emit. That lowering handles ABI arguments and results, paired 32-bit registers, spill homes, coalescing, critical-edge copies, variadic saves, block layout and persistent callee-saved allocation. The driver accepts several input files as separate translation units, whose record definitions must agree in pointer constness and volatility, hash maps own their keys, and the lexer, preprocessor and parser share their repeated paths. A missing source file is reported as an error instead of aborting, an integer stored into a wider object is widened, a shift takes its type from the left operand alone, and a function-like macro named at the end of an expansion is invoked. Optimization runs by default, and --no-opt skips the optional VIR passes for debugging and for isolating an optimizer fault. Graded levels would ask a question with one useful answer: a lighter schedule generates the same code on nearly every program and compiles no faster, because the unoptimized graph leaves more values to lower. VIR regression suites, a corpus comparison with and without optimization and a host-compiler fuzzer guard the new pipeline. They run target binaries, the stage 2 compiler included, through the emulator like the rest of the suite, and the cross-target CI lanes get an hour, since their stage 2 suites run the self-hosted compiler under qemu for every case.
Reduce repeated address calculations with descending-loop strength reduction, reuse loads along effect-free predecessor chains, and keep machine values resident across safe block boundaries. Expand the ARM64 register bank, save only the callee-saved registers a function uses, fold branches, immediates, selects and indexed memory operations, and repeat a small loop test at its back edge. On x86-64, fold a memory read into the arithmetic that consumes it, fix compare and AND branch fusion to respect both successors' live-outs and invert an edge whose true successor falls through. Parser expression and qualifier handling is shared along the way, and a constant shift whose count reaches the operand's width is rejected for 64-bit operands as well as 32-bit ones. The throughput gate requires 80% of GCC -O1 in both wall and child CPU time, and all seven workloads meet it: ARM64 81.3-92.5%, x86-64 80.3-101.8%.
The verifier counted value and block IDs by rescanning the function, the SSA builder searched one flat binding list on every read, block membership walked the block list, SCCP located each edge by scanning all of them, and constant replacement repeated a CFG walk for every use it checked. Register pinning walked forward from each writer over every block it reached, rescanning the other value's uses per block, and reachability rescanned every function once per newly reached one. Each of these grew quadratically with function size. Keep a block table per function, track seen value IDs, index SSA bindings by block, number SCCP edges once, walk reachability once per replacement, answer phi affinity from the dominator tree, find interference by walking backward from the survivor's uses, which its live range bounds, and drive reachability from a worklist. Compiling src/main.c takes 24 times fewer instructions by default and 27 times fewer with --no-opt. The SSA builder also made nearly a million small allocations per self-compile, which the self-hosted compiler serves from shecc's own libc, whose malloc scans its whole free list. Bindings, block states and edge scratch now come from an arena that grows geometrically, and parameter edits keep their edge records inline, so the self-hosted compiler compiles itself in 1.9 seconds instead of 7.0 on x86-64. Generated code is byte-identical on every target. Add a script that compares end-to-end compilation time against GCC.
Branches and jumps now take their shortest form once the layout is known: x86-64 relaxes rel32 to rel8 and drops jumps to the next instruction, RISC-V picks among BEQZ/BNEZ/JAL forms by iterating the layout to a fixed point, and ARM and ARM64 omit a jump into the block laid out next. Leaf functions with no frame skip the frame setup on x86-64 and ARM64, x86-64 addresses frame slots through a biased RBP with 8-bit displacements, and small constants and immediates use the shorter encodings. Loop literals in ARM64 leaves stay in caller-saved registers. The shared lowerer pins a value whose live range crosses no call to a caller-saved register, or leaves a parameter in the register it arrives in, and orders edge copies so that overlapping memory homes no longer force every argument through the snapshot area. A load that reads back the value the same block just stored there is forwarded. RISC-V gains s6-s11 as lanes and saves exactly the callee-saved registers a function names, since the lowerer stages through the last of them. The stage 1 compiler shrinks by 25% on x86-64, 31% on RISC-V, 11% on ARM and 8% on ARM64, for 4% more instructions in a self-compile.
Give VIR one reference page for its name, invariants, compiler options, optimization schedule, shared machine lowering, validation commands and implementation map, and point the README at it. The page also records why shecc moves to VIR: what block parameters, ordered effects and fixed call signatures buy over classic SSA, and what the move costs in compile time. The README now describes the VIR pipeline and its options instead of the retired two-pass SSA design.
The allocated instructions the shared lowerer produces were still named after the retired two-pass SSA design, defined among the parser types and built by helpers spread over globals.c, peephole.c and a one-pass file. They are VIR's machine layer, so they now live with the lowerer: the operation type and its prototypes in vir-lower.h, and its creation, flattening, register dataflow and frame queries in vir-lower.c. Names follow: a machine operation is a vir_mop_t, a block keeps its mops, and the flat emission order is MOP_FLAT. The backends share what each had copied: one flattening loop, one word counter for the size estimate, and one frameless-leaf test. ARM's fall-through marks move into its own backend, and peephole.c keeps only rewrites. Nothing changes in what any target generates: the stage 0 compilers before and after produce byte-identical output for every test program and for shecc's own source, with and without optimization.
Many VIR test scripts and fixtures were written during the migration and now repeat what other tests cover: arm64 fixtures that vary one qualifier or type of the same case, statistics and large-CFG scripts that rerun the baseline, and driver cases that duplicate others exactly. Remove them, fold the ARM verifier check into the stage 0 harness, and name the remaining fallback fixtures after what they exercise. The optimization baseline now compares the stage 0 and stage 2 output checksums directly, and the ABI scripts drop argument counts that the larger cases already cover.
Every function's VIR graph stayed allocated until the compiler exited, and the SSA builder, the preprocessed token arena and the allocator's free list all kept memory that was no longer needed. Release each graph right after it is lowered, and graphs nothing references before lowering starts; free the SSA builder once a function is finished; copy the parser's token stream into a fresh arena so the preprocessor's dead tokens can be dropped; and remove a block parameter in place instead of reallocating every incoming edge. The C library's free list becomes size-segregated bins, so malloc no longer walks every freed chunk, and chunks of 512 KiB or more go back to the kernel. A self-hosted compiler relies on it, and the single list made it slower once more memory was freed. Peak RSS of a stage 0 self-compile drops from 332 to 194 MB, and the stage 2 compiler's from 403 to 221 MB.
A second file-scope typedef of the same name was ignored even when it named a different type, so later uses silently kept the first, and the qualifier list after a typedef name in a cast accepted restrict on a non-pointer type. Array and record sizes are ints, and an extent past INT_MAX wrapped: int a[1073741825] got four bytes and writes through it clobbered the neighbouring globals. A record member of incomplete type crashed the compiler when a struct contained itself. Diagnose each. Deeply nested macro calls, #if parentheses and conditional operator chains recursed without bound and exhausted the machine stack. Bound macro expansion at 256 levels and count the other two against the existing expression depth limit, so each reports an error.
Several checks during VIR construction and lowering scanned whole lists: whether a value is still linked into its block, whether an address root conflicts with an earlier one, which SSA binding holds a variable, and which phi feeders a pinned value affects. Choosing which values get a fixed register also rescanned every value on each round for the best score. Each value now records its own membership, address roots and SSA bindings are hashed, the pin candidates are sorted once by score with ties kept in ID order, and the pin loop queues only the values a new pin can affect. Frontend completion no longer verifies each graph, since every graph that is dumped or lowered is verified after optimization anyway. Output is byte-identical. A stage 0 self-compile runs 13 percent fewer instructions, and 25 percent fewer with optimization off.
Drop functions, macros and a struct field that nothing uses any more, along with the x86-64 trace flag that was never set. Internal errors for calls to undefined functions now go through fatal() with the function's name, and the ARM and RISC-V encoders shift into bit 31 in unsigned arithmetic instead of overflowing a signed int. Every target generates byte-identical code.
A loop-carried recurrence such as a = a * k + i + C added the constant to the chain on every iteration. A reassociation pass now moves the constant onto the operand that is ready on block entry, taking one addition off the chain. Values competing for a fixed register were ranked by how many blocks used them, so an address that is cheap to rebuild could take the register from the value an inner loop runs on. A use outside the defining block now counts eight times more for every loop around it, up to four levels, unless the value can be rebuilt as a literal. Against gcc -O1 on x86-64, the rng benchmark goes from 0.81 to 1.00 of its speed and sort from 0.94 to 0.99; the others are unchanged.
55df91f to
783ac3a
Compare
Generated ARM64 code still spent instructions where a shorter form exists. Comparisons now take an immediate operand, fuse into CBZ and CBNZ, or become TST after an AND. Logical operations take any bitmask immediate, and add, sub and the logical operations take shifted register operands, so a separate shift disappears. Adds take immediates shifted by 12, a multiplication by 2^k+1 becomes a shifted add, a product added at the same width becomes MADD, and literals and addresses sink next to their single use so these folds can see them. Stores of zero use the zero register, constant extensions fold at compile time, and a load followed by a same-width sign extension becomes one extending load. An int extended and shifted left is one SBFIZ or UBFIZ, and a load followed by a small step of its base register writes the base back itself. Accesses through the address of a global or a frame slot take its offset directly, an int index extends inside the access, and a load lands in the register of the copy after it. Small constants and global addresses used only by such folds are no longer pinned to a register, scalar globals are laid out ahead of arrays so that a single offset reaches them, an outer loop's block parameter may share its register with the inner loop's, and definitions that the folds leave unread are dropped. The last saved pair of a frame of up to 480 bytes also allocates the locals, saving a SUB and an ADD. Blocks jumped to from their only predecessor are merged into it, a value is computed directly into the register its copy or return wants, additions whose result is only zero extended use W registers, and entry tests whose outcome is known on the incoming edge, from literals or from two offsets of one pointer, are settled there. Large frame offsets use a shifted add, 32-bit constants with one 0xffff halfword take a single MOVN, and static images reach rodata, data and functions with ADR while they stay under 1 MiB. Startup loads argc and argv only when main takes them and emits the syscall helper only when it is used, and early returns share an epilogue of at least four words. A function that starts by testing its arguments and returning one of them, as fib does, runs that test and return ahead of its prologue. Shorter code moved a hot loop across a 64-byte fetch block on the eMAG, which cost a fifth of its throughput. An innermost call-free loop of up to 64 bytes that would straddle one is now padded to a 32- or 64-byte boundary, measured by address because code starts after the ELF headers. Constants hoist only out of real loop bodies. The frozen stage 1 compiler shrinks by 26%, the throughput fixtures from 2328 to 1088 bytes against 996 for GCC -O1, and throughput on the eMAG rises to 95-104% of GCC -O1. A self-compile runs 13% fewer instructions, as machine liveness reads each operation once.
Generated x86-64 code was 70% larger than GCC -O1 on the throughput fixtures. Zero is loaded with XOR when no flags are live across it, a call result that nothing reads stays in RAX, addresses of rodata and functions and the global base use a 32-bit MOV instead of MOVABS, and a mask that fits a byte is tested with an 8-bit immediate. Later returns branch to a function's first epilogue when that is at least seven bytes long. A comparison sets its byte result directly where that needs no prefix, and one that is only returned forms it in EAX. A load whose only use is a comparison becomes the comparison's memory operand, an access through a global's address that nothing else reads addresses the global itself, and the entry stub and an empty global initializer shrink to what main needs. A rotated loop keeps the constants its single entry left in registers, so a first test of two known constants becomes a jump. Literals now move to the immediate operand of an equality comparison as of other commutative operations. Shorter code moves hot code, and on Zen 1 a 57-byte loop straddling a 64-byte fetch block ran at six sevenths of its speed and fib at nine tenths. Loops and functions of up to 64 bytes that would straddle one are padded to a 32- or 64-byte boundary by address, and longer loops start on a 32-byte one, within 32 bytes of padding. The frozen stage 1 compiler shrinks by 10% and the fixtures from 1675 to 1396 bytes against 988 for GCC -O1, with throughput unchanged.
Every function's VIR graph kept all the memory it was built with until it was lowered, including values the optimizer had already removed and the frontend's per-function bookkeeping. Each function is now optimized as soon as it is parsed, and its live graph is copied into one arena sized to fit, so the dead nodes go with the old arena. The frontend's block and source tables come from a scratch arena reset after every function. A use record holds one tagged owner instead of five owner pointers, a value keeps its address fields behind a pointer only address values carry, and var_t's fields are ordered to drop padding. Parsing also did work nothing used: two join blocks were created for every expression, though only those with a logical operator need them, and generated temporaries stayed on their scope's name lookup list. SSA completion declared a block parameter in every block it reached, even one with a single predecessor, where the predecessor's value is the answer; it now follows such chains and binds that value instead. Those parameters often outlived optimization, so the stage 1 compiler shrinks by about 4 percent on every target. The native throughput fixtures compile to the same bytes. Because every function is optimized and verified when it is parsed, an optimizer failure in a function nothing calls now stops the compile. A stage 0 self-compile on x86-64 peaks at 91 MB instead of 204 MB and runs 12 percent fewer instructions. Stage 0 is now built with -O2, which takes off a further quarter.
There was a problem hiding this comment.
2 issues found across 16 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/vir-frontend.c">
<violation number="1">
P2: This walk can loop forever on a disconnected CFG cycle whose blocks each have one predecessor and no binding for `var`, such as unreachable `dead: if (c) goto dead;` followed by a local read. `native_finish` reaches this before removing unreachable blocks, so detect and resolve predecessor cycles instead of following them indefinitely.</violation>
</file>
<file name="src/vir.c">
<violation number="1">
P2: `vir_function_addr` initializes only `addr->name`; the `kind`, `slot`, `size`, `alignment`, and `root_next` fields of the new `vir_address_t` are left to whatever `vir_arena_alloc` hands back. `vir_verify_block` requires exactly `addr->kind == VIR_ADDRESS_NONE`, `slot == 0`, `size == 0`, and `alignment == 0` for `FUNC_ADDR` values, so every function-address value now depends on the arena delivering zero-filled memory. The same change's `new_addr` explicitly sets every field, and `vir_root_index_insert` memsets its freshly allocated table — evidence that raw arena allocations are not guaranteed zeroed. Initialize `kind` (and the zero fields) explicitly so correctness does not hinge on the arena's clearing behavior.</violation>
</file>
Tip: Get a Slack DM when cubic finishes reviewing your PRs. Connect Slack
View guided diff | Turn on auto-fix | Re-trigger cubic
| @@ -0,0 +1,1309 @@ | |||
| #include "vir-frontend.h" | |||
There was a problem hiding this comment.
P2: This walk can loop forever on a disconnected CFG cycle whose blocks each have one predecessor and no binding for var, such as unreachable dead: if (c) goto dead; followed by a local read. native_finish reaches this before removing unreachable blocks, so detect and resolve predecessor cycles instead of following them indefinitely.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/vir-frontend.c, line 977:
<comment>This walk can loop forever on a disconnected CFG cycle whose blocks each have one predecessor and no binding for `var`, such as unreachable `dead: if (c) goto dead;` followed by a local read. `native_finish` reaches this before removing unreachable blocks, so detect and resolve predecessor cycles instead of following them indefinitely.</comment>
<file context>
@@ -934,6 +958,31 @@ static vir_value_t *native_incoming(native_function_t *ctx,
+ basic_block_t *top = cursor;
+ while (top != ctx->source->bbs && top->prev_idx == 1 && top->prev[0].bb &&
+ !vir_ssa_read(ctx->ssa, native_block(ctx, top)->cursor->vir, id))
+ top = top->prev[0].bb;
+ vir_value_t *value = native_incoming(ctx, top, var);
+ for (basic_block_t *bb = cursor; bb != top; bb = bb->prev[0].bb)
</file context>
| @@ -0,0 +1,6789 @@ | |||
| #include "vir.h" | |||
There was a problem hiding this comment.
P2: vir_function_addr initializes only addr->name; the kind, slot, size, alignment, and root_next fields of the new vir_address_t are left to whatever vir_arena_alloc hands back. vir_verify_block requires exactly addr->kind == VIR_ADDRESS_NONE, slot == 0, size == 0, and alignment == 0 for FUNC_ADDR values, so every function-address value now depends on the arena delivering zero-filled memory. The same change's new_addr explicitly sets every field, and vir_root_index_insert memsets its freshly allocated table — evidence that raw arena allocations are not guaranteed zeroed. Initialize kind (and the zero fields) explicitly so correctness does not hinge on the arena's clearing behavior.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/vir.c, line 1158:
<comment>`vir_function_addr` initializes only `addr->name`; the `kind`, `slot`, `size`, `alignment`, and `root_next` fields of the new `vir_address_t` are left to whatever `vir_arena_alloc` hands back. `vir_verify_block` requires exactly `addr->kind == VIR_ADDRESS_NONE`, `slot == 0`, `size == 0`, and `alignment == 0` for `FUNC_ADDR` values, so every function-address value now depends on the arena delivering zero-filled memory. The same change's `new_addr` explicitly sets every field, and `vir_root_index_insert` memsets its freshly allocated table — evidence that raw arena allocations are not guaranteed zeroed. Initialize `kind` (and the zero fields) explicitly so correctness does not hinge on the arena's clearing behavior.</comment>
<file context>
@@ -727,17 +1142,20 @@ static vir_value_t *new_addr(vir_function_t *func,
+ addr->size = size;
+ addr->alignment = alignment;
+ addr->name = copy;
+ value->addr = addr;
vir_root_index_insert(func, value);
return value;
</file context>
This replaces the legacy SSA middle end, its register allocator and the standalone SCCP pass with VIR, a value-oriented IR with block parameters, explicit ordered effects and a verifier. The parser builds VIR directly, and one shared machine lowering produces allocated machine IR for all four backends. The ARM64 and x86-64 backends then gain the folding, branch layout and register residency needed to come within 10% of GCC -O1 on the native throughput fixtures. Optimization is on by default, and
--no-optturns it off for debugging and for isolating an optimizer fault. docs/vir.md describes the IR, why shecc moves to it, its options and the validation commands.Two independent fixes come first: malloc chunk reuse and libc error checks, and RISC-V direct calls through AUIPC and JALR, because the RV32 self-hosted image is already past the 1 MiB a JAL reaches and the following commits push callees out of range.
"Shrink branches, frames and edge copies" then cuts generated code size on every target:
The stage 1 compiler shrinks by 25% on x86-64, 31% on RISC-V, 11% on ARM and 8% on ARM64, for 4% more instructions in a self-compile.
"Fold the phase 2 IR into VIR's machine layer" then retires the last name from the old design. The allocated instructions become VIR machine operations (
vir_mop_t), defined in vir-lower.h; their creation, flattening, register dataflow and frame queries live in vir-lower.c. The backends share what each had copied (one flattening loop, one word counter for the size estimate, one frameless-leaf test), ARM's fall-through marks move into its backend, and peephole.c keeps only rewrites.Stage 0 output is byte-identical across that commit for every test program and for shecc's own source, with and without
--no-opt, on all four targets.Nine follow-up commits come from a review of the whole series and from code-size and throughput measurements:
int a[1073741825]used to get four bytes), records that contain themselves, and macro,#ifand conditional-operator nesting deep enough to exhaust the stack.--no-opt.fatal(), and shifts into bit 31 unsigned in the ARM and RISC-V encoders. Every target generates byte-identical code.var_tloses its padding. The parser stops creating join blocks for expressions without a logical operator and stops keeping generated temporaries on the name lookup list, and SSA completion binds a single predecessor's value instead of declaring a block parameter, which leaves the stage 1 compiler about 4% smaller on every target. Stage 0 is built with-O2. A stage 0 self-compile on x86-64 peaks at 91 MB instead of 204 MB and runs 12% fewer instructions with the same host flags; on the eMAG it peaks at 88 MB. A function nothing calls is now optimized and verified too, so an optimizer failure there stops the compile. The throughput fixtures compile to the same bytes.Verification:
make bootstrapproduces byte-identical stage 1 and stage 2 compilers, andmake checkpasses, on arm, arm64, riscv and x64 at the tip. With dynamic linking,make checkalso passes on arm, arm64 and x64, andmake check-sanitizerpasses on x64.make check-stylepasses on every commit. Each of the first six follow-up commits passesmake bootstrapandmake checkon x64, and the memory commit, which rewrites the libc allocator, also on ARM. The ARM64 and x86-64 code-size commits passmake bootstrapandmake checkon arm, arm64 (statically and dynamically linked), riscv and x64, andmake check-sanitizeron x64, and a self-compile by the ARM64 stage 1 matches the previous stage 1's output on a frozen source. The memory commit passes the same set at the tip.Performance against GCC -O1
shecc runs at its default optimization and GCC at
-O1 -fwrapv, each benchmark pinned to one CPU of an otherwise idle host:Throughput and code size were measured at b3ef55c; the last commit leaves the fixtures' generated code byte-identical. Compilation time was measured at 0301660, the tip.
Generated-code throughput from
tests/native-throughput.py: GCC time divided by shecc time over nine rounds of eight runs, every checksum verified, so 100% means parity with GCC.Compilation time from
tests/compiler-throughput.py, median wall time including process startup, preprocessing and linking; shecc compiles its bundled libc with every program.On x86-64 shecc compiles 3.7-4.0 times faster than GCC on the fixtures and 10.6 times faster on its own source; on AArch64, 4.0-4.4 and 12.7 times. With
--no-libca fixture takes 1.5-2.3 ms on x86-64 and 2.3-3.1 ms on AArch64; GCC still links its C library there, so that is not a like-for-like ratio.Code size in bytes. The shecc figure is its whole executable segment after the ELF headers, so it includes shecc's startup code and any read-only data; the GCC figure is
.text, including the shared minimalstart.S. At fa69209 the x86-64 shecc column was 191-573 bytes and the AArch64 one 292-560.Against master on the same x86-64 host, generated code moves up from 30.6-80.7% of GCC wall throughput, and a self-compile takes less time than with the legacy pipeline (0.52 s against 0.70 s, each compiling its own source).
Summary by cubic
Replaces the legacy SSA middle end and register allocator with VIR, a value-oriented SSA IR with block parameters, ordered effects and a verifier. The parser builds VIR directly, and one shared lowering pipeline handles ABI, register allocation, spills, coalescing and edge copies for ARMv7, AArch64, RV32 and x86-64. Optimization runs by default (
--no-optdisables it); native throughput reaches 95–104% of GCC -O1 on ARM64 and 80–102% on x86-64. Peak memory of a self-compile drops by half and generated code shrinks by up to 31%, withdocs/vir.mddocumenting the IR.#ifnesting.make bootstrapyields byte-identical stage 1 and stage 2 compilers andmake checkpasses on all four targets, with native-throughput, compiler-throughput, sanitizer and fuzzing suites guarding the change.Written for commit 0301660. Summary will update on new commits.