Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
1fe37b3
JIT executor Phase 3 (P3.1a): run a real SwiftUI View body in the age…
obj-p Jun 2, 2026
8e4fa08
Add docs/jit-executor-phase3-plan.md (Phase 3 living plan) (#189)
obj-p Jun 2, 2026
51784cc
JIT executor Phase 3 (P3.1b-i): build an NSHostingView on the agent's…
obj-p Jun 2, 2026
b5d20ae
JIT executor Phase 3 (P3.1b-ii): render a JIT'd SwiftUI view to a bit…
obj-p Jun 2, 2026
a8d5909
JIT executor Phase 3: anonymous contiguous slab mapper, replacing the…
obj-p Jun 2, 2026
e55f89b
Update Phase 3 plan: shared-memory slab reverted as macOS-incompatibl…
obj-p Jun 2, 2026
d8b7a81
Phase 3 plan: P3.2 resume pointer + pitfalls; flag the confounded Com…
obj-p Jun 2, 2026
302abe4
JIT executor Phase 3 (P3.2): hot update via agent respawn (#189)
obj-p Jun 2, 2026
8ee5b1d
docs/jit-executor-phase3-plan: record recompile-narrowing gaps (#189)
obj-p Jun 2, 2026
85bf7b8
Phase 3 plan: record P3.4 render-surface decision (agent bitmaps) + c…
obj-p Jun 2, 2026
e19f217
JIT executor Phase 3 (P3.4a): agent renders a bitmap to a host-suppli…
obj-p Jun 3, 2026
020f63c
docs/jit-executor-phase3-plan: refine G1 with W4 findings (#189)
obj-p Jun 3, 2026
d6216ec
JIT executor Phase 3 (P3.4b): render seam in BridgeGenerator for the …
obj-p Jun 3, 2026
568048d
docs/jit-executor-phase3-plan: fold in W4/W5 verdict (#189)
obj-p Jun 3, 2026
bb68a86
docs/jit-executor-phase3-plan: fold in W7 verdict, resolve model mism…
obj-p Jun 4, 2026
a143851
Phase 3 plan: P3.4c protocol-seam split + defer JIT-in-CI as infra (#…
obj-p Jun 4, 2026
b5783da
docs/jit-executor-phase3-plan: integrated POC PASSED; soak gates exec…
obj-p Jun 4, 2026
c5cd096
docs/jit-executor-phase3-plan: amend respawn-first to respawn-on-cap …
obj-p Jun 4, 2026
0ab1e67
JIT executor Phase 3 (P3.4c-i-1): StructuralReloader seam in Previews…
obj-p Jun 4, 2026
c448d6f
JIT executor Phase 3 (P3.4c-i-2): JITStructuralReloader over the agen…
obj-p Jun 4, 2026
ebac13a
docs/jit-executor-phase3-plan: W6 closed; record the executor edit-ti…
obj-p Jun 4, 2026
32c113c
JIT executor Phase 3 (P3.4c-i-3): wire the JIT structural reload into…
obj-p Jun 4, 2026
d54d4a2
JIT executor Phase 3 (P3.4c-ii): route agent-backed literal edits to …
obj-p Jun 4, 2026
c689a16
JIT executor Phase 3 (P3.4d): measure structural reload latency (#189)
obj-p Jun 4, 2026
38f8745
JIT executor Phase 3 (P3.4c-ii-1): seed DesignTimeStore from a JSON f…
obj-p Jun 4, 2026
728e14b
JIT executor Phase 3 (P3.4c-ii-2): no-recompile literal re-render via…
obj-p Jun 4, 2026
859eed3
JIT executor Phase 3 (P3.4c-ii-3): wire the no-recompile literal path…
obj-p Jun 4, 2026
70e8f0d
Fix swift-format lint --strict on Phase 3 P3.4 files (#189)
obj-p Jun 4, 2026
02b3b08
Phase 3 plan: P3.4 core complete; resume pointer for the next phase (…
obj-p Jun 4, 2026
34613af
JIT executor Phase 4 (P4.1-a): Compiler split primitive for recompile…
obj-p Jun 4, 2026
58893fe
JIT executor Phase 4 (P4.1-b): wire compileObjectForJIT to the Tier-2…
obj-p Jun 4, 2026
82f9898
JIT executor Phase 4 (P4.1-c): cache the stable module across hot-fil…
obj-p Jun 4, 2026
37f29e3
JIT executor Phase 4 (P4.1-d): link the split's stable objects in the…
obj-p Jun 4, 2026
5d6e48c
JIT executor Phase 4 (P4.1-e): examples E2E for the split; surfaces d…
obj-p Jun 4, 2026
dbdf999
JIT executor Phase 4 (P4.1-f / G3-a): load static dependency archives…
obj-p Jun 4, 2026
ef497b0
JIT executor Phase 4 (G3-b): dlopen the target's binary frameworks in…
obj-p Jun 4, 2026
04e9ddf
JIT executor Phase 4 (G3-c): load compiler-rt builtins; real Tier-2 p…
obj-p Jun 4, 2026
1392dd3
JIT executor Phase 4 (item 2, chunk 1): newGeneration for capped-pers…
obj-p Jun 4, 2026
c74e8d3
JIT executor Phase 4 (item 2, chunk 2): capped-persistent actor reloa…
obj-p Jun 4, 2026
82a05bf
JIT executor Phase 4 (item 2, chunk 2b): unique editable module + in-…
obj-p Jun 4, 2026
dc7c7b6
JIT executor Phase 4 (item 4): run JIT tests in CI behind a cached LL…
obj-p Jun 4, 2026
10a2531
CI: make jit-tests Disable Spotlight non-fatal (#191)
obj-p Jun 4, 2026
a5c9cbb
CI: run JIT tests serially to avoid LLVM-assertion flakiness (#191)
obj-p Jun 4, 2026
305993f
JIT executor Phase 4: consolidate StructuralReloader.render to take t…
obj-p Jun 4, 2026
9814f8c
JIT tests: relax two timing thresholds for slow CI runners (#191)
obj-p Jun 4, 2026
d4f56d3
G2 chunk 1: FileWatcher delivers the changed file path (#191)
obj-p Jun 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/cache-warm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,32 @@ jobs:
run: |
swift build
swift build --package-path examples/spm

warm-jit-cache:
name: Warm jit-llvm cache
runs-on: macos-15
steps:
- uses: actions/checkout@v4

- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_26.3.app

- name: Install cmake and ninja
run: brew install cmake ninja

- name: Cache JIT LLVM build
uses: actions/cache@v4
with:
path: |
third_party/llvm-build
third_party/llvm-build-rt
third_party/llvm-project
key: jit-llvm-${{ runner.os }}-${{ hashFiles('scripts/build-jit-llvm.sh') }}

- name: Build JIT LLVM (skips on cache hit)
run: |
if [ -f third_party/llvm-build-rt/lib/darwin/liborc_rt_osx.a ]; then
echo "JIT LLVM already cached; nothing to build."
else
scripts/build-jit-llvm.sh
fi
68 changes: 68 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -268,3 +268,71 @@ jobs:
xcrun simctl list devices booted 2>/dev/null || true
echo "--- orphan simctl/previewsmcp procs ---"
ps -o pid,command -ax | grep -E "(simctl|previewsmcp)" | grep -v grep || true

jit-tests:
needs: changes
if: needs.changes.outputs.src == 'true'
runs-on: macos-15
# 90m absorbs a cache-miss libLLVM build from source (only on a
# build-jit-llvm.sh change / first run); the cache-warm.yml warm-jit-cache
# job keeps the cache warm so the normal path just restores and tests.
timeout-minutes: 90
steps:
- uses: actions/checkout@v4

- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_26.3.app

- name: Disable Spotlight
# Non-fatal: mdutil can exit 1 when a volume is mid-transition
# (kMDConfigSearchLevelTransitioning); it is only a build-speed hint.
run: sudo mdutil -a -i off || true

- name: Install cmake and ninja
run: brew install cmake ninja

- name: Cache JIT LLVM build
uses: actions/cache@v4
with:
path: |
third_party/llvm-build
third_party/llvm-build-rt
third_party/llvm-project
key: jit-llvm-${{ runner.os }}-${{ hashFiles('scripts/build-jit-llvm.sh') }}

- name: Cache SPM build
uses: actions/cache@v4
with:
path: |
.build
examples/spm/.build
key: spm-${{ runner.os }}-jit-${{ hashFiles('Package.resolved') }}
restore-keys: |
spm-${{ runner.os }}-jit-
spm-${{ runner.os }}-

- name: Build JIT LLVM if cache missed
run: |
if [ -f third_party/llvm-build-rt/lib/darwin/liborc_rt_osx.a ]; then
echo "JIT LLVM restored from cache."
else
echo "JIT LLVM cache miss; building from source (slow)."
scripts/build-jit-llvm.sh
fi

- name: Build
run: |
swift build
swift build --package-path examples/spm

- name: JIT tests
# The suite builds PreviewAgent and renders real previews in spawned
# agents; 20m covers the cold test-target + agent compile plus runs.
# --no-parallel: the JIT layer (shared in-process LLJIT + LLVM ORC) is
# not safe under Swift Testing's default parallel execution; concurrent
# compile/link/teardown trips LLVM assertions (assertions are on in our
# build) and SIGABRTs intermittently. Serial is stable.
timeout-minutes: 20
env:
NSUnbufferedIO: "YES"
run: swift test --filter "PreviewsJITLinkTests" --no-parallel
11 changes: 9 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ let jitEnabled =
FileManager.default.fileExists(atPath: llvmBuild)
&& FileManager.default.fileExists(atPath: orcRuntimeArchive)

// Composition-root wiring for the JIT structural-reload path. Only the executable
// references the gated PreviewsJITLink, behind one `#if PREVIEWSMCP_JIT`; the daemon
// logic depends solely on the JIT-free `StructuralReloader` protocol in PreviewsCore.
let jitCLIDependencies: [Target.Dependency] = jitEnabled ? ["PreviewsJITLink"] : []
let jitCLISwiftSettings: [SwiftSetting] = jitEnabled ? [.define("PREVIEWSMCP_JIT")] : []

var targets: [Target] = [
.target(
name: "SimulatorBridge",
Expand Down Expand Up @@ -53,7 +59,8 @@ var targets: [Target] = [
"PreviewsEngine",
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "MCP", package: "swift-sdk"),
],
] + jitCLIDependencies,
swiftSettings: jitCLISwiftSettings,
plugins: [.plugin(name: "GenerateVersion")]
),
.executableTarget(
Expand Down Expand Up @@ -117,7 +124,7 @@ if jitEnabled {
targets += [
.target(
name: "PreviewsJITLink",
dependencies: ["PreviewsJITLinkCxx"],
dependencies: ["PreviewsJITLinkCxx", "PreviewsCore"],
plugins: [.plugin(name: "BundleOrcRuntime")]
),
.target(
Expand Down
202 changes: 172 additions & 30 deletions Sources/PreviewAgent/main.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#include "llvm/ExecutionEngine/Orc/Shared/AllocationActions.h"
#include "llvm/ExecutionEngine/Orc/Shared/ExecutorAddress.h"
#include "llvm/ExecutionEngine/Orc/Shared/MemoryFlags.h"
#include "llvm/ExecutionEngine/Orc/Shared/TargetProcessControlTypes.h"
#include "llvm/ExecutionEngine/Orc/Shared/WrapperFunctionUtils.h"
#include "llvm/ExecutionEngine/Orc/TargetProcess/ExecutorSharedMemoryMapperService.h"
Expand All @@ -8,11 +10,18 @@
#include "llvm/ExecutionEngine/Orc/TargetProcess/SimpleExecutorMemoryManager.h"
#include "llvm/ExecutionEngine/Orc/TargetProcess/SimpleRemoteEPCServer.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/Memory.h"
#include "llvm/Support/raw_ostream.h"

#include <cstdlib>
#include <cstring>
#include <dispatch/dispatch.h>
#include <dlfcn.h>
#include <map>
#include <mutex>
#include <string>
#include <thread>
#include <vector>

using namespace llvm;
using namespace llvm::orc;
Expand Down Expand Up @@ -62,6 +71,110 @@ CWrapperFunctionResult previewsmcp_register_types(const char *ArgData,
.release();
}

struct MainThreadCall {
int32_t (*Fn)();
int32_t Result;
};

void invokeOnMain(void *Ctx) {
auto *Call = static_cast<MainThreadCall *>(Ctx);
Call->Result = Call->Fn();
}

CWrapperFunctionResult previewsmcp_run_on_main(const char *ArgData,
size_t ArgSize) {
return WrapperFunction<int32_t(SPSExecutorAddr)>::handle(
ArgData, ArgSize,
[](llvm::orc::ExecutorAddr FnAddr) -> int32_t {
MainThreadCall Call{FnAddr.toPtr<int32_t (*)()>(), 0};
dispatch_sync_f(dispatch_get_main_queue(), &Call, invokeOnMain);
return Call.Result;
})
.release();
}

std::mutex AnonMapperMutex;
std::map<void *, size_t> AnonReservations;

CWrapperFunctionResult previewsmcp_anon_reserve(const char *ArgData,
size_t ArgSize) {
return WrapperFunction<SPSExpected<SPSExecutorAddr>(uint64_t)>::handle(
ArgData, ArgSize,
[](uint64_t Size) -> Expected<llvm::orc::ExecutorAddr> {
std::error_code EC;
auto MB = sys::Memory::allocateMappedMemory(
Size, nullptr, sys::Memory::MF_READ | sys::Memory::MF_WRITE,
EC);
if (EC)
return errorCodeToError(EC);
std::lock_guard<std::mutex> Lock(AnonMapperMutex);
AnonReservations[MB.base()] = Size;
return llvm::orc::ExecutorAddr::fromPtr(MB.base());
})
.release();
}

CWrapperFunctionResult previewsmcp_anon_initialize(const char *ArgData,
size_t ArgSize) {
using namespace llvm::orc::tpctypes;
return WrapperFunction<SPSExpected<SPSExecutorAddr>(SPSFinalizeRequest)>::
handle(ArgData, ArgSize,
[](FinalizeRequest FR) -> Expected<llvm::orc::ExecutorAddr> {
llvm::orc::ExecutorAddr Base(~0ULL);
for (auto &Seg : FR.Segments)
Base = std::min(Base, Seg.Addr);
for (auto &Seg : FR.Segments) {
char *Mem = Seg.Addr.toPtr<char *>();
if (!Seg.Content.empty())
memcpy(Mem, Seg.Content.data(), Seg.Content.size());
memset(Mem + Seg.Content.size(), 0,
Seg.Size - Seg.Content.size());
sys::MemoryBlock MB(Mem, Seg.Size);
if (auto EC = sys::Memory::protectMappedMemory(
MB, toSysMemoryProtectionFlags(Seg.RAG.Prot)))
return errorCodeToError(EC);
if ((Seg.RAG.Prot & MemProt::Exec) == MemProt::Exec)
sys::Memory::InvalidateInstructionCache(Mem, Seg.Size);
}
auto Dealloc = runFinalizeActions(FR.Actions);
if (!Dealloc)
return Dealloc.takeError();
return Base;
})
.release();
}

CWrapperFunctionResult previewsmcp_anon_deinitialize(const char *ArgData,
size_t ArgSize) {
return WrapperFunction<SPSError(SPSSequence<SPSExecutorAddr>)>::handle(
ArgData, ArgSize,
[](std::vector<llvm::orc::ExecutorAddr>) -> Error {
return Error::success();
})
.release();
}

CWrapperFunctionResult previewsmcp_anon_release(const char *ArgData,
size_t ArgSize) {
return WrapperFunction<SPSError(SPSSequence<SPSExecutorAddr>)>::handle(
ArgData, ArgSize,
[](std::vector<llvm::orc::ExecutorAddr> Bases) -> Error {
Error Err = Error::success();
std::lock_guard<std::mutex> Lock(AnonMapperMutex);
for (auto B : Bases) {
auto I = AnonReservations.find(B.toPtr<void *>());
if (I == AnonReservations.end())
continue;
sys::MemoryBlock MB(I->first, I->second);
if (auto EC = sys::Memory::releaseMappedMemory(MB))
Err = joinErrors(std::move(Err), errorCodeToError(EC));
AnonReservations.erase(I);
}
return Err;
})
.release();
}

CWrapperFunctionResult previewsmcp_write_pointers(const char *ArgData,
size_t ArgSize) {
using namespace llvm::orc::tpctypes;
Expand All @@ -83,13 +196,14 @@ static void printErrorAndExit(Twine ErrMsg) {
}

int main(int argc, char *argv[]) {
ExitOnError ExitOnErr;
ExitOnErr.setBanner(std::string(argv[0]) + ": ");

dlopen("/usr/lib/swift/libswiftCore.dylib", RTLD_NOW | RTLD_GLOBAL);
dlopen("/usr/lib/swift/libswift_Concurrency.dylib", RTLD_NOW | RTLD_GLOBAL);
dlopen("/usr/lib/swift/libswiftFoundation.dylib", RTLD_NOW | RTLD_GLOBAL);
dlopen("/usr/lib/swift/libswiftDispatch.dylib", RTLD_NOW | RTLD_GLOBAL);
dlopen("/System/Library/Frameworks/AppKit.framework/AppKit",
RTLD_NOW | RTLD_GLOBAL);
dlopen("/System/Library/Frameworks/SwiftUI.framework/SwiftUI",
RTLD_NOW | RTLD_GLOBAL);

if (argc != 2)
printErrorAndExit("expected exactly one argument");
Expand All @@ -107,31 +221,59 @@ int main(int argc, char *argv[]) {
if (OutFDStr.getAsInteger(10, OutFD))
printErrorAndExit(OutFDStr + " is not a valid file descriptor");

auto Server =
ExitOnErr(SimpleRemoteEPCServer::Create<FDSimpleRemoteEPCTransport>(
[](SimpleRemoteEPCServer::Setup &S) -> Error {
S.setDispatcher(
std::make_unique<SimpleRemoteEPCServer::ThreadDispatcher>());
S.bootstrapSymbols() =
SimpleRemoteEPCServer::defaultBootstrapSymbols();
S.bootstrapSymbols()["__previewsmcp_register_conformances"] =
llvm::orc::ExecutorAddr::fromPtr(
&previewsmcp_register_conformances);
S.bootstrapSymbols()["__previewsmcp_register_types"] =
llvm::orc::ExecutorAddr::fromPtr(&previewsmcp_register_types);
S.bootstrapSymbols()["__previewsmcp_write_pointers"] =
llvm::orc::ExecutorAddr::fromPtr(&previewsmcp_write_pointers);
S.services().push_back(
std::make_unique<rt_bootstrap::SimpleExecutorDylibManager>());
S.services().push_back(
std::make_unique<rt_bootstrap::SimpleExecutorMemoryManager>());
S.services().push_back(
std::make_unique<
rt_bootstrap::ExecutorSharedMemoryMapperService>());
return Error::success();
},
InFD, OutFD));

ExitOnErr(Server->waitForDisconnect());
return 0;
std::thread ServerThread([InFD, OutFD] {
ExitOnError ExitOnErr;
ExitOnErr.setBanner("PreviewAgent: ");
auto Server = ExitOnErr(SimpleRemoteEPCServer::Create<
FDSimpleRemoteEPCTransport>(
[](SimpleRemoteEPCServer::Setup &S) -> Error {
S.setDispatcher(
std::make_unique<SimpleRemoteEPCServer::ThreadDispatcher>());
S.bootstrapSymbols() =
SimpleRemoteEPCServer::defaultBootstrapSymbols();
S.bootstrapSymbols()["__previewsmcp_register_conformances"] =
llvm::orc::ExecutorAddr::fromPtr(
&previewsmcp_register_conformances);
S.bootstrapSymbols()["__previewsmcp_register_types"] =
llvm::orc::ExecutorAddr::fromPtr(&previewsmcp_register_types);
S.bootstrapSymbols()["__previewsmcp_write_pointers"] =
llvm::orc::ExecutorAddr::fromPtr(&previewsmcp_write_pointers);
S.bootstrapSymbols()["__previewsmcp_run_on_main"] =
llvm::orc::ExecutorAddr::fromPtr(&previewsmcp_run_on_main);
S.bootstrapSymbols()["__previewsmcp_anon_reserve"] =
llvm::orc::ExecutorAddr::fromPtr(&previewsmcp_anon_reserve);
S.bootstrapSymbols()["__previewsmcp_anon_initialize"] =
llvm::orc::ExecutorAddr::fromPtr(&previewsmcp_anon_initialize);
S.bootstrapSymbols()["__previewsmcp_anon_deinitialize"] =
llvm::orc::ExecutorAddr::fromPtr(&previewsmcp_anon_deinitialize);
S.bootstrapSymbols()["__previewsmcp_anon_release"] =
llvm::orc::ExecutorAddr::fromPtr(&previewsmcp_anon_release);
S.services().push_back(
std::make_unique<rt_bootstrap::SimpleExecutorDylibManager>());
S.services().push_back(
std::make_unique<rt_bootstrap::SimpleExecutorMemoryManager>());
S.services().push_back(
std::make_unique<
rt_bootstrap::ExecutorSharedMemoryMapperService>());
return Error::success();
},
InFD, OutFD));
ExitOnErr(Server->waitForDisconnect());
std::_Exit(0);
});
ServerThread.detach();

if (auto *Load = reinterpret_cast<bool (*)()>(
dlsym(RTLD_DEFAULT, "NSApplicationLoad")))
Load();

auto *RunInMode =
reinterpret_cast<int (*)(const void *, double, unsigned char)>(
dlsym(RTLD_DEFAULT, "CFRunLoopRunInMode"));
auto *DefaultMode = reinterpret_cast<const void *const *>(
dlsym(RTLD_DEFAULT, "kCFRunLoopDefaultMode"));
if (!RunInMode || !DefaultMode)
printErrorAndExit("CoreFoundation run loop symbols not found");
while (true)
RunInMode(*DefaultMode, 0.25, false);
}
2 changes: 1 addition & 1 deletion Sources/PreviewsCLI/Handlers/PreviewStartHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ private func handleIOSPreviewStart(
let sessionID = session.id
let allPaths = [fileURL.path] + (buildContext?.sourceFiles?.map(\.path) ?? [])
let iosState = ctx.iosState
let watcher = try? FileWatcher(paths: allPaths) {
let watcher = try? FileWatcher(paths: allPaths) { _ in
Task {
Log.info("MCP: iOS file change detected, reloading session \(sessionID)...")
do {
Expand Down
Loading
Loading