kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

Linker (planned work)

This roadmap covers where the kit linker is headed beyond the static and JIT linking it does today. Two strands: incremental linking (making a relink cost O(change) instead of O(program)) and system-linker compatibility (making kit ld a drop-in replacement for the platform linker over kit's target set). For the linker's current architecture, passes, and invariants see ../LINK.md; for the object substrate and per-format machinery see ../OBJ.md; for how a resolved image runs in process see ../JIT.md. Design rationale lives in those docs — this file tracks open work, not background.

Shared invariants

Everything below rests on three linker invariants that the full-link path already upholds and no incremental or compat path may violate:

Incrementality is an accelerator gated on a soundness check: a correct-but-slow full link always beats a fast-but-wrong patch, and any path that cannot prove a change local falls back to a clean full link.


Incremental linking

Baseline (done): append-only JIT link

The in-process append path is implemented and is the foundation the rest builds on. kit_jit_publish (an append/replace batch over a KitLinkSession, reporting a bumped kit_jit_generation) grows one live KitJit image with additional compiled objects while keeping every previously published runtime address stable: append cursors with reserved RX/R/RW/TLS slack over one master VA reservation, transactional rollback on failure, generation-bumped invalidation of the cached kit_jit_view, resolution against the existing image + append batch + external resolver with duplicate-strong detection, and a dbg REPL that drives compile → append → DWARF refresh with the worker stopped. This is not hot reload: existing code is never replaced or repatched. Mechanics live in ../LINK.md / ../JIT.md / ../DBG.md.

Open

File-based incremental object link (the "m2" redesign)

Not yet built. Goal: after editing one TU in an N-TU project, the link cost is O(changed atoms + their relocations). Compile-side caching, dep scanning, and the build graph are the build system's problem (../BUILD.md) — this is the obj/link substrate that layer stands on. Incremental link is a -O0/-O1 dev feature; release builds always full-link and stay the canonical reproducible artifact.

The current full-link path already preserves the invariants above and carries the hooks the redesign plugs into (see the comments threaded through src/link/link.c / link_layout.c), but none of the patch machinery exists yet. Design intent and the full trigger/rollback set live in ../LINK.md; the actionable items:

Frontend contract and debug info

All frontends converge to ObjBuilder at obj_finalize, so the machinery attaches frontend-agnostically (Toy, asm, WASM included, no per-frontend code). To be incrementally safe a frontend must be a pure function of (source, flags, target, deps), declare its external dependency set (C reuses KitDepIter; single-source frontends report none), use stable source-derived symbol names, and expose frontend_id + schema_version that salt the build-system key. Toy's durable-module REPL path is not pure → folds the module snapshot into the key or opts out of caching.

On debug info: on any changed atom, re-emit that TU's full .debug_*. kit emits one monolithic .debug_line and one .debug_info CU with intra-CU DW_FORM_ref4 offsets, so a function's rows can't be spliced in isolation, and a body change rewrites the instruction→line map even without a move. Per-TU regen is O(changed TU) and unchanged TUs stay byte-stable (their atoms keep their addresses). Per-function CUs for O(atom) debug are a future option, not pursued now. See ../DWARF.md.

Acceptance per format

Author the suite test-first (test/link-incremental/, red → green) over a synthetic multi-TU fixture (core TUs archived into a static lib linked into two exes that share it; no third-party deps): in-slack body edit (PATCHED, every vaddr stable, the whole-program resolve counter does not increment), grow-past-slack edit (PATCHED, atom moves, jump island at old address, caller bytes byte-identical), the soundness gate (each non-local edit ⇒ FELL_BACK_FULL matching a from-scratch link), multi-output consistency, determinism, and a no-op relink. The two correctness gates are vaddr-stability on a patch and fall-back on a non-local edit; both must be green before a format is "done." ELF/aa64 + ELF/x64 first; COFF, Mach-O, and rv64 each repeat the bar. See ../TESTING.md.


System-linker compatibility

Make kit ld act as the system linker over kit's support set. Near-term driver: Rust through rustc; target: C, Rust, asm objects, archives, DSOs/dylibs/import libs, PIE exes, shared libs, and relocatable links all behaving like the platform linker for the supported arch/OS pairs (Mach-O on macOS aa64/x64; ELF on Linux glibc/musl aa64/x64/rv64, FreeBSD, and freestanding aa64/x64/rv64/rv32; COFF/PE on Windows UCRT MinGW aa64/x64). The linker preserves format boundaries — ELF --as-needed/TLSDESC rules never leak into Mach-O dylib or COFF import-library semantics.

Covered (tested via test-link)

These paths are landed and exercised by the linker harness; only widen as gaps surface:

This is a useful subset, not completion. The work below is the gap between "Rust examples link" and "the platform linker can be replaced."

Open workstreams

1. Ordered input and dependency selection (real --as-needed)

The driver parses --as-needed/--no-as-needed into per-input KitLinkMode, but the resolver does not act on it: collect_needed (src/obj/elf/link_dyn.c) pulls every explicitly-supplied DSO into DT_NEEDED regardless of mode. Make as-needed real linker semantics by modeling, per ordered DSO input: current link mode, selected/not-selected state and why, SONAME/fallback identity for DT_NEEDED, and exports made available to later inputs. Then:

2. ELF TLS model planning

Plan ELF TLS before final reloc application and dynamic-section synthesis. A planner classifies each TLS reloc/sequence by symbol locality/visibility, imported-preemptible vs. non-preemptible, weak-undef, output kind (exe/PIE/shared/-r), static vs. dynamic, and target TLS variant + thread-pointer bias, then picks an explicit action: relax to local-exec, synthesize initial-exec, preserve/synthesize dynamic TLS/TLSDESC, preserve relocs for -r, or reject with a precise diagnostic. This pass owns the decision; arch code owns only the validated instruction-byte rewrite.

3. TLSDESC completion

Today only executable-local cases relax (materializing the local-exec offset). Full support needs: complete per-arch TLSDESC surfaces (RelocKind, ELF mapping, names, descriptor metadata, read/write, objdump); descriptor GOT/data allocation (two words per unique (symbol, addend)); TLSDESC dynamic relocations in .rela.dyn; integration with .dynsym, symbol versions, DT_NEEDED, PIE shifts, section GC, and DSO selection; checked sequence relaxation aa64 → x86_64 → other ELF arches; and shared-library output that preserves/synthesizes dynamic TLS where final layout is unknown. Imported TLSDESC references are real DSO uses and must select their provider under --as-needed (workstream 1).

4. Architecture relocation and relaxation parity

Per-arch reloc coverage + checked relaxations for modern-toolchain code shapes: aa64 ELF (dynamic TLSDESC path, local-sequence relaxation validated as a unit, host GOT/TLS/unwind shapes); x86_64 ELF (full TLS-LD/GD/IE/LE relaxation + TLSDESC once modeled); RISC-V ELF (expand TLS-GD/LD conservatively, compressed variants, preserve dynamic TLS where local relaxation is illegal); Mach-O aa64/x64 (GOT/TLV/unwind parity vs. Apple objects/archives); COFF aa64/x64 (SECREL, unwind, pdata/xdata, CRT object patterns). Every enum addition pins both descriptor table and a byte patch immediately.

5. Shared libraries and relocatable links — broaden coverage

Baseline -shared/-r emit (above) is exercised mostly on final executables. Deliberately broaden: -shared dynamic symbols/relocs/SONAME + install-names/import-tables + per-format TLS; -r preserving relocs and deferring final TLS/layout decisions; PIE vs. non-PIE staying distinct; and copy relocations, protected visibility, weak imports, COMDAT/section groups, init/fini arrays, unwind tables, and build IDs composing with GC and dynamic linking.

6. Runtime/sysroot interoperability

Find the same runtime inputs as the platform toolchain: crt objects + startup ordering; libc/libm/libpthread/librt/libdl/libutil + libc linker scripts; builtins/unwind libs (libgcc_s, compiler-rt, Rust compiler_builtins); dynamic loaders/interpreters; MinGW UCRT CRT + import libs; FreeBSD versioned libc. The driver prefers explicit command-line inputs, then sysroot/target layout, then host defaults only where safe. Runtime validation distinguishes a linker failure from a too-minimal container (e.g. Alpine missing libgcc_s.so.1). See SYSROOTS.md.

7. Diagnostics and tracing

Precise diagnostics for unsupported cases — target, input file/member, symbol, reloc kind, output mode; why a DSO was selected/skipped and which symbol kept an as-needed DSO; why a TLS access can't relax or be represented dynamically; which runtime/sysroot path was searched. Opt-in structured tracing via KIT_TRACE; no global state.

Validation matrix

Per supported arch/OS/libc family, track: Rust std link + runtime exec (where a runner exists), Rust no-std/freestanding link, C/asm object-corpus link, archive order/group behavior, DSO/dylib/import-lib selection, TLS local + imported access, shared-library output, relocatable -r output, and debug/unwind preservation. First priority is the verified Rust support set (a good driver producing real toolchain objects); second is small focused fixtures per feature so failures are explainable without large external runtimes.

kit ld is the system linker for a target when: Rust and C toolchains use it directly with only target/sysroot/runtime config (no per-target wrapper rewrites); exes and shared libs run under the target loader; readelf/otool/llvm-readobj dynamic metadata matches the platform linker for covered cases; -r preserves the relocation surface; local TLS relaxations are legal and checked and dynamic TLS is emitted when required; archive/DSO/import-lib selection follows format semantics; and negative cases fail early with actionable diagnostics.