Contributing#
This page is for anyone who changes EmbCC: how to build it, the rules the code and its tests follow, how commits are written, how to add an option, an attribute, a builtin, a warning, an IR operation or a target, and what a change has to show before it is merged. The testing side is described in full in Testing; the reasoning behind the project's standing choices is in Design decisions.
Ground rules#
The refusal rule#
A capability may only be claimed if it is genuinely present. A refusal is only honest if the thing is truly absent.
For a compiler this means: when EmbCC cannot do something correctly, it stops with an error that names the thing it cannot do. It never emits code that does something else. A missing feature is expected; a wrong one is a bug that surfaces months later as a program that crashes for no visible reason.
The rule applies everywhere a promise is made:
- Language constructs. An unsupported construct is a located error
naming it:
__attribute__((cleanup)) is not supported: the cleanup function would never run. - Targets. A backend that cannot lower an operation says which one and
where:
the RV32 backend cannot lower a 128-bit value yet (function f). It never falls through to another target's code generator. - Options. A flag that promises something about the code (
-fPIC,-flto,-fsanitize=address) is refused by name if EmbCC would emit ordinary code instead. A flag is accepted silently only when EmbCC already behaves that way (-fwrapv,-fno-strict-aliasing), and the reason is written next to the code that accepts it. - Attributes. An attribute whose loss changes what the program
computes is refused; one with genuinely nothing to do is accepted; an
unknown one is warned about under
-Wattributes. - Documentation and
--help. Neither may name an option or a feature the compiler does not have.
Each refusal states the reason, so a reader who meets it knows whether the construct is absent, partly present, or deliberately not planned.
A change is done when a test exercises the invariant#
For EmbCC the invariant is almost always "the machine runs what we emitted, and the result is correct". A change is not done because it compiles, and a test that only checks that the compiler exits 0, or that an object "looks valid", does not establish it. Prefer a test that runs the program and asserts its result against a reference; see Testing.
Four ways a green result lies, each of which has happened:
- The binary under test was stale. Something rebuilt and something did
not, and the test measured yesterday's artifact. Every shell test prints
a
TEST-MARKERline the runner looks for; check that what you think ran is what ran. - The code never ran. A test that silently skipped, a redirect that left the previous log in place, a pipeline that died early. If the marker is missing, doubt the harness before the code.
- A green build is not a green test. After touching a subsystem, run that subsystem's tests, not the ones in muscle memory.
- Emulated time is not wall time. Under QEMU a guest runs at a fraction of native speed; no output for a minute is usually slow, not hung. Take a register or state dump before concluding anything.
Check exit statuses, never only grepped output, and capture the status
before any command substitution in a script (rc=$? first).
Prove on the host, confirm on the target#
Iteration on the host takes seconds; booting an operating system takes
minutes. Compare EmbCC's output against a reference compiler and with
readelf, objdump and nm; link EmbCC's objects with another linker to
check symbol closure independently of EmbLD; then run the result on the
target, which is the final judge but not the first one
(D-005).
Scope#
EmbCC is a separate project from EmbLinkOS, which is the parent and wins any contest for attention (D-001). A change to EmbCC that would require destabilizing the operating system to test is a sign that the change is wrong or premature.
Building#
EmbCC builds with a C99 compiler, make and a POSIX shell:
make embcc # the compiler
make all # embcc, embread, embld, embas, embls, embidx
make embdbg # the debugger (not part of `all`)
Name the target. Plain make with no target builds only
build/embdbg_core.o, the first rule in the Makefile.
The default flags are CFLAGS = -std=c99 -Wall -Wextra -Werror -g, so a
warning is a build failure. Objects go to build/ (BUILD=); every object
depends on every header, so no object can be stale after a header edit.
make CC=clang BUILD=/tmp/x /tmp/x/embcc builds a second compiler in a
private directory without touching ./embcc.
| Target | Builds |
|---|---|
libc-x86_64, libc-aarch64 |
EmbCC's C library for the freestanding targets, compiled by EmbCC |
libc-linux-x86_64, libc-linux-aarch64 |
the C library for the static Linux targets |
libc-emblinkos |
the C library over the EmbLinkOS backend |
libcxx-x86_64, libcxx-aarch64, libcxx-linux-x86_64, libcxx-linux-aarch64 |
EmbCC's C++ runtime |
rt-embedded |
librt.a for each embedded target (tools/build-rt.sh) |
install |
everything, installed under PREFIX (default /usr/local), staged under DESTDIR |
clean |
removes the build products |
make DEFAULT_TARGET=riscv32-unknown-elf embcc builds a compiler whose
default target is that triple; every backend is still present
(D-017).
Lists that are kept by hand#
Some lists cannot be derived, and a new source file has to join them:
SRCSin the Makefile, for every compiler source. The self-host test reads this list from the Makefile.EMBLS_SRCS, when the new file defines anything thatsema.c,irgen.corpredef.creaches.make checkdoes not buildembls, so a missing entry breaks onlymake test, at the build step, before any test runs.- the
embldrule, when the linker needs the file (it links the encoders it uses to write start-up code). build.ebm, the EmbBuild manifest that builds EmbCC on EmbLinkOS. It records each source's header closure, so it goes stale when a source file is added or when an#includeline changes. Regenerate it withtools/gen-embbuild-manifest.sh > build.ebm;tests/golden/x86_64/embbuild.shfails on a stale one.
Generated files are changed only by their generators. The predefined-macro
tables (src/arch/<arch>/predef.c, predef_cxx.c) come from
tools/gen-predef.sh, which reads them off the reference compiler for each
target; they are never edited by hand.
Code style#
- C99. The compiler is built with
-std=c99 -Wall -Wextra -Werror. Keep it buildable by a strict C99 compiler. - The self-hosting subset.
src/must also compile with EmbCC itself:tests/golden/x86_64/self-host.shcompiles every file inSRCSwith./embcc. Use only what EmbCC implements. In particular, EmbCC does not implement the math builtins (__builtin_floor,__builtin_ldexpand the rest of that family), so a call to one insrc/breaks the self-host test even though the host compiler accepts it. Before using any__builtin_*insrc/, check thatsema.chandles it. - Determinism. EmbCC's output must be a function of its input only.
It must not depend on the host compiler that built EmbCC
(
tests/golden/host-agnostic.shbuilds EmbCC with two host compilers and compares): do not pass two calls that emit IR as arguments of one call, because their order of evaluation is unspecified. Two runs on the same input produce the same bytes, and-O0output is byte-identical to the output with no-Oflag; the self-hosting fixed point depends on both (tests/golden/x86_64/self-host.sh,optimizer.sh). - No host knowledge above the platform layer. Every interaction with
the host (files, the environment, the console) goes through
src/platform/platform.h. Nothing reads the host's architecture, nothing spawns a process (EmbLinkOS has nofork/exec), and there is no#ifdef __APPLE__or similar outside the platform layer. - Target knowledge in
src/arch/. The lexer, parser, sema, IR, optimizer and object writers never name a machine. They ask a question by name (target_ptr_size(),target_char_unsigned(),target_va_list_is_pointer()), and a new question becomes a new function, not a comparison against one architecture. - Layout. Four-space indentation, no tabs. A function's opening brace
on its own line; other braces on the line of their statement. Comments
in
/* */only. Keep lines within 80 columns where practical. - Names.
snake_case, with a prefix per module or target (ra_,ir_,diag_,a64_,t_,rv_,avr_,x86_). File-local functions and data arestatic. - Errors.
diag_fatal(file, line, fmt, ...)for a refusal or any error that ends the compile, anddiag_atfor the same with a column for the caret;diag_error_atfollowed bydiag_note_atfor an error that carries notes;diag_warn_opt(file, line, col, "name", fmt, ...)for a warning that has a-Wname (all insrc/driver/util.h). Allocate withxmalloc,xcallocandxrealloc, which never return NULL. - Comments say why. A comment explains what the code decides and the
reason, including what would go wrong if it were done the obvious other
way. Where a recorded decision governs the code, cite it (
D-014).
Commit messages#
area: what is now true, in one line
What was wrong, shown by the smallest case that exposes it. Why it was
wrong. What changed, and what the change deliberately does not handle.
The evidence: the test that failed before this change and passes after
it (and what it printed before), which suites were run, and any
measurement, with how it was taken.
- The subject is
area: summary. The area is the part of the tree or the target:opt,regalloc,sema,parse,cpp,lex,ir,irgen,driver,link,as,rt,tests,build,docs, or a target (x86,aarch64,thumb,riscv,avr,windows,darwin). A change that spans targets lists them:thumb, riscv:. - The summary states the behaviour after the change ("a constant divisor is not paired with its remainder"), not the activity ("fix divmod").
- The body is evidence, not narrative. Name the test that proves the
change, say that it failed against the previous code, and list what else
was run (
make check, the target's goldens, the full matrix). - Name what the change does not handle rather than implying completeness.
- Do not add a
Co-Authored-Byline.
Adding to the compiler#
Each item below ends in the same two obligations: a test that fails without the change, and an entry in the user documentation (the Using EmbCC manual) when users can see the change.
An option#
Options are parsed in the argument loop of main in
src/driver/main.c. Decide which of these the option is:
| Kind | What to do |
|---|---|
| EmbCC can honour it | implement it, and list it in print_options (the --help text) |
| EmbCC already behaves this way | accept it in the block that accepts such flags, and add the reason to that block's comment |
| accepting it unimplemented costs nothing (the objects are still correct) | accept it, and say in --help that it does nothing yet (-fstrict-aliasing is the model) |
| it promises something EmbCC does not do | refuse it by name with the reason, in the refusal block |
An unknown argument is already an error (unknown argument '...'); never
add a flag to a list that swallows it. Optimizer passes need no driver
code: -f<pass> and -fno-<pass> come from the pass table g_pass in
src/opt/opt.c, which --help also reads.
An attribute#
Attributes are listed in attr_table in src/parse/parse.c, each with a
disposition:
| Disposition | Meaning |
|---|---|
ATTR_HONOURED |
acted on |
ATTR_REFUSED |
ignoring it would change what the program computes; refused by name, printing the entry's reason |
ATTR_NOOP |
there is nothing to do here (what it asks for is already true, or it only affects a diagnostic EmbCC does not issue); accepted in silence |
ATTR_WARNED |
ignoring it is not a miscompile but loses something the program asked for; accepted with a warning that says what is lost |
The entry's text is printed to the user, so it must be true. An attribute
that is not in the table is warned about under -Wattributes and ignored.
An attribute whose effect is visible only in the object (used,
visibility, section) is tested by reading the object with another tool
(tests/golden/attrs.sh).
A builtin#
Builtins are lowered by name in src/sema/sema.c. Add the name to
g_named_builtins beside the dispatch, so that __has_builtin answers for
it; tests/golden/has-feature.sh extracts the names from the dispatch and
fails if one is handled but not listed. The lowering must work on every
target, or the target that cannot lower it must refuse it by name (for
example, __builtin_sqrt is refused on the soft-float targets, where it
would be a library call). Test it by running it on every target against
the host compiler, at every optimization level, with the inputs where the
obvious implementation is wrong (tests/golden/float-bits.sh is the
pattern).
A warning#
Warnings are listed in g_warns in src/driver/diag.c with their default
state and whether -Wall or -Wextra turns them on, and are emitted with
diag_warn_opt under that name. --help-warnings lists them. A warning
must not fire where gcc does not: tests/golden/warnings.sh and
format-check.sh compile the same code with gcc and require that every
line EmbCC warns about is one gcc warns about. Most of a warning's test is
code that must not warn.
An IR operation#
An operation is added to enum ir_op in src/ir/ir.h, and then every
consumer has to learn it. Most of them are hand-maintained switch
statements, so a missed one is a silent miscompile rather than a build
error. Find them by searching for an existing operation of the same kind;
for a terminator, search for IR_UD2 and IR_IGOTO:
- the textual form:
ir_opnameinsrc/ir/irprint.cand the parser insrc/ir/irparse.c(tests/golden/ir-roundtrip.shchecks print, parse, print); - the optimizer (
src/opt/opt.c): the operand walkers (each_read,each_label),compute_defs,build_cfg(leaders and successors), mem2reg's out-of-SSA step,pass_cfgclean, PRE, LICM, the unroller, the inliner's instruction remapping, and the verifier; - the register allocator (
src/arch/regalloc.c):ra_each_useand the liveness successors; - every backend's lowering switch: lower it, or refuse it by name (the AVR
backend's
a_refuse).
Run the corpus with EMBCC_VERIFY=1 (the runner always does) and make a
deliberately broken variant to see that the verifier catches it. When
adding a field to struct ir_ins, search for instructions built by hand in
passes and backends: they do not go through the IR builder's helpers, and
the field's default must be the safe value.
A target#
A target is a row in each of these:
enum target_archinsrc/arch/target.h, and a family file in the target database,src/targets/<family>.def: its data model (DATA_MODEL) and its triples (TRIPLE), listed insrc/targets/targets.def.- The relocation mapping (
target_reloc_type) and the ELF machine (target_elf_machine). - The predefined-macro tables, generated by
tools/gen-predef.shfrom the target's reference compiler, and selected insrc/arch/predef.c. - A backend directory
src/arch/<arch>/:codegen.c(IR to machine code and the calling convention),emit.c(the encoder),irgen.c(the target's share of IR generation,va_argat least), andasm.cfor inline and file assembly. The backend declares its entry point insrc/arch/backend.h, and the driver dispatches to it. It uses the shared register allocator through astruct ra_target(src/arch/regalloc.h). - Runtime routines the backend calls in
lib/rt, and the target inRT_EMBEDDEDandtools/build-rt.sh. - Linker support in
src/link/link.cif EmbLD is to link it. - A harness
tests/harness/<arch>/withlink.shandrun.sh, and golden tests: the triples and data model, the encoder against a reference assembler, programs run against a reference compiler on the same board, the ABI pairing in both directions, and callee-saved register preservation. SRCSandEMBLS_SRCSin the Makefile; the target's section in Targets.
Everything the new backend cannot lower is refused by name from its first
commit. A target is never "the other one" in a two-way test: code that
asked target == X ? a : b before the target existed has to be found and
turned into a question by name.
Recording decisions and facts#
A design decision goes into decisions.md with the reason
for it and what would reopen it, under a new D-0NN identifier;
identifiers are never reused, and code cites them. A decision that is later
reversed is marked superseded, not deleted.
A target fact that cost a debugging session (an ABI rule that is easy to misread, a tool that misreports a field) goes into the page that describes that target or component in this manual, so the next person does not pay for it twice.
Review and tests before merging#
make test passing is the merge condition, and make test-arm64 with it
for any change that can affect aarch64. The reviewer checks above all:
- does anything claim more than it does (an option accepted and ignored, a
refusal turned into silence, a document or
--helpline ahead of the code); - does a new test fail without the change (Testing);
- does the change keep target knowledge in
src/arch/and host knowledge insrc/platform/.
The full matrix is slow, so it is run at milestones, not after every edit:
| Change | Before committing |
|---|---|
| any change, while working | make check |
| confined to one backend and its tests | make check and that target's golden tests |
shared code (src/opt, src/ir, src/sema, src/parse, src/arch/regalloc.c, src/arch/target.c) |
make test and make test-arm64; tools/x86-identity.sh when x86-64 output is meant to stay the same |
| register allocation, scratch registers, spilling | the above, and the exec goldens with EMBCC_RA_MAXPOOL at 3, 1 and 0 |
| a batch of related commits | the full matrix once, before moving to another area |
Say in the commit message which of these ran. Never build embcc or edit
src/ or the test scripts while a suite is running: make test rebuilds
the compiler, and the tests read the tree.