commit 9182c95cbe31e38578e3f0fcdc239722ce7eb349
parent 633d6444262a670bdf4f7517be3d0bb70cfbfcbf
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Thu, 14 May 2026 19:03:07 -0700
docs: design incremental jit linking
Diffstat:
3 files changed, 863 insertions(+), 0 deletions(-)
diff --git a/doc/HOT_RELOAD.md b/doc/HOT_RELOAD.md
@@ -0,0 +1,443 @@
+# Function-only hot reload
+
+This document extends `doc/INCREMENTAL_LINK.md`. It assumes cfree already
+has append-only incremental JIT linking: new code can be compiled and
+placed in a live image without moving old code.
+
+Hot reload adds replacement. In v1, only functions can be replaced. Data
+symbols, TLS, type layouts, initializers, destructors, and object lifetime
+changes are out of scope.
+
+## 1. Goals
+
+- Replace the implementation of an existing function in a live JIT image.
+- Keep the public address of the function stable when possible.
+- Avoid patching every caller for the baseline implementation.
+- Allow new code to call old code and replaced code through the same symbol
+ name.
+- Keep old function bodies alive while any stack frame may still return
+ into them.
+- Integrate with `dbg` so a stopped debuggee can reload a function and
+ continue execution.
+
+## 2. Non-goals
+
+- Data replacement or migration.
+- Changing function ABI: parameter types, return type, calling convention,
+ variadic-ness, or visibility.
+- Replacing inline copies already compiled into other functions.
+- Fully concurrent multi-threaded reload.
+- Unloading old generations immediately.
+- Replacing functions in external DSOs.
+- A production dynamic linker ABI.
+
+## 3. Core idea
+
+Append-only linking gives us a way to add a new function body. Hot reload
+adds a stable function entry that indirects to the current body.
+
+For a reloadable function `foo`, callers see:
+
+```text
+foo entry/trampoline -> foo.slot -> current foo body
+```
+
+Reloading `foo` compiles and appends a new body, relocates it, then updates
+`foo.slot` to point at the new body. Existing pointers to `foo` remain
+valid because they point at the stable entry/trampoline, not a specific
+body generation.
+
+## 4. Reloadable function representation
+
+Add a per-function record in the link session:
+
+```c
+typedef struct LinkReloadFunc {
+ Sym name;
+ LinkSymId public_sym;
+ LinkSymId current_body_sym;
+ uint64_t entry_vaddr;
+ uint64_t slot_vaddr;
+ uint64_t current_body_vaddr;
+ uint32_t generation;
+ uint8_t abi_hash[16];
+} LinkReloadFunc;
+```
+
+The exact hash shape can be internal. It must identify the C ABI contract:
+
+- return type ABI class and size
+- parameter ABI classes and sizes
+- variadic flag
+- target ABI
+- calling convention once cfree has more than one
+
+The hash is not a C type-system identity. It is a runtime-callability
+identity.
+
+## 5. Entry/trampoline shape
+
+Each supported architecture needs one stable entry sequence.
+
+AArch64 example:
+
+```asm
+foo:
+ adrp x16, foo.slot
+ ldr x16, [x16, #:lo12:foo.slot]
+ br x16
+foo.slot:
+ .quad foo.body.0
+```
+
+x86-64 example:
+
+```asm
+foo:
+ jmp *foo.slot(%rip)
+foo.slot:
+ .quad foo.body.0
+```
+
+RISC-V example:
+
+```asm
+foo:
+ auipc t0, %pcrel_hi(foo.slot)
+ ld t0, %pcrel_lo(foo)(t0)
+ jr t0
+foo.slot:
+ .quad foo.body.0
+```
+
+The entry lives in RX memory. The slot lives in writable data or in a
+JIT-managed patchable cell with the same W^X discipline used elsewhere.
+
+Slot updates must be pointer-width atomic when the target ABI can observe
+the function concurrently. `dbg` v1 can require the worker to be stopped,
+but the representation should not rule out atomic publication later.
+
+## 6. Symbol semantics
+
+The public symbol name resolves to the stable entry:
+
+```text
+cfree_jit_lookup("foo") == runtime address of foo entry
+```
+
+The body symbol is internal and generationed:
+
+```text
+foo$body$0
+foo$body$1
+foo$body$2
+```
+
+Debug and inspector surfaces should present the public function as `foo`,
+with the active body generation as implementation detail. Low-level
+symbol iteration may expose generationed body names only under an
+internal/debug flag.
+
+Relocations against `foo` use the public entry by default. Direct body
+references are allowed only for linker-synthesized records.
+
+## 7. Baseline call policy
+
+The baseline policy is simple:
+
+- Calls to reloadable functions target the stable entry/trampoline.
+- Address-taking of reloadable functions returns the stable entry.
+- The slot points at the active body.
+
+This means reloading a function usually patches one pointer-sized cell,
+not every call site.
+
+Later optimization can patch selected call sites directly to the current
+body. That requires a patch-site index and invalidation. It should be a
+separate performance phase, not the correctness baseline.
+
+## 8. Selecting reloadable functions
+
+Do not make every function reloadable by default in all JIT modes. The
+trampoline cost is real and unnecessary for normal `cfree run`.
+
+Enable it through a JIT/debug option:
+
+- `dbg` hot-reload mode: externally visible functions are reloadable.
+- Optional attribute later: only marked functions are reloadable.
+- Internal static functions are not reloadable in v1 unless the reload
+ command names a containing translation unit and the compiler preserves a
+ stable synthetic identity for them.
+
+For v1, restrict reload to global functions with C linkage names visible
+to `cfree_jit_lookup`.
+
+## 9. Reload flow
+
+Debugger-side:
+
+```text
+reload foo from replacement source
+ -> compile replacement source to ObjBuilder
+ -> identify exactly one replacement body for foo
+ -> verify ABI compatibility
+ -> append new body and dependencies
+ -> update foo.slot
+ -> refresh JIT view / DWARF
+```
+
+Link-side:
+
+```text
+link_session_reload_function(session, "foo", new_obj)
+ -> resolve new object against current globals
+ -> reject data definitions and unsupported side effects
+ -> place new function sections append-only
+ -> assign vaddrs
+ -> apply relocations
+ -> publish body symbol as generation N+1
+ -> atomically store body runtime address into foo.slot
+```
+
+The public symbol table entry for `foo` does not move.
+
+## 10. Input restrictions for v1
+
+The replacement object may contain:
+
+- the replacement function body
+- private helper functions used only by the replacement
+- constants and read-only literals needed by the replacement
+- debug sections
+- undefined references to already-linked symbols or external resolver
+ symbols
+
+The replacement object may not contain:
+
+- new writable global data
+- TLS
+- constructors or destructors
+- public definitions other than the function being replaced
+- strong definitions that collide with existing non-target symbols
+- COMDAT/group semantics that require replacing prior selected members
+
+This keeps reload function-only in practice, not just in name.
+
+## 11. ABI compatibility
+
+Before publishing a replacement, verify that the replacement can be called
+through the old entry.
+
+For C frontend replacements, record a compact ABI signature at compile time
+for each function definition. The linker should not need to understand full
+C types.
+
+Suggested record:
+
+```c
+typedef struct CfreeFuncAbiSig {
+ uint8_t target_arch;
+ uint8_t target_os;
+ uint8_t call_conv;
+ uint8_t variadic;
+ uint8_t ret_class;
+ uint8_t ret_size_log2;
+ uint8_t nargs;
+ uint8_t arg_class[CFREE_ABI_MAX_ARGS];
+ uint8_t arg_size_log2[CFREE_ABI_MAX_ARGS];
+} CfreeFuncAbiSig;
+```
+
+No VLA. If the signature exceeds a fixed bound, mark the function
+non-reloadable until a heap-backed encoding is added.
+
+For non-C objects or missing signatures, v1 should reject reload unless
+the user explicitly opts into unchecked replacement.
+
+## 12. Old generation lifetime
+
+After a slot update, old bodies remain mapped.
+
+In `dbg` v1, reload occurs only while the worker is stopped. Even then, the
+current stack may contain frames inside the old function. Continuing after
+reload must be valid:
+
+- Existing frames finish in the old body.
+- New calls enter the new body.
+- Breakpoints in old body addresses remain attached to old code unless
+ the driver chooses to move source breakpoints.
+
+Retirement policy:
+
+- v1: never reclaim old generations until `cfree_jit_free`.
+- later: retire when the debugger can prove no stopped/running frame has a
+ PC or return address inside the old generation.
+
+Never unmap old code immediately after publishing a replacement.
+
+## 13. Debugger behavior
+
+The debugger must distinguish symbol breakpoints from address breakpoints.
+
+Address breakpoint:
+
+```text
+b *0x1234
+```
+
+Stays at that exact address, even if it belongs to an old generation.
+
+Symbol/source breakpoint:
+
+```text
+b foo
+b file.c:42
+```
+
+Should be rebound after reload if the source location exists in the new
+generation. The old breakpoint should be cleared or marked stale depending
+on user policy.
+
+For v1, a pragmatic rule:
+
+- Breakpoints set by exact address stay exact.
+- Breakpoints set by symbol are moved to the active generation.
+- Breakpoints set by file/line are re-resolved after DWARF refresh.
+- If re-resolution fails, keep the old breakpoint but mark it stale in
+ `info breakpoints`.
+
+The session should be stopped while reload changes breakpoint bindings.
+
+## 14. DWARF and JIT view
+
+Every reload increments the JIT generation, same as append-only extension.
+`cfree_jit_view` rebuilds on generation mismatch.
+
+DWARF consumers need enough information to answer two questions:
+
+- What is the active source location for `foo`?
+- If the PC is in an old generation, can we still render its source line?
+
+v1 can keep old debug info in the rebuilt view. That lets backtraces from
+old frames still resolve. Active symbol lookup should prefer the latest
+generation for name-to-address queries.
+
+## 15. Patch-site index
+
+The baseline does not need caller patching, but a patch-site index is still
+useful for future fast mode and for non-function slot fixups later.
+
+Build the index from durable relocation records:
+
+```c
+target LinkSymId -> LinkRelocApply ids
+owner input id -> LinkRelocApply ids
+write section id -> LinkRelocApply ids
+```
+
+Do not scan every relocation on reload. When direct-call optimization lands,
+the linker can patch only relocation sites that target the reloaded symbol.
+
+For v1, it is acceptable to create the data structures but use them only in
+assertions/tests.
+
+## 16. Concurrency and publication
+
+`dbg` v1 reload is single-threaded:
+
+1. Worker is stopped.
+2. REPL compiles and links replacement.
+3. Slot is updated.
+4. Breakpoints and DWARF are refreshed.
+5. Worker resumes.
+
+The slot update still should be implemented as an atomic aligned pointer
+store. That makes the representation compatible with future multi-threaded
+sessions.
+
+If compilation or relocation fails, the slot is not updated and the old
+generation remains active.
+
+## 17. API sketch
+
+Debugger-facing experimental surface:
+
+```c
+typedef struct CfreeJitReloadOptions {
+ const char* symbol;
+ CfreeObjBuilder* obj;
+ uint32_t flags;
+} CfreeJitReloadOptions;
+
+int cfree_jit_reload_function(CfreeJit*, const CfreeJitReloadOptions*);
+```
+
+Internal link session surface:
+
+```c
+int link_session_mark_reloadable(LinkSession*, Sym name);
+int link_session_reload_function(LinkSession*, LinkImage*, Sym name,
+ ObjBuilder* replacement);
+```
+
+Initial JIT link needs an option to create reloadable entries:
+
+```c
+typedef enum CfreeJitIndirectionMode {
+ CFREE_JIT_INDIRECT_NONE,
+ CFREE_JIT_INDIRECT_EXPORTED_FUNCS,
+} CfreeJitIndirectionMode;
+```
+
+This should not affect AOT executable links.
+
+## 18. Failure behavior
+
+Reload must be transactional:
+
+- ABI mismatch: reject, old body remains active.
+- Replacement contains data/TLS/init arrays: reject, old body remains
+ active.
+- Unresolved symbol: reject, old body remains active.
+- Out of append capacity: reject, old body remains active.
+- Relocation failure: reject, old body remains active.
+
+If new pages were committed before failure, they may remain reserved as
+dead space, but no public symbol or slot may point at them.
+
+## 19. Test plan
+
+Targeted tests:
+
+- JIT unit: `cfree_jit_lookup("foo")` returns the same address before and
+ after reload.
+- JIT unit: calling `foo` before reload returns old result; after reload
+ returns new result.
+- JIT unit: a saved function pointer from before reload calls the new body.
+- JIT unit: old body remains mapped and `addr_to_sym` can describe an old
+ PC.
+- Negative: ABI mismatch rejects.
+- Negative: replacement defining writable global data rejects.
+- Negative: duplicate public non-target definition rejects.
+- Debug smoke: stop in `foo`, reload `foo`, finish old frame, call `foo`
+ again and observe new behavior.
+- Debug smoke: symbol breakpoint on `foo` moves to the active generation.
+
+Run these on one JIT target first. Cross-arch trampoline encoding gets its
+own arch-specific tests.
+
+## 20. Implementation sequence
+
+1. Land append-only incremental link and debugger snippet append.
+2. Add reloadable function entries and slots for selected exported
+ functions.
+3. Make `cfree_jit_lookup` return stable entries for reloadable functions.
+4. Add function ABI signature emission from the C frontend.
+5. Implement replacement object validation.
+6. Append replacement body and publish through slot update.
+7. Refresh JIT view/DWARF and rebind symbol breakpoints.
+8. Add optional direct-call patching only after the baseline is correct.
+
+The first usable milestone is: in `dbg`, reload a global function while the
+worker is stopped; existing function pointers keep working; new calls hit
+the new body; old frames can return safely.
diff --git a/doc/INCREMENTAL_LINK.md b/doc/INCREMENTAL_LINK.md
@@ -0,0 +1,416 @@
+# Append-only incremental link
+
+This document describes the first incremental-linking step for cfree:
+append-only growth of a live JIT image. It is sequenced before
+`doc/HOT_RELOAD.md`.
+
+The first concrete use case is `cfree dbg`: while stopped at the debugger
+REPL, a user should be able to enter C code, JIT it into the existing
+debuggee image, and call or inspect the new symbols as if they had been
+present in the original link.
+
+`cfree emu` also wants append-only linking, but it is a separate
+workstream. This document intentionally keeps the motivating path in
+`dbg`.
+
+## 1. Goals
+
+- Grow one live `CfreeJit` image with additional object inputs.
+- Keep all previously published runtime addresses stable.
+- Let new code reference old symbols from the original debuggee.
+- Let old debugger surfaces see new symbols: `cfree_jit_lookup`,
+ `cfree_jit_addr_to_sym`, symbol iteration, breakpoints, PC translation,
+ and the JIT debug view.
+- Preserve relocation records as durable data so later work can index and
+ selectively reapply them.
+- Keep the public surface small until the implementation has one real
+ consumer. The first API can be private to `src/link/`, `src/dbg/`, and
+ `driver/dbg.c`.
+
+## 2. Non-goals
+
+- Replacing or removing existing code. That is hot reload and is covered
+ by `doc/HOT_RELOAD.md`.
+- Reclaiming appended code. Debug sessions are short-lived; v1 may leak
+ appended code until `cfree_jit_free`.
+- Data-symbol migration. New code may define new data, but existing data
+ addresses do not move and are not replaced.
+- Cross-thread debuggee mutation. `dbg` remains a single-worker session.
+- A general dynamic loader ABI. This is an in-process cfree JIT facility,
+ not `dlopen`.
+- Expression parsing as a linker feature. C expression evaluation is a
+ frontend/driver problem that can be implemented by wrapping an
+ expression in a generated function.
+
+## 3. User model in `dbg`
+
+The REPL grows a new command family around JIT extension. Exact spelling is
+driver policy; the link-side contract should support these shapes:
+
+```text
+(cfree) jit {
+int twice(int x) { return x * 2; }
+}
+(cfree) p twice
+(cfree) call twice(21)
+```
+
+and:
+
+```text
+(cfree) jit {
+extern int existing_func(int);
+int probe(int x) { return existing_func(x) + 1; }
+}
+(cfree) b probe
+(cfree) call probe(41)
+```
+
+The minimal v1 can require full C declarations and function definitions in
+the snippet. A later REPL expression command can synthesize:
+
+```c
+static <T> __cfree_dbg_expr_N(void) { return <user-expression>; }
+```
+
+The important linker property is that the synthesized object is just
+another input appended to the same live JIT.
+
+## 4. Current shape
+
+Today the JIT path is single-shot:
+
+```text
+objects / archives
+ -> Linker
+ -> link_resolve()
+ -> LinkImage
+ -> cfree_jit_from_image()
+ -> CfreeJit
+```
+
+`CfreeJit` owns the mapped pages and the resolved `LinkImage`. The debugger
+owns a `CfreeJitSession` over that `CfreeJit`.
+
+The linker already has the right discipline for incremental work:
+
+- `LinkInputId` values are stable for the lifetime of a `Linker`.
+- `ObjBuilder` inputs are not consumed by `link_resolve`.
+- `LinkRelocApply` records survive as data.
+- Resolution is structured as a function from linker inputs to an image.
+
+The missing piece is a live link session that survives after initial JIT
+mapping and can append newly compiled objects.
+
+## 5. Proposed internal model
+
+Add an internal session owned by `CfreeJit`:
+
+```c
+typedef struct LinkSession LinkSession;
+
+struct CfreeJit {
+ Compiler* c;
+ LinkSession* link;
+ LinkImage* image;
+ CfreeExecMemRegion master;
+ ...
+};
+```
+
+`LinkSession` owns state that must outlive one `link_resolve` call:
+
+- the `Linker`-style input list
+- a watermark for inputs already placed into the live image
+- the global symbol hash
+- per-input `InputMap` entries
+- append cursors for each segment class
+- executable-memory capacity and committed ranges
+- durable relocation records
+- optional relocation indexes, introduced in later phases
+
+`LinkImage` remains the read-side view consumed by JIT inspection,
+debugging, and DWARF. In v1 it can still hold the arrays directly, but they
+must become growable or session-backed:
+
+- symbols: appendable
+- sections: appendable
+- segments: fixed count where possible
+- relocations: appendable
+- debug input list: appendable
+
+The simplest v1 should keep the same segment classes as normal JIT layout:
+
+- RX
+- R
+- RW
+- TLS
+
+Instead of creating new segments for every append, the JIT reserves one
+larger contiguous master region up front, then commits pages as appended
+sections land.
+
+## 6. Address stability
+
+Append-only incremental link has one hard invariant:
+
+> Once a runtime address is observable, it never changes.
+
+Observable addresses include:
+
+- `cfree_jit_lookup` results
+- breakpoint locations
+- return addresses on the worker stack
+- addresses shown by `info functions`
+- addresses captured by host code that called into the JIT
+- DWARF PC ranges already handed to debugger consumers
+
+Therefore `link_extend` never compacts, reorders, or lays out old
+sections. It only appends new sections at segment append cursors.
+
+## 7. Reservation and commit
+
+The current JIT maps a contiguous reservation sized to the initial image.
+Append-only linking needs slack.
+
+Add a JIT option internally:
+
+```c
+typedef struct LinkJitReserveOptions {
+ uint64_t reserve_rx;
+ uint64_t reserve_r;
+ uint64_t reserve_rw;
+ uint64_t reserve_tls;
+} LinkJitReserveOptions;
+```
+
+For `dbg`, default to a conservative fixed budget, for example 64 MiB RX
+and smaller R/RW budgets. The exact number should be target/host tunable,
+but the first implementation can choose a simple constant.
+
+The reservation model:
+
+1. Reserve one contiguous master VA range large enough for the initial
+ image plus append budgets.
+2. Lay out initial segments at the front of each class range.
+3. Copy and relocate initial bytes.
+4. Protect committed pages.
+5. Keep uncommitted slack inaccessible.
+6. On append, commit the pages covering newly used ranges, write bytes,
+ apply relocations, flush icache, then protect RX pages.
+
+This keeps AArch64 branch and ADRP proximity behavior predictable because
+old and new code live in one planned range.
+
+## 8. Append flow
+
+Debugger-side flow:
+
+```text
+REPL snippet
+ -> compile as C input through the existing frontend
+ -> ObjBuilder
+ -> cfree_jit_append_obj(jit, obj)
+ -> update dbg's DWARF/JIT view bindings
+```
+
+Link-side flow:
+
+```text
+link_session_add_obj(session, obj)
+ -> read new input summaries
+ -> resolve new definitions and undefs
+ -> place new sections at append cursors
+ -> assign vaddrs for new symbols
+ -> synthesize any needed GOT/stub/helper sections
+ -> emit relocation-apply records for new sections
+ -> write new section bytes into the live mapping
+ -> apply new relocations
+ -> publish new symbols
+```
+
+Only new sections and new relocation records are processed during append.
+Old relocation records remain valid but are not revisited.
+
+## 9. Symbol resolution
+
+New inputs resolve against:
+
+1. Existing global definitions in the live image.
+2. New definitions from this append batch.
+3. The registered external resolver.
+4. Archive members, if archive support is enabled for incremental sessions.
+
+v1 should probably skip archive reselection for REPL snippets. The initial
+debuggee link can include archives as normal, but appending a snippet should
+resolve against the already-linked result plus the external resolver. That
+keeps the first cut smaller.
+
+Duplicate definitions:
+
+- A new strong definition of an existing strong global is an error.
+- A new weak definition of an existing global is ignored for global
+ resolution but remains present as an object-local symbol.
+- A new strong definition can satisfy prior unresolved weak references only
+ if those references belong to new code. Old code is not repatched in
+ append-only mode.
+
+The last point is deliberate. If old code needs to start calling a new
+definition, that is replacement/patching territory and belongs in hot
+reload.
+
+## 10. Relocations
+
+The append pass emits `LinkRelocApply` records for new sections only.
+Each record must include enough information to reapply the relocation later:
+
+- write location as image vaddr
+- write width
+- relocation kind
+- target `LinkSymId`
+- addend
+- owning input and section
+
+For the live JIT mapping, relocation application translates image vaddr to
+the write alias, computes target runtime address or image address according
+to the relocation kind, writes the bytes, and flushes icache for executable
+patches.
+
+Old relocations are not re-run. New code can refer to old code. Old code
+does not learn about new code unless it already had an indirect call through
+some user-controlled data slot.
+
+## 11. Debug info and JIT view
+
+`cfree_jit_view` currently builds a borrowed object view lazily from debug
+inputs. Append-only linking needs invalidation:
+
+- Every append increments `jit->generation`.
+- The cached view records the generation it was built for.
+- `cfree_jit_view` rebuilds when generations differ.
+- `CfreeDebugInfo` attached to a `CfreeJitSession` must be refreshed after
+ append.
+
+For `dbg`, the REPL can handle this directly:
+
+1. Append object.
+2. Drop the old `CfreeDebugInfo`.
+3. Call `cfree_jit_view`.
+4. Open a new `CfreeDebugInfo`.
+5. Attach it to the existing session.
+
+The worker should be stopped while this happens. That keeps debugger state
+single-threaded and avoids racing line-table replacement with a running
+thread.
+
+## 12. Breakpoints
+
+Existing breakpoints remain valid because old addresses remain valid.
+
+New breakpoints can be set against appended symbols after the append
+publishes the symbol table. Breakpoint specs by name should resolve through
+the normal `cfree_jit_lookup` path.
+
+If a source-level breakpoint was pending by file/line and the file was not
+covered before the append, `dbg` can either:
+
+- leave it unresolved until the user retries, or
+- maintain pending source breakpoint specs and arm them after each append.
+
+The second behavior is nicer but not required for the first linker cut.
+
+## 13. API sketch
+
+Keep the first surface private or experimental:
+
+```c
+int cfree_jit_append_obj(CfreeJit*, CfreeObjBuilder*);
+uint64_t cfree_jit_generation(CfreeJit*);
+```
+
+Internally this maps to:
+
+```c
+LinkSession* link_session_from_initial(Linker*, LinkImage*);
+void link_session_set_reserve(LinkSession*, const LinkJitReserveOptions*);
+void link_session_add_obj(LinkSession*, ObjBuilder*);
+void link_session_extend(LinkSession*, LinkImage*);
+```
+
+Once the REPL path is proven, the public API can be made more general:
+
+```c
+typedef struct CfreeJitAppendOptions {
+ CfreeObjBuilder* const* objs;
+ uint32_t nobjs;
+} CfreeJitAppendOptions;
+
+int cfree_jit_append(CfreeJit*, const CfreeJitAppendOptions*);
+```
+
+## 14. Failure behavior
+
+Append should be transactional from the user's point of view:
+
+- If compile fails, the JIT is unchanged.
+- If symbol resolution fails, the JIT is unchanged.
+- If reservation capacity is exhausted, the JIT is unchanged.
+- If relocation application fails, the new allocation is not published.
+
+Implementation detail: pages may have been committed before a late failure.
+That memory can stay reserved and unused, but symbols must not become
+visible and append cursors must roll back.
+
+Use a small append transaction:
+
+```c
+typedef struct LinkAppendTxn {
+ uint32_t old_nsyms;
+ uint32_t old_nsections;
+ uint32_t old_nrelocs;
+ uint64_t old_rx_cursor;
+ uint64_t old_r_cursor;
+ uint64_t old_rw_cursor;
+ uint64_t old_tls_cursor;
+} LinkAppendTxn;
+```
+
+No VLA. The transaction hangs off the link session or the stack as a fixed
+struct.
+
+## 15. Test plan
+
+Targeted tests:
+
+- Link unit: initial object plus appended object where appended code calls
+ an initial function.
+- Link unit: appended duplicate strong definition fails without changing
+ existing lookup results.
+- Link unit: appended object with unresolved symbol fails transactionally.
+- JIT unit: `cfree_jit_lookup` sees appended function and old function
+ addresses are unchanged.
+- JIT unit: `cfree_jit_addr_to_sym` maps PCs in both initial and appended
+ code.
+- Debug smoke: scripted `cfree dbg`, append `twice`, set breakpoint on it,
+ call it, observe stop.
+- Debug smoke: append code with `-g`, refresh DWARF, `b file:line` inside
+ appended code.
+
+Prefer narrow tests by target/arch. AArch64 JIT on the host is enough for
+the first debugger path; ELF/Mach-O file emission should not be in scope.
+
+## 16. Implementation sequence
+
+1. Convert `CfreeJit` to retain a link session or enough linker state to
+ append.
+2. Reserve JIT slack and track append cursors.
+3. Implement append placement for RX/R/RW sections without archives.
+4. Apply new relocations into live mappings.
+5. Grow JIT symbol lookup, addr-to-symbol, and iteration.
+6. Invalidate/rebuild `cfree_jit_view` by generation.
+7. Add `dbg` REPL command for full C snippets.
+8. Add a small `call` command or equivalent helper for invoking appended
+ functions.
+
+The first usable milestone is: append a function in `dbg`, call it, and set
+a breakpoint in it without changing any original address.
diff --git a/doc/JIT.md b/doc/JIT.md
@@ -10,6 +10,10 @@ Companion docs:
- `doc/DESIGN.md` §5.5 — `LinkImage` / `CfreeJit` ownership and lifetime.
- `doc/MACHO.md` §3 — Mach-O Path-J reloc-apply gaps (the longest list).
- `doc/DBG.md` §12 — JIT debugger checklist (session, view, REPL).
+- `doc/INCREMENTAL_LINK.md` — append-only incremental JIT linking, first
+ for `dbg` REPL snippets.
+- `doc/HOT_RELOAD.md` — function-only hot reload built on append-only
+ incremental linking.
- `doc/EMU.md` §6 — per-block JIT on a growing `LinkImage` (separate scheme).
## Driver — `cfree run`