Status: implemented (ADR 0047)
Move Talk's target-independent finalized MIR into one public talk-mir crate,
then make bytecode, C, and LLVM three concrete adapters over that MIR. Rename
the existing VM crate from talk-runtime to talk-vm. Rename the existing C
embedding crate from talk-c to talk-ffi, preserving its language-neutral C
interface and the current TalkSwift interface. Reuse the freed talk-c package
name for the C backend.
The end state is:
TypedProgram
-> compiler-owned MIR construction and ownership checking
-> compiler-owned optimization, register allocation, and frame shaping
-> talk-mir::Module
| | |
v v v
talk-bytecode talk-c talk-llvm
|
v
talk-vm
talk-ffi remains the language-neutral embedding interface. It depends on the
compiler, bytecode adapter, and VM so TalkSwift keeps formatting, analysis,
workspace, package, REPL, bytecode, and dynamic source-execution behavior.
- Finalized MIR is the public target seam. There is no separate public
codegenrepresentation. - MIR construction and ownership analysis remain private compiler implementation.
- Bytecode lowering is a separate
talk-bytecodecrate. - The bytecode format, validation, interpreter, memory, host IO, and execution
accounting belong to
talk-vm, which replacestalk-runtime. - The package name
talk-cbelongs to the C backend. - The existing C ABI facade becomes
talk-ffiand is language-neutral rather than Swift-specific. - TalkSwift remains and preserves its current public Swift interface.
- TalkSwift continues to use bytecode and the VM for dynamic execution, REPL, package run/test, and the self-hosted frontend.
- C and LLVM share one native C runtime source owned by
talk-native-runtime. - There is no generic backend trait or backend registry. Each real adapter consumes public MIR directly.
- No new third-party dependencies are required.
The talk compiler owns source semantics and target-independent lowering only:
- typed frontend output;
- MIR construction;
- ownership and initialization checking;
- layout selection;
- optimization;
- register allocation;
- frame-shaping facts;
- source-facing MIR diagnostics.
It does not own C emission, VM bytecode encoding, VM instruction fusion, or VM execution wrappers.
C, bytecode, and LLVM consume the same finalized MIR module. Adding a MIR instruction or changing a layout contract must make all three adapters account for the change through exhaustive matching.
Delete the current public talk::codegen model and its exhaustive projection.
The final MIR data types are the public contract. The compiler's private
builder and analyses construct those types directly.
talk-ffi owns a stable C ABI suitable for Swift and other C-compatible hosts.
TalkSwift is one binding over that interface, not its owner.
The extraction must preserve:
- bytecode image bytes unless a separately reviewed format change is intended;
- the checked-in self-hosted frontend fixed point;
- VM behavior and resource accounting;
- generated C behavior;
- LLVM differential behavior;
- existing C ABI function symbols;
- the public TalkSwift source interface;
- existing CLI behavior, including
talk run,talk build,talk c,talk bytecode, andtalk build --native.
This plan does not:
- serialize MIR;
- add a MIR compatibility version;
- add plugin loading for backend libraries;
- add a backend trait;
- make C replace VM execution in TalkSwift;
- add a host-callable native C library ABI for compiled Talk programs;
- change Talk language semantics;
- change the bytecode format;
- redesign the VM;
- remove the VM used by the self-hosted frontend;
- rename historical paths inside old ADRs, changelog entries, or commit reviews where the old names are historically correct;
- add dynamic-library distribution for
talk-ffiin the first extraction. The C interface remains usable from other languages through the existing static library, and hostcdylibpackaging can be added independently.
talk-mir
Public finalized MIR data and target-neutral metadata.
talk
Frontend, TypedProgram, MIR production, ownership checking, optimization,
register allocation, frame shaping, CLI, REPL, package compiler, and
self-hosted frontend host.
talk-bytecode
MIR-to-VM-bytecode adapter and compiled executable wrapper.
talk-vm
VM bytecode model, bytecode encoding/decoding/validation, interpreter,
memory, objects, IO, host values, budgets, and VM statistics.
talk-native-runtime
Shared native C runtime source used by C and LLVM.
talk-c
MIR-to-C adapter.
talk-llvm
MIR-to-LLVM adapter and its existing command-line extension.
talk-ffi
Language-neutral C ABI over the compiler, bytecode adapter, and VM.
talk-swift
Swift binding over talk-ffi.
talk-static
Existing static bytecode runner, updated to depend on talk-vm.
wasm
Existing browser interface, updated to the new compiler/bytecode names.
The intended dependency directions are:
+--------------------+
| talk-mir |
+--------------------+
^ ^ ^
| | |
+--------+ | +----------+
| | |
talk talk-bytecode talk-c
| | |
| v v
+-------------> talk-vm talk-native-runtime
| ^
| |
| talk-llvm
|
talk-ffi
|
talk-swift
More precisely:
talk-mirdepends only on the Rust standard library unless a currently existing workspace dependency is demonstrably required by the data contract.talk-vmdoes not depend ontalkortalk-mir.talk-bytecodedepends ontalk-mirandtalk-vm, never ontalk.- The
talk-clibrary depends ontalk-mirandtalk-native-runtime, never ontalk. talkmay depend ontalk-bytecodeandtalk-vm: the compiler host uses the VM for the self-hosted frontend, procedural macros, REPL, testing, bootstrap, and current convenience surfaces. This does not give target-specific lowering ownership back to the compiler.talk-cis an optional dependency of the root package'sclifeature so the existing built-in C commands can remain unchanged without making C part of adefault-features = falsecompiler build.talk-llvmdepends ontalkfor source/package compilation, ontalk-mirfor its emitter interface, and ontalk-native-runtimefor native runtime source.talk-ffidepends ontalk,talk-bytecode, andtalk-vm.
Only finalized target MIR is public:
TypedProgram
-> build MIR and check ownership
-> optimize MIR
-> compute pre-allocation parameter escape summaries
-> register allocation
-> stamp local layouts and frame-local sites under final numbering
-> publish talk_mir::Module
The ordering matters. The current C path computes parameter escape summaries before register allocation and stamps frame facts afterward. The single public MIR producer must do this for every target so C does not need a compiler-private prepass.
The unoptimized MIR used for debugging and ownership work remains private.
talk mir --no-opt may continue rendering that private form, but no target
adapter receives it.
Move the target-facing data vocabulary from src/backend/mir into
talk-mir:
LocalId,BlockId,FuncId, andLayoutId;- constants and operands;
- scalar operation and comparison kinds;
- the full instruction enum;
- terminators;
- blocks;
- functions;
- program/module entry and host-callable exports;
- global slot count;
- layout table;
Layout,Shape,FieldRepr, andSlotKind;LocalInfo;- parameter representation and return representation;
- frame-local construction sites;
- display names and type/member metadata needed by C, LLVM, and the VM;
- well-known runtime aggregate identities required for String and Storage.
The layout classifier itself stays in the compiler because it reads Talk types
and the type catalog. Only its resulting layout data belongs to talk-mir.
Likewise, ownership flow records, checker catalogs, source spans, type
substitutions, and compiler worklists do not enter the public crate.
Public MIR must not expose the complete source-level
name_resolution::Symbol enum. Define a compact executable identity containing
only facts that survive to targets:
MirSymbol
kind: Struct | Enum | Effect | Protocol
module: u16
local: u32
The compiler performs the single source-symbol-to-MIR-symbol translation when
it emits MIR. Display names are metadata, never identity. C and LLVM can intern
these identities for generated tables. The bytecode adapter maps aggregate
identities structurally to talk_vm::symbol::Symbol and interns effect
identities in the VM module.
Every current Symbol occurrence in src/backend/mir falls into one of four
buckets, and only the first survives into the public crate:
- Instruction operands:
PushHandler.effect,FindHandler.effect, andExistentialPack.protocol. These becomeMirSymbolat emission. - Layout and aggregate identities plus runtime metadata (struct/enum
identities behind the layout table, display names, String and Storage).
These become
MirSymbol-keyed metadata on the public module. Aggregate construction is already layout-keyed (Aggregate { layout, tag, .. }), so no instruction carries a Struct or Enum symbol today. - Well-known type comparisons (
Symbol::Int,Symbol::RawPtr, and similar) in the builder and classifier. These are compiler-private decisions; only the String and Storage identities they feed are published. - Builder-private construction indexes: callable and instance keys, global
slot maps, catalog indexes, pattern bindings, type parameters, and
substitutions. These never leave the compiler. Functions are already
FuncIdand globals are alreadyu32slot indices in the data definitions, so no instruction position needs a new identity type beyond bucket 1.
A compiler-produced public module guarantees:
- every function, block, local, layout, and global reference is in range;
- every block has one terminator;
- block argument counts match block parameters;
- all source ownership and cleanup decisions are already explicit;
- every executable instance is monomorphic or is an intentional check-only trap form already accepted by the current pipeline;
- layout IDs, field offsets, member layouts, local layouts, parameter representations, return representations, and frame sites are final;
- function local tables use final register numbering;
- host-callable exports name existing wrapper functions;
- display metadata and runtime identities agree with layout identities;
- target adapters need no
TypedProgram, type catalog, parser node, source span, or name-resolution query.
The module is trusted in-process data. Do not add serialization-grade MIR validation, a fixture-only builder framework, or a second proof wrapper. The compiler's existing construction checks and tests establish the invariants. Adapters return target errors for malformed manually constructed values, but they do not replay source semantic checking.
Replace target-specific methods on Driver<Typed> with one public MIR
publication interface, conceptually:
pub enum MirEntry<'a> {
Script,
Named(&'a str),
Exports {
names: &'a [String],
allowed_effects: &'a [String],
},
}
pub struct MirOutput {
pub module: talk_mir::Module,
pub optimizations: OptimizationStats,
}
impl Driver<Typed> {
pub fn compile_mir(&self, entry: MirEntry<'_>) -> Result<MirOutput, CompileError>;
}Exact ownership and lifetime syntax may differ, but the semantic surface should
not. compile_mir is the only target compilation interface.
Keep compiler-owned operations in talk:
check_ownership;- unoptimized and optimized MIR rendering;
- source-location rendering for MIR construction failures.
Move or remove target-specific compiler interfaces:
compile_executablemoves to composition withtalk-bytecode;compile_servicemoves tocompile_mir(MirEntry::Exports)plustalk_bytecode::compile;execute_moduleandexecute_imagemove out ofcompiling::driver;render_candrender_c_servicebecomecompile_mirplustalk_c::emit;codegenandPackageProject::codegen_binaryare replaced by MIR-named equivalents.
The root CLI, REPL, test harness, bootstrap, package runner, procedural macro
host, wasm facade, and talk-ffi compose these concrete interfaces. They do
not require a backend trait.
talk-bytecode owns the complete MIR-to-VM adapter:
- current
src/backend/lower.rs; - current
src/backend/checked_indexed_load.rs; - target constant, trap, static-data, effect, argument, and switch pools;
- block linearization and target patching;
- mapping MIR layouts and symbols to VM layouts and symbols;
- bytecode-only optimization statistics;
- the compiled executable wrapper that combines a VM module, rendering names, and bytecode-adapter statistics.
Conceptual interface:
pub fn compile(module: &talk_mir::Module) -> Result<Executable, CompileError>;
pub struct Executable {
// private talk_vm::Module, display metadata, and backend statistics
}Executable owns the current convenience behavior:
encode_bytecode;- bytecode rendering;
- ordinary execution;
- host-export execution;
- execution with VM statistics;
- access to bytecode-adapter statistics.
Compiler optimization counts, bytecode-adapter counts, and VM execution counts stay with their owning modules:
MirOutput
compiler optimizations
Executable
bytecode adapter optimizations
VmStats
VM execution statistics
A CLI or profiling caller may render the three together, but
talk-bytecode::compile receives only talk_mir::Module and does not acquire a
dependency on compiler-owned MirOutput. The current checked_indexed_load
count must not masquerade as a compiler-owned MIR pass after the extraction.
Rename the existing talk-runtime package and crate to talk-vm /
talk_vm. It owns:
Insn,Chunk,Module, constants, memory kinds, layouts, and symbols;- bytecode format versions;
- encode and decode;
- validation of byte images;
- interpreter frames, closures, continuations, effects, and exports;
- memory allocations and pointer provenance;
- heap objects, cells, and regions;
- host IO;
- budgets;
HostValue,RunOutcome, and value rendering;- VM statistics and profiling.
It does not know MIR and does not depend on talk-mir.
There are two trust paths:
talk-bytecodeconstructs an in-memory module under compiler invariants;talk_vm::Module::decode_bytecodetreats bytes as untrusted and validates before execution.
Keep these paths distinct. Do not move MIR lowering into talk-vm, and do not
make decoded bytes trusted because the compiler can also construct modules.
The existing talk-static C entry point may keep its
talk_runtime_run exported symbol for compatibility while its Rust dependency
changes to talk-vm.
After the old embedding package moves to talk-ffi, reuse talk-c for the C
backend. Move:
src/backend/c.rs;- its focused emitter tests;
- its target error type.
Conceptual interface:
pub struct Artifact {
pub source: String,
}
pub fn emit(module: &talk_mir::Module) -> Result<Artifact, Error>;Returning an artifact rather than driving a host compiler keeps target translation separate from CLI/toolchain policy. The root CLI continues to own:
--cc;--targetand Zig selection;--cflag;- output paths;
- scratch files;
--keep-c.
The talk-c package has no dependency on talk. Its tests may use talk as a
dev-dependency for source-to-MIR and differential fixtures.
The current generated-program contract remains unchanged: one self-contained C
translation unit with main. A stable host-callable C library ABI for compiled
Talk programs is explicitly outside this extraction.
Change the LLVM emitter to consume talk_mir::Module directly. Remove:
talk::codegenimports and re-exports;- the copied codegen model;
Runtime::native_prelude.
Keep talk-llvm's current artifact interface and CLI behavior. It may depend on
talk for source and package compilation, while its emitter module depends
only on talk-mir and talk-native-runtime concepts.
Move src/backend/c_prelude.c into a small first-party crate. Its interface can
remain narrow:
pub fn source() -> &'static str;This module earns its seam because C and LLVM are two real consumers of one large runtime implementation. Neither backend should depend on the other to obtain it.
talk-llvm/src/llvm_runtime.c remains LLVM-owned because it is the LLVM
pointer-ABI bridge, not the shared native runtime itself.
Rename the current talk-c embedding package to talk-ffi:
crates/talk-c/Cargo.toml -> crates/talk-ffi/Cargo.toml
crates/talk-c/src/lib.rs -> crates/talk-ffi/src/lib.rs
crates/talk-c/include/talk_c.h -> crates/talk-ffi/include/talk_ffi.h
crates/talk-c/README.md -> crates/talk-ffi/README.md
Use package and crate name talk-ffi / talk_ffi, producing
libtalk_ffi.a. The Swift binary artifact becomes TalkFFI.xcframework, and
the internal Swift system module becomes CTalkFFI.
The public Swift module remains TalkSwift, and its public Swift types and
methods remain unchanged.
Preserve every existing exported talk_* function name and its argument,
return, ownership, and error behavior. Renaming the package and header must not
silently change the ABI.
Add an explicit interface version distinct from the compiler version:
#define TALK_FFI_ABI_VERSION 1
uint32_t talk_ffi_abi_version(void);Document in talk_ffi.h and the README:
- ownership of every returned buffer and opaque handle;
- the exact matching free function;
- how long borrowed
TalkStringRefand view data remain valid; - callback lifetime and synchronous-callback rules;
- handle thread-affinity;
- UTF-8 requirements;
- panic containment;
- status and error conventions.
Keep the existing catch_unwind protection at every exported entry path.
talk-ffi continues to expose the complete existing behavior:
- formatter and highlighter;
- one-shot checking and execution;
- bytecode rendering and bytecode image compilation;
- package creation, installation, run, and test;
- package source-provider callbacks;
- workspace diagnostics and editor queries;
- REPL evaluation, type queries, completion, and input continuation;
- all typed result handles and free functions.
Internally, source execution changes from compiler-private backend calls to:
source -> talk::compile_mir -> talk_bytecode::compile -> talk_vm::execute
This is an ownership change, not a behavior change.
Update implementation-only names:
CTalkC -> CTalkFFI
import CTalkC -> import CTalkFFI
TalkC.xcframework -> TalkFFI.xcframework
TalkC.xcframework.zip -> TalkFFI.xcframework.zip
libtalk_c.a -> libtalk_ffi.a
talk_c.h -> talk_ffi.h
talkCReleaseURL -> talkFFIReleaseURL
talkCReleaseChecksum -> talkFFIReleaseChecksum
Do not rename public Swift declarations. Existing TalkSwift tests should build without source changes beyond the private C module import.
The root Package.swift, crates/talk-swift/Package.swift, module map,
crates/talk-swift/scripts/build-xcframework.sh, CI workflow, release workflow, and
TalkSwift README all move together in one stage.
A C interface is the shared language seam. Do not add language-specific Rust
entry points. Add a small C smoke client that includes talk_ffi.h, links the
host static library, calls representative one-shot and handle APIs, and frees
all results. That test proves the interface independently of Swift and becomes
the template for future Python, Kotlin/Native, C#, or other bindings.
Host dynamic-library packaging is a follow-up. It does not block other hosts that can link the static C library, and it should not be added to the iOS crate type list without validating every Apple target.
MIR construction failures retain source spans and are rendered by talk:
- source-invalid constructs that survived recovery;
- ownership, move, borrow, initialization, or cleanup diagnostics;
- entry/export selection failures;
- violated compiler invariants.
C, bytecode, and LLVM errors do not depend on parser spans or TypedProgram. They identify:
- malformed public MIR supplied manually;
- target representability failures;
- target-internal invariant failures.
A compiler-produced finalized MIR module should not receive a capability rejection from one parity backend. ADR 0037's completeness requirement remains in force.
Runtime failures remain owned by talk-vm:
- malformed decoded images;
- invalid dynamic memory operations;
- instruction or memory budgets;
- host IO failures;
- traps;
- resource-balance failures.
talk-ffi translates compiler, adapter, and VM errors into the existing C
status/result shapes. It catches Rust panics and never unwinds across C.
Every stage must build and test independently. Temporary compatibility re-exports are allowed within a stage sequence, but none remain in the final state.
- Add an ADR following ADR 0046 that amends ADR 0034's implementation shape:
- three adapters justify a public finalized MIR seam;
- bytecode remains the parity execution target but is no longer private compiler implementation;
- the VM and bytecode adapter are separate modules;
- in-process MIR remains trusted and unserialized;
talk-ffiremains VM-backed.
- Record current package and target dependency graphs with
cargo tree. - Record current nonblank production line counts for target-neutral compiler, C adapter, bytecode adapter, VM, LLVM, and FFI separately.
- Record the current exported
talk_*C symbol list fromlibtalk_c.aas the ABI migration oracle. - Inventory every caller of the
Driver<Typed>backend methods (compile_executable,compile_service,render_c,render_c_service,codegen,render_mir) so no consumer is discovered mid-stage: CLI, REPL, LSP server, test harness, bootstrap, package runner, procedural macro host, wasm,talk-ffi, and benches. - Run and record the baseline validation commands listed below.
No production behavior changes. The plan and ADR agree on ownership and names.
talk-runtime/->crates/talk-vm/;- package name
talk-runtime->talk-vm; - Rust imports
talk_runtime->talk_vm; - root features and path dependencies;
talk-static,talk-cbefore its FFI rename,talk-llvmtests, wasm, tests, scripts, and current documentation.
- This is a mechanical rename only.
- Do not move MIR lowering into the VM.
- Do not change bytecode format constants or encoded bytes.
- Do not rename historical references in old ADRs or commit reviews when they describe the old repository accurately. Add current-name notes where needed.
- Keep the
talk_runtime_runC symbol intalk-staticunless a separate ABI decision changes it.
- Direct VM tests pass under package
talk-vm. - Workspace tests pass.
talk bootstrap --checkreports the checked-in artifact is current.- A before/after encoded fixture is byte-identical.
- move
crates/talk-c/tocrates/talk-ffi/; - update workspace members and Cargo lockfile;
- rename the Rust crate and static archive;
- rename and document the public header;
- update root and nested Swift package manifests;
- update
crates/talk-swift/Sources/CTalkCtoCTalkFFI; - update the XCFramework build script;
- update CI and release workflow names, paths, release asset, URL stamping, and checksum variables;
- update current READMEs and current parity documentation.
- Preserve all existing
talk_*exported functions. - Preserve public TalkSwift source declarations and behavior.
- Do not change result layouts while renaming.
- Keep VM-backed execution.
- Add the ABI version and C smoke test, but do not otherwise redesign the C interface in this stage.
This stage is a mechanical rename with exactly two intentional additive
exceptions: the new talk_ffi_abi_version symbol and the C smoke client. The
symbol oracle therefore changes by design (+1 symbol), and both additions are
covered by their own acceptance checks so the rename itself remains
behavior-preserving and reviewable as such.
cargo test -p talk-ffi --lockedpasses.- The exported
talk_*symbol oracle matches, plus the new ABI-version symbol. - The host C smoke client compiles, links, runs, and frees all results.
swift testpasses againsttarget/debug/libtalk_ffi.a.- the XCFramework builds and TalkSwift tests pass against it;
- the iOS simulator build passes.
At this point the talk-c package name is free.
This is the highest-risk structural stage and should be split into small green
commits. src/backend/mir/mod.rs is roughly 10,000 lines with around 150
Symbol uses, so the split is larger than "move data definitions" suggests;
plan commits accordingly.
The Symbol cut must come first. Instruction data carrying
name_resolution::Symbol (PushHandler, FindHandler, ExistentialPack)
cannot move into talk-mir at all until those fields become MirSymbol,
because talk-mir cannot depend on the compiler. Perform the translation
while everything is still in one crate, where the change is a type substitution
with full compiler and test feedback, then move the cleaned definitions.
- Define
MirSymbolin the compiler and translate at MIR emission (bucket 1 in "Executable symbol identity"). Keep every otherSymboluse compiler-private. This commit touches the instruction enum, the builder emission points, the renderers, and all three targets' reads of those fields, and it is the last commit in which a mistake is cheap to find. - Create
talk-mir. - Move instruction, terminator, block, function, program, local, and layout data definitions into it.
- Split data definitions out of
src/backend/mir/layout.rs; keep type/catalog classification in the compiler. - Make compiler MIR construction, verification, optimization, register allocation, escape analysis, and renderers import the shared types.
- Keep private compiler-only helper state and algorithms in
talk. - Use temporary re-exports under
crate::backend::mirto keep changes mechanical while callers migrate.
Acceptance: all existing targets still consume the same values and workspace tests pass.
- Move layout/aggregate identity metadata onto
MirSymbolkeys (bucket 2 in "Executable symbol identity"). - Move display/type/member metadata production into the compiler's MIR output.
- Add exports, runtime aggregate identities, final local layouts, parameter/return representations, and frame sites to the public contract.
- Make the one finalized producer compute parameter summaries before register allocation and stamp frames after it.
- Add
MirEntry,MirOutput, andDriver<Typed>::compile_mir. - Add package-level MIR publication for selected binaries.
- Add interface tests proving a source fixture publishes every target-required fact.
Acceptance: C, bytecode, and LLVM can be driven from the public output without
reading TypedProgram or compiler-private MIR modules.
- Change LLVM to consume
talk-mir. - Remove
src/codegen.rs. - Remove
src/backend/codegen.rs. - Remove
talk::codegenandcodegen_binary. - Update
docs/llvm-backend-spike.mdto describe public MIR.
Acceptance:
rg "crate::codegen|talk::codegen|backend::codegen" src talk-llvmreturns no production matches. Repeat the repository-wide form after the C and bytecode crates exist.
- Create
talk-bytecodewith dependencies ontalk-mirandtalk-vm. - Move bytecode lowering and target fusion out of
src/backend. - Move
Executable, target execution conveniences, and bytecode-adapter statistics. - Convert runtime symbol mapping to consume MIR symbols and metadata.
- Change root CLI run/build/image/bytecode paths to compose
compile_mirandtalk_bytecode::compile. - Change bootstrap and procedural macro compilation to the same composition.
- Change REPL, testing, package execution, LSP, benches, wasm, and
talk-ffi. The LSP server is expected to need onlycheck_ownershipand diagnostics, which stay in the compiler; confirm against the Stage 0 caller inventory rather than assuming it. - Move focused lowering and fusion tests into
talk-bytecode. - Keep source-level differential and integration tests at the workspace level.
- Remove bytecode-specific imports and types from the compiler MIR module.
talk-bytecodehas no dependency ontalk.talk-vmhas no dependency ontalk-mir.- the self-hosted frontend artifact remains byte-identical;
- run, build, image, service export, procedural macro, REPL, package, wasm, and TalkSwift tests pass;
- VM allocation/object balance behavior is unchanged;
- bytecode disassembly snapshots are unchanged unless separately reviewed.
- Create
talk-native-runtime. - Move
src/backend/c_prelude.cinto it. - Change the still-internal C emitter to consume its source.
- Change LLVM to consume the same source directly.
- Remove
talk::codegen::native_runtime_cand any equivalent compiler-owned runtime accessor.
- C and LLVM generated programs pass differential tests.
- There is one tracked copy of the shared prelude.
- Neither backend depends on the other.
talkno longer embeds the native C runtime source.
- Create package
talk-cwith dependencies ontalk-mirandtalk-native-runtime. - Move
src/backend/c.rsand its focused tests. - Give it a target-local
Errorwith no parserSpandependency. - Expose
emit(&talk_mir::Module). - Add it as an optional root dependency enabled by
cli. - Change
talk candtalk build --nativeto callcompile_mirfollowed bytalk_c::emit. - Keep host compiler selection and filesystem/toolchain policy in
src/bin/talk.rs. - Update C differential tests and the corpus sweep to exercise the external crate interface.
talk-cproduction dependencies contain neithertalknortalk-vm.- C emission has no access to TypedProgram, source spans, the type catalog, or name resolution.
- GCC and Clang corpus sweeps pass.
- cross-target C compilation tests retain their current coverage.
talk build --nativeremains behaviorally compatible.
Although LLVM first moves to public MIR in Stage 3C, this stage removes any remaining transitional compatibility:
- simplify
talk-llvm::emitto accept the complete MIR module; - remove generic symbol plumbing made unnecessary by
MirSymbol; - source display and runtime identities only from MIR metadata;
- remove the runtime-prelude argument;
- update LLVM docs and focused emission tests;
- verify its command-line extension still supports packages, stdin, named entries, emission, and build.
Acceptance: LLVM's library emitter needs only MIR plus native runtime source, and all differential programs agree with the VM.
- Move the remaining target-independent
src/backendimplementation undersrc/compiling/miror another compiler-owned name chosen in the ADR:- MIR builder;
- ownership verifier;
- release planning;
- layout classifier;
- entries and glue generation;
- optimization;
- register allocation;
- escape/frame shaping.
- Delete target-specific files from the compiler tree.
- Update current architecture documentation, backend parity docs, profiling scripts, size reports, CI labels, and READMEs.
- Keep historical ADR and commit-review paths unchanged where they describe history; add a superseding current ADR rather than rewriting the record.
- Report final production, test, comment, and generated-source line counts by module.
The compiler tree contains no C emitter, native prelude, bytecode lowering, or VM executable wrapper.
| Module | Required focused tests |
|---|---|
talk-mir |
layout/data helpers, debug rendering, identity equality, target-required fixture shapes |
talk-vm |
encode/decode/validation, interpreter, memory, objects, IO, budgets, stats |
talk-bytecode |
lowering, pools, branches, unwind patching, layout mapping, fusion, malformed MIR errors |
talk-native-runtime |
source availability and native smoke compilation through consumers |
talk-c |
focused emitted-C structure and target errors |
talk-llvm |
focused LLVM emission and runtime bridge |
talk-ffi |
C ABI handles, errors, callbacks, panic containment, symbol oracle, C smoke client |
talk-swift |
existing Swift behavior without public source changes |
Use real producer output rather than hand-maintained duplicate fixtures for the important seams:
- source -> finalized public MIR;
- public MIR -> bytecode -> VM result;
- public MIR -> C -> native result;
- public MIR -> LLVM -> native result;
- source ->
talk-ffi-> VM result; talk-ffi-> TalkSwift value/result translation.
Internal MIR unit fixtures remain useful for optimizer and adapter edge cases, but black-box source fixtures remain the semantic oracle.
Run after every stage that can affect them:
cargo build --workspace --locked
cargo test --workspace --all-targets --locked
target/debug/talk bootstrap --check
./scripts/c-backend-sweep.sh
./scripts/c-backend-sweep.sh --cc clang
cargo test -p talk-llvm --locked
cargo test -p talk-ffi --locked
swift test -Xlinker -L -Xlinker "$PWD/target/debug"
./crates/talk-swift/scripts/build-xcframework.sh
swift package reset
swift test
xcodebuild -scheme TalkSwift -destination "generic/platform=iOS Simulator" build
git diff --checkUse the just-built binary for the bootstrap command, for example
target/debug/talk bootstrap --check, so PATH cannot select a stale compiler.
Add CI checks or repository assertions for:
talk-vm must not depend on talk or talk-mir
talk-bytecode must not depend on talk
talk-c must not depend on talk or talk-vm
talk-mir must not depend on talk, talk-vm, or a target adapter
talk-native-runtime must not depend on a backend
talk-ffi may depend on talk, talk-bytecode, and talk-vm
talk-llvm may depend on talk for its CLI, but its emitter consumes MIR
- Talk language behavior;
- bytecode wire format and version;
- checked-in frontend artifact bytes;
- all current
talk_*C function symbols and result contracts; - public TalkSwift declarations and behavior;
- CLI command spelling and behavior;
- native generated-program behavior;
- LLVM command behavior.
- Rust package/crate
talk-runtime/talk_runtime->talk-vm/talk_vm; - embedding package/crate
talk-c/talk_c->talk-ffi/talk_ffi; - embedding header and binary artifact names from C-specific to FFI-specific;
- internal Swift C module
CTalkC->CTalkFFI; - existing C backend package name becomes
talk-c.
- public
talk::codegen; - private-to-public codegen projection;
- compiler-owned C emitter;
- compiler-owned bytecode lowering and target fusion;
- compiler-driver ownership of VM executable types;
- compiler ownership of the native C prelude.
Moving lowering code can accidentally alter deterministic pool or function ordering.
Mitigation: require talk bootstrap --check after the MIR and bytecode stages;
do not regenerate the artifact during a supposedly mechanical extraction.
Moving current types mechanically could pull source symbols, spans, type
catalogs, or checker state into talk-mir.
Mitigation: define MirSymbol, target metadata, and the forbidden dependency
list before moving adapters. talk-mir cannot depend on talk.
The current public codegen model is too small for the C backend, and current frame shaping runs only on the C path.
Mitigation: make pre-allocation summaries and post-allocation frame stamping part of the one finalized MIR publication path before extracting C.
If talk-bytecode depends on talk, talk cannot use it for bootstrap,
procedural macros, REPL, or testing.
Mitigation: talk-bytecode depends only on talk-mir and talk-vm.
Package names, archive names, module maps, XCFramework names, release URLs, and checksums are spread across Cargo, two Swift manifests, scripts, CI, and release workflows.
Mitigation: perform the FFI rename as one behavior-preserving stage, compare exported symbols, and test both host-static and XCFramework resolution modes.
Blind replacement of talk-runtime and talk-c would make historical ADRs
and commit reviews inaccurate.
Mitigation: update current docs and add a superseding ADR; retain historical names where they describe the repository at that time.
External adapters no longer have compiler spans, which can tempt callers to wrap all adapter errors as source failures.
Mitigation: keep compiler, adapter, VM, and FFI error types distinct. The compiler locates source errors before publishing MIR; adapter invariant errors remain target errors.
Because the C backend and C FFI are being renamed together, it is easy to conflate generated C with the language-neutral embedding interface.
Mitigation: keep talk-c and talk-ffi independent. TalkSwift remains
VM-backed. A native host-callable ABI for compiled Talk modules requires a
separate decision.
The extraction is complete when all of the following hold:
talk-miris the only public target input model.src/codegen.rsandsrc/backend/codegen.rsdo not exist.- C, bytecode, and LLVM consume
talk_mir::Moduleonly. - No target adapter imports TypedProgram, source AST, parser spans, type catalogs, or name-resolution data.
talk-bytecodeowns MIR-to-bytecode lowering and target fusion.talk-vmowns the bytecode format, validation, and execution, with no MIR dependency.talk-cowns C emission and has no compiler dependency.talk-native-runtimeis the single owner of the shared C runtime source.talk-ffiowns every exported language-neutraltalk_*C function.- TalkSwift links
TalkFFI.xcframeworkand preserves its public interface. - The compiler source tree contains only target-neutral MIR work.
- Existing CLI commands remain available.
- The checked-in frontend artifact remains at a verified fixed point.
- Workspace, C differential, LLVM differential, C ABI, Swift host, XCFramework, and iOS simulator gates all pass.
- No new third-party dependency was added.
Keep each numbered item independently green:
- ADR, baseline measurements, and ABI symbol oracle.
- Mechanical
talk-runtime->talk-vmrename. - Mechanical
talk-cembedding ->talk-ffirename, including TalkSwift packaging. MirSymbolinstruction cut, thentalk-mirdata definitions with private compiler re-exports.- Complete finalized MIR metadata and
compile_mir. - Move LLVM to public MIR and delete
talk::codegenplus its projection. - Extract
talk-bytecodeand migrate all VM-backed callers. - Extract
talk-native-runtime. - Create the new
talk-cbackend and migrate C CLI paths. - Remove compatibility re-exports, consolidate compiler MIR files, update current documentation, and publish final size/dependency accounting.
Do not combine the VM rename, FFI packaging rename, public MIR conversion, and bytecode extraction in one change. Each has a different regression oracle, and keeping them separate makes failures attributable and rollback safe.