Update, July 2026 — the posts above are from December, when the dialect was in
the semi-early stages of being developed seriously. It’s now been through five
releases of hardening, RepliBuild is landing in General as v3.0.0, and a couple of
things I wrote back then deserve corrections now that they’ve been battle-tested.
Since this thread apparently gets read (and quoted) as reference material for
“MLIR for FFI”, here’s the honest current state.
Correction first: “verified at IR level”
I wrote that JLCS gives you “type-safe operations verified at IR level.” As of
today no JLCS op has a hasVerifier. Two ops segfault during lowering if you
hand them malformed IR — jlcs.scope with mismatched managed_ptrs/destructors
arity, and jlcs.marshal_arg with mismatched memberTypes/juliaOffsets — instead
of diagnosing. They’re tracked as two @test_broken entries in the test suite.
This isn’t reachable from the production DWARF→codegen path (the producers
co-generate the arrays, so they can’t disagree), but if you’re hand-writing JLCS
IR: there are no guardrails yet. Verifiers are near the top of the roadmap
precisely because this thread’s description got ahead of the implementation.
What actually changed since December
jlcs.vcall emits all the way to LLVM IR now. This one’s a useful war story
for anyone hand-building llvm.call in a lowering. The op lowered cleanly but
SIGSEGV’d inside translateModuleToLLVMIR at emit time. Root cause: my
VirtualCallOpLowering built the indirect call via a raw OperationState and set
operandSegmentSizes = {1, nArgs, 0} — three entries. But llvm.call carries
AttrSizedOperandSegments with two operand groups (callee_operands,
op_bundle_operands); for an indirect call the callee pointer is the first
element of callee_operands, so the correct value is {1 + nArgs, 0}. The
translator split a 3-entry array against a 2-segment op and walked off the end.
Fix: use the dedicated indirect-call builder CallOp(LLVMFunctionType, ValueRange) and let it set operandSegmentSizes and var_callee_type itself.
The op definition from my earlier post is unchanged — the lowering was the bug.
Honest op liveness map, because “the dialect has 12 ops” says nothing about
what’s real:
- Emitted by the production pipeline:
jlcs.type_info, jlcs.ffe_call,
jlcs.try_call (C++ exceptions → landing pad → Julia CxxException),
jlcs.marshal_arg/marshal_ret, and the !jlcs.c_struct type.
- Exercised only by hand-written test IR:
jlcs.vcall, ctor_call/dtor_call,
scope/yield (region RAII — lowers correctly, reverse-order destruction
confirmed, but no DWARF-driven producer emits it yet).
- Functional but producer-less:
jlcs.load/store_array_element +
!jlcs.array_view. They parse and lower cleanly; nothing generates them.
Toolchain: the dialect rebuilds clean against LLVM/MLIR 22.1.6 with zero
TableGen or C++ source changes from 22.1.5 — patch bumps within a minor have been
non-events. The dual-LLVM setup (Julia’s internal libLLVM for the C path, system
MLIR for the dialect) is deliberate and documented.
Outside the dialect but relevant to the FFI story: the C generator now runs an
exact-layout proof before emitting any struct — every member typed with known
size/alignment, then the emitter proves Julia’s layout reproduces each DWARF
offset and the total byte size. Proof passes → named fields; any doubt → opaque
byte blob; ABI crossings that can’t be made safe refuse loudly at the call site
instead of corrupting. “Exact or opaque, never approximate” turned out to be the
load-bearing design rule of the whole project.
What’s being worked on
- Op verifiers —
scope and marshal_arg arity checks first (see above).
- Multiple-inheritance
this-adjustment — the vcall emit fix unblocked
observing the secondary-base case: the vtable is read from the right offset but
this still passes unadjusted. Remaining work is a this_offset on vcall
plus a multi-base offset table on type_info.
- Per-function bitcode slicing —
Base.llvmcall embeds the whole linked
module per call site, which works at toy scale and falls over at whole-library
scale (and duplicates file-local statics, so mixed-tier dispatch can diverge on
internal state). Slicing is the fix; until then production configs pin ccall.
- Producers for scope-RAII and the strided-array ops — the dialect side works,
the DWARF-driven generation doesn’t exist yet.
Docs (readable for curiosity, examples need specific toolchain versions):
GitHub - obsidianjulua/RepliBuild.jl · GitHub — full changelog
including the v3.0.0 “breaking changes since v2.5.7” section is in the repo.
RepliBuild Hub (prebuilt configs for lua/sqlite/cjson/box2d/etc.):
GitHub - obsidianjulua/RepliBuild-Hub: Stores the toml configurations for the RepliBuild.jl FFE generator · GitHub