# Fieldtest — #55 eliminate-Implicit-mode cutover — 2026-06-02

**Status:** Draft — awaiting orchestrator triage
**Author:** fieldtester (dispatched by fieldtest skill)

## Scope

The #55 cutover (commit `76b21c0`, spec 0062, plan 0121) deleted
`ParamMode::Implicit`. Every fn-type slot on every signature now carries
an explicit `(own T)` or `(borrow T)`; there is no default. The parser
rejects a bare fn-type slot; `borrow-over-value` rejects `(borrow V)` on
a value type (Int/Bool/Float/Unit); `borrow-return` rejects a borrowed
return at the signature; consuming a borrowed value is rejected
(`consume-while-borrowed`); and the `over-strict-mode` advisory lint
suggests relaxing an `(own T)` heap param that the body never consumes to
`(borrow T)`. This fieldtest exercises those decision points as a
downstream LLM author who has only the design ledger, the CLI, and the
examples corpus. Build exercised: `cargo run -q -p ail` against HEAD
(workspace built clean before the run).

## Examples

### examples/fieldtest/cut55_1_readonly_length.ail — read-only list helper (decision a)
- A `length` that walks a list and counts, never moving a heap field out.
- Fits scope: the canonical "should I reach for `borrow`?" decision. From
  the ledger ("borrow only to read/pass-through a lent heap value") I
  wrote `(borrow (con List))` without hesitation.
- Outcome: check ok (exit 0); run prints `3`. Correct first try.

### examples/fieldtest/cut55_1b_overstrict_own.ail — over-strict own, recursive (decision a, wrong mode)
- `length` annotated `(own (con List))` while only reading.
- Fits scope: probes the `over-strict-mode` lint.
- Outcome: check ok, NO warning. Correct: the recursive call passes the
  tail `t` (heap-typed binder) to an own param, so condition 2 of the
  lint (no heap binder consumed) legitimately fails — the lint stays
  silent. Confirms the lint is not naive.

### examples/fieldtest/cut55_1c_overstrict_isempty.ail — over-strict own, true read-only (decision a, wrong mode)
- `is_empty` inspects only the head ctor; no binder consumed; `(own)`.
- Fits scope: the clean over-strict trigger.
- Outcome: check prints
  `warning: [over-strict-mode] is_empty: param \`xs\` is annotated \`(own ...)\` but the body does not consume it; \`(borrow ...)\` would suffice`
  then `ok`, exit 0. Names fn, param, current+suggested mode. Exemplary.

### examples/fieldtest/cut55_2_consume_map.ail — transform an owned list (decision b)
- `bump` rebuilds a list with each element +1; `print_all` consumes it.
- Fits scope: `own` territory; tests the consume analysis + drop soundness
  the cutover activated.
- Outcome: check ok; run prints `11 / 21 / 31`. No crash, no leak symptom,
  correct. (Authoring note: my first draft used `unit` for the Nil arm and
  got `[unbound-var]: unit`; the canonical Unit literal is `(lit-unit)`,
  recoverable from the corpus — not a cutover finding.)

### examples/fieldtest/cut55_2b_borrow_traversal_clean.ail — clean borrow-traverse-and-rebuild (decision b, false-positive guard)
- `bump` borrows xs and rebuilds a fresh +1 list; the recursive call's
  param is `(borrow ...)`, so the tail `t` is only borrowed, never moved
  into an `(own)` slot — nothing of the borrowed xs is consumed.
- Fits scope: the false-positive guard for the #58 fix — a borrow-position
  use of a heap sub-binder must stay accepted.
- Outcome: check ok (exit 0) — accepted, as required. (The fixture was
  originally mislabelled `cut55_2b_consume_borrow_reject` claiming a
  consume-while-borrowed reject; that analysis was wrong — the recursive
  param is `(borrow)`, not `(own)`, so nothing is consumed. Renamed and
  reclassified to the accepted set; see #55-cutover commit `b129af1`.)

### examples/fieldtest/cut55_2c_consume_borrow_reject.ail — consume a tail binder of a borrowed scrutinee (decision b, intended-wrong)
- `leak` borrows xs, passes the tail binder `t` to an `(own)` sink `sum_own`.
- Fits scope: the consume-while-borrowed boundary.
- Outcome: check errors
  `[consume-while-borrowed] leak: \`t\` is consumed while a borrow of it is still live`, exit 1. Rejected as expected (post-#58; see finding below).

### examples/fieldtest/cut55_2d_consume_borrow_whole.ail — consume the whole borrowed param (decision b, wrong mode)
- `passthrough` borrows xs and hands the whole xs to an `(own)` sink.
- Fits scope: the direct consume-while-borrowed case.
- Outcome: check errors
  `[consume-while-borrowed] passthrough: \`xs\` is consumed while a borrow of it is still live`, exit 1. Rejected as expected.

### examples/fieldtest/cut55_3_value_param.ail — value-typed params read repeatedly (decision c)
- `clamp` reads three Int params multiple times.
- Fits scope: confirms the author writes `own` for value types and the
  over-strict lint stays silent on them.
- Outcome: check ok (no warning); run prints `5 / 3`. Correct.

### examples/fieldtest/cut55_3b_borrow_over_value.ail — borrow on an Int (decision c, wrong mode)
- `double` annotated `(borrow (con Int))`.
- Fits scope: the `borrow-over-value` reject.
- Outcome: check errors
  `[borrow-over-value] double: borrow over value type in \`double\`: \`Int\` is an unboxed value type and cannot be borrowed; use \`(own Int)\``, exit 1. Names the fix. Exemplary.

### examples/fieldtest/cut55_4_borrow_return_reject.ail — borrowed return (decision d, wrong mode)
- `tail_view` declares `(ret (borrow (con List)))`.
- Fits scope: the borrow-may-not-escape rule at the signature.
- Outcome: check errors
  `[borrow-return-not-permitted] tail_view: borrow-return not permitted ... a (borrow …) return is refcount-invisible and can outlive its source; this language version forbids it ...`, exit 1. Clear wall; see friction below re: fix not named.

### examples/fieldtest/cut55_4b_clone_to_return.ail — projection via clone (decision d)
- `head_or_zero` borrows, returns the value-typed head (own Int);
  `tail_copy` returns `(clone t)` to lift a borrowed heap field to own.
- Fits scope: the correct escape hatch after borrow-return is rejected.
- Outcome: check ok; run prints `10`. The `clone` hatch works as the
  ledger describes.

### examples/fieldtest/cut55_5_bare_slot_reject.ail — bare param slot (the cutover's core promise)
- `length` with a bare `(con List)` param slot, no mode wrapper.
- Fits scope: "no default; Implicit is gone."
- Outcome: surface parse error
  `[surface-parse-error] ... fn-type slot requires a mode: write (own T) or (borrow T) at byte 642`, exit 1. Names both legal forms. Exemplary.

## Findings

### [working] borrow / own / value-mode authoring is naturally guessable from the ledger
- Examples: cut55_1, cut55_2, cut55_3, cut55_4b.
- For every example I knew which mode to write from the ledger alone, no
  guessing: read-only heap ⇒ `borrow`; transform/consume ⇒ `own`; value
  type ⇒ `own`; escape a projection ⇒ return `own` + `clone`. The
  decision rule in model/contract 0008 maps cleanly onto realistic tasks.
- This is the feature-acceptance (LLM-utility) evidence: an author
  produces correct-moded code on first try, and the modes carry real
  meaning (the same `length` is `borrow` while `bump` is `own`).
- Recommended action: carry-on.

### [working] the four reject diagnostics are precise and (mostly) name the fix
- Examples: cut55_3b (borrow-over-value), cut55_5 (bare slot),
  cut55_2d (consume-while-borrowed), cut55_1c (over-strict-mode).
- `borrow-over-value` and the bare-slot parse error both name the exact
  legal rewrite (`use (own Int)` / `write (own T) or (borrow T)`).
  `over-strict-mode` names param + suggested mode. All exit with the
  right code (errors exit 1, the warning exits 0). This is the surface
  doing its job for an LLM consumer.
- Recommended action: carry-on.

### [spec_gap — RESOLVED by #58] consuming a heap field-binder of a borrowed scrutinee
- Examples: cut55_2c vs cut55_2d.
- What happened at fieldtest time: `leak` borrows `xs` and moves the Cons
  tail `t` into an `(own)`-param sink — accepted (exit 0, the missed
  reject). `passthrough` borrows `xs` and moves the whole `xs` into the
  same sink — rejected (`consume-while-borrowed`, exit 1).
- Why spec_gap: the ledger stated only "consuming a borrowed value is
  rejected" and "a borrow may not escape into a return". It did not say
  whether a pattern-binder projected out of a borrowed scrutinee is
  itself borrowed. Two readings: (i) sub-binders inherit borrow, so moving
  `t` into an own sink is an illegal escape (then cut55_2c is a missed
  reject — a latent unsoundness, a double-free of the caller's
  allocation); (ii) only the named param is protected (then cut55_2c is
  correct and the ledger is silent).
- Resolution: reading (i) was adopted and shipped as the #58 fix
  (`b129af1`): `walk_arm` now seeds a heap-typed sub-binder of a borrowed
  scrutinee with `borrow_count = 1`, so cut55_2c now rejects
  (`consume-while-borrowed` on `t`, exit 1). The false-positive boundary
  is pinned by cut55_2b (a borrow-position use of such a sub-binder stays
  accepted). Value-typed sub-binders (the `Int` head) stay freely
  consumable.

### [friction] borrow-return error explains *why* but does not name the fix
- Example: cut55_4.
- The `borrow-return-not-permitted` message gives a thorough rationale
  (refcount-invisible, can outlive its source) but stops there. An author
  is told what is forbidden, not what to do instead. The natural remedy —
  return `(own ...)` and `(clone ...)` the projected field (which I had to
  derive myself in cut55_4b) — is exactly the escape hatch the language
  provides and would be cheap to name. Sibling diagnostics
  (`borrow-over-value`, the bare-slot error) DO name the fix, so this one
  reads as inconsistent.
- Recommended action: plan a one-line diagnostic-text addendum suggesting
  "return `(own T)` and `(clone …)` the value you want to escape."

### [friction] consume-while-borrowed wording implies a separate live borrow alias that isn't present
- Example: cut55_2d.
- Message: "`xs` is consumed while a borrow of it is still live." In this
  case `xs` IS the borrowed param being consumed; there is no separate
  live borrow alias. The wording (lifted from the let-alias class, spec
  0064 class 2) reads oddly for the whole-param case and, like the
  borrow-return error, does not name the fix (annotate `own`, or `clone`).
- Recommended action: plan — branch the wording for the
  "param-itself-consumed" case vs the "alias-still-live" case, and append
  the fix hint.

## Recommendation summary

| Finding | Action |
|---|---|
| working — mode authoring guessable from ledger | carry-on |
| working — reject diagnostics precise & name fix | carry-on |
| spec_gap — borrowed-scrutinee field consume accepted vs whole rejected | RESOLVED by #58 (`b129af1`, reading (i)); cut55_2c now rejects, cut55_2b guards the boundary |
| friction — borrow-return error omits the fix | plan (tidy diagnostic) |
| friction — consume-while-borrowed wording + no fix hint | plan (tidy diagnostic) |
