Skip to content
UNDER THE HOOD

Compile pipeline

weavec-cc compiles a C translation unit as Clang does, in-process, with these additions in an enforcing mode (-fweavec-checks=trap, the default, report or verify; RFC 0035 §1 and amendment 1, RFC 0036 §1):

  1. Zero-initialised locals. -ftrivial-auto-var-init=zero, for locals up to 4 KiB unless the command line sets its own limit (-fno-weavec-zero-init turns it off). The runtime’s allocator zero-fills every block.

  2. Array bounds. -fsanitize=array-bounds, reported through the runtime’s weak __ubsan_handle_out_of_bounds and __ubsan_handle_out_of_bounds_abort, unless the command line already asks for a sanitizer; report mode adds -fsanitize-recover for it. A program that also links UBSan’s runtime gets UBSan’s handlers.

  3. Locations. Without -g, the unit is compiled with location tracking only (DebugInfoKind=LocTrackingOnly, with columns): instructions carry source locations for the passes and the reports, and no debug information is emitted.

  4. Lifetime markers at every -O level (SanitizeAddressUseAfterScope), so a use after scope traps at -O0 too.

  5. No conditional compares on AArch64 (-aarch64-enable-ccmp=false; a later -mllvm option of the user’s wins). The backend folded a guard’s partial-granule test back into its fast path.

  6. Fortification (§1, §6). On an optimising glibc build whose command line neither defines nor undefines _FORTIFY_SOURCE, the driver adds -D_FORTIFY_SOURCE=3; Darwin’s SDK fortifies at level 2 by default. The guards check the object size a fortified call carries.

  7. ASan’s contract (§1, §5). The unit is parsed with AddressSanitizer in its language options, so __has_feature(address_sanitizer) is true, __SANITIZE_ADDRESS__ is defined and no_sanitize("address") is accepted; at the end of the unit, before code generation, the option is taken out again, so ASan’s instrumentation never runs, and GuardInsert removes the sanitize_address function attribute. Clang’s _FORTIFY_SOURCE=0 predefine for ASan on Darwin is undone unless the command line chose a level. Preprocessor-only jobs get the same macros. -fno-weavec-asan-compat keeps the language options as the command line set them, and a command line that asks for an address sanitizer itself keeps its own.

  8. The unsafe-region collector (UnsafeRegions) runs over the AST before code generation (see Unsafe regions).

  9. Four LLVM passes (registerGuardPasses in lib/Frontend/GuardPass.cpp), at every optimisation level:

    Pass Extension point Files Does
    GuardInsert PipelineStartEP GuardInsert.cpp, GuardLibrary.cpp puts a marker before each access and each memory argument of a library call, redirects the library calls whose wrappers write, records the ledger rows, drops sanitize_address
    GuardPrune PeepholeEP GuardInsert.cpp after inlining, removes the markers rule 6.1 proves on a local or global at constant offsets, so that SROA still promotes those locals; manages the scope barriers
    GuardLoop VectorizerStartEP GuardLoops.cpp rule 6.4: versions loops whose whole ranges one check covers
    GuardExpand OptimizerLastEP GuardExpand.cpp, GuardFrames.cpp, GuardGlobals.cpp, GuardLibrary.cpp rules 6.1–6.3; the frame layout, shared space and scopes; global redzones; the unpoisoning before noreturn calls; the late library redirections; each marker into its inline check

    GuardPass.cpp and the private header GuardPassImpl.h hold what the passes share: the module’s context, the markers, the runtime’s entry points, the site records, and the inline checks of the shadow. The passes run on 64-bit targets only, and skip declarations, naked functions and the runtime’s own functions. An available_externally body (a C99 inline definition) is guarded, since the inliner copies it into the unit. Verify mode skips GuardLoop.

Guards are inserted first and expanded last because the optimiser exploits undefined behaviour: an out-of-bounds read in a loop can make the loop infinite, and a read after free can become undef, before a pass at the end of the pipeline sees the access. GuardInsert’s markers are

void __weavec.guard(ptr at, i64 width, i32 flags, ptr nullBase, i32 row)
void __weavec.range(ptr at, i64 length, i32 flags, i32 row)
i64 __weavec.strlen(ptr string, i64 max, i32 flags, i32 row)
void __weavec.disjoint(ptr destination, ptr source, i64 length)

They are nounwind and nomerge, read only the runtime’s memory (a string marker the string too), capture nothing, and are not willreturn: the optimiser may neither drop nor merge them, nor move an access past one, but it may hoist and remove them as it does loads. Beside each lifetime marker GuardInsert puts __weavec.scope(), which writes the runtime’s memory, so that no marker is hoisted above the lifetime.start of a local declared in a loop; GuardPrune drops the barriers of locals SROA promoted and adds them beside the lifetime markers inlining brought (RFC 0035 amendment 2).

What is collected (RFC 0035 §2.1): a load, store, atomicrmw or cmpxchg is a guard of the bytes of its type; llvm.memcpy, llvm.memmove and llvm.memset a range for each memory operand; a masked load or store, a gather or a scatter one access per lane under its mask bit; a call of a library-table function a range or a string for each memory argument. An access to a stack or global object at a constant offset inside it gets no marker and no ledger row, so the object stays promotable to registers. Inline assembly operands, accesses in a non-default address space and the passes’ own shadow accesses are not collected.

Nothing else changes: no AST is rewritten and code generation is not deferred. A unit compiled with -fweavec-checks=none or -fno-weavec, or as C++, Objective-C, OpenCL or CUDA, is compiled as Clang compiles it.