Files
AILang/design/contracts/embedding-abi.md
T
Brummel 8ad91e7f24 iter design-ledger-formal-links.1 (DONE 5/5): clause-5 hard gate + 7 prose-ref conversions + 2 disposition-(b) homeless removals + honesty-rule positive-half (whole milestone in one iter)
Positive-half completion of the DESIGN.md -> design/ split: design/
body cross-references are now formal, file-relative Markdown links
into the durable tier (design/ or source), and a new in-tree hard
gate (design_index_pin.rs clause-5,
design_body_links_are_durable_and_resolve) walks every
design/contracts/*.md + design/models/*.md, strips fenced code
(strip_fences toggles on ```/~~~ lines so a ](  inside a fence is not
treated as a link), extracts every ](path), and asserts the target
resolves file-relative to a real file under design/-or-crates/-or-
runtime/; never docs/, never an in-file #anchor.

RED-first via identity-stubbed strip_fences (four embedded synthetic
vectors -- first one FAILS); replacing the stub with the real
toggle-on-fence impl turns the test GREEN. clause-5 composes with
clause-3 into the complete invariant the milestone establishes:
every contract cross-reference is EITHER a resolving durable
file-link OR clause-3-forbidden decision-record prose.

Conversions (recon-and-corpus-verified closed set):
  Task 2 (7 prose refs, 8 link tokens):
    float-semantics.md:69    Prelude classes -> [..](typeclasses.md)
    float-semantics.md:100   bare-path -> [Str ABI](str-abi.md)
    embedding-abi.md:45      "Frozen value layout" -> [..](frozen-value-layout.md)  (drop stale "below")
    memory-model.md:44       Data model -> [..](data-model.md)
    memory-model.md:105-106  Method dispatch -> [..](typeclasses.md)  (drop stale "below"; the target heading lives in typeclasses.md:227, not in this file)
    scope-boundaries.md:48   Str ABI -> [..](str-abi.md)
    scope-boundaries.md:88   mixed split: ailang-core::desugar -> source link + Pipeline -> ../models/pipeline.md (drop stale "above")
  Task 3 (2 disposition-(b) homeless removals):
    pipeline.md:60-61            (see docs/PROSE_ROUNDTRIP.md) pointer removed, CLI prose preserved
    authoring-surface.md:178-181 cross-tier pointer clause removed, ail merge-prose sentence preserved
  Task 4: honesty-rule.md positive-half paragraph inserted between L14 and the existing L15-blank-L16; both docs_honesty_pin.rs-pinned phrases byte-identical at L14/L19 (now shifted to L19 -> L25 by the +6 lines).

Out of scope, preserved (asserted independently): every intra-file
"above/below"; embedding-abi.md:51 "frozen value layout below
specifies" (no quoted title, no (see) form); data-model.md
38/66/79/206/226 (in-fence ```jsonc schema annotations -- the inline
analog of the nominal-mention carve-out). INDEX.md and the
decision-records journal byte-unchanged; clauses 1-4 of
design_index_pin.rs source byte-unchanged (the only `-` lines in
the diff are the two-line //! header rewrite Task 1 Step 5 itself
delivers).

Boss-verified independently (not on agent report alone):
  cargo test --workspace               647 passed / 0 failed
                                       (+1 vs pre-milestone 646:
                                        the new clause-5)
  cargo test --test design_index_pin   5 / 5 passed
  cargo test --test docs_honesty_pin   5 / 5 passed (additive
                                       paragraph is pin-safe)
  grep ](.../docs/.../) under design/  zero
  grep ](#)        under design/  zero
  ](-link count under design/          8 (closed convert-set)
  git diff --quiet design/INDEX.md     ok
  git diff --quiet decision-records    ok
  embedding-abi.md:48 pinned phrase    byte-identical

One Concerns item: Task-5 Step-7's plan-predicted "`-` line count = 1"
was actually 2 because Task 1 Step 5 rewrote the //! header 5 -> 8
lines (removing the original L4 + L5, not just L5). Planner self-
review-item-8 miss on my part -- a verification-arithmetic error in
the plan, NOT an implementation defect. The substantive assertion
(clauses 1-4 source byte-unchanged) is fully satisfied; the
implementer correctly flagged it and proceeded. The plan stands as
written; the assertion's `1` should have been `2`. Lesson noted for
future header-rewrite tasks.

Spec: docs/specs/2026-05-19-design-ledger-formal-links.md
(grounding-check PASS x3 across two corpus-grounded amendments --
clause-6 + cross-ref definition; clause-5 fence-skip + closed
convert-set enumeration).

Next: mandatory milestone-close audit (no fieldtest -- zero
authoring-surface change, reasoned exclusion).
2026-05-19 23:31:30 +02:00

3.3 KiB

Embedding ABI

Embedding ABI

ail build --emit=staticlib compiles a module to a relocatable lib<entry>.a (program objects only) plus a separate libailang_rt.a (the RC runtime: rc.c + str.c), with no @main trampoline and no MissingEntryMain requirement — a kernel module is a library.

Each fn carrying (export "<sym>") (schema: FnDef.export) is emitted as an externally-visible C entrypoint @<sym> forwarding to the internal @ail_<module>_<fn>. The symbol is author-chosen and decoupled from the ail_<module>_<def> mangling so a module/fn rename does not move the C symbol.

The set that can cross the boundary is exactly: Int (lowered i64), Float (lowered double), or a single-constructor record whose every field is one of those (crossing as a bare ptr to the box layout below); the fn's effect set must be empty. There is no general value-marshalling layer — the host hand-constructs and hand-reads that box layout directly, so the enumerated set is the whole contract, narrow by construction, not a subset of a wider embedding ABI. Because the box layout is the host's contract surface with no accessor indirection, it is a one-way commitment frozen as of M3: a compiler change MUST NOT move the box offsets below for an exported type, nor invert the host-free rule. These are enforced at ail check (export-non-scalar-signature, export-has-effects) — an effectful or non-scalar export fails to typecheck. Every exported entrypoint takes a mandatory leading ailang_ctx_t* (M2): a per-thread embedding context created by ailang_ctx_new() and released by ailang_ctx_free(), owned by the calling thread for its lifetime. The host links one ailang_ctx_t per OS worker thread and the runtime accounts RC alloc/free into it: the per-allocation hot path (the per-ctx counters and the per-object refcount header) is non-atomic by design and never shared — a box never crosses a thread (Ctx: !Send) and each ailang_ctx_t is single-thread-per-ctx. The one datum a multi-threaded host shares is the global RC-stats fallback counter (used when no ctx is bound); it is atomic-relaxed so the swarm's leak accounting is exact. The swarm artefact is data-race-free, sanitiser-verified. The staticlib swarm artefact is RC-only: ail build --emit=staticlib rejects --alloc=gc/--alloc=bump (the shared Boehm collector is not swarm-safe). The value/record layout is frozen as of M3 (see Frozen value layout); the ctx-threaded C signature is the M2 shape.

Export parameters are written bare: a scalar type carries no own/borrow mode (a single-constructor record export parameter, by contrast, carries own/borrow — the ownership contract the frozen value layout below specifies). The canonical M1 export shape:

(fn step
  (export "backtest_step")
  (type
    (fn-type
      (params (con Int) (con Int))
      (ret (con Int))))
  (params state sample)
  (body
    (app + state (app * sample sample))))

ail emit-ir <module> --emit=staticlib prints this kernel's LLVM IR (the external @<sym> forwarders, no @main) instead of the executable-path main-required rejection — the Decision-5 IR-readability affordance for a main-free kernel.

Ratified by: crates/ailang-codegen/tests/embed_record_layout_pin.rs.