JIT executor Phase 3: SwiftUI session-lifecycle integration + hot-reload#190
Merged
Conversation
…nt (#189) The agent only dlopened Core/Concurrency/Foundation/Dispatch, so a JIT-linked object referencing SwiftUI failed to materialize ("Symbols not found" for SwiftUI.View/Text). Load SwiftUI in the agent so its process-symbol generator resolves SwiftUI symbols; AppKit is pulled transitively by dyld. Adds swiftui_probe.swift (a trivial View whose body builds a Text) and a remote test that links it in the agent and evaluates the body, returning 7. Isolates one unknown: SwiftUI links and loads in the agent. AppKit-free and main-thread- free on purpose; offscreen rendering (NSHostingView, main-thread marshaling) is P3.1b. 26 tests green, zero orphan agents. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirrors the Phase 1/2 plan docs. Records the respawn-first decision and its rationale, the unknowns as verification gates, the P3.1-P3.5 chunking, and P3.1a as done. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… main thread (#189) Phase 2 only ran nullary functions via runAsMain on an EPC worker thread. To run real AppKit/SwiftUI the agent needs main-thread execution. Restructure the agent: the EPC server runs on a background thread and the main thread runs a CoreFoundation run loop (CFRunLoopRunInMode after NSApplicationLoad). New executor wrapper __previewsmcp_run_on_main dispatch_syncs a JIT'd function onto the main queue and returns its int32_t, mirroring Apple's run_program_on_main_thread. Host side: previewsmcp_jit_session_run_on_main fetches the bootstrap symbol and calls it via callSPSWrapper; Swift runOnMain(symbol:). Fixture hosting_probe.swift builds an NSHostingView on the main thread and returns 1 when fittingSize.width > 0. Retires U-A (Cocoa hosts in a headless agent) and U-B (Swift View entry on the main thread) for the construction case. Also fix a pre-existing race the restructure surfaced: native-target init ran under two separate call_once flags (in-process makeJIT and remote_session_create), so a concurrent first-time in-process + remote session create corrupted LLVM's global TargetRegistry (a hang via lookupTarget spin, or a SmallVector assert). Unify both behind one initNativeTargetOnce. 27 tests green; 12/12 parallel runs clean; zero orphan agents. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…map in the agent (#189) The agent now rasterizes a real JIT-linked SwiftUI view. render_probe.swift renders a known-color view via ImageRenderer on the agent's main thread (through the run_on_main surface), samples the center pixel, and returns it; the test asserts the pixel is red. Root cause of the SwiftUI-in-agent instability (Phase 2's deferred unknown U-A): the remote path used the default per-allocation mmap memory manager, which scatters each section into its own page-rounded mmap. The Swift/SwiftUI runtime's metadata walks (conformances, AttributeGraph layout descriptors) read one entry past a section into the unmapped gap and segfaulted. The six POC scenarios were small enough to dodge it; SwiftUI trips it. Fix: give the remote agent a contiguous slab via SharedMemoryMapper + MapperJITLinkMemoryManager (mirroring llvm-jitlink's createSharedMemoryManager); the agent already hosted ExecutorSharedMemoryMapperService. Suite is now 40/40 green in parallel. Two supporting fixes surfaced under the asserts build + parallel runner: - Unify native-target init behind one initNativeTargetOnce (two separate call_once flags raced on LLVM's global TargetRegistry: a hang or SmallVector assert), and serialize the shared in-process LLJIT::initialize with a mutex. - Agent _Exit(0) on disconnect instead of unwinding the Server on the background thread (a use-after-free vs the transport listen thread in handleDisconnect); the host SIGKILLs the agent regardless. Known residual (Phase 4 hardening): after the slab fix the remaining agent crashes are all post-result, during JIT'd SwiftUI object teardown (NSHostingView deinit, AttributeGraph). They fail no test. Not masked on purpose. 40/40 parallel runs green, zero orphan agents. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… shared-memory slab (#189) The shared-memory slab (added for U-A contiguity) requires mprotect-to-exec on MAP_SHARED memory, which macOS denies (EACCES), so every remote session failed with "Failed to materialize symbols { <Platform> ___mh_executable_header, ___dso_handle }: Permission denied". macOS allows mmap-exec-from-start but not the mprotect transition; the JIT entitlement does not help. Apple's XCPreviewAgent sidesteps this entirely: it carries no JIT entitlement and drives anonymous executable memory over the wire (XOJITExecutor's write_mem), never shared memory. Match Apple: replace the shared-memory slab with a contiguous slab backed by anonymous executor memory. - Agent: a small mapper service (reserve = anonymous mmap; initialize = memcpy the wire-transferred content + mprotect + invalidate icache + run finalize actions; deinitialize = no-op; release = munmap), advertised as bootstrap symbols. - Host: PreviewsAnonymousMapper (a MemoryMapper that keeps a local working buffer and ships segment content over the wire), driving MapperJITLinkMemoryManager for one contiguous reservation per session. This fixes U-A (the SwiftUI metadata "read past region" crashes) via contiguity, using anonymous memory so the macOS shared-exec restriction never applies. Two details were load-bearing: the agent must InvalidateInstructionCache after writing exec segments (ARM64 executes stale icache otherwise, an intermittent garbage-execution crash), and it discards deallocation actions (the JIT image is process-lived in the respawn model per Phase 1 D3, so running JIT'd destructors at teardown is both unnecessary and unsafe). Suite 15/15 green across parallel runs, zero agent crashes, zero orphans. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e; anonymous mapper (P3.1b-iii) (#189) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…piler-target finding (#189) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Recompile a render-probe source to a new .o, respawn the agent (fresh remote JITSession), and confirm the new render reflects the edit. v1 renders red; the same source edited to blue renders blue in a fresh agent. Proves recompile -> respawn -> re-render through the real Compiler, respawn-first (no in-place write_mem). The earlier "Compiler.compileObject's default arm64-apple-macosx14.0 target fails in the JIT" finding was confounded by the macOS shared-exec outage. Re-verified cleanly on the anonymous mapper: the default target links and renders in the agent, so compileObject is unchanged (no host-OS target option). Test only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
P3.4 routes structural edits to JIT respawn but still recompiles the whole module; respawn removes the relink, not the compile. Add G1 (single-file incremental compile vs prebuilt .swiftmodule), G2 (file-identifying FileWatcher), the stable-module vs any-file model mismatch, and the caveat that Apple's compile-side narrowing is inferred (W3) not captured. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hunking (#189) Model A (agent-rendered bitmaps) chosen over in-process JIT-in-daemon: in-process linking reintroduces the per-edit Swift-metadata leak that respawn-first exists to avoid. Cost is small because macOS is snapshot-only (preview_touch is iOS-only, separate process, out of scope), so only preview_snapshot moves to the agent bitmap. Records the new unknowns U-E (variable-size bitmap return) and U-F (real bridge render) and the de-risk-first chunking P3.4a-d. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ed file (#189) Retires U-E (variable-size bitmap return from the agent) via file transport: the agent renders a SwiftUI view and writes the PNG to a host-supplied path; the host reads and decodes it. Reuses the runOnMain Int32 status surface and the existing bridge-source-templating pattern, so no new EPC/C++ wire code is needed. The EPC byte-return wrapper stays a fallback only if file transport proves inadequate (it won't for snapshots). Test only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
W4 (previews-research) closed the compile-side question: Apple recompiles one file. Refine G1 to split cross-module (prebuilt .swiftmodule, size-independent) from same-module (full parse + incremental codegen, size-dependent; JIT must relink the set of changed objects). Name the two impl paths (adopt swiftc incremental vs the stable-module/thunk split) and tie verify to 500/1000-file scaling. Update the Apple caveat to W4-CLOSED with its residual gaps and the design-time-injection bonus. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…agent (#189) Retires U-F (the real Compiler bridge renders in the agent, not a probe). Adds generateCombinedSource(renderOutputPath:), which emits a nullary @_cdecl("renderPreviewToFile") for macOS that builds the same viewCode as createPreviewView, rasterizes it headless via ImageRenderer, and writes a PNG to the baked path. Nullary keeps it on the agent's runOnMain surface; the path is baked because the daemon recompiles per structural edit (model A, file transport). createPreviewView is unchanged, so the in-daemon path is undisturbed. Test drives a real combined bridge (DesignTimeStore + __PreviewBridge + thunk'd user #Preview) through the agent and asserts the host-decoded PNG is green. 31/31 JIT green, 69/69 BridgeGenerator core green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
W4 live-captured Apple's preview thunk argv (one -primary-file, -vfsoverlay, explicit module map => G1 cross-module shape confirmed) and showed previews use PreviewRegistry-reentry + respawn, NOT dynamic replacement on Xcode 26.x. W5 measured same-module incremental scaling (linear, 200ms budget breaks ~25 files) vs the split (flat ~0.144s to 1000 files), making the stable-module/editable-unit split mandatory; fan-out is 1 object per body edit, 1+K per interface edit. W7 auto-split is now the critical path. Update G1 verdict + Apple-evidence status. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…atch (#189) W7 closed: auto-split is feasible for the common case (internal view in a separate editable unit via @testable against a -enable-testing stable module; edit->relink flat ~0.14s at N=200/1000). Record the four break-cases (private/fileprivate, @_spi, stable-interface edits, package decls) and the two soft spots, both covered by the assigned integrated POC. Resolve the model- mismatch decision: preview-layer edits get the flat fast path; stable-interface edits accept W5 same-module cost; any-file sub-second is explicitly a non-goal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
) Records the agreed daemon seam: a StructuralReloader protocol in PreviewsCore (JIT-free), implemented by PreviewsJITLink, composed at the executable, injected only when jitEnabled. Chosen on layering merit, not to appease CI; the #if alternative was the actual workaround. Splits P3.4c into c-i (protocol seam + structural->agent snapshot) and c-ii (literal-after-structural). Logs JIT-in-CI (cache the prebuilt LLVM artifact so CI can run the JIT path) as a deferred infra task, separate from the architecture. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…utor shape (#189) Fold in the integrated POC result (cfe9bda on previews-research): the full split -> @testable single-file compile -> JIT-link -> render chain is proven end-to-end (v1 red -> v2 blue), closing W7's two soft spots. Numbers: ~233ms edit->pixels with respawn semantics vs ~167ms with a persistent agent + fresh-JD-per-edit. Record the load-bearing findings (LLJIT::initialize per generation registers __swift5_* metadata; ObjCSelrefPlugin + ExecutorNativePlatform required) and the file-granularity split rule that makes break-case 1 impossible by construction. The persistent shape conflicts with respawn-first's leak rationale, so a generation-soak (assigned) gates the decision; respawn-first stands until it lands. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…189) Generation-soak (500 gens, previews-research) ratifies the executor shape: capped-persistent. Latency is flat (conformance scans do not slow) but RSS leaks ~87KB/generation since __swift5_* metadata cannot deregister, so one persistent agent + fresh JITDylib per edit with a background respawn every ~100 generations bounds the leak at ~+9MB and amortizes warmup to ~0.7ms/edit (~167ms vs ~233ms per edit). Respawn stays the cleanup mechanism, on the cap instead of every edit. Record the soak data in the evidence section. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Core (#189) The daemon seam for routing structural edits to the JIT path, kept in the JIT-free base module so the daemon depends on the abstraction, not the gated JIT target (chosen on layering merit; non-JIT build compiles by construction). - StructuralReloader protocol: renderObject(at:entrySymbol:), agnostic to respawn-vs-capped-persistent so the executor-shape choice swaps under the impl without touching Core or the daemon. - PreviewSession.compileObjectForJIT(): generates a render bridge with a baked PNG path, compiles a .o, returns a JITRenderBuild (object + image path + entry symbol). Standalone combined-source mode for now. Test (mock reloader) asserts the .o exports _renderPreviewToFile and the plumbing routes. 305/305 PreviewsCore green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#189) Implements StructuralReloader in PreviewsJITLink: spawn a fresh agent, addObject, runOnMain(entrySymbol) which writes the preview PNG to the baked path; throw on non-zero status. Respawn-per-edit via JITSession deinit (a capped-persistent variant can replace the body without touching the protocol). Adds PreviewsCore as a PreviewsJITLink dependency (the JIT module now implements a Core protocol; also resolves a transitive _SwiftSyntaxCShims module error). Inside the jitEnabled block, so the non-JIT build is unaffected. Test drives a real PreviewSession.compileObjectForJIT() .o through the reloader and asserts the agent-written PNG is green. 32/32 JIT green, 3/3 parallel, zero orphans. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…er model (#189) W6 (previews-research) closes the research arc (W3-W7 all done). Canvas-is-split refinement: Apple's canvas thunk compile is single -primary-file + -vfsoverlay + explicit module map with NO filelist/incremental, i.e. already the W5/W7 split shape. Design-time injection lifecycle modeled (#salt_n IDs -> value table -> UpdatePayload re-injection, no recompile/respawn for literal edits); our DesignTimeStore mirrors it, boundary = LiteralDiffer skeleton-equality incl. the UIKit downgrade (#160). Record the three-tier executor model: literal SwiftUI = value push; structural = split compile + JIT-link (capped-persistent); UIKit literal = tier 2. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… the daemon (#189) Routes structural edits through the agent when a StructuralReloader is injected, while the literal fast path and the in-daemon dylib path are untouched. - PreviewHost: structuralReloader: (any StructuralReloader)? + an agentImagePaths map; jitStructuralReload() compiles the .o, renders in the agent, records the PNG. watchFile's structural branch prefers it when injected, else the existing compile()+loadPreview() dylib path. - MacOSPreviewHandle.snapshot serves the agent PNG for agent-backed sessions (new Snapshot.encode: PNG passthrough / JPEG transcode). - Composition root: one #if PREVIEWSMCP_JIT in PreviewsMCPApp injects JITStructuralReloader(); Package.swift adds PreviewsJITLink to PreviewsCLI and defines PREVIEWSMCP_JIT only when jitEnabled. Daemon logic stays #if-free (depends only on the Core protocol); the single conditional lives at the app entry where composition belongs. Tests: PreviewHostJITReloadTests (records the agent image; nil reloader falls through) + MacOSPreviewHandleAgentSnapshotTests (snapshot returns the agent PNG). macOS 3 / engine 9 / JIT 32 green; full JIT build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…the JIT path (#189) Once a session is agent-rendered its view lives in the agent, so the in-daemon DesignTimeStore literal path can no longer update it (applyLiteralChanges no-ops without a loader). watchFile now gates the literal fast path on agentSnapshotPath == nil, so an agent-backed session routes any edit through the structural JIT reload. Correct, not the ~10ms path; the proper agent-side DesignTimeStore re-seed is deferred (needs an agent setter surface). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Integrated compileObjectForJIT() + JITStructuralReloader.renderObject() on a small module, steady-state, respawn-per-edit: compile ~218ms, link+respawn+agent-render ~62ms, total ~281ms. The render/respawn half is well within the <200ms budget; the whole-module compile dominates and is the entire gap. Closing it needs the deferred recompile-narrowing (stable-module/editable-unit split), a compile-side lever, not a JIT-executor change. Measurement test prints the breakdown and asserts a loose 5s regression tripwire. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ile in the agent (#189) Proper literal-after-structural path, step 1. The agent runs only nullary entries, so rather than call the designTimeSet* setters over the wire, the render bridge seeds DesignTimeStore.shared.values from a baked JSON path before building the view (value file-transport, mirroring the PNG choice). compileObjectForJIT bakes the path and writes the literals' initial values; JITRenderBuild gains valuesPath + literals. This sets up the no-recompile literal re-render (c-ii-2): rewrite the JSON and re-run renderPreviewToFile on the same .o. Test asserts the values JSON is written with the literals; existing agent render tests still produce the correct color through the seeded path. JIT 33 / BridgeGenerator 69 / PreviewsCore 306 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… JSON rewrite (#189) Proves the cheap literal path: compile a Color(white: 0.2) preview once, render in the agent (dark), rewrite the baked design-time values JSON to set the white literal to 0.9, then re-run renderPreviewToFile on the SAME .o. Brightness flips dark->light with no second compile. Test only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… into the daemon (#189) Completes the proper literal-after-structural path. PreviewSession caches the last JITRenderBuild and adds applyLiteralValuesForJIT(), which merges the changed literals into the design-time values JSON and returns the build to re-render. PreviewHost.jitLiteralReload() rewrites the values and re-renders the same object in the agent. watchFile's literal branch now routes agent-backed sessions there (no recompile) while non-agent sessions keep the in-daemon DesignTimeStore path. Test: jitLiteralReload rewrites the values JSON (hello -> world) and records the image. PreviewsCore 306 / macOS 4 / engine 9 / JIT 34 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Re-indent multiline string fixtures and the #if PREVIEWSMCP_JIT import to satisfy swift-format --strict (the repo .swift-format config, found via --recursive). Formatting only; PreviewsCore 306 / macOS 4 / engine 9 / JIT 34 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…189) Mark Phase 3 core complete (P3.1/P3.2/P3.4 a-d landed; P3.3 conditional, examples E2E pending). Rewrite the resume pointer with the current tip, the P3.4 seam key-files map, and the next-phase priorities (recompile-narrowing first, then capped-persistent reloader, examples E2E, JIT-in-CI). Carry the swift-format --recursive lint pitfall. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-narrowing (#191) Add Compiler.emitStableModule: compile the bulk of a module once into a whole-module .o plus a -enable-testing .swiftmodule. The editable unit then compiles a single file against it (-I modulesDir, @testable import), so an edit never re-parses the bulk. SplitCompileTests proves the mechanism at the unit layer: the editable unit consumes a stable-module symbol cross-module, JIT-links stable.o + editable.o, and renders correct pixels; the stable module is reused across edits; and the per-edit compile stays flat while the whole-module baseline grows with size (N=24: editable-only 180ms vs whole-module 312ms), reproducing the W5/W7 lever. This is the Compiler half only. Wiring PreviewSession.compileObjectForJIT() and JITRenderBuild to carry the two objects is the next step. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… split (#191) In Tier-2 project mode, compileObjectForJIT() now narrows the recompile to the hot preview file. The bulk (ctx.sourceFiles, which the build systems already exclude the preview file from) is built once into a -enable-testing stable module; the hot file compiles as a separate module PreviewEdit_<moduleName> that @testable imports it (-I stable.modulesDir). Standalone mode is unchanged. JITRenderBuild gains supportObjectPaths (the stable .o, linked before the editable object); empty for standalone so the live daemon path is untouched. generateCombinedSource gains stableModuleImport to emit the @testable import. Compiler gains a file-based emitStableModule(sourceFiles:) that compiles the project sources in place. PreviewSessionSplitTests: the hot preview file consumes a bulk Palette symbol cross-module, renders red; a structural edit re-renders blue. Proves the session-level split end to end. Reloader/JITRenderBuild plumbing (linking both objects in the daemon) is B3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e edits (#191) PreviewSession now caches the prebuilt stable module, keyed by the bulk files' modification dates. Repeated edits to the hot preview file reuse it, so the per-edit compile recompiles only the hot file (the flat fast path). A change to any bulk file invalidates the cache and rebuilds. Within a session the hot file is fixed (self.sourceFile), so the changed-hot-file case is a different session. PreviewSessionSplitTests: two hot-file edits share supportObjectPaths (cached); touching a bulk file produces a fresh stable object. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… reloader (#191) StructuralReloader.renderObject gains supportObjectPaths: the JIT reloader adds those prebuilt stable-module objects before the editable object, so the agent links stable.o then editable.o and resolves the editable unit's @testable references. The daemon callers (jitStructuralReload/jitLiteralReload) pass build.supportObjectPaths; standalone passes [] so its path is unchanged. reloaderRendersSplitBuildThroughBothObjects renders a project-mode split build red through the real reloader. Test mocks and direct callers updated for the new signature. This completes the P4.1 split end to end through the daemon seam: a Tier-2 structural edit recompiles only the hot file and renders via stable+editable objects. Capped-persistent reloader, examples E2E, and the FileWatcher path (G2) remain. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ep-loading gap (#191) ExamplesSplitE2ETests drives a real SPMBuildSystem BuildContext for examples/spm (ToDo target). The split COMPILES a real cross-file preview (Summary.swift: same-module Item + sibling ToDoExtras package-scoped decls) — the real -I/-package-name/@testable flags flow correctly and both objects emit; the literal path classifies a real preview and reuses the same object. Surfaced G3: the JIT agent does not load the target's dependency archives (libToDoExtras.a / libLocalDep.a), so their symbols are unresolved at JIT-link time. The non-JIT Tier-2 path links a dylib with -L/-l so dyld resolves them; the JIT path only links our stable.o + editable.o. This is a prerequisite for JIT Tier-2 on any real multi-target project, orthogonal to recompile-narrowing. The in-agent render test is .disabled pending G3; the fix is to add the -L/-l archives as StaticLibraryDefinitionGenerators (+ dlopen binary dylib deps). Captured in docs/jit-executor-phase3-plan.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… into the JIT agent (#191) The examples E2E showed real Tier-2 previews fail to render because the agent JIT-links only our stable.o + editable.o, leaving the target's dependency symbols unresolved. G3-a handles the static-archive layer: SPMBuildSystem archives sibling/cross-package targets into lib<Dep>.a and passes -L/-l. - C API previewsmcp_jit_session_add_archive → StaticLibraryDefinitionGenerator::Load on the session JITDylib (lazy archive linking pulls in only the referenced dep objects). - Swift JITSession.addArchive; JITRenderBuild.archivePaths, parsed from ctx.compilerFlags (-L/-l) by PreviewSession.dependencyArchives. - StructuralReloader.renderObject gains archivePaths; the JIT reloader adds the archives before the objects. Standalone passes [] (unchanged). ArchiveLoadingTests proves a JIT-linked object resolves a symbol from a libtool static archive in the agent. The real E2E now resolves ToDoExtras/LocalDep. Render of a full real preview is still blocked (the stable module bundles every target file, so a dep-free hot file still pulls the whole target's deps): G3-b (dlopen binary frameworks, e.g. Lottie, via EPCDynamicLibrarySearchGenerator) and G3-c (the compiler-rt builtin ___isPlatformVersionAtLeast from #available). splitRendersRealPreviewInAgent stays .disabled with those precise blockers; captured in docs/jit-executor-phase3-plan.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… the JIT agent (#191) After G3-a, the real Tier-2 E2E still failed on the Lottie binary framework: the split's stable module bundles ToDoView.swift, which uses Lottie, so the agent must load it. G3-b loads the target's -F/-framework deps into the agent. - C API previewsmcp_jit_session_add_dylib → EPCDynamicLibrarySearchGenerator::Load (dlopens the library in the agent over EPC and resolves its symbols). - Swift JITSession.addDylib; JITRenderBuild.dylibPaths, parsed from -F/-framework to <F-dir>/<name>.framework/<name> by PreviewSession.dependencyDylibs. - StructuralReloader.renderObject gains dylibPaths; the reloader adds dylibs before archives and objects. Standalone passes [] (unchanged). ArchiveLoadingTests.resolvesSymbolFromDylibInAgent proves a JIT object resolves a symbol from a dylib dlopen'd in the agent. In the real E2E the Lottie symbols now resolve; the only remaining unresolved symbol is the compiler-rt builtin ___isPlatformVersionAtLeast (G3-c). The render test stays .disabled on G3-c. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…review renders (#191) The last unresolved symbol after G3-a/G3-b was the compiler-rt builtin ___isPlatformVersionAtLeast (emitted by #available, hit by all real SwiftUI). It is a defined symbol in the toolchain's libclang_rt.osx.a. - Toolchain.compilerRuntimeArchivePath() locates it via `xcrun clang -print-runtime-dir` + libclang_rt.osx.a. - PreviewSession's split branch appends it to archivePaths, so it loads via the existing G3-a static-archive mechanism (lazy: pulls only referenced builtins). This completes the JIT agent's Tier-2 dependency closure. The examples E2E render test (splitRendersRealPreviewInAgent) is now ENABLED and renders Summary.swift from examples/spm (with ToDoExtras + Lottie deps) to a non-empty PNG end to end. The earlier NSForwarding/JSONObjectWithData abort was teardown noise after the failing link; it clears once the link succeeds. With G3 complete, #191 item 3 (examples E2E) is fully done: a real Tier-2 structural edit recompiles only the hot file and renders in the agent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…istent reuse (#191) Add previewsmcp_jit_session_new_generation: create a fresh JITDylib on the same agent's LLJIT, point the session at it, and reset the initialized flag so the next run_on_main re-runs LLJIT::initialize on the new JD (re-registers __swift5_* metadata, which segfaults if skipped). Swift JITSession.newGeneration wraps it. This lets one persistent agent serve many edits, each isolated in its own JD, without respawning. CappedPersistentTests reuses one session across five generations, each linking a distinct-color render object and rendering its own color, proving fresh-JD isolation + per-generation re-initialize with a single agent. Next (chunk 2): make JITStructuralReloader an actor that holds the persistent session, calls newGeneration per edit, and respawns at a generation cap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…der (#191) JITStructuralReloader is now an actor that keeps one agent alive across edits. nextSession() links each edit into a fresh JITDylib (newGeneration) under the cap and respawns (replacing the session, whose deinit kills the old agent) at generationCap (default 100). This amortizes the ~70ms agent spawn over many edits and makes the literal path's re-render cheap. The StructuralReloader protocol is unchanged. All existing reloader/latency/E2E tests pass. New: reloaderRespawnsAtGenerationCap (cap=2, 5 renders, crosses two respawns) and splitRendersRealPreviewAcrossGenerations (the real Summary preview rendered twice, so generation 2 re-links ToDoExtras/Lottie/builtins into a fresh JD) — both green, so U2 (duplicate dependency registration across generations) does not crash at low generation counts. Known follow-up (chunk 2b): each generation re-links the editable object, which defines DesignTimeStore under the same module name, so the ObjC runtime warns about a duplicate class registration across generations. Renders still succeed, but it accumulates toward the cap; the fix is a unique -module-name per compile. Captured in docs/jit-executor-phase3-plan.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…place literal re-render (#191) The capped-persistent agent re-registered the same DesignTimeStore ObjC class across generations, warning "Class _TtC...DesignTimeStore is implemented in both ...". Two causes, both fixed: 1. Distinct structural compiles reused the editable module name. Give the editable unit a unique -module-name per compile (PreviewSession.uniqueModuleToken = "g" + fresh UUID hex; the stable module stays ctx.moduleName for @testable), so each generation's classes mangle distinctly. 2. The literal re-render re-linked the SAME object into a new generation. The reloader now re-runs the entry in place when objectPath == lastObjectPath (no newGeneration, no re-link) — also the design's in-place ~10ms literal path. Verified by editableModuleNameIsUniquePerCompile (two compiles produce disjoint _$s...DesignTimeStore symbols) and the full JIT suite rendering across generations with zero "implemented in both" warnings. Note: the token is computed statelessly on purpose. Storing a counter property on the PreviewSession actor shifted its layout and deterministically surfaced a latent EXC_ARM_DA_ALIGN (SIGBUS) in the async main-queue drain at exit of a mock-reloader test that never runs JIT'd code (a trivial whitespace recompile did not trigger it). The stateless token adds no stored property and sidesteps it; the underlying latent corruption is uncharacterized and flagged in the plan. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…VM (#191) CI skipped the JIT targets because they are gated on third_party/llvm-build (+ llvm-build-rt + the llvm-project source headers), which runners lack. Building that pinned Swift-fork LLVM from source is slow, so it is cached. - cache-warm.yml gains warm-jit-cache: builds via scripts/build-jit-llvm.sh and caches the three third_party dirs, keyed on hashFiles(build-jit-llvm.sh) so it only rebuilds when the recipe / pinned SHA changes. Weekly cron + dispatch keep it warm; the build skips on a cache hit. - ci.yml gains jit-tests (gated on changes.src): restores the cache, builds from source on a miss (90m timeout absorbs it), builds the package + SPM example, then runs swift test --filter PreviewsJITLinkTests. First PR run cache-misses and pays the full libLLVM build; after merge, dispatch warm-jit-cache once so main's cache is warm and later PRs just restore. The build guard and `swift test --filter PreviewsJITLinkTests` are already proven locally; the GHA cache/build behavior is verified by watching the jit-tests job. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
mdutil -a -i off exits 1 when a volume is mid-transition (kMDConfigSearchLevelTransitioning), which aborted the jit-tests job before it could build/test. The step is only a build-speed hint, so tolerate the flake. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The JIT suite SIGABRTs intermittently under Swift Testing's default parallel execution: concurrent compile/link/teardown over the shared in-process LLJIT trips LLVM ORC assertions (assertions are on in our build) — varied between SymbolStringPool dangling-reference and SmallVector set_size aborts. Serial is stable (verified 8/8 locally vs ~1-2 crashes per 5-8 parallel runs). Add --no-parallel to the jit-tests step. The deeper fix (thread-safe in-process JIT) is deferred. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…he build (#191) renderObject had grown to five arguments (objectPath, supportObjectPaths, archivePaths, dylibPaths, entrySymbol), all of which the JITRenderBuild already carries. Replace it with render(_ build: JITRenderBuild): one argument, no more signature churn when a new link-input category is added. Pure refactor; the daemon callers and every reloader test/mock pass the build they already hold. 50 JIT tests + 8 seam/host tests pass (serial). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The GitHub macos runners are ~15-28x slower than a dev machine, so two hard thresholds tuned locally failed on CI (a ~280ms reload measured ~7.8s there). Neither is a real regression. - StructuralReloadLatencyTests: raise the total cap 5000 -> 30000ms (a hang/ regression sanity check, not a perf gate). - SplitCompileTests: assert direction-only (splitMs < wholeMs) instead of a 20% margin; the split recompiles one file vs N+1, so it is faster and the gap widens with N, but the ratio compresses under CI noise. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
FileWatcher's FSEvents trampoline already knows which watched path matched but discarded it, calling a 0-arg callback. Change the callback to (String) -> Void and pass the matched canonical path, so a watcher over a multi-file set can tell which file changed (the prerequisite for identity-based reload routing). Callers that do not need it ignore the argument. BuildSystemTests.fileWatcherDeliversChangedPath: editing the second of two watched files delivers that file's path to the callback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
obj-p
added a commit
that referenced
this pull request
Jun 10, 2026
… plan Adds the "Update 2026-06-09" section that supersedes the stale "Immediate next step" pointer and the CI-green claims: PR #190 merged, GitHub Actions CI is disabled, and the merge bar is now local (unit tests plus the integration-test skill). Documents the non-leaf duplicate/missing-symbol bug, the incremental whole-module fix this branch lands, its tradeoffs, and the verification table. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
obj-p
added a commit
that referenced
this pull request
Jun 10, 2026
) * Snapshot: pin window capture to deterministic 1x bitmapImageRepForCachingDisplay inherits the host window's backingScaleFactor, so on a Retina (2x) display the snapshot came out 800x1200 instead of the logical 400x600, making output depend on which machine the daemon ran on. Build the NSBitmapImageRep manually at the point bounds so capture is 1x and reproducible across machines. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * JIT: hot-reload non-leaf files via single-module incremental split The structural split compiled the bulk into a prebuilt stable module that the hot file imported. That is one-directional, so a non-leaf hot file (one the bulk references, e.g. a previewed view used by another preview) failed with "cannot find <type> in scope" when the bulk compiled without it. Detect the non-leaf case (the stable-module compile fails) and compile the whole target module incrementally instead: the editable overlay plus the other sources under one module name, where the Swift driver recompiles only the hot file and reuses the bulk objects. References resolve in both directions. Because the overlay then uses the target's own module name, its @observable DesignTimeStore would re-register across generations, so non-leaf builds respawn the agent each structural edit. hotReloadStructural now exercises this on the JIT daemon. Its sync marker moves from "Compiled:" (only the non-JIT recompile logs it) to "Reloaded", which both daemon kinds log on a successful structural edit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * JIT C++: fix lookupInitialized lock and anon-mapper bounds lookupInitialized read session->initialized outside the lock with no re-check, so two threads could both pass the guard and double-initialize the JITDylib (and the bool itself was a data race). Hold the lock across the check and the init so the flag is only touched under the lock. PreviewsAnonymousMapper::prepare and ::initialize decremented the upper_bound iterator with no bounds check, which is undefined behavior if the reservation map is empty or the address precedes all reservations. Guard r == begin(): prepare returns nullptr, initialize reports an Expected error. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Docs: record non-leaf fix, CI removal, and local merge bar in Phase 3 plan Adds the "Update 2026-06-09" section that supersedes the stale "Immediate next step" pointer and the CI-green claims: PR #190 merged, GitHub Actions CI is disabled, and the merge bar is now local (unit tests plus the integration-test skill). Documents the non-leaf duplicate/missing-symbol bug, the incremental whole-module fix this branch lands, its tradeoffs, and the verification table. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * JIT: render agent preview in an off-screen NSWindow, not ImageRenderer ImageRenderer cannot rasterize AppKit-backed SwiftUI: with NavigationStack { List } as the root it returns a nil cgImage (the agent entry fails with status -1) and with any modifier wrapping the List it returns a tiny blank raster at ideal size. Every real preview hit one of the two; the daemon then either reported "Recompilation failed" or served a blank snapshot. The cases slipped through because the existing assertions only checked "snapshot changed" / "PNG non-empty" and the session-start render goes through the dylib window path, so the agent had never rendered an unedited body. renderPreviewToFile now hosts the view in a borderless NSWindow positioned off-screen via NSHostingView, which gives AppKit-backed views a real backing hierarchy, and captures at a deterministic 1x into an NSBitmapImageRep the same way Snapshot.capture does. No run-loop settle is needed; cacheDisplay draws synchronously after layoutSubtreeIfNeeded. nonLeafRendersUneditedPreviewInAgent guards the regression: it renders the unedited non-leaf ToDoView.swift (NavigationStack + List) through compileObjectForJIT and the reloader, exactly the path a cross-file bulk edit takes as a session's first structural reload. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * HostApp: drop stale agent PNG when the window reloads a dylib snapshot() prefers the agent-rendered PNG whenever agentImagePaths has an entry, but only the JIT reload paths wrote it and nothing cleared it when the dylib window path re-rendered. After a session became agent-backed, preview_switch and preview_configure updated the window while snapshots kept serving the old agent PNG. loadPreview now removes the entry, making the window the source of truth again until the next JIT reload repopulates it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * gitignore: match the third_party symlink in worktrees "/third_party/" only matches directories. Worktrees point third_party at the main checkout's prebuilt LLVM via a symlink, which therefore showed as untracked. Dropping the trailing slash covers both. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 3 of the custom JIT executor: wire it into PreviewsMCP's session lifecycle (design §8 Phase 3). Tracks #189. Builds on Phase 1 (#185) and Phase 2 (#186). Next-phase work is tracked in #191.
CI does not build the JIT targets (no
third_partyLLVM there), so the new JIT tests run local-only; this PR keeps the non-JIT build green by depending only on a JIT-freeStructuralReloaderprotocol inPreviewsCore, with the concrete impl injected at the executable behind one#if PREVIEWSMCP_JIT.Landed
Viewbody, renders offscreen to a bitmap on the agent's main thread, over a contiguous anonymous executor-memory slab (macOS deniesmprotect-exec onMAP_SHARED; shared-memory slab reverted).Compiler(defaultarm64-apple-macosx14.0target works in the agent; nocompileObjectchange).BridgeGeneratorrender seam:renderPreviewToFilerasterizes viaImageRendererto a baked path.StructuralReloaderprotocol (Core) +JITStructuralReloader(PreviewsJITLink) + host wiring: structural edits render in the agent;preview_snapshotserves the agent PNG; injected via#if PREVIEWSMCP_JIT+jitEnabled-onlyPackage.swiftdeps.DesignTimeStorefrom a baked JSON; a literal edit rewrites the JSON and re-renders the same.o(no recompile).Status
Phase 3 core complete. Plan/source of truth:
docs/jit-executor-phase3-plan.md. Still draft pending theexamples/E2E verify and the recompile-narrowing that gets structural edits under the <200ms target — both tracked in #191 (recompile-narrowing + capped-persistent reloader + examples E2E + JIT-in-CI). P3.3 (in-placewrite_mem+ handshake) stays conditional.🤖 Generated with Claude Code