diff --git a/docs/specs/2026-05-15-fieldtest-mut-local.md b/docs/specs/2026-05-15-fieldtest-mut-local.md new file mode 100644 index 0000000..f719c5f --- /dev/null +++ b/docs/specs/2026-05-15-fieldtest-mut-local.md @@ -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 ) +... )` declares mutable bindings of types {Int, Float, Bool, +Unit}; `(assign )` 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 | diff --git a/examples/fieldtest/mut-local_1_factorial.ail b/examples/fieldtest/mut-local_1_factorial.ail new file mode 100644 index 0000000..d917122 --- /dev/null +++ b/examples/fieldtest/mut-local_1_factorial.ail @@ -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))))) diff --git a/examples/fieldtest/mut-local_2_classify_temp.ail b/examples/fieldtest/mut-local_2_classify_temp.ail new file mode 100644 index 0000000..2ed1135 --- /dev/null +++ b/examples/fieldtest/mut-local_2_classify_temp.ail @@ -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))))) diff --git a/examples/fieldtest/mut-local_3_horner.ail b/examples/fieldtest/mut-local_3_horner.ail new file mode 100644 index 0000000..4d95ae7 --- /dev/null +++ b/examples/fieldtest/mut-local_3_horner.ail @@ -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))))) diff --git a/examples/fieldtest/mut-local_4_has_small_factor.ail b/examples/fieldtest/mut-local_4_has_small_factor.ail new file mode 100644 index 0000000..4b9c790 --- /dev/null +++ b/examples/fieldtest/mut-local_4_has_small_factor.ail @@ -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))))) diff --git a/examples/fieldtest/mut-local_5_lambda_capture_probe.ail b/examples/fieldtest/mut-local_5_lambda_capture_probe.ail new file mode 100644 index 0000000..c613053 --- /dev/null +++ b/examples/fieldtest/mut-local_5_lambda_capture_probe.ail @@ -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))))) diff --git a/examples/fieldtest/mut-local_6_diag_probe.ail b/examples/fieldtest/mut-local_6_diag_probe.ail new file mode 100644 index 0000000..e4abf9e --- /dev/null +++ b/examples/fieldtest/mut-local_6_diag_probe.ail @@ -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)))))