Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ config:
check-vir-opt-baseline

check: check-stage0 check-stage2 check-abi-stage0 check-abi-stage2 \
check-vir-core check-vir-frontend-stage0 check-vir-frontend-stage2 check-vir-fuzz check-vir-stage0 check-vir-stage2 \
check-vir-core check-vir-essential check-vir-frontend-stage0 check-vir-frontend-stage2 check-vir-fuzz check-vir-stage0 check-vir-stage2 \
check-vir-multi-input-stage0 check-vir-multi-input-stage2 \
check-vir-baseline check-vir-opt-baseline \
check-vir-corpus-stage0 check-fuzz-stage0 check-vir-frontend-policy-stage0
Expand Down Expand Up @@ -375,6 +375,11 @@ check-vir-core: tests/vir-core.c src/vir.c src/vir.h
$(CC) $(CFLAGS) -Isrc tests/vir-core.c src/vir.c -o $(OUT)/vir-core
$(OUT)/vir-core

.PHONY: check-vir-essential
check-vir-essential: tests/vir-essential-core.c src/vir.c src/vir.h
$(CC) $(CFLAGS) -Isrc tests/vir-essential-core.c src/vir.c -o $(OUT)/vir-essential-core
$(OUT)/vir-essential-core

check-vir-frontend-stage0: $(OUT)/$(STAGE0) tests/vir-frontend.sh tests/vir-frontend-workloads.txt $(VIR_FRONTEND_FIXTURES)
TARGET_EXEC="$(TARGET_EXEC)" bash tests/vir-frontend.sh 0

Expand All @@ -391,14 +396,16 @@ check-vir-fuzz: tests/vir-fuzz.c src/vir.c src/vir.h
$(CC) $(CFLAGS) -Isrc tests/vir-fuzz.c src/vir.c -o $(OUT)/vir-fuzz
$(OUT)/vir-fuzz

check-vir-sanitizer: tests/vir-core.c tests/vir-fuzz.c src/vir.c src/vir.h
check-vir-sanitizer: tests/vir-core.c tests/vir-essential-core.c tests/vir-fuzz.c src/vir.c src/vir.h
$(CC) $(CFLAGS) $(SAN_CFLAGS) -Isrc tests/vir-core.c src/vir.c -o $(OUT)/vir-core-sanitize
ASAN_OPTIONS=detect_leaks=1 $(OUT)/vir-core-sanitize
$(CC) $(CFLAGS) $(SAN_CFLAGS) -Isrc tests/vir-fuzz.c src/vir.c -o $(OUT)/vir-fuzz-sanitize
ASAN_OPTIONS=detect_leaks=1 $(OUT)/vir-fuzz-sanitize
$(CC) $(CFLAGS) $(SAN_CFLAGS) -Isrc tests/vir-essential-core.c src/vir.c -o $(OUT)/vir-essential-sanitize
ASAN_OPTIONS=detect_leaks=1 $(OUT)/vir-essential-sanitize

.PHONY: check-vir
check-vir: check-vir-core check-vir-frontend-stage0 \
check-vir: check-vir-core check-vir-essential check-vir-frontend-stage0 \
check-vir-frontend-stage2 check-vir-fuzz check-vir-stage0 check-vir-stage2 \
check-vir-frontend-policy-stage0 \
check-vir-frontend-policy-stage2 \
Expand Down
90 changes: 84 additions & 6 deletions docs/vir.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,15 @@ merges, effects, and ABI information are represented alongside those values.
typed operands. Construction may fold constants, remove identities, and
order commutative operands when their semantics permit it. Common unary
and binary operands stay inline; unusual arities use side storage.
- Typed i32/i64 bitwise builders normalize idempotence, complements,
absorption, repeated AND/OR operands, and XOR with an all-one mask. Unary
builders remove double negation and double complement. These construction
folds also apply at O0; raw `vir_binary()` retains its explicit shape.
At O1/O2, `vir_normalize()` reapplies the rules after SSA parameter
simplification and CSE/GVN, which can reveal equal operands after construction.
When normalization changes the graph, SCCP runs again to propagate newly
exposed constants and simplify dependent branches.
Folding a pure result leaves its operands' ordered effects in the graph.
- Blocks and edges carry control. SSA merges use block parameters and edge
arguments. Edge mutation must preserve argument indices, types, and reverse
uses, including duplicate successors and loop backedges.
Expand Down Expand Up @@ -254,22 +263,79 @@ onto one computed earlier, so it leaves a loop-carried chain. Loop-invariant
code motion (LICM) moves eligible pure computations out of loops, and dead-code
elimination (DCE) removes unused work.

1. Remove unreachable blocks.
1. Inline small leaf callees defined earlier, then remove unreachable blocks.
2. SCCP and CFG simplification.
3. Block-parameter simplification: a parameter that receives one value on
every edge, or itself on a back edge, is replaced by that value, so a
loop-invariant input does not look loop-defined to CSE, GVN and LICM.
4. Local CSE.
5. GVN.
6. Reassociation, LICM and pointer-induction strength reduction.
7. Local CSE and DCE, including ordinary overwritten-store elimination.
8. CFG simplification, then a final local CSE and DCE over the simplified graph.
9. Full graph verification before dumps and machine lowering.
6. Integer, bitwise and unary normalization, followed by SCCP when normalization changes
the graph.
7. Reassociation and LICM, followed by affine-loop summarization. If loops
disappear, simplify their parameters and propagate constants again.
8. Pointer-induction strength reduction, local CSE, and DCE, including ordinary
overwritten-store elimination, then reduction unrolling.
9. CFG simplification, then a final local CSE and DCE over the simplified graph.
10. Full graph verification before dumps and machine lowering.
11. Eliminate eligible direct/additive tail recursion, rerun the pipeline when
changed, then compact the graph.

| Level | Scheduled transformations |
| --- | --- |
| O0 | Construction folds, unreachable-block removal, and mandatory verification; optional optimization passes return without transforming the graph. |
| O2 (default) | SCCP, CFG simplification, block-parameter simplification, local CSE, GVN, reassociation, LICM, strength reduction, and DCE. |
| O2 (default) | Leaf inlining, SCCP, CFG simplification, block-parameter simplification, local CSE, GVN, integer/bitwise/unary normalization, reassociation, LICM, affine-loop summarization, tail-recursion elimination, strength reduction, reduction unrolling, and DCE. |

Integer normalization lowers multiplication by a power of two to a shift, and
unsigned division/remainder to shifts/masks. Positive power-of-two signed
quotients add a sign-dependent bias before shifting to preserve truncation
toward zero; signed remainders use `x - quotient * divisor`. For nonconstant
dividends, zero and negative signed divisors stay explicit. The signed minimum
divided by minus one retains its explicit operation as well.

Reduction unrolling doubles the step of a counted pointer loop whose body only
loads through the pointer and adds the value into an i32/i64 accumulator,
summing odd elements into a second accumulator that is added to the first after
the loop. The pointer's start and end must be constant offsets from one base an
even number of steps apart, so no remainder iteration is needed; the
accumulator may be read only after the loop. Splitting the add chain halves the
loop-carried dependency, which bounds such loops on wide cores.

Affine-loop summarization recognizes two word-sized header parameters: an
induction `i += D` and a modular recurrence `a = A*a + B*i + C`. The initial
index and bound must be constant and the controlling `<` test must be signed
or unsigned with a proven positive, nonwrapping step. A single-entry linear
body must have no memory/call effects, trapping operations, or escaping body
values. Repeated composition by squaring computes the exact final recurrence
in logarithmic compile time, even when the accumulator wraps. Its initial
value may be a runtime argument. Zero-trip, negative signed index, dynamic-bound,
nonlinear, and potentially wrapping control loops are left intact.

Inlining runs first at O2, before SCCP, on each function as its parsing ends. A
direct call to a function defined earlier is replaced by a copy of that
function's already optimized graph: the call's block is split at the call, its
end jumps to the cloned entry with the arguments, and every cloned return jumps
to a continuation block whose parameter carries the result, so multiple returns
need no extra phi. The callee must be a leaf without stack objects, in at most
16 blocks, whose parameter and result types match the call exactly; variadic,
aggregate-returning, address-taken and recursive callees stay calls. A static
callee of up to 40 values is copied. An extern callee is copied only when it
holds at most 6 values, or 16 at a call inside a loop of the caller. Any callee
whose calls are all inlined leaves the image, since only functions reachable
from `main` are emitted when there is one; linkage serves as a proxy for how
many callers keep a copy anyway, and on a self-compile both a uniform limit and
dropping the static one made the generated compiler larger and slower. One
caller absorbs at most 400 cloned values. A call to a function defined later is
not a candidate, which is how a test keeps a call: declare the callee first and
define it after its callers.

Tail-recursion elimination accepts functions with i32/i64 parameters and returns,
no stack objects, no trapping operations, and no effects other than direct self
calls. Both `return self(args)` and `return self(args) + value` become back edges;
an SSA accumulator preserves addition modulo the result width. Every ordinary
return adds the accumulator, and every recursive edge passes all argument
values together, preserving parameter permutations. Narrow and variadic return
paths and calls with additional result uses remain unchanged.

Pass-local tables are temporary. O0 does not allocate the optional CSE, GVN,
loop, or optimization worklists. Mandatory verification still checks dominance,
Expand All @@ -280,6 +346,7 @@ types, use-def links, edges, and ordered effects.
| Command | Coverage |
| --- | --- |
| `make check-vir-core` | Builders, core invariants, and transformations. |
| `make check-vir-essential` | Integer strength reduction, affine recurrences, tail recursion, and pure leaf inlining. |
| `make check-vir-lower` | Shared allocation and lowering invariants, plus an executable smoke test for the configured target. |
| `make check-vir` | Core, frontend, optimization policy, self-hosted VIR, fuzzing, and sanitizer gates. |
| `make check` | Full configured-target stage 0/2 behavior, ABI, and VIR checks. |
Expand All @@ -300,6 +367,17 @@ verifier, and stage-1/stage-2 bootstrap identity. The AArch64 native harness
adds its default dynamic QEMU sysroot only when the configured runner has not
already supplied `-L`.

### Reference-driven bitwise normalization

The construction rules follow the width-aware folding approach in
`ir_fold.h` of [IR](https://github.com/dstogov/ir) and
`src/mim/plug/core/normalizers.cpp` of [MimIR](https://github.com/mimir/mimir).
IR commit `62214815` fixes a wrong-operand return in nested MAX folding; the
regressions here likewise distinguish retaining a nested AND/OR from returning
its absorbed leaf. Mimir commit `6a1694301` ("more idx folding") illustrates why
integer-domain restrictions matter: these rules apply only to shecc's i32/i64
word domains, with all-one constants masked to the result width.

## Implementation map

| Concern | Location |
Expand Down
25 changes: 19 additions & 6 deletions src/defs.h
Original file line number Diff line number Diff line change
Expand Up @@ -363,18 +363,25 @@ typedef enum {
T_hashhash
} token_kind_t;

/* The file a token came from. One record is shared by every token of a file,
* and #line points the tokens after it at a new one, so a location carries one
* pointer for both names.
*/
typedef struct {
/* Immutable physical path used for quoted-include lookup. #line changes the
* logical name only, which diagnostics and __FILE__ report.
*/
char *physical;
char *logical;
} source_file_t;

/* Source location tracking for better error reporting */
typedef struct {
int pos; /* raw source file position */
int len; /* length of token */
int line;
int column;

/* Immutable physical path used for quoted-include lookup. #line changes
* filename only, which remains the logical diagnostic/__FILE__ name.
*/
char *physical_filename;
char *filename;
source_file_t *file;
} source_location_t;

typedef struct token {
Expand Down Expand Up @@ -1073,6 +1080,12 @@ struct func {
/* Callee-saved registers the backend must preserve. */
int saved_regs;

/* Seen as a function pointer while parsing. A heuristic for inlining: such
* a callee keeps its out-of-line copy anyway, so copying it into its direct
* callers too would only duplicate code.
*/
bool address_taken;
Comment thread
jserv marked this conversation as resolved.
Resolved

/* Information used for dynamic linking */
bool is_used;
/* A direct aggregate-return call requires shecc's private sret ABI. */
Expand Down
30 changes: 27 additions & 3 deletions src/globals.c
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@ type_t *TY_long_double;
/* BLOCK_ARENA is responsible for block_t / var_t allocation */
arena_t *BLOCK_ARENA;

/* TEMP_VAR_ARENA holds the generated temporaries of the function being parsed.
* Nothing reads them once its graph is built, so it is rewound to
* temp_var_empty after each function; they were most of the variables a
* self-compile kept alive until the end.
*/
arena_t *TEMP_VAR_ARENA;
arena_mark_t temp_var_empty;

/* BB_ARENA is responsible for basic_block_t / vir_mop_t allocation */
arena_t *BB_ARENA;

Expand Down Expand Up @@ -469,6 +477,13 @@ bool arena_rewind(arena_t *arena, arena_mark_t mark)
return true;
}

/* Drop the temporaries of the function just finished; see TEMP_VAR_ARENA. */
void release_function_temporaries(void)
{
if (!arena_rewind(TEMP_VAR_ARENA, temp_var_empty))
fatal("Cannot release function temporaries");
}

/* arena_alloc() plus explicit zero‑initialization.
* @arena: The arena to allocate memory from. Must not be NULL.
* @n: Number of elements.
Expand Down Expand Up @@ -2399,6 +2414,8 @@ void global_init(void)
BLOCK_ARENA = arena_init(DEFAULT_ARENA_SIZE); /* Variables/blocks */
BB_ARENA = arena_init(SMALL_ARENA_SIZE); /* Basic blocks - low usage */
TOKEN_ARENA = arena_init(LARGE_ARENA_SIZE);
TEMP_VAR_ARENA = arena_init(DEFAULT_ARENA_SIZE);
temp_var_empty = arena_mark(TEMP_VAR_ARENA);
GENERAL_ARENA =
arena_init(DEFAULT_ARENA_SIZE); /* For TYPES and flattened IR */

Expand Down Expand Up @@ -2631,6 +2648,8 @@ void release_block_arena(void)
{
arena_free(BLOCK_ARENA);
BLOCK_ARENA = NULL;
arena_free(TEMP_VAR_ARENA);
TEMP_VAR_ARENA = NULL;
#ifdef __GLIBC__
/* main() keeps freed memory around for reuse, but these variables sit in
* the middle of the heap and the backend allocates nothing like them.
Expand Down Expand Up @@ -2675,6 +2694,7 @@ void global_release(void)
TU_ORDINARY_MAP = NULL;

arena_free(BLOCK_ARENA);
arena_free(TEMP_VAR_ARENA);
arena_free(BB_ARENA);
arena_free(GENERAL_ARENA); /* free TYPES */

Expand Down Expand Up @@ -2798,7 +2818,11 @@ __noreturn void error_at(char *msg, source_location_t *loc)
len = loc->len;
pos = loc->pos;

strbuf_t *src = hashmap_get(SRC_FILE_MAP, loc->filename);
/* The buffer is keyed by the physical path, which #line leaves alone, and
* @pos indexes it; the message names the logical file.
*/
strbuf_t *src =
loc->file ? hashmap_get(SRC_FILE_MAP, loc->file->physical) : NULL;

/* The source text is no longer on hand, which changes what can be shown and
* not what went wrong: still a mistake in the input, so still an exit
Expand All @@ -2813,8 +2837,8 @@ __noreturn void error_at(char *msg, source_location_t *loc)
if (len < 1)
len = 1;

printf("%s:%d:%d: [Error]: %s\n", loc->filename, loc->line, loc->column,
msg);
printf("%s:%d:%d: [Error]: %s\n", loc->file->logical, loc->line,
loc->column, msg);
printf("%6d | ", loc->line);

/* Finds line's start position */
Expand Down
17 changes: 15 additions & 2 deletions src/lexer.c
Original file line number Diff line number Diff line change
Expand Up @@ -1358,6 +1358,17 @@ static char *normalize_filename(const char *filename)
return intern_string(path);
}

/* A file record naming @physical, reported as @logical. It outlives the token
* arenas, which compaction retires while the copied tokens still point here.
*/
source_file_t *new_source_file(char *physical, char *logical)
{
source_file_t *file = arena_alloc(GENERAL_ARENA, sizeof(source_file_t));
file->physical = physical;
file->logical = logical;
return file;
}

static token_stream_t *cache_token_stream(char *filename,
token_t *head,
token_t *tail)
Expand All @@ -1381,14 +1392,15 @@ token_stream_t *gen_file_token_stream(char *filename)
* set to 1.
*/
filename = normalize_filename(filename);
source_location_t loc = {0, 1, 1, 1, filename, filename};
source_location_t loc = {0, 1, 1, 1, NULL};
strbuf_t *buf;

tks = hashmap_get(TOKEN_CACHE, filename);

/* Already cached, just return the computed token stream */
if (tks)
return tks;
loc.file = new_source_file(filename, filename);

buf = get_file_buf(filename);
buf->plain_end = next_phase_mark(buf, 0);
Expand Down Expand Up @@ -1426,12 +1438,13 @@ token_stream_t *gen_libc_token_stream(void)
token_stream_t *tks;
char *filename = dynlink ? "lib/c.h" : "lib/c.c";
strbuf_t *buf = LIBC_SRC;
source_location_t loc = {0, 1, 1, 1, filename, filename};
source_location_t loc = {0, 1, 1, 1, NULL};

tks = hashmap_get(TOKEN_CACHE, filename);

if (tks)
return tks;
loc.file = new_source_file(filename, filename);

if (!hashmap_contains(SRC_FILE_MAP, filename))
hashmap_put_borrowed(SRC_FILE_MAP, filename, LIBC_SRC);
Expand Down
Loading