fieldtest: mut-local — 6 examples (4 positive + 2 negative probes), 0 bugs / 3 friction / 1 spec_gap
Boss-dispatched fieldtest after audit-mut-local closed. Six AIL
Surface (.ail) fixtures under examples/fieldtest/ exercise the
shipped mut-local surface from a downstream-LLM-author's perspective
(no compiler-source access; DESIGN.md + public examples only).
Positive fixtures all run end-to-end first-try:
- mut-local_1_factorial.ail: straight-line Int accumulator
unroll (5!), prints 120.
- mut-local_2_classify_temp.ail: nested-if assigns into a Unit-
typed mut block, prints classification code.
- mut-local_3_horner.ail: Float mut accumulator for polynomial
evaluation, prints 18.
- mut-local_4_has_small_factor.ail: Bool mut flag via four
if-then-assign checks, prints true.
Negative probes confirm diagnostics fire as documented:
- mut-local_5_lambda_capture_probe.ail: [mut-var-captured-by-lambda].
- mut-local_6_diag_probe.ail: [mut-var-unsupported-type].
Findings:
[friction] F1 — mut without iteration: the accumulator-over-an-
iteration shape never materializes. Without while/for, the LLM-
author still writes a tail-recursive helper (the very pattern
the milestone Goal said mut would replace). examples/mut_counter.ail
illustrates the degeneration. Routing: planner for a while/for
iteration OR tighten DESIGN.md to name the gap honestly.
[friction] F2 — all four mut-related diagnostics emit their
bracketed [code] twice ('error: [code] fn-name: [code] message').
Mechanical bug: the #[error('[code] ...')] Display attributes I
authored in mut.2/mut.4-tidy include the bracketed prefix in the
message body, and the cli-diag-human formatter adds another from
CheckError::code(). Routing: debug (mechanical message-body
cleanup).
[friction] F3 — no surface form to call a zero-arg fn ('(app f)'
rejected at parse with 'expected at least one argument').
Orthogonal to mut-local but surfaced building the closure-factory
probe. Routing: planner for a small tidy iter.
[spec_gap] F4 — DESIGN.md does not name the 'use a tail-rec
helper instead' workaround for the iteration-over-accumulator
shape that mut alone cannot express. Routing: ratify in DESIGN.md
alongside F1's resolution.
[working] W1/W2/W3 — surface reachable on first read; diagnostics
pinpoint cause (mut-assign-out-of-scope even lists available
vars); composes cleanly with if + lambda-without-capture +
final-expression position.
Spec: docs/specs/2026-05-15-fieldtest-mut-local.md (333 lines).
Refs: docs/specs/2026-05-15-mut-local.md, audit-mut-local close
at 8685e96.
This commit is contained in:
@@ -0,0 +1,333 @@
|
|||||||
|
# Fieldtest — mut-local — 2026-05-15
|
||||||
|
|
||||||
|
**Status:** Draft — awaiting orchestrator triage
|
||||||
|
**Author:** ailang-fieldtester (dispatched by skills/fieldtest)
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
The mut-local milestone shipped local mutable state in fn bodies as a
|
||||||
|
sealed, lexically-scoped construct: `(mut (var <name> <type> <init>)
|
||||||
|
... <body>)` declares mutable bindings of types {Int, Float, Bool,
|
||||||
|
Unit}; `(assign <name> <value>)` updates a binding; references read
|
||||||
|
the current value. The seal-by-construction promise is enforced via
|
||||||
|
four diagnostics, and the enclosing fn signature does not gain a
|
||||||
|
`!Mut` effect.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### `examples/fieldtest/mut-local_1_factorial.ail` — straight-line factorial 5!
|
||||||
|
|
||||||
|
- What it does: declares `prod : Int` initially 1; runs five
|
||||||
|
straight-line `(assign prod (app * prod K))` statements (K = 1..5);
|
||||||
|
reads `prod` as the block's value and prints it.
|
||||||
|
- Why this task fits the milestone's scope: straight-line accumulator
|
||||||
|
unroll is the cleanest possible exercise of a mut-Int var. No
|
||||||
|
iteration, no helper fn, no branching — just the unsugared
|
||||||
|
imperative shape the milestone exists to enable.
|
||||||
|
- Outcome: compiles clean (`ok (21 symbols across 2 modules)`),
|
||||||
|
builds clean, runs and prints `120`. First-try correct.
|
||||||
|
|
||||||
|
### `examples/fieldtest/mut-local_2_classify_temp.ail` — temperature category via nested-if assigns
|
||||||
|
|
||||||
|
- What it does: `classify(t : Int) -> Int` opens a mut block with
|
||||||
|
`code : Int = 0`, then a nested `(if ...)` cascade where each
|
||||||
|
branch is a single `(assign code N)`, and returns `code`. `main`
|
||||||
|
calls `classify(22)` and prints the result.
|
||||||
|
- Why this task fits the milestone's scope: tests composition of `mut`
|
||||||
|
with `if`. Each branch is Unit-typed (because `(assign ...)` is
|
||||||
|
Unit), so the outer `(if ...)` is itself Unit and serves as one of
|
||||||
|
the mut-block's body statements before the final read of `code`.
|
||||||
|
- Outcome: compiles clean (`ok (22 symbols across 2 modules)`), builds
|
||||||
|
clean, runs and prints `2`. First-try correct.
|
||||||
|
|
||||||
|
### `examples/fieldtest/mut-local_3_horner.ail` — Horner polynomial in a Float mut-var
|
||||||
|
|
||||||
|
- What it does: evaluates `2x³ − 3x² + 5x − 7` at x = 2.5 by Horner's
|
||||||
|
method, with the running accumulator held in `acc : Float`
|
||||||
|
initialised to the leading coefficient `2.0`, then three straight-
|
||||||
|
line `(assign acc (app + (app * acc 2.5) ...))` statements, and a
|
||||||
|
final read of `acc`.
|
||||||
|
- Why this task fits the milestone's scope: exercises Float mut-vars
|
||||||
|
and demonstrates the cleanest non-Int use case — Horner is a
|
||||||
|
textbook example of "one running accumulator, repeated multiply-
|
||||||
|
add" that maps 1:1 onto mut's straight-line shape.
|
||||||
|
- Outcome: compiles clean, builds clean, runs and prints `18`
|
||||||
|
(Float 18.0 rendered via the same `%g` convention as
|
||||||
|
`examples/mut_sum_floats.ail`). First-attempt error: I miscomputed
|
||||||
|
the expected value in my own head (`64.875` instead of the correct
|
||||||
|
`18`) and the runtime gave me `18`. Tracing by hand confirmed the
|
||||||
|
runtime is right and the comment in the fixture has been corrected.
|
||||||
|
No language friction here — the language was right and I was
|
||||||
|
wrong, which is itself a good sign about transparency of behaviour.
|
||||||
|
|
||||||
|
### `examples/fieldtest/mut-local_4_has_small_factor.ail` — Bool mut-var "found-a-factor"
|
||||||
|
|
||||||
|
- What it does: `has_small_factor(n : Int) -> Bool` opens a mut block
|
||||||
|
with `found : Bool = false`; runs four `(if ((n % p) == 0) (assign
|
||||||
|
found true) (lit-unit))` checks for p ∈ {2,3,5,7}; reads `found`.
|
||||||
|
`main` runs it on n = 91 (= 7×13).
|
||||||
|
- Why this task fits the milestone's scope: tests Bool mut-vars plus
|
||||||
|
the natural OR-flag idiom. AILang has no `||` operator on the
|
||||||
|
surface, so the alternative shape (`if c1 true (if c2 true (if c3
|
||||||
|
true c4))`) is unpleasantly deeply nested. mut-Bool flattens it.
|
||||||
|
- Outcome: compiles clean (`ok (22 symbols across 2 modules)`), builds
|
||||||
|
clean, runs and prints `true`. First-try correct.
|
||||||
|
|
||||||
|
### `examples/fieldtest/mut-local_5_lambda_capture_probe.ail` — deliberate seal-probe (negative test)
|
||||||
|
|
||||||
|
- What it does: tries to construct a closure that captures a mut-var
|
||||||
|
(`count`) from an enclosing mut block. Expected to FAIL at
|
||||||
|
typecheck with `[mut-var-captured-by-lambda]`.
|
||||||
|
- Why this task fits the milestone's scope: directly probes the
|
||||||
|
seal-by-construction promise. The shape ("a callback that
|
||||||
|
references the running count") is the most natural way an
|
||||||
|
LLM-author would think to *escape* a mut-var; the rejection
|
||||||
|
enforces the seal.
|
||||||
|
- Outcome: typecheck fails with the expected diagnostic, the message
|
||||||
|
is actionable (names the var, says why it's forbidden, suggests
|
||||||
|
passing the value as a parameter instead). One small wart: the
|
||||||
|
diagnostic code is repeated inside the message (`[mut-var-
|
||||||
|
captured-by-lambda] make_bumper: [mut-var-captured-by-lambda]
|
||||||
|
mut-var ...`). See Finding F2.
|
||||||
|
|
||||||
|
### `examples/fieldtest/mut-local_6_diag_probe.ail` — diagnostic probe for `mut-var-unsupported-type`
|
||||||
|
|
||||||
|
- What it does: declares `var s (con Str) "hello"`. Expected to FAIL
|
||||||
|
at typecheck with `[mut-var-unsupported-type]`. (Sibling probe in
|
||||||
|
the same shape — assigning a Float to an Int var — was run
|
||||||
|
transiently to confirm `[assign-type-mismatch]` fires the same
|
||||||
|
way; the fixture now hosts the Str variant.)
|
||||||
|
- Why this task fits the milestone's scope: the milestone's most
|
||||||
|
user-visible restriction is the four-supported-types list. An
|
||||||
|
LLM-author who has not internalised this will reach for Str the
|
||||||
|
first time they want to accumulate a string; the diagnostic must
|
||||||
|
point at the right line and explain.
|
||||||
|
- Outcome: typecheck fails with the expected diagnostic, the
|
||||||
|
message is informative and names the unsupported type. Same
|
||||||
|
double-bracket-code wart as F2.
|
||||||
|
|
||||||
|
In addition to the six fixtures above, four transient probes were
|
||||||
|
run against `/tmp/probe_*.ail` (not checked into the working tree):
|
||||||
|
|
||||||
|
- `(assign y 5)` against a mut-frame containing only `x` — fires
|
||||||
|
`[mut-assign-out-of-scope]` with the message naming the
|
||||||
|
available mut-vars `[x]`. Diagnostic is helpful.
|
||||||
|
- `(mut (var x (con Int) 0))` with no body — fires the parse-time
|
||||||
|
`(mut ...) requires at least one body expression after vars`.
|
||||||
|
Clean.
|
||||||
|
- `(mut (var t ...) ... t)` *inside* a lambda body whose only free
|
||||||
|
variable is a lambda parameter (not a mut-var) — typechecks and
|
||||||
|
runs. The seal-of-capture is calibrated to mut-vars only, not to
|
||||||
|
"no mut blocks inside lambdas at all".
|
||||||
|
- `(assign x 5)` as the FINAL expression of a mut block (no trailing
|
||||||
|
read) — typechecks and runs; the block's static type is Unit, the
|
||||||
|
enclosing `main` accepts it. Confirms that `Term::Assign` is a
|
||||||
|
first-class Unit-typed expression in body position, not just a
|
||||||
|
"statement".
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
### [friction] F1 — mut without iteration: the "accumulator over an iteration" shape never materialises
|
||||||
|
|
||||||
|
The spec (§Goal) states that mut-local exists to remove friction from
|
||||||
|
"accumulators inside a fn body that today must be expressed by tail-
|
||||||
|
recursive helpers carrying explicit running total parameters". But
|
||||||
|
the milestone explicitly defers `while` / `for` (§Out of scope:
|
||||||
|
"Loops. No while, no for. Iteration in this milestone is still tail
|
||||||
|
recursion"), and the seal-by-construction rule forbids capturing a
|
||||||
|
mut-var into a lambda body. Together, these two constraints mean the
|
||||||
|
LLM-author **cannot** write the canonical accumulator-in-a-loop
|
||||||
|
shape — there is no surface form in which a mut-var is updated once
|
||||||
|
per loop iteration. The author still has to write a tail-recursive
|
||||||
|
helper that threads the running total as an explicit parameter, and
|
||||||
|
the mut block at most stores the helper's *final return value* into
|
||||||
|
a var that gets immediately read out. This is what the
|
||||||
|
`examples/mut_counter.ail` shipped with the milestone does — the mut
|
||||||
|
block adds no information that a plain `(app print (app sum_helper 1
|
||||||
|
10 0))` wouldn't have.
|
||||||
|
|
||||||
|
The shapes that DO work clean with mut:
|
||||||
|
|
||||||
|
1. Straight-line unrolled accumulator (fieldtest #1, #3).
|
||||||
|
2. Cascade of `(if ... (assign x ...) ...)` branches (fieldtest #2, #4).
|
||||||
|
|
||||||
|
Both are real wins — fieldtest #2 (no-`||`-operator OR flag) and
|
||||||
|
fieldtest #4 (no-deep-nested-if classify) are measurably cleaner with
|
||||||
|
mut than the alternative. But neither is the "running total over an
|
||||||
|
iteration" shape the spec's motivation paragraph leads the LLM-author
|
||||||
|
to expect.
|
||||||
|
|
||||||
|
- Where it surfaced: I started writing fieldtest #1 as a factorial
|
||||||
|
via tail-recursive helper that threads the partial product through
|
||||||
|
a mut-var, before realising the helper couldn't write to the
|
||||||
|
mut-var (lambda-capture rule) and the helper's result was being
|
||||||
|
thrown away into a mut-var only to be read back out as the final
|
||||||
|
expression. I rewrote it as a straight-line unroll.
|
||||||
|
- Recommended downstream action: `plan` (tidy iteration) — either
|
||||||
|
add at least one short-loop shape to the milestone (the most
|
||||||
|
obvious being a `(for ...)` block whose body is a fragment that
|
||||||
|
may write to enclosing mut-vars and may NOT capture them into
|
||||||
|
closures), OR add a paragraph to DESIGN.md's "What is supported"
|
||||||
|
entry that explicitly says "the accumulator-in-a-loop shape from
|
||||||
|
the spec's motivation is not yet achievable; until `while` lands
|
||||||
|
in the follow-on milestone, the workaround is the tail-recursive
|
||||||
|
accumulator-parameter pattern in `examples/mut_counter.ail` and
|
||||||
|
mut-local is useful only for straight-line and cascade shapes".
|
||||||
|
|
||||||
|
### [friction] F2 — diagnostic code is doubled (`[code] fn-name: [code] message`)
|
||||||
|
|
||||||
|
All four mut-related diagnostics emit their code twice when rendered
|
||||||
|
in the human-stderr formatter: once as the leading bracket prefix and
|
||||||
|
once embedded inside the message body. The leading bracket is the
|
||||||
|
canonical `[code]:` format set by the cli-diag-human iter (per
|
||||||
|
DESIGN.md). The embedded second bracket appears to be the message
|
||||||
|
text itself starting with `[code] ...`, which then gets concatenated
|
||||||
|
with the prefix.
|
||||||
|
|
||||||
|
Verbatim samples:
|
||||||
|
|
||||||
|
```
|
||||||
|
error: [mut-var-captured-by-lambda] make_bumper: [mut-var-captured-by-lambda] mut-var `count` cannot be captured by a lambda — ...
|
||||||
|
error: [assign-type-mismatch] main: [assign-type-mismatch] cannot assign `Float` to mut-var `n : Int` — ...
|
||||||
|
error: [mut-var-unsupported-type] main: [mut-var-unsupported-type] mut-var `s : Str` is not supported in this milestone — ...
|
||||||
|
error: [mut-assign-out-of-scope] main: [mut-assign-out-of-scope] cannot assign to `y` here — not declared as a mut-var in the enclosing (mut ...) block. Available mut-vars in scope: [x]
|
||||||
|
```
|
||||||
|
|
||||||
|
The fn-name prefix (`make_bumper:`, `main:`) IS useful — it tells the
|
||||||
|
LLM-author which fn the error is in. The second `[code]` is pure
|
||||||
|
noise: the bracketed prefix already supplies it. An LLM reading the
|
||||||
|
diagnostic without prior calibration will likely treat the doubled
|
||||||
|
code as a structured signal it should pay attention to, slowing
|
||||||
|
remediation rather than helping it.
|
||||||
|
|
||||||
|
- Where it surfaced: all four diagnostic probes (fieldtest #5, #6,
|
||||||
|
and the transient `/tmp/probe_oos.ail`).
|
||||||
|
- Recommended downstream action: `debug` — the message bodies
|
||||||
|
registered for these four codes evidently include the bracketed
|
||||||
|
prefix in their own text (probably as a leftover from a pre-
|
||||||
|
cli-diag-human format where messages carried their own
|
||||||
|
bracketed prefix). The fix is mechanical: strip the leading
|
||||||
|
`[code]` from each message body so the formatter's prefix
|
||||||
|
appears exactly once.
|
||||||
|
|
||||||
|
### [friction] F3 — no surface form to call a zero-arg fn from AILang
|
||||||
|
|
||||||
|
This is orthogonal to mut-local but surfaced while building the
|
||||||
|
closure-factory probe for fieldtest #5. Form-a's grammar is
|
||||||
|
`(app FN ARG+)` — at least one argument required. A fn declared as
|
||||||
|
`(fn make_bumper (type (fn-type (params) (ret ...))) (params) ...)`
|
||||||
|
cannot be invoked from another fn's body, because the call shape
|
||||||
|
`(app make_bumper)` is rejected at parse with
|
||||||
|
`expected at least one argument at byte ...`. Workaround: give every
|
||||||
|
"factory" fn a dummy Int parameter and pass a sacrificial value.
|
||||||
|
|
||||||
|
This is orthogonal to mut-local but it makes the *most natural*
|
||||||
|
LLM-author shape for testing seal-by-construction — a closure
|
||||||
|
factory — strictly inexpressible at zero arity. Fieldtest #5 had to
|
||||||
|
be rewritten with a dummy `seed` parameter (then I could probe the
|
||||||
|
capture rule with `count` initialised from `seed`). For the
|
||||||
|
mut-local audience this manifests as: any closure-factory shape
|
||||||
|
already needs a workaround unrelated to the mut work, which adds
|
||||||
|
noise to authoring.
|
||||||
|
|
||||||
|
- Where it surfaced: fieldtest #5, first attempt.
|
||||||
|
- Recommended downstream action: `plan` (tidy iteration) or
|
||||||
|
`brainstorm` — outside this milestone's scope, but worth filing
|
||||||
|
on the roadmap. Allowing `(app f)` for zero-arity fns is a one-
|
||||||
|
liner grammar change with no schema impact (the JSON-AST already
|
||||||
|
has `args: [Term...]` and the empty array is well-formed).
|
||||||
|
|
||||||
|
### [spec_gap] F4 — DESIGN.md does not name the "use this instead" workaround for the rejected shapes
|
||||||
|
|
||||||
|
When the `[mut-var-captured-by-lambda]` diagnostic fires, its
|
||||||
|
message says: "deferring escape to a future milestone with ref-types
|
||||||
|
and the !Mut effect". When `[mut-var-unsupported-type]` fires on
|
||||||
|
`Str`, the message says "deferred to a follow-on milestone". Both
|
||||||
|
are compiler-internal-reader phrasings. An LLM-author seeing these
|
||||||
|
needs to know what to write *right now*. The DESIGN.md "What is
|
||||||
|
supported" entry for local mutable state describes the seal but
|
||||||
|
does not enumerate the working idioms (the straight-line unroll
|
||||||
|
and the cascade-of-if; see F1) nor name the fallback shapes for
|
||||||
|
the rejected ones (the tail-recursive accumulator-parameter
|
||||||
|
pattern; see `examples/mut_counter.ail`).
|
||||||
|
|
||||||
|
- Where it surfaced: while writing fieldtest #1 and #5, I had to
|
||||||
|
derive the workarounds from re-reading the spec's §"Out of scope"
|
||||||
|
paragraph and inferring backwards. An LLM-author with only
|
||||||
|
DESIGN.md and the public corpus would land on `examples/mut_counter
|
||||||
|
.ail` only by coincidence (the file IS in the corpus, but its
|
||||||
|
comment doesn't say "this is the canonical pattern when mut alone
|
||||||
|
can't iterate").
|
||||||
|
- Recommended downstream action: tighten DESIGN.md — add to the
|
||||||
|
"Local mutable state" §"What is supported" entry a short
|
||||||
|
enumeration of working idioms (straight-line unroll; cascade of
|
||||||
|
if-assigns; cascade of match-arm-assigns) and a one-liner
|
||||||
|
pointer to the tail-rec-helper pattern as the fallback when the
|
||||||
|
per-iteration shape is wanted.
|
||||||
|
|
||||||
|
### [working] W1 — mut's surface is reachable on first read of DESIGN.md and form-a.md
|
||||||
|
|
||||||
|
The four supported shapes (Int, Float, Bool, Unit) all parse and
|
||||||
|
typecheck on first try given the spec and the `examples/mut.ail`
|
||||||
|
fixture. The S-expression layout `(mut (var name (con T) init) ...
|
||||||
|
body)` is consistent with the rest of the surface (no surprise
|
||||||
|
positional rules, no new keywords beyond `mut`/`var`/`assign`). I
|
||||||
|
did not hit any "what does the form actually look like?" friction.
|
||||||
|
Both `mut_counter.ail` and `mut_sum_floats.ail` in the public
|
||||||
|
corpus are good examples, and the form-a §Terms grammar entry
|
||||||
|
documents the shape precisely.
|
||||||
|
|
||||||
|
- Where it surfaced: every happy-path fieldtest example
|
||||||
|
(#1, #2, #3, #4) compiled on first try.
|
||||||
|
- Recommended downstream action: `carry-on`. This is a clear
|
||||||
|
signal that the *surface design* of mut is sound. The friction
|
||||||
|
recorded above is about *what mut can be used for*, not about
|
||||||
|
*how mut is written*.
|
||||||
|
|
||||||
|
### [working] W2 — diagnostics fire correctly and pinpoint the cause
|
||||||
|
|
||||||
|
All four mut-related diagnostics (`mut-var-captured-by-lambda`,
|
||||||
|
`mut-var-unsupported-type`, `assign-type-mismatch`,
|
||||||
|
`mut-assign-out-of-scope`) fire on the cases that should trip them,
|
||||||
|
name the offending var, and supply a short rationale. The doubled
|
||||||
|
code (F2) is noise on top of an otherwise solid signal. For
|
||||||
|
`mut-assign-out-of-scope`, the message even enumerates the
|
||||||
|
available mut-vars in scope (`Available mut-vars in scope: [x]`),
|
||||||
|
which is exactly the kind of context-supplying-without-being-asked
|
||||||
|
diagnostic the LLM-author benefits from most.
|
||||||
|
|
||||||
|
- Where it surfaced: fieldtest #5 (capture-by-lambda), fieldtest #6
|
||||||
|
(unsupported-type), transient probe (assign-type-mismatch and
|
||||||
|
assign-out-of-scope).
|
||||||
|
- Recommended downstream action: `carry-on`. Fix F2 (doubled code)
|
||||||
|
separately; the diagnostic *bodies* are good.
|
||||||
|
|
||||||
|
### [working] W3 — mut composes cleanly with `if`, with lambda bodies (no capture), and with the final-expression-position
|
||||||
|
|
||||||
|
The two non-trivial composition cases — `if` cascades inside a mut
|
||||||
|
block (fieldtest #2, #4) and a mut block inside a lambda body
|
||||||
|
whose mut-vars do not escape (transient `/tmp/probe_mut_in_lam.ail`)
|
||||||
|
— both work without surprise. Also: `(assign x 5)` is a legitimate
|
||||||
|
final expression of a mut block (the block's type is then Unit),
|
||||||
|
and the parser accepts a mut block whose body is one such final
|
||||||
|
expression with no preceding statements. The composition story
|
||||||
|
is consistent.
|
||||||
|
|
||||||
|
- Where it surfaced: fieldtest #2, #4; transient probes.
|
||||||
|
- Recommended downstream action: `carry-on`. The composition
|
||||||
|
invariants ("mut-vars do not escape" / "mut-vars shadow let-bound
|
||||||
|
names of the same identifier") are spec'd in
|
||||||
|
`2026-05-15-mut-local.md` §Term::Var resolution and behave as
|
||||||
|
specified.
|
||||||
|
|
||||||
|
## Recommendation summary
|
||||||
|
|
||||||
|
| Finding | Class | Action |
|
||||||
|
|---|---|---|
|
||||||
|
| F1 — accumulator-in-a-loop shape inexpressible | friction | `plan` (add `while`/`for`) OR tighten DESIGN.md to name the gap (less work, more honest) |
|
||||||
|
| F2 — diagnostic code doubled in render | friction | `debug` (mechanical message-body cleanup) |
|
||||||
|
| F3 — zero-arg fn cannot be called from AILang code | friction | `plan` (tidy iteration, outside mut-local scope but on roadmap) |
|
||||||
|
| F4 — DESIGN.md does not name working idioms / fallbacks | spec_gap | tighten DESIGN.md (add idioms + fallback pointer) |
|
||||||
|
| W1 — mut surface reachable on first read | working | carry-on |
|
||||||
|
| W2 — diagnostics fire correctly with useful context | working | carry-on (modulo F2) |
|
||||||
|
| W3 — composition with if / lambda-body / final-position is consistent | working | carry-on |
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
; Fieldtest mut-local #1 — factorial 5! via straight-line mut updates.
|
||||||
|
;
|
||||||
|
; Task: print 5! (= 120) using a `mut` block that names a running
|
||||||
|
; product and unrolls five multiplications as straight-line statements.
|
||||||
|
; This is the most direct possible use of mut-local: no helper fn, no
|
||||||
|
; iteration, just a sequence of in-block updates terminated by reading
|
||||||
|
; the var. The LLM-author's mental model of "I want a local accumulator"
|
||||||
|
; maps 1:1 onto the surface here.
|
||||||
|
;
|
||||||
|
; Why this fits mut-local's scope: the milestone supplies only sealed
|
||||||
|
; lexically-scoped mutables, with no `while` or `for`. Straight-line
|
||||||
|
; unroll is the *only* shape inside one mut block that needs no helper.
|
||||||
|
;
|
||||||
|
; Expected stdout: 120
|
||||||
|
|
||||||
|
(module mut-local_1_factorial
|
||||||
|
|
||||||
|
(fn main
|
||||||
|
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||||
|
(params)
|
||||||
|
(body
|
||||||
|
(app print
|
||||||
|
(mut
|
||||||
|
(var prod (con Int) 1)
|
||||||
|
(assign prod (app * prod 1))
|
||||||
|
(assign prod (app * prod 2))
|
||||||
|
(assign prod (app * prod 3))
|
||||||
|
(assign prod (app * prod 4))
|
||||||
|
(assign prod (app * prod 5))
|
||||||
|
prod)))))
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
; Fieldtest mut-local #2 — classify a temperature into a band using
|
||||||
|
; nested if-branches that each update a mut-Int "category code".
|
||||||
|
;
|
||||||
|
; Task: given a temperature value, set a category-code mut-var to
|
||||||
|
; 0 (freezing), 1 (cold), 2 (warm), 3 (hot) by walking through a
|
||||||
|
; cascade of if-branches. Print the resulting code.
|
||||||
|
;
|
||||||
|
; Why this fits mut-local's scope: this exercises mut composed with
|
||||||
|
; `if` — each branch contains a single `(assign ...)`. The seal-by-
|
||||||
|
; construction promise says the if-branch can write to the var, and
|
||||||
|
; the var's value flows out of the branch as the latest store. This is
|
||||||
|
; a use of mut that *replaces* what a chain of let-rebinds would
|
||||||
|
; otherwise do, and a chain of let-rebinds is the AILang author's
|
||||||
|
; usual workaround for "set this variable conditionally" — so the
|
||||||
|
; mut form should be measurably cleaner here.
|
||||||
|
;
|
||||||
|
; Expected stdout: 2 (room temperature 22 = "warm")
|
||||||
|
|
||||||
|
(module mut-local_2_classify_temp
|
||||||
|
|
||||||
|
(fn classify
|
||||||
|
(doc "Return category code 0..3 for temperature t in degrees C.")
|
||||||
|
(type (fn-type (params (con Int)) (ret (con Int))))
|
||||||
|
(params t)
|
||||||
|
(body
|
||||||
|
(mut
|
||||||
|
(var code (con Int) 0)
|
||||||
|
(if (app < t 0)
|
||||||
|
(assign code 0)
|
||||||
|
(if (app < t 15)
|
||||||
|
(assign code 1)
|
||||||
|
(if (app < t 28)
|
||||||
|
(assign code 2)
|
||||||
|
(assign code 3))))
|
||||||
|
code)))
|
||||||
|
|
||||||
|
(fn main
|
||||||
|
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||||
|
(params)
|
||||||
|
(body
|
||||||
|
(app print (app classify 22)))))
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
; Fieldtest mut-local #3 — evaluate the polynomial
|
||||||
|
; p(x) = 2 x^3 - 3 x^2 + 5 x - 7
|
||||||
|
; at x = 2.5 by Horner's method, using a Float mut-var as the running
|
||||||
|
; accumulator and unrolling the four Horner steps as straight-line
|
||||||
|
; assigns.
|
||||||
|
;
|
||||||
|
; Why this fits mut-local's scope: the accumulator is a Float, the
|
||||||
|
; updates are straight-line (no iteration), and the mut form removes
|
||||||
|
; the four nested let-rebinds an LLM-author would otherwise write
|
||||||
|
; ("p1 = ..., p2 = p1*x + ..., p3 = p2*x + ...") — each rebind needing
|
||||||
|
; a fresh name. Reusing one name for the running accumulator is the
|
||||||
|
; natural shape, and mut supplies it.
|
||||||
|
;
|
||||||
|
; Hand-check (Horner): start with leading coeff 2.0, then for each
|
||||||
|
; lower coefficient do acc = acc * x + c:
|
||||||
|
; 2.0 * 2.5 + (-3) = 5.0 - 3 = 2.0
|
||||||
|
; 2.0 * 2.5 + 5 = 5.0 + 5 = 10.0
|
||||||
|
; 10.0 * 2.5 + (-7) = 25.0 - 7 = 18.0
|
||||||
|
; Expected stdout: 18 (Float 18.0 via %g; print drops the trailing
|
||||||
|
; ".0" the same way it does for the Float fixture mut_sum_floats.ail.)
|
||||||
|
|
||||||
|
(module mut-local_3_horner
|
||||||
|
|
||||||
|
(fn main
|
||||||
|
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||||
|
(params)
|
||||||
|
(body
|
||||||
|
(app print
|
||||||
|
(mut
|
||||||
|
(var acc (con Float) 2.0)
|
||||||
|
(assign acc (app - (app * acc 2.5) 3.0))
|
||||||
|
(assign acc (app + (app * acc 2.5) 5.0))
|
||||||
|
(assign acc (app - (app * acc 2.5) 7.0))
|
||||||
|
acc)))))
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
; Fieldtest mut-local #4 — Bool mut-var "found-a-factor" flag.
|
||||||
|
;
|
||||||
|
; Task: probe whether n has a small prime factor (2, 3, 5, or 7) by
|
||||||
|
; running four straight-line checks; if any check matches, set a Bool
|
||||||
|
; mut-var to true. Print the flag at the end.
|
||||||
|
;
|
||||||
|
; The straight-line form here is the natural shape: an LLM-author asked
|
||||||
|
; to "test these four conditions and OR the results" would otherwise
|
||||||
|
; write a chain of `||` operators (no such operator in AILang surface)
|
||||||
|
; or a nested chain of `(if ... (if ... ))`. The mut form replaces both
|
||||||
|
; with a flat sequence whose intent ("set this flag if any of these
|
||||||
|
; matches") reads top-to-bottom.
|
||||||
|
;
|
||||||
|
; Test against n = 91 = 7 * 13 — only the divisible-by-7 check fires.
|
||||||
|
; Expected stdout: true
|
||||||
|
|
||||||
|
(module mut-local_4_has_small_factor
|
||||||
|
|
||||||
|
(fn has_small_factor
|
||||||
|
(type (fn-type (params (con Int)) (ret (con Bool))))
|
||||||
|
(params n)
|
||||||
|
(body
|
||||||
|
(mut
|
||||||
|
(var found (con Bool) false)
|
||||||
|
(if (app == (app % n 2) 0) (assign found true) (lit-unit))
|
||||||
|
(if (app == (app % n 3) 0) (assign found true) (lit-unit))
|
||||||
|
(if (app == (app % n 5) 0) (assign found true) (lit-unit))
|
||||||
|
(if (app == (app % n 7) 0) (assign found true) (lit-unit))
|
||||||
|
found)))
|
||||||
|
|
||||||
|
(fn main
|
||||||
|
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||||
|
(params)
|
||||||
|
(body
|
||||||
|
(app print (app has_small_factor 91)))))
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
; Fieldtest mut-local #5 — deliberate probe of the seal-by-construction
|
||||||
|
; promise: try to lift a mut-var into a lambda closure.
|
||||||
|
;
|
||||||
|
; Spec §"Out of scope": "Lambda capture of a mut-var. A lambda body
|
||||||
|
; whose free vars include a mut-var of an enclosing Term::Mut is
|
||||||
|
; rejected at typecheck with CheckError::MutVarCapturedByLambda."
|
||||||
|
;
|
||||||
|
; This file is EXPECTED TO FAIL at `ail check` with the
|
||||||
|
; `mut-var-captured-by-lambda` diagnostic. The purpose is to probe:
|
||||||
|
; - that the diagnostic actually fires
|
||||||
|
; - that its rendered text is actionable
|
||||||
|
; - that it points at the lambda site, not somewhere else
|
||||||
|
;
|
||||||
|
; The shape: a mut block declares `count`, builds a closure that would
|
||||||
|
; close over `count`, and tries to return the closure. An LLM-author
|
||||||
|
; might write this naïvely thinking "I just need a small callback that
|
||||||
|
; updates the running count" — exactly the shape the seal forbids.
|
||||||
|
|
||||||
|
(module mut-local_5_lambda_capture_probe
|
||||||
|
|
||||||
|
(fn make_bumper
|
||||||
|
(type
|
||||||
|
(fn-type
|
||||||
|
(params (con Int))
|
||||||
|
(ret (fn-type (params (con Int)) (ret (con Int))))))
|
||||||
|
(params seed)
|
||||||
|
(body
|
||||||
|
(mut
|
||||||
|
(var count (con Int) 0)
|
||||||
|
(assign count seed)
|
||||||
|
(lam (params (typed n (con Int)))
|
||||||
|
(ret (con Int))
|
||||||
|
(body (app + n count))))))
|
||||||
|
|
||||||
|
(fn main
|
||||||
|
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||||
|
(params)
|
||||||
|
(body
|
||||||
|
(app print (app (app make_bumper 10) 5)))))
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
; Fieldtest mut-local #6 — deliberate diagnostic probe. EXPECTED TO
|
||||||
|
; FAIL at `ail check`.
|
||||||
|
;
|
||||||
|
; Probes `mut-var-unsupported-type` — declaring a Str mut-var.
|
||||||
|
; (A sibling fixture used to probe `assign-type-mismatch` by assigning
|
||||||
|
; 1.5 to an Int var; on the same surface the diagnostic also fires
|
||||||
|
; with the same double-bracket-prefix shape.)
|
||||||
|
|
||||||
|
(module mut-local_6_diag_probe
|
||||||
|
|
||||||
|
(fn main
|
||||||
|
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||||
|
(params)
|
||||||
|
(body
|
||||||
|
(app print
|
||||||
|
(mut
|
||||||
|
(var s (con Str) "hello")
|
||||||
|
s)))))
|
||||||
Reference in New Issue
Block a user