docs: the live krapper service + FIR client design (#186 brick 3) - #204
Conversation
Design only; no production change. Written against main @ 34fb7a3 after bricks 1 (#184) and 2 (#185) landed. This was accidentally swept into the 0.3.5 release-prep commit by a `git add -A` and caught in review of #203 — a release commit that claimed "one line" while carrying 704 unrelated lines. Split out here, which is where it belonged. The document's three load-bearing findings CORRECT the framing the issue was opened with, so they are worth stating in the commit as well as the file: 1. **There is no live Clang AST today, anywhere.** `CppParser.parse` wraps the whole parse in `memScoped`, so the `ASTUnit` — and `ASTContext`/`Sema` with it — is destroyed when the parse returns; only the pure-Kotlin `WrappedTU` survives, and `clang_slice.h` binds zero Sema symbols. "A persistent Clang instance HAS Sema" is not something persistence unlocks; it is new capability work that persistence makes affordable. Probe-gated. 2. **A compiler cannot materialize a binding mid-compilation.** Generated bindings enter the build as a source dir plus a cinterop klib, both frozen before `compileKotlinNative`; a FIR-synthesized class would have no backing native symbol. The compiler can ASK and REQUEST; the unit of materialization is the next sync. 3. **Lazy binding cannot be the default.** `broad-forcing-resolve.md:216` already rated bounding the closure "disqualifying as the primary fix" because it changes bound output, and featuregen depends on INCLUDE_MISSING's transitivity. So #10's two premises become ATTACKABLE, not dissolved — an honest downgrade from what was claimed when the direction was set. What the design proposes instead is a canonical binding index emitted by krapper and consumed by the FIR plugin as a compilation-start snapshot, with persistence, Sema and lazy binding as separate probe-gated bricks above it. §11 argues bricks B1-B3 are ~80% of the realizable value with no persistence, no socket and no Sema — and they fix a live defect: the FIR plugin re-derives krapper's KotlinWriter name mangling by hand (`CppVectorMapping.kt:248-268`) with nothing checking the two implementations against each other, so a generator change silently degrades to a spurious SYNC_REQUIRED on a binding that exists. §9 lists seven probes, each cheap and each allowed to come back negative and kill its brick. §12 carries four open questions for the owner. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NLWsoRAe7f1RdQrYJ2ZXqm
Review: PR #204 (docs-only,
|
Both problems found in review; neither changes the document's conclusions. **§7 was stale, and wrong in its recommendation.** It was drafted while #194 was open and framed it as a live blocker needing a choice between two remedies, recommending option 2 (isolated-classloader Gradle worker). But #194 was CLOSED before this doc was committed — fixed via option 1 (shade/relocate, PR #200, `0a5a705`) and shipped in v0.3.5 — and this branch's own base is two commits after that fix. So the section recommended the option we did not take, against a blocker that no longer existed. Rewritten to record the outcome, and the outcome is better for #186 than what §7 originally proposed: shading REMOVES the version conflict rather than isolating the plugin from it, so a persistent connection holding that stack open across builds is no more exposed than a per-task one. The superseded recommendation is preserved in a collapsed block, because the reasoning that turned out wrong is worth keeping — it optimised for giving the daemon handle a stable home and treated the conflict as something to work around. Propagated: the L2 layer note, B5's and B9's "blocked on #194" cells (B5 now depends only on B4 + probe P3), risk R5 (narrowed to the real residual — ksrpc on the *kotlinc* plugin classpath, where shading the Gradle plugin does not help), probe P7 (mostly done: `samples/multiproject` landed with #200 and was verified to fail against the real released 0.3.4), and open question §12.3 (struck — no longer a question). **One bad citation.** §1.1 cited `compiler/gradle/build.gradle.kts:76-84` for the "both sides compile the same source" claim; that range does not support it at any commit checked. The actual mechanism is `:144-147`, which adds `../../krapper/src/commonMain/kotlin` and `../../krapper_model/src/commonMain/kotlin` as `srcDir`s of the plugin's `main` source set, with the rationale comment at `:104-107`. **Minor line nits**, also from review: `Parsing.kt` 97/103/134 -> 98/104/135, and `DropLedger.kt` 67 -> 78. The six load-bearing citations the review prioritised all checked out exactly, including both halves of the "no live Clang AST" finding (the `memScoped` wrapping and zero bound Sema symbols) and the faithful reading of `broad-forcing-resolve.md:210-224`. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NLWsoRAe7f1RdQrYJ2ZXqm
monkopedia-reviewer
left a comment
There was a problem hiding this comment.
Re-reviewed the delta at 3f95faa.
§7 rewrite: confirmed accurate. Grepped the whole document for 194 (13 hits) plus a
broader sweep for "block/prerequisite/hard dependency/must not land" — every remaining mention
now correctly describes #194 as closed (shaded, 0a5a705/PR #200, shipped v0.3.5), including
the diagram (§3 L2 line), B5's "Blocked on" cell (now B4, P3 with a discharge note, not a
blocker), B9's row (no longer lists #194), R5 (narrowed to the real residual — kotlinc-plugin-
classpath ksrpc, where shading the Gradle plugin doesn't help), P7 (correctly marked "mostly
DONE" — I verified against PR #200's own description that samples/multiproject was in fact
built and confirmed to reproduce the failure against the real released 0.3.4), and §12.3
(struck, not left dangling). No sixth stale site found.
Corrected citations, re-verified against main:
compiler/gradle/build.gradle.kts:144-147— exactly thekotlin.sourceSets.named("main") { kotlin.srcDir(...) }block (the twokrapper/krapper_modelcommonMain srcDirs). Confirmed.:104-107— exactly the "#185: ONE definition... compiled for BOTH sides" rationale comment.
Confirmed.Parsing.kt:98,104,135— exactly thecppParseIncludeDirs/cppModelDumpDir/
cppBaseModelTudeclaration lines (previously off-by-one onto the comment above each).
Confirmed.DropLedger.kt:78— exactly the "runs one request at a time underrunBlocking" line
(previously pointed at an unrelated closing brace 11 lines away). Confirmed.
Also verified: v0.3.5 is in fact released (gh release list shows it, 2026-07-29), so the
header's "§7 updated... after #194 was fixed and v0.3.5 was cut" claim is accurate, not aspirational.
The six priority citations from the original review were untouched by this delta and remain
verified from the previous pass. Clean now — approving.
Reviewed: 3f95faa
Design document only — no production change. Split out of PR #203, where a
git add -Ahad accidentally swept it into the 0.3.5 release-prep commit; the review caught that #203 claimed "one line" while carrying 704 unrelated lines. This is where it belonged.Why this is worth reading rather than just filing
The design corrects the framing #186 was opened with, in three specific ways. The owner ruled "the FIR plugin becomes a live client" partly on the strength of that framing, so the corrections are the most important content here:
CppParser.parsewraps the entire parse inmemScoped(CppParser.kt:76-83), so theASTUnit— andASTContext/Semawith it — is destroyed the moment the parse returns. Only the pure-KotlinWrappedTUsurvives, andclang_slice.hbinds zero Sema symbols. "A persistent Clang instance HAS Sema" is not a property persistence unlocks; it is new capability work that persistence makes affordable. Probe-gated (P1/P2), and the likely mechanism is a PCH +-fsyntax-onlyprobe TU rather than a retained Sema at all.KPlusPlusCompilerGradlePlugin.kt:819) plus a cinterop klib (:803-815), both frozen beforecompileKotlinNativestarts; a FIR-synthesized class would have no backing native symbol and would fail to link. The compiler can ask and request — the unit of materialization is the next sync.docs/design/broad-forcing-resolve.md:210-224already evaluated bounding the closure and rated it "disqualifying as the primary fix" — because it changes what gets bound, and featuregen's cross-instantiation story depends onINCLUDE_MISSING's transitivity.⇒ #10's two premises become attackable, not dissolved. That is an honest downgrade from what was claimed when the direction was set.
What it proposes instead
A canonical binding index emitted by krapper (
krapped/binding-index.json) and consumed by the FIR plugin as a compilation-start snapshot, with persistence, Sema and lazy binding as separate probe-gated bricks stacked above it.§11 argues B1–B3 are ~80% of the realizable value with no persistence, no socket and no Sema — and they fix a live present-day defect: the FIR plugin re-derives krapper's
KotlinWritername mangling by hand (CppVectorMapping.kt:248-268) with nothing checking the two implementations against each other, so a generator-side change silently degrades to a spuriousSYNC_REQUIREDon a binding that exists. It also carries a 4-entry Kotlin→C++ type table, socppVector<Boolean>()is rejected even though krapper handles it.Other content: a determinism contract (§5) with per-brick machine-checkable gates; the IDE/incrementality constraint that keeps bindings as files (§6); the #194 interaction (§7) including a hard constraint that the FIR plugin must stay dependency-free; a 10-brick sequence (§8); seven probes (§9), each cheap and each allowed to come back negative and kill its brick; risks (§10); and four open questions for the owner (§12).
Reviewer note
Docs-only. The useful review here is whether the citations are accurate and the reasoning holds — particularly the three corrections above, since work will be sequenced on them. Every claim cites
file:line; spot-check the load-bearing ones rather than the prose.