Files
AILang/docs/plans/0127-cma-revival.1.md
T
Brummel 426ce88d2a plan: cma-revival.1 cross-model harness corpus revival (refs #68)
Bite-sized plan for spec 0069. Six tasks: complete schema fields across the
13 examples + 4 references (own default, existing borrow preserved); rewrite
param_modes_all + spec.md section 4 (own Int / borrow boxed-ADT, value/boxed
rule); add loop_sum + new_rawbuf examples; spec_completeness variant coverage
+ Intrinsic allowlist; regenerate rendered + green render suite; migrate
mock_full_run fixture + green harness suite.

All verbatim .ail/.ail.json bodies parse-gate verified this session. The gate
caught a real language drift the spec had to absorb: borrow over an unboxed
value type (Int) is now a borrow-over-value error, so param_modes_all borrows
over a boxed ADT instead.
2026-06-02 16:32:31 +02:00

16 KiB
Raw Blame History

cma-revival.1 — Cross-model harness corpus revival — Implementation Plan

Parent spec: docs/specs/0069-cma-corpus-revival.md

For agentic workers: REQUIRED SUB-SKILL: use the implement skill to run this plan. Steps use - [ ] checkboxes for tracking.

Goal: Bring the schema-dead corpus of the cross-model authoring-form harness (experiments/2026-05-12-cross-model-authoring/) up to today's language and get both crates' cargo test suites green in mock mode — no live IONOS call.

Architecture: The corpus .ail.json files lost validity when the implicit-cutover made param_modes/ret_mode mandatory. Most files already carry their param_modes; the repair completes the missing fields in place (preserving deliberate borrow annotations), adds two new author-facing examples (loop/recur, new) plus two spec.md sections, removes the deleted ParamMode::Implicit from the completeness test while allowlisting the non-authorable Term::Intrinsic out, and migrates the mock fixture's embedded programs so the harness score assertions still hold.

Tech Stack: experiments/2026-05-12-cross-model-authoring/ (render + harness Cargo crates outside the root workspace, built via --manifest-path); ail check/ail parse from target/debug/ail as the per-file oracle.

Files this plan creates or modifies:

  • Modify: experiments/.../master/examples/*.ail.json (13) — complete schema fields
  • Modify: experiments/.../master/tasks/*.reference.ail.json (4) — complete schema fields
  • Modify: experiments/.../master/examples/param_modes_all.ail.json — rewrite (own/borrow only)
  • Create: experiments/.../master/examples/loop_sum.ail.json — Loop/Recur example
  • Create: experiments/.../master/examples/new_rawbuf.ail.json — New example
  • Modify: experiments/.../master/spec.md — §4 rewrite + two new sections
  • Modify: experiments/.../render/tests/spec_completeness.rs — variant coverage + Intrinsic allowlist
  • Modify: experiments/.../rendered/json.md, experiments/.../rendered/ail.md — regenerated
  • Modify: experiments/.../harness/tests/fixtures/mock_full_run.json — migrate embedded programs

Throughout, AIL = target/debug/ail (built, mtime 2026-06-02) and XP = experiments/2026-05-12-cross-model-authoring.


Task 1: Complete schema fields in the 13 examples + 4 reference solutions

Files:

  • Modify: $XP/master/examples/*.ail.json (all 13 except param_modes_all, which is Task 2)
  • Modify: $XP/master/tasks/*.reference.ail.json (all 4)

The transformation, applied to every "type": { "k": "fn", … } object in each file: preserve any existing param_modes verbatim, then ensure both mode fields are present:

  • param_modes: if absent, set [] for a 0-parameter fn, else one "own" per parameter (the natural consume default). If present, leave unchanged.
  • ret_mode: if absent, set "own" (borrow-return is forbidden by contract 0015, so own is the only legal value).

Field placement: param_modes directly after params, ret_mode directly after ret, matching the canonical key order in the shipped examples/ fixtures.

  • Step 1: Worked example A — fn_returns_int.ail.json (0-param)

The answer fn's type block changes from:

      "type": {
        "k": "fn",
        "params": [],
        "ret": { "k": "con", "name": "Int" },
        "effects": []
      },

to:

      "type": {
        "k": "fn",
        "params": [],
        "param_modes": [],
        "ret": { "k": "con", "name": "Int" },
        "ret_mode": "own",
        "effects": []
      },
  • Step 2: Worked example B — data_with_match.ail.json (existing borrow, add ret_mode)

Both fns already carry "param_modes": ["borrow"] over List. List is a boxed ADT, so borrow is legal here — leave it untouched; add "ret_mode": "own" directly after each ret. Do NOT rewrite borrow to own. (This is the only legal borrow in the corpus; the illegal (borrow Int) lived only in param_modes_all, handled in Task 2. borrow over an unboxed value type is a borrow-over-value error — ail check in Step 4 catches any such slip.)

  • Step 3: Apply the transformation to all remaining corpus files

Files needing only ret_mode (already have param_modes): data_simple.ail.json. Files needing param_modes + ret_mode: bool_str, fn_calls_prelude, fn_returns_int, fn_with_do_seq, fn_with_lambda, forall_polymorphic, match_literal_pattern, and the 4 tasks/*.reference.ail.json. Files with no fn defs (no change to a fn type, but still verify they load): class_def, floats, instance_def. For each value-taking fn, default each parameter to "own".

  • Step 4: Verify every file checks green

Run:

for f in $XP/master/examples/*.ail.json $XP/master/tasks/*.reference.ail.json; do
  target/debug/ail check "$f" >/dev/null 2>&1 && echo "OK   $f" || echo "FAIL $f"
done

Expected: every line OK (param_modes_all may FAIL here — it is rewritten in Task 2; re-run after Task 2). A FAIL on a value-taking fn means a wrong mode guess — flip that parameter ownborrow per the check error and re-run.


Task 2: Rewrite param_modes_all and spec.md §4 (Implicit is deleted)

Files:

  • Modify: $XP/master/examples/param_modes_all.ail.json
  • Modify: $XP/master/spec.md (§4 "Mode annotations", lines ~134-193)

Why not "two Int functions": the typechecker rejects (borrow Int) with borrow-over-value — unboxed value types (Int/Bool/Float/Unit) are always own; borrow is legal only over a boxed type (an ADT). So the example carries a small boxed ADT and borrows over that. The .ail below is verified to parse + check green (parse-gate, 2026-06-02).

  • Step 1: Author the rewritten example as .ail and canonicalise

Write /tmp/param_modes_all.ail (module name MUST be param_modes_all — the checker enforces module-name = filename):

(module param_modes_all
  (data List
    (ctor Nil)
    (ctor Cons (con Int) (con List)))
  (fn f_own_value
    (doc "(own Int) — unboxed value types (Int/Bool/Float/Unit) are always own; borrow over them is a borrow-over-value error.")
    (type (fn-type (params (own (con Int))) (ret (own (con Int)))))
    (params x)
    (body x))
  (fn f_borrow_boxed
    (doc "(borrow List) — boxed types may be borrowed; the callee reads without consuming.")
    (type (fn-type (params (borrow (con List))) (ret (own (con Int)))))
    (params xs)
    (body
      (match xs
        (case (pat-ctor Nil) 0)
        (case (pat-ctor Cons h t) (app + 1 (app f_borrow_boxed t)))))))

Run: target/debug/ail parse /tmp/param_modes_all.ail > $XP/master/examples/param_modes_all.ail.json

  • Step 2: Verify it checks green

Run: target/debug/ail check $XP/master/examples/param_modes_all.ail.json Expected: exit 0. (Canonical output carries f_own_value param_modes ["own"], f_borrow_boxed param_modes ["borrow"], both ret_mode "own".)

  • Step 3: Rewrite spec.md §4 prose

In $XP/master/spec.md §4 "Mode annotations", remove every mention of a defaulted/omitted/implicit mode. Present the exhaustive rule: every Type::Fn parameter carries exactly one of own (callee consumes) or borrow (callee may not consume); param_modes is a list with one entry per parameter; ret_mode is always own. State the value/boxed rule: own is mandatory for unboxed value types (Int/Bool/Float/Unit) — borrow over them is a borrow-over-value error; borrow is available only for boxed types (ADTs), as the f_borrow_boxed/List example shows. Leave the {example: param_modes_all} marker in place.


Task 3: Add the two new author-facing examples + spec.md sections

Files:

  • Create: $XP/master/examples/loop_sum.ail.json
  • Create: $XP/master/examples/new_rawbuf.ail.json
  • Modify: $XP/master/spec.md (two new sections)

The module name in each .ail MUST equal its target filename stem (loop_sum, new_rawbuf) — the checker enforces module-name = filename. Both .ail bodies below are verified parse + check green (parse-gate, 2026-06-02).

  • Step 1: Author loop_sum.ail and canonicalise

Write /tmp/loop_sum.ail:

(module loop_sum
  (fn sum_to
    (type (fn-type (params (own (con Int))) (ret (own (con Int)))))
    (params n)
    (body
      (loop (acc (con Int) 0) (i (con Int) 1)
        (if (app gt i n)
            acc
            (recur (app + acc i) (app + i 1)))))))

Run: target/debug/ail parse /tmp/loop_sum.ail > $XP/master/examples/loop_sum.ail.json Then: target/debug/ail check $XP/master/examples/loop_sum.ail.json Expected: parse exit 0, check exit 0.

  • Step 2: Author new_rawbuf.ail and canonicalise

Write /tmp/new_rawbuf.ail:

(module new_rawbuf
  (fn main
    (type (fn-type (params) (ret (own (con Unit))) (effects IO)))
    (params)
    (body
      (let b (new RawBuf (con Int) 3)
        (app print (app RawBuf.size b))))))

Run: target/debug/ail parse /tmp/new_rawbuf.ail > $XP/master/examples/new_rawbuf.ail.json Then: target/debug/ail check $XP/master/examples/new_rawbuf.ail.json Expected: parse exit 0, check exit 0.

  • Step 3: Add two spec.md sections with example markers

In $XP/master/spec.md, after the existing function/ADT material and before §"The prelude", add two sections:

  • § Iteration (loop / recur) — prose: a loop declares one or more typed binders with initial values; the body re-enters the loop via recur with new positional values for those binders, or falls through with a final value; recur is valid only in tail position. Add a {form-only: json} block, a {form-only: ail} block, and the marker {example: loop_sum}.
  • § Kernel-extension types (new) — prose: (new T arg+) constructs a value of an extension type T by calling T's new definition; args are types or values per the def's signature. Add a {form-only: json} block, a {form-only: ail} block, and the marker {example: new_rawbuf}.

Keep {form-only: …} token volume roughly balanced across the two cohorts (the token_balance test allows ±5%).


Task 4: Update spec_completeness.rs — variant coverage + Intrinsic allowlist

Files:

  • Modify: $XP/render/tests/spec_completeness.rs

  • Step 1: Update the VariantTag enum (:22-64)

After TermReuseAs, insert:

    TermLoop,
    TermRecur,
    TermNew,

Remove the line ParamModeImplicit, (in the // ParamMode group).

  • Step 2: Update EXPECTED_VARIANTS (:70-105)

After VariantTag::TermReuseAs, insert:

    VariantTag::TermLoop,
    VariantTag::TermRecur,
    VariantTag::TermNew,

Remove the line VariantTag::ParamModeImplicit,. Do NOT add a tag for Intrinsic — it is allowlisted out (Step 4).

  • Step 3: Remove the Implicit arm in visit_param_mode (:298-310)

Delete:

        ParamMode::Implicit => {
            observed.insert(VariantTag::ParamModeImplicit);
        }

The remaining Own/Borrow arms make the match exhaustive against the current two-variant ParamMode.

  • Step 4: Add the new visit_term arms (after the ReuseAs arm, :220-224)

Immediately before the closing } of the match t in visit_term, add:

        Term::Loop { binders, body } => {
            observed.insert(VariantTag::TermLoop);
            for b in binders {
                visit_type(&b.ty, observed);
                visit_term(&b.init, observed);
            }
            visit_term(body, observed);
        }
        Term::Recur { args } => {
            observed.insert(VariantTag::TermRecur);
            for a in args {
                visit_term(a, observed);
            }
        }
        Term::New { type_name: _, args } => {
            observed.insert(VariantTag::TermNew);
            for a in args {
                match a {
                    NewArg::Type(t) => visit_type(t, observed),
                    NewArg::Value(v) => visit_term(v, observed),
                }
            }
        }
        // `Intrinsic` is the compiler-supplied body marker — non-authorable
        // (absent from the parser's term dispatch; typecheck enforces
        // kernel-tier-only via IntrinsicOutsideKernelTier). A foreign-author
        // mini-spec must not show it and no master example can legally
        // contain it, so it is allowlisted OUT of completeness: no VariantTag,
        // no EXPECTED entry, a no-op arm here so the match compiles without
        // claiming coverage.
        Term::Intrinsic => {}

Add NewArg to the ailang_core::ast::{…} import line at the top of the file if it is not already imported (the build will name it if missing).

  • Step 5: Compile-gate the test crate

Run: cargo test --manifest-path $XP/render/Cargo.toml --no-run Expected: compiles with 0 errors (the Implicit unknown-variant error and the non-exhaustive visit_term match are both resolved). If the build names a wrong field (e.g. LoopBinder.init), correct it per the compiler message.


Task 5: Regenerate rendered/ and green the render suite

Files:

  • Modify: $XP/rendered/json.md, $XP/rendered/ail.md

  • Step 1: Run the renderer

Run:

cargo run --manifest-path $XP/render/Cargo.toml -- \
  --master $XP/master --rendered $XP/rendered

Expected: exit 0, no load crash (it crashed before on the first example's missing param_modes).

  • Step 2: Run the full render test suite

Run: cargo test --manifest-path $XP/render/Cargo.toml Expected: PASS — example_roundtrip (every example loads, prints to AIL, reparses to the same canonical bytes), spec_completeness (every EXPECTED_VARIANTS tag observed, including TermLoop/TermRecur/TermNew), token_balance (±5%), splitter_unit. If spec_completeness reports a tag not exercised, the corresponding new example or spec section is missing its construct.


Task 6: Migrate mock_full_run.json and green the harness suite

Files:

  • Modify: $XP/harness/tests/fixtures/mock_full_run.json

The fixture maps cohort → task → turn → {content, usage}, where content is a string-embedded .ail.json program (simulated model output). All are legacy-schema and now fail ail check. Migrate the embedded programs to the current schema, preserving the usage fields byte-for-byte (budget_abort depends on them).

  • Step 1: Migrate the programs that must reach green

The hard assertions in mock_full_run.rs are json,t3_main_prints,false,2 and ail,t1_add_three,false,INF. So:

  • json.t3_main_prints.2.content (turn 2) MUST become a valid current-schema program that passes ail check + ail build + run — give its fn types the param_modes/ret_mode fields per Task 1's rule. (Leave json.t3_main_prints.1.content as the intentional "defs":[] broken first attempt.)

  • Migrate the other green-intended embedded programs (the first-attempt-green cohort×task cells) the same way, so the fixture is an honest simulation rather than a uniform schema-fail.

  • ail.t1_add_three.*.content stays failing to the turn limit (its INF row); let its miss be content-driven, not merely a schema artefact.

  • Step 2: Verify each migrated embedded program checks green standalone

For each content you migrated to be green, extract it to a temp file and run target/debug/ail check /tmp/x.ail.json — expected exit 0. (The harness will also ail build it; a green check that fails build means the program is schema-valid but not runnable — fix the body.)

  • Step 3: Run the full harness test suite

Run: cargo test --manifest-path $XP/harness/Cargo.toml Expected: PASS — mock_full_run (8 rows; json,t3_main_prints,false,2 and ail,t1_add_three,false,INF both present; RUN_STATUS=ok), budget_abort (RUN_STATUS=budget_exceeded, ≥1 budget_abort row), verify_references (every master/tasks/*.reference.ail.json drives green through real ail+clang), strip_locations (5 .stderr fixtures, including missing field \type``).