832375f2ac
All 176 files in the four accumulating directories now use a zero-padded 4-digit counter prefix that reflects creation order (`NNNN-slug.md`). The counter is assigned per directory in strict git-log creation order; ties broken alphabetically by original name. The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is dropped — the date is recoverable from git log and the counter carries the ordering. A file's counter is stable for the life of the file: never reassigned, never reused, never compacted. Deleted files retire their counter; subsequent files do not fill the gap. This is the property that lets cross-references stay literal — refs use the full filename including the counter (`design/contracts/0007-honesty-rule.md`) so they grep cleanly and resolve directly without a glob step. 313 cross-references updated across .md/.rs/.toml/.c/.json files (test pins, include_str! paths, design-INDEX entries, baseline notes, runtime C comments, inter-contract markdown links incl. bare basename and `../models/foo.md` forms). CLAUDE.md gets a new "File-naming convention" section spelling out the rule and rationale. skills/brainstorm/SKILL.md and skills/planner/SKILL.md updated so new spec/plan creation produces counter-prefixed names from the start. The full test suite (cargo test --workspace) passes.
13 KiB
13 KiB
Fieldtest — embedding-abi-m1 — 2026-05-18
Status: Draft — awaiting orchestrator triage Author: ailang-fieldtester (dispatched by skills/fieldtest)
Scope
Embedding ABI M1 makes a compiled AILang scalar fn callable
in-process from a C/Rust host. It adds the Form-A (export "<sym>")
fn modifier, an additive FnDef.export: Option<String> schema field,
a new ail build --emit=staticlib mode that produces a relocatable
lib<entry>.a (program objects only) plus a separate
libailang_rt.a, a typecheck-time export gate (Int/Float params
& return, empty effect set — codes export-non-scalar-signature,
export-has-effects), and a DESIGN.md §"Embedding ABI (M1)" that
labels the M1 C signature provisional until M3. No
ADT/record/list/Str/effect crosses the boundary in M1.
Examples
examples/fieldtest/embabi1_1_ema_step.ail — fixed-point EMA accumulator (Int positive axis)
- What it does:
ema_step(state, sample) = state + (sample - state)/4, a fixed-point exponential moving average; the(State, Sample) -> Stateaccumulator a host drives one sample per call. - Why it fits: this is exactly the positive authoring axis — a pure numeric fold made callable from a C host loop, the headline M1 use case.
- Outcome: authored from DESIGN.md §"Embedding ABI (M1)" +
crates/ailang-core/specs/form_a.md+ the publicembed_backtest_step.ailform.ail check→okfirst try.ail build --emit=staticlib -o /tmp/ft1→ producedlibembabi1_1_ema_step.a+libailang_rt.a. A 4-call C host (cc host.c lib*.a libailang_rt.a) linked and ran;s == 67assertion held; exit 0. Compiles, links, runs, matches.
examples/fieldtest/embabi1_2_leaky_integrator.ail — leaky integrator (Float scalar-coverage axis)
- What it does:
leaky_step(state, sample) = state*0.5 + sampleoverFloat; the Float counterpart of example 1. - Why it fits: the optional scalar-coverage axis — exercises the
other M1 scalar type (
Float→ Cdouble) through the same author → build → C-call path. - Outcome:
ail check→okfirst try.ail build --emit=staticlib→ two archives. C host withextern double leaky_step(double, double)linked and ran; exacts == 1.875assertion held; exit 0. Compiles, links, runs, matches. Author experience byte-for-byte identical to the Int axis.
examples/fieldtest/embabi1_3_export_list_rejected.ail — list-param export (rejection axis, non-scalar)
- What it does: an
IntList-foldingsum_chunkcarrying(export "sum_chunk")— the imperative-trained "pass the whole batch in" reflex. - Why it fits: the clause-3 rejection axis — a non-scalar
signature an author would naturally try and that M1 must reject at
ail check. - Outcome:
ail check→ exit 1, single diagnostic (verbatim below). Correctly rejected; diagnostic is self-correcting.
examples/fieldtest/embabi1_4_export_logged_counter_rejected.ail — effectful export (rejection axis, effects-only)
- What it does:
tick(n) = n + 1with a(do io/print_str "tick")trace line; signature is fully scalar(Int)->Intbut the effect set is!IO. - Why it fits: isolates the
export-has-effectsclause from the non-scalar clause (the publicembed_export_effectful_rejected.ailmixesStrparam +IO; this one is scalar-clean so only the effect clause can fire). - Outcome:
ail check→ exit 1, singleexport-has-effectsdiagnostic. Correctly rejected; diagnostic is self-correcting.
Findings
[working] Positive scalar authoring (Int and Float) is first-try clean end-to-end
- Examples: 1, 2.
- What happened: With only DESIGN.md §"Embedding ABI (M1)",
form_a.md, the publicembed_*corpus, andail … --help, both the Int and the Float(State,Sample)->Statefolds were authored, checked, built asstaticlib, linked from a hand-written C host, and run with the asserted return holding — first try, no re-roll, no guessing on the AILang side. The(export "<sym>")modifier position (after(export …)in the(fn …)head, before(type …)) and the C scalar mapping (Int→int64_t,Float→double) were both unambiguous from DESIGN.md + form_a.md. The two-archive output (lib<entry>.a+libailang_rt.a) and the-o-is-a-directory rule were learned fromail build --help(public surface) and matched DESIGN.md's prose. The C symbol being author-chosen (ema_step/leaky_step, decoupled from theail_<module>_<fn>mangling) worked exactly as documented. - Why working: the new surface was reached for naturally, used, and correct on first try with no diagnostic needed. This is the capability the milestone exists to deliver, proven from the downstream-author position.
- Recommended action: carry-on.
[working] Both clause-3 rejection diagnostics are precise and self-correcting
- Examples: 3, 4.
- What happened, verbatim:
- Ex 3:
error: [export-non-scalar-signature] sum_chunk: export \sum_chunk`: M1 embedding ABI is scalar-only — parameter type `IntList` is not `Int`/`Float`` (exit 1). - Ex 4:
error: [export-has-effects] tick: export \tick` must be pure — declared effects !["IO"] are not allowed on an embedding boundary` (exit 1). Each names the code, the fn, the C symbol, the specific offending position (the parameter type; the effect set), the rule, and (for non-scalar) the allowed types. An author with no compiler access can self-correct from the message alone: "make the param scalar" / "remove the effect / drop the trace line".
- Ex 3:
- Why working: clause-3 ("wrong code fails to typecheck, with a message you can act on") is the load-bearing acceptance criterion for this strategic milestone, and it holds empirically with high-quality diagnostics.
- Recommended action: carry-on.
[working] (export) orthogonality + zero-export staticlib guard behave exactly as spec'd
- Examples: out-of-fixture probes (recorded for completeness; no fixture kept — these are spec-conformance confirmations).
- What happened: (a) a module with
(export "…")on a fn plus a realmain, built/run in default exe mode, checkedokand ran with no diagnostic about the inert(export)— matches the spec's "accepted and ignored" clause. (b)ail build --emit=staticlibon a module with zero(export)fns →Error: staticlib target needs at least one \(export "")` fn` (exit 1) — matches the spec's zero-export guard wording. - Why working: confirms the field's orthogonality and error-path promises hold from the CLI surface.
- Recommended action: carry-on.
[friction] The published authoring rule steers an LLM away from the only spelling that works for scalar export params
- Examples: 1, 2 (succeeded because I followed the public
embed_backtest_step.ailfixture, not the prose rule); probes with(own (con Int))/(borrow (con Int))(failed). - What happened:
crates/ailang-core/specs/form_a.md"Schema invariants" item 1 states: "Every type in the(params …)clause of a(fn …)definition's(fn-type …)MUST be wrapped in(own T)or(borrow T). … Implicit mode on a(fn …)def is rejected." The Pitfalls section repeats: "Wrap every(fn …)parameter in(own …)or(borrow …)." An LLM author reading form_a.md before generating (its stated purpose) would write the M1 export as(params (own (con Int)) (own (con Int))). That yields, verbatim:error: [use-after-consume] step: \sample` is used after it was already consumed(the scalar is read twice insample*sample). Switching to(borrow (con Int))instead yields threeerror: [consume-while-borrowed] step: `…` is consumed while a borrow of it is still live. The *only* spelling that checks is bare(con Int)— precisely the spelling form_a.md item 1 and the Pitfalls bullet tell the author is "rejected" / must be wrapped. The headline M1 deliverable is "an LLM, asked to make a scalar fold callable, writes the export"; the published authoring spec the LLM consults first actively misdirects that exact task, and the resultinguse-after-consume/consume-while-borrowed` diagnostics point at the body, not at the real cause (scalars must not carry a mode). - Why friction: it compiles and runs once the author copies the public fixture's bare-scalar form, but the documented authoring rule forces a wrong first attempt and an unrelated-looking linearity diagnostic — exactly the re-roll cost form_a.md claims to remove. M1 did not introduce scalar-mode behaviour but made it acute: this is now the surface of the milestone's headline task.
- Recommended action: plan (tidy iteration) — reconcile
form_a.mditem 1 + the Pitfalls bullet with actual behaviour by stating the scalar-type carve-out for parameters (form_a.md already carves the return type for non-heap-shaped types; the symmetric parameter carve-out is missing). DESIGN.md §"Embedding ABI (M1)" should also show the canonical export param spelling so the M1 author has an authoritative bare-scalar example outside the example corpus.
[spec_gap] form_a.md mandates a mode on every fn param; scalar params accept (and require) no mode — the doc is unqualified
- Examples: non-exported probe
(fn plain (params (con Int) (con Int)) …)→ail checkokexit 0. - What happened: the friction above has a doc-level root that is
not export-specific:
form_a.md"Schema invariants" item 1 is written as an unconditional universal ("MUST be wrapped … Implicit mode on a(fn …)def is rejected"). Behaviour: a plain non-exported fn with bare(con Int)params checksok. So the published invariant overstates the rule for scalar parameter positions, with no stated exemption (the return-type sentence carves outInt/Bool/Unit/Strfor returns only). An LLM author cannot derive the true rule ("scalars take no mode; heap-shaped params takeown/borrow") from the public docs — it has to be reverse-engineered from the example corpus, which the Iron Law explicitly forbids treating as a behaviour oracle. - Why spec_gap: the canonical authoring spec does not constrain the case correctly; two readings (literal "every param needs a mode" vs. corpus-implied "scalars are bare") are both supportable from the public text, and the compiler picks the second.
- Recommended action: tighten
form_a.md(and mirror the rule in DESIGN.md if DESIGN.md is silent on scalar-param modes) — state the scalar-parameter carve-out symmetrically with the existing return-type carve-out. This is the doc fix that subsumes the friction finding's first half.
[spec_gap] No public path emits the staticlib IR, though DESIGN.md Decision 5 makes IR readability a first-class LLM affordance
- Examples:
ail emit-ir examples/fieldtest/embabi1_1_ema_step.ail→Error: entry module \embabi1_1_ema_step` has no `main` def` (exit 1). - What happened: DESIGN.md (Decision 5 / "the compiler emits
LLVM IR as text so the LLM can read what it generated") establishes
reading generated IR as an intended author affordance, and the M1
spec/DESIGN.md describe the precise staticlib IR shape (
@<sym>forwarder beside the unchanged@ail_<module>_<fn>, no@main). Butail emit-ir --helpshows only-o— there is no--emit=staticlibflag onemit-ir(onlyail buildhas it). Runningemit-iron amain-free kernel hits the executable-pathMissingEntryMainrejection. An author who wants to verify the generated forwarder for an exported kernel — the exact thing Decision 5 says they should be able to do — has no documented way to obtain that.ll. Whetherail buildleaves an intermediate.llis not derivable from any public source and was not inferred from compiler internals (Iron Law); the absence of the affordance via the documentedemit-irsurface is the finding. - Why spec_gap: DESIGN.md asserts an affordance the M1 surface
does not provide for the artefact M1 introduces; neither DESIGN.md
nor
--helpresolves how an author inspects a kernel's IR. - Recommended action: ratify the intended behaviour — either add
--emit=staticlibtoail emit-ir(symmetry withail build) or add one sentence to DESIGN.md §"Embedding ABI (M1)" stating how a kernel's IR is obtained (e.g. a documentedail buildartefact). Orchestrator triage:ratifyvs. tighten DESIGN.md.
Recommendation summary
| Finding | Class | Action |
|---|---|---|
| Positive Int+Float authoring first-try clean E2E | working | carry-on |
| Both clause-3 rejection diagnostics precise & self-correcting | working | carry-on |
(export) orthogonality + zero-export guard match spec |
working | carry-on |
| form_a.md mode rule misdirects the headline export task | friction | plan (tidy iteration) |
| form_a.md "every param needs a mode" unqualified vs. scalar behaviour | spec_gap | tighten form_a.md / DESIGN.md |
| No public path emits the staticlib kernel IR vs. Decision 5 | spec_gap | ratify or tighten DESIGN.md |