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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
17 changes: 14 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ jobs:
- name: Configure
# --debug enables CONFIG_TCC_DEBUG so the compiler supports -dump-ir;
# without it the frontend types/ tests skip (they need IR dumps).
run: ./configure --enable-cross --enable-O2 --debug
run: ./configure --enable-cross --enable-O2 --debug --enable-asan

- name: Unity groups are current
# source/unity/ must match the source lists (see scripts/gen_unity.py);
# a stale manifest still builds, but slower and without the checks.
run: python3 scripts/gen_unity.py --check

- name: Build and test
shell: bash
Expand Down Expand Up @@ -148,7 +153,7 @@ jobs:
- name: Build cross compiler
# --enable-O2/--debug only affect tcc's own binary, not the code it
# emits, so the codesize numbers are unaffected by this config.
run: ./configure --enable-cross --enable-O2 --debug && make cross -j$(nproc)
run: ./configure --enable-cross --enable-O2 --debug --enable-asan && make cross -j$(nproc)

- name: Set up test venv
# record.py's codesize child (regression_disasm.py --csv) imports the
Expand Down Expand Up @@ -223,7 +228,13 @@ jobs:
# the shell would fall back to interpreting it: "Syntax error: )
# unexpected"). Needed here because run_benchmark.py builds the RP2350
# perf firmware with this freshly built armv8m-tcc.
run: ./configure --enable-cross --enable-O2 --debug && make cross -j$(nproc)
#
# No --debug / --enable-asan here: the cloud jobs already run the tests
# under ASan, this compiler only builds benchmark firmware (its output
# does not depend on either), and on the Pi's four cores ASan + -g cost
# 2.3x the CPU of the build (182 s vs 79 s measured on x86) and slow
# every benchmark compile after it.
run: ./configure --enable-cross --enable-O2 && make cross -j$(nproc)

- name: Download cloud codesize db
uses: actions/download-artifact@v4
Expand Down
33 changes: 23 additions & 10 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ library).

```bash
# One-time setup
./configure # AddressSanitizer is ON by default; ./configure --disable-asan for fast/production builds
./configure # AddressSanitizer is off; ./configure --enable-asan for a sanitized build
make download-gcc-tests # optional: sparse-fetch GCC torture tests (~16 MB, not the full gcc repo)

# Build ARMv8-M cross compiler
Expand Down Expand Up @@ -67,9 +67,10 @@ C Source → Preprocessor (source/frontend/tccpp.c)
→ Parser + type checker (source/frontend/gen/)
→ IR generation (source/ir/tccir.h, source/ir/gen/)
→ IR optimizations (source/opt/)
→ Register allocation (source/machine/tccls.c + source/ir/regalloc.c)
→ Thumb-2 code gen (source/backend/arch/arm/thumb/arm-thumb-gen.c)
→ ELF output (source/obj/tccelf.c, source/obj/tccld.c)
→ Register allocation (source/machine/tccls.c + source/ir/regalloc*.c)
→ Thumb-2 code gen (source/backend/arch/arm/thumb/arm-thumb-gen.c and
arm-thumb-{emit,alu,mem,fp,frame,call}.c, sharing arm-thumb-gen.h)
→ ELF output (source/obj/tccelf*.c, shared tccelf_priv.h; source/obj/tccld.c)
```

## Source Tree
Expand All @@ -96,18 +97,30 @@ libraries into `armv8m-tcc`. The repo root holds only build inputs
| `source/backend/` | `libarm.a`, `libgenerators.a` | `arch/arm/` (+ `thumb/`, `fpu/`) and the generic `generators/` |

The module libraries are linked `--whole-archive`. That is load-bearing, not
cosmetic: several TUs are referenced by nothing (the gdb pretty-printers, opt
passes not yet wired into the pipeline table), and an ordinary archive link
drops them silently — 53 symbols and 67 KB of `.text` on the first attempt.
cosmetic: several TUs are referenced by nothing (the gdb pretty-printers, the
pass registry), and an ordinary archive link drops them silently — 53 symbols and 67 KB of `.text` on the first attempt.
`ARCH_LIB` deliberately stays outside the group so its orphaned members keep
being dropped as before.

Adding a module: create `source/<name>/` with a `Makefile` defining
`<NAME>_INC` / `<NAME>_SRC` / `<NAME>_HDRS`, an object rule, and a
`<NAME>_INC` / `<NAME>_SRC` / `<NAME>_HDRS`, an object rule,
`<NAME>_OBJ = $(call lib-objs,<name>,$(<NAME>_SRC))`, and a
`$(<NAME>_LIB): $(<NAME>_OBJ)` rule whose recipe is `$(ar-lib)`. Then add the
`include` line and the `_INC` / `_LIB` / `_SRC` references to the top-level
`Makefile`. Include order matters — see the comment above the include block.

**Unity build (default).** The library sources compile as ~30 grouped TUs
listed in `source/unity/` (`unity.mk` + one `.c` per group), not one TU per
file: every file includes `tcc.h`, and re-reading it 360 times was a fifth of an
-O2 self-compile. `scripts/gen_unity.py` writes those files; it keeps a file
on its own when grouping could change its meaning (it redefines a header macro,
or it is over 6000 lines) and never groups files whose file-scope names clash.
After adding, removing or renaming a `.c` file, or adding a file-scope
`static`/macro, run `make unity` (CI runs `scripts/gen_unity.py --check`). A
file the manifest does not know yet still builds, as its own TU; a file it
lists that no longer builds is a make error. `./configure --disable-unity` or
`make UNITY=no` goes back to one TU per file.

## Code Architecture

### IR Subsystem (`source/ir/`)
Expand All @@ -120,7 +133,7 @@ Public IR interface is `source/ir/tccir.h`.
| `source/ir/gen/` | IR construction and manipulation (the former `ir/core.c`) |
| `source/ir/gen/live.c` | Liveness analysis for register allocation |
| `source/ir/codegen.c` | Central dispatch: unified two-pass loop (dry-run + real-run) routing IR ops to backend `_mop` handlers |
| `source/ir/regalloc.c` | SSA register allocator, parameterized by `RegAllocTarget` |
| `source/ir/regalloc*.c` | SSA register allocator, parameterized by `RegAllocTarget` (intervals, `_scan`, `_phi`, `_entry`, `_post`; shared `regalloc_priv.h`) |
| `source/ir/vreg.c` | Virtual register management |
| `source/ir/stack.c` | Stack frame layout |

Expand Down Expand Up @@ -284,7 +297,7 @@ switch) for a real A/B. See "Re-checking only the seeds that failed" in

**New IR instruction:**
1. Add opcode to `TccIrOp` in `tccir.h`
2. Add lowering in `arm-thumb-gen.c`
2. Add lowering in the matching `arm-thumb-{alu,mem,fp,frame,call}.c`
3. Add test in `tests/ir_tests/`

**New assembly instruction:**
Expand Down
72 changes: 65 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,36 @@ TCC_CORE_DEPS = $(wildcard *.h) \
$(foreach d,$(TCC_HDR_DIRS),$(wildcard $(d)/*.h)) \
config.mak

# Unity build: compile the sources as a few dozen grouped TUs instead of one TU
# per file. Every file includes tcc.h, which alone is ~560 KB / 14k lines of
# preprocessing and ~3 MB of symbol-table allocations, so building the 324
# library sources one at a time spends 20% of an -O2 self-compile (65% at -O0)
# re-reading the same headers; grouped, a clean build is ~25% less CPU under both
# gcc and tcc. That matters on a Raspberry Pi and far more on the device.
#
# The groups live in source/unity/ (unity.mk plus one .c per group), generated by
# scripts/gen_unity.py from source/unity/groups.txt: a source joins its
# directory's group unless groups.txt names another one, and the script refuses
# a group whose members could see each other's file-local names (see it for the
# rules). Sources the manifest does not
# cover -- new files, or files it keeps apart on purpose -- still build as their
# own TU, so a stale manifest costs speed, never correctness.
#
# `./configure --disable-unity` or `make UNITY=no` builds one TU per file.
UNITY ?= $(if $(CONFIG_unity),$(CONFIG_unity),yes)
-include source/unity/unity.mk

# $(call lib-objs,NAME,SOURCES): the objects a module library is built from.
# Also records SOURCES as UNITY_SRC_NAME for `make unity-info`, which is how
# gen_unity.py learns what each library holds.
unity-members = $(foreach g,$(UNITY_GROUPS_$1),$(UNITY_MEMBERS_$g))
lib-objs = $(eval UNITY_SRC_$1 := $2)$(eval UNITY_LIB_NAMES := $(sort $(UNITY_LIB_NAMES) $1))$\
$(if $(filter yes,$(UNITY)),$(call lib-objs-unity,$1,$2),$(patsubst %.c,$(X)%.o,$2))
lib-objs-unity = $(if $(filter-out $2,$(call unity-members,$1)),$\
$(error source/unity/unity.mk lists $(filter-out $2,$(call unity-members,$1)) under '$1', which no longer builds it; run scripts/gen_unity.py))$\
$(foreach g,$(UNITY_GROUPS_$1),$(X)source/unity/$g.o) $\
$(patsubst %.c,$(X)%.o,$(filter-out $(call unity-members,$1),$2))

# Archive a module's objects into its library. Shared by every module Makefile
# below so they all agree on how a lib is produced.
define ar-lib
Expand Down Expand Up @@ -425,11 +455,10 @@ ARCH_LIB = $($T_ARCH_LIB)
# MODULE_LIBS is linked --whole-archive, which is not decoration. A normal
# archive scan only pulls the members that resolve an undefined symbol, and
# these modules hold TUs nothing references by design: the SValue/Sym
# pretty-printers exist to be called from gdb, and several opt passes are
# compiled but not yet wired into the pipeline table. Linking the archives
# a la carte dropped 53 symbols (all of tccmachine.c, the pass registry, eight
# tcc_ir_opt_* passes and their _ex thunks) and 67 KB of .text without a word
# of warning. --whole-archive reproduces exactly what listing the objects
# pretty-printers exist to be called from gdb. Linking the archives a la
# carte dropped 53 symbols (all of tccmachine.c, the pass registry, and eight
# tcc_ir_opt_* passes that were never wired into the pipeline, since deleted)
# and 67 KB of .text without a word of warning. --whole-archive reproduces exactly what listing the objects
# individually used to do. Both GNU ld and tcc's own linker implement it
# (libtcc.c "?whole-archive" -> AFF_WHOLE_ARCHIVE), which matters because the
# YasOS self-host bootstrap relinks this with armv8m-tcc itself.
Expand Down Expand Up @@ -480,6 +509,9 @@ tccdefs_table_.h : tccdefs_.h gen_predef_table.c
$(addsuffix ,$(DEFINES) $(filter -D%,$(CFLAGS))) \
&& ./gen_predef_table-$T$(EXESUF) $@ && rm -f gen_predef_table-$T$(EXESUF)

# Feature-test macros for every TU, not just those that include tcc.h before
# their first system header (tcc.h defines them too, for other builds).
DEFINES += -D_GNU_SOURCE -D_DARWIN_C_SOURCE
DEFINES += -I$(TOP) $(CORE_INC) $(DRIVER_INC) $(FRONTEND_INC) $(IR_INC) \
$(MACHINE_INC) $(OBJ_INC) $(SUPPORT_INC) -I$(TOP)/source/opt/include \
$(OPT_DSL_INC) $(SSA_OPT_INC) $(FLAT_OPT_INC) $(MEMORY_INC) $(UTILS_INC) \
Expand Down Expand Up @@ -518,6 +550,30 @@ $(X)%.o : %.c $(LIBTCC_INC) config.mak
@mkdir -p $(dir $@)
$S$(CC) -o $@ -c $< $(addsuffix ,$(DEFINES) $(CFLAGS))

# Unity TUs. A group sees every header its members do, so it depends on all of
# them, plus each member source (listed per group below). The extra -I's are
# the ones function_pipeline.c and backend/generators/ get from their own rules.
UNITY_HDRS = $(wildcard *.h $(addsuffix /*.h,source source/* source/*/* \
source/*/*/* source/*/*/*/* source/*/*/*/*/* source/*/*/*/*/*/*))
$(X)source/unity/%.o : source/unity/%.c $(UNITY_HDRS) config.mak
@mkdir -p $(dir $@)
$S$(CC) -o $@ -c $< $(addsuffix ,$(DEFINES) $(CFLAGS)) $(OPT_PIPELINE_INC) $(BACKEND_GENERATORS_INC)
$(foreach l,$(UNITY_LIB_NAMES),$(foreach g,$(UNITY_GROUPS_$l),$(eval $(X)source/unity/$g.o : $(UNITY_MEMBERS_$g))))

# Rewrite source/unity/ after adding, removing or renaming a library source.
unity:
python3 scripts/gen_unity.py
.PHONY: unity

# What scripts/gen_unity.py needs: the compile line and each library's sources.
# It preprocesses every source, so the generated headers they #include must
# exist first -- on a fresh checkout (CI runs it straight after configure)
# nothing has built them yet.
unity-info: $(TCCDEFS_H) tccdefs_table_.h
@echo 'CC $(CC) $(DEFINES) $(CFLAGS) $(OPT_PIPELINE_INC) $(BACKEND_GENERATORS_INC)'
@$(foreach l,$(UNITY_LIB_NAMES),echo 'LIB $l $(UNITY_SRC_$l)';)
.PHONY: unity-info

# Architecture library — built by nested Makefile
TARGET_ARCH_NAME = $($T_ARCH)
ARCH_DEFINES = $(subst -I.,-I$(CURDIR),$(DEFINES))
Expand All @@ -532,7 +588,7 @@ $(ARCH_LIB): FORCE
fi
$S$(MAKE) --no-print-directory -C source/backend ARCH=$(TARGET_ARCH_NAME) \
TOP=$(CURDIR) BUILD_DIR=$(CURDIR)/$(dir $(ARCH_LIB)) \
CC="$(CC)" AR="$(AR)" CFLAGS="$(CFLAGS)" DEFINES="$(ARCH_DEFINES)"
CC="$(CC)" AR="$(AR)" CFLAGS="$(CFLAGS)" DEFINES="$(ARCH_DEFINES)" UNITY=$(UNITY)

# additional dependencies
# tcctools.c is #included by tcc.c rather than compiled on its own.
Expand Down Expand Up @@ -865,14 +921,16 @@ warn-check: armv8m-tcc$(EXESUF) patch-newlib
.PHONY: optflag-check
optflag-check: armv8m-tcc$(EXESUF)
@echo "------------ optflag-check: -finline-limit vs -O argument order ------------"
@# blend calls out so it is no small straight-line leaf: those inline at any limit.
@d=$$(mktemp -d) ; \
trap 'rm -rf "$$d"' EXIT ; \
printf '%s\n' \
'int ext(int);' \
'static int blend(int a, int b, int c) {' \
' int t = a * 3 + b * 5;' \
' int u = c ^ (a << 2);' \
' int v = (t > u) ? (t - u) : (u - t);' \
' return v + t - u + (a & b) + (b | c);' \
' return v + t - u + (a & b) + ext(b | c);' \
'}' \
'int sink1(int x) { return blend(x, x + 1, x + 2); }' \
'int sink2(int x) { return blend(x * 2, x, x - 7); }' \
Expand Down
4 changes: 2 additions & 2 deletions README
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,13 @@ Frequently used `./configure` options:
|------|-------------|
| `--enable-cross` | Build the ARMv8-M cross compiler (`armv8m-tcc`) |
| `--enable-O0` … `--enable-O3`, `--enable-Os`, `--enable-Og` | Optimization level TCC itself is built with (`-O2` default) |
| `--disable-asan` | **AddressSanitizer is ON by default**; disable it for fast or production builds |
| `--enable-asan` | Build TinyCC with AddressSanitizer (off by default) |
| `--enable-ubsan`, `--enable-lsan` | Additional sanitizers |
| `--debug` | Include debug info in the resulting binaries |

Example:
```bash
./configure --enable-cross --disable-asan
./configure --enable-cross
make cross
```

Expand Down
19 changes: 13 additions & 6 deletions configure
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ for opt do
;;
--enable-ubsan) confvars_set ubsan
;;
--enable-unity) confvars_set unity
;;
--disable-unity) confvars_set unity=no
;;
--enable-lsan) confvars_set lsan
;;
--with-libgcc) confvars_set libgcc
Expand All @@ -197,10 +201,10 @@ for opt do
esac
done

# AddressSanitizer is enabled by default for this fork; opt out with
# --disable-asan (e.g. for fast production / firmware builds).
if test "$asan_disabled" != "yes"; then
confvars_has asan || confvars_set asan
# AddressSanitizer is opt-in (--enable-asan); --disable-asan, the default,
# wins over an earlier --enable-asan.
if test "$asan_disabled" = "yes"; then
confvars=$(echo " $confvars " | sed 's/ asan / /g')
fi

show_help() {
Expand Down Expand Up @@ -231,9 +235,12 @@ Advanced options (experts only):
--extra-ldflags= specify linker options [$LDFLAGS]

--debug include debug info with resulting binaries
--enable-asan enable AddressSanitizer (ASan) [default]
--disable-asan disable AddressSanitizer (ASan)
--enable-asan enable AddressSanitizer (ASan)
--disable-asan disable AddressSanitizer (ASan) [default]
--enable-ubsan enable UndefinedBehaviorSanitizer (UBSan)
--enable-unity compile the tcc sources as the grouped TUs listed
in source/unity/unity.mk [default]
--disable-unity compile every tcc source file as its own TU
--enable-lsan enable LeakSanitizer (LSan)
--enable-O0 disable optimizations (GCC -O0)
--enable-O1 basic optimizations (GCC -O1)
Expand Down
22 changes: 22 additions & 0 deletions docs/bugs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Open bug reports

One file per known, unfixed tinycc bug: `docs/bugs/<short-slug>.md`. A bug
found but not fixed in the session that found it goes here; a session that
comes across a report here fixes it.

Format:

```
# <one-line statement of the defect>

**Status:** open · **Severity:** correctness (miscompile) | performance | ... · **Found:** YYYY-MM-DD (how)

## Summary
## Reproducer (C source + -O level + the wrong output or code)
## Root cause (file:function, what check is missing)
## Regression lock (unit test pinning the current behaviour, if any)
## Likely fix
```

When fixed: flip any `_bug` unit-test lock the report names, add an IR test,
and delete the report in the fixing commit (git history keeps it).
63 changes: 63 additions & 0 deletions docs/bugs/ctags-entry-c-kconfig-choice-name.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# ctags main/entry.c at -O1/-O2 loses the name a Kconfig `choice` takes from its `prompt`

**Status:** open · **Severity:** correctness (miscompile) · **Found:** 2026-09-28 (yasos ctags on QEMU, after the sccp fix 67aa75f6)

## Summary

universal-ctags (apps/ctags, pruned build) tagging this Kconfig:

```
menu "Kernel"
config SMP
bool "SMP support"
choice
prompt "Scheduler"
config SCHED_RR
bool "round robin"
endchoice
endmenu
```

should rename the anonymous choice after its prompt (host ctags 6.2, and the
yasos build with every TU at -O0):

```
Scheduler ... /^ prompt "Scheduler"$/;" C menu:Kernel
SCHED_RR ... c choice:Kernel""Scheduler typeref:typename:bool
```

With `main/entry.c` compiled by tcc at -O1 or -O2 (all other TUs either level)
the tag keeps its anonymous name:

```
choice3228b3730104 ... /^choice$/;" C menu:Kernel
```

## Reproducer

TU bisection (every object -O0 except a subset at -O2, device run per step)
isolates `main/libctags_a-entry.o`: all -O0 + entry.o -O2 is BAD, all -O2 +
entry.o -O0 is OK. Still BAD with `-O1`, with `-O2 -fno-inline`, and with
**every** `TCC_DISABLE_PASS` knob of the 147 listed by `-dump-ir-passes=all`
disabled at once -- so the culprit is an ungated transform, codegen or the
register allocator, not one of the knobbed passes.

Build the pruned ctags as build_rootfs.sh's build_ctags() does, run on QEMU
via the FAT drive (`--language-force=Kconfig -f - /mnt/KCONFIG`).

The rename goes through the optscript DSL (optlib/kconfig.ctags: the prompt's
string replaces the choice tag's name); the entry.c side of that is presumably
the tag-name update on the cork queue entry -- a guess, not yet checked.

## Root cause

Not yet known. Next step: per-function mix of entry.c (-O0 vs -O2 bodies) to
find the function, then diff its -O0/-O2 code.

## Regression lock

None yet.

## Likely fix

Unknown until the function is found.
Loading
Loading