From 2052f4dfcc83a7dce9335f1377823b289876ff62 Mon Sep 17 00:00:00 2001 From: Brummel Date: Sun, 10 May 2026 16:57:38 +0200 Subject: [PATCH] =?UTF-8?q?fieldtest:=20floats=20=E2=80=94=204=20examples,?= =?UTF-8?q?=206=20findings=20(1=20bug,=201=20friction,=201=20spec=5Fgap,?= =?UTF-8?q?=203=20working)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/specs/2026-05-10-fieldtest-floats.md | 307 ++++++++++++++++++ .../fieldtest/floats_1_newton_sqrt.ail.json | 1 + examples/fieldtest/floats_1_newton_sqrt.ailx | 38 +++ .../floats_2_average_int_list.ail.json | 1 + .../fieldtest/floats_2_average_int_list.ailx | 67 ++++ .../fieldtest/floats_3_safe_division.ail.json | 1 + .../fieldtest/floats_3_safe_division.ailx | 55 ++++ .../floats_4_float_to_str_reach.ail.json | 1 + .../floats_4_float_to_str_reach.ailx | 26 ++ 9 files changed, 497 insertions(+) create mode 100644 docs/specs/2026-05-10-fieldtest-floats.md create mode 100644 examples/fieldtest/floats_1_newton_sqrt.ail.json create mode 100644 examples/fieldtest/floats_1_newton_sqrt.ailx create mode 100644 examples/fieldtest/floats_2_average_int_list.ail.json create mode 100644 examples/fieldtest/floats_2_average_int_list.ailx create mode 100644 examples/fieldtest/floats_3_safe_division.ail.json create mode 100644 examples/fieldtest/floats_3_safe_division.ailx create mode 100644 examples/fieldtest/floats_4_float_to_str_reach.ail.json create mode 100644 examples/fieldtest/floats_4_float_to_str_reach.ailx diff --git a/docs/specs/2026-05-10-fieldtest-floats.md b/docs/specs/2026-05-10-fieldtest-floats.md new file mode 100644 index 0000000..5a98671 --- /dev/null +++ b/docs/specs/2026-05-10-fieldtest-floats.md @@ -0,0 +1,307 @@ +# Fieldtest — Floats milestone — 2026-05-10 + +**Status:** Draft — awaiting orchestrator triage +**Author:** ailang-fieldtester (dispatched by skills/fieldtest) + +## Scope + +The Floats milestone (closed 2026-05-10) added `Float` as a fourth +zero-arity primitive type (IEEE-754 binary64). Surface accepts +`1.5`, `1.5e3`, `1e10`, `-1.5`. Operators `+`/`-`/`*`/`/` and +`<`/`<=`/`>`/`>=`/`!=`/`==` widened to polymorphic over `{Int,Float}` +via codegen-dispatch. New builtins: `neg` (poly), `int_to_float`, +`float_to_int_truncate`, `is_nan`. Constants `nan`, `inf`, +`neg_inf` (bit-pattern globals). New effect op `io/print_float`. +`float_to_str` is type-installed but codegen-deferred. Per +DESIGN.md §"Float semantics", `Pattern::Lit::Float` is supposed +to be hard-rejected at typecheck via `CheckError::FloatPatternNotAllowed`. + +## Examples + +### `examples/fieldtest/floats_1_newton_sqrt.ailx` — Newton's method for √2 + +- 20-step Newton iteration `x' = 0.5 * (x + N/x)` for `N = 2.0`, + printed via `io/print_float`. +- Exercises Float literals (`0.5`, `2.0`), polymorphic `*` and `+` + and `/` and `==` and `-` (the loop counter is `Int`), tail- + recursive `Float`-typed accumulator, `io/print_float`. +- **Outcome:** typechecks, builds, runs first try. stdout: `1.41421` + (matches `sqrt(2)` to 5 decimals — the IEEE-conformant truncated + `%g` printf). + +### `examples/fieldtest/floats_2_average_int_list.ailx` — mean of an Int list as Float + +- Sum and count an `IntList` with two tail-recursive accumulators, + then promote both to `Float` via `int_to_float` and divide. +- Exercises `int_to_float` at both numerator and denominator (the + natural shape — neither operand can be left as `Int` because `+`, + `-`, `*`, `/` reject mixed-type args). +- **Outcome:** typechecks, builds, runs first try. stdout: `3.2` + (sum 16 / count 5 = exact 3.2 since 5 is a power of 5 not + representable in binary, but the printer lands on the + shortest-decimal that round-trips). + +### `examples/fieldtest/floats_3_safe_division.ailx` — IEEE division + `is_nan` + +- Three division cases: `6.0/3.0` (normal), `1.0/0.0` (+Inf), + `0.0/0.0` (NaN). For each, `classify` returns `-1` if the value is + NaN (via `is_nan`), `0` if infinite (via comparison against + `1.0e308` and `-1.0e308`), `1` otherwise. +- Exercises `is_nan`, polymorphic `>`, `<`, large literal + exponents, `io/print_float` on Inf/NaN values, and the IEEE + `0.0/0.0 = NaN` semantics. +- **Outcome:** typechecks, builds, runs first try (after a paren- + count typo fix). stdout: + ``` + 2 + 1 + inf + 0 + -nan + -1 + ``` + Three observations from the actual stdout vs. the expected: + 1. `6.0/3.0` printed as `2`, not `2.0` (see finding F1). + 2. NaN printed as `-nan` (the platform's qNaN sign-bit + representation). + 3. `is_nan` correctly returned `true` for the `0.0/0.0` result + and `false` for `1.0/0.0`. + +### `examples/fieldtest/floats_4_float_to_str_reach.ailx` — reach for `float_to_str` + +- `(do io/print_str (app float_to_str 3.14))` — the natural + reach: an LLM-author who finds `float_to_str : (Float) -> Str` + in `ail builtins` will write this, expecting a `Str`-printable + Float. +- **Outcome:** `ail check` succeeds (typechecks fine). `ail build` + fails with the structured codegen-deferred error — the diagnostic + is excellent, see finding F2. + +## Findings + +### [bug] B1 — `Pattern::Lit::Float` is NOT rejected at typecheck (DESIGN.md says it must be) + +DESIGN.md §"Float semantics" (lines 2076-2081): + +> **Pattern matching:** `Pattern::Lit` on `Literal::Float` is hard- +> rejected at typecheck (`CheckError::FloatPatternNotAllowed`). IEEE +> semantics make Float patterns semantically dubious — NaN never +> matches via IEEE-`==`, and bit-exact equality is rarely what an +> LLM-author wants. Use ordering operators (`<`, `>`, ...) and +> `is_nan` to discriminate Floats. + +JOURNAL Floats.3 (line 13559-13560): + +> `Pattern::Lit::Float` typecheck-rejected via new +> `CheckError::FloatPatternNotAllowed`. + +Floats.5 milestone-close JOURNAL repeats the claim. But probing +with this fixture (saved at `/tmp/probe_pat_float.ailx`, not +committed; reproduced below): + +``` +(module probe_pat_float + (fn classify + (type (fn-type (params (con Float)) (ret (con Int)))) + (params x) + (body + (match x + (case (pat-lit 0.0) 1) + (case _ 0)))) + (fn main + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body (do io/print_int (app classify 0.0))))) +``` + +`ail check` reports `ok (2 symbols across 1 modules)`. `ail build` +succeeds. The compiled binary returns `1` for `classify(0.0)` — +the Float pattern *runs*. Multi-arm probe confirms IEEE semantics: +NaN does not match `0.0` (yields the `_` branch), `-0.0` (via +`(neg 0.0)`) DOES match `(pat-lit 0.0)` (yields `1`). So the +underlying behaviour is "option (b)" from the spec's out-of-scope +note (Floats spec line 729-731) — Float patterns work with +IEEE-`==` semantics, despite DESIGN.md and JOURNAL both promising +the option-(a) hard-reject. + +- **Repro:** the four-line probe above; check exits 0 instead + of error. +- **Recommended action:** `debug` — RED-first test that + `(case (pat-lit 0.0) ...)` produces + `CheckError::FloatPatternNotAllowed`, then implement the missing + reject path in `crates/ailang-check/`. Alternative: if the team + decides option (b) (silent Float-pattern with IEEE-`==`) is + actually the right semantics on reflection, then DESIGN.md and + JOURNAL must be amended — but this is a substantive design + reversal and should go through `brainstorm`, not be silently + papered over. + +### [working] W1 — `float_to_str` deferred-codegen diagnostic is excellent + +Diagnostic from `ail build` on example 4: + +``` +Error: module `floats_4_float_to_str_reach`: def `main`: internal: +`float_to_str` codegen lowering is not yet implemented (requires +dynamic Str allocation in the runtime) +``` + +This is the gold-standard form for an LLM-author-facing diagnostic: + +- **Names the builtin** that triggered the error (`float_to_str`). +- **Names the failure layer** (codegen, not typecheck — so the LLM + knows the type signature is correct and not to second-guess it). +- **Names the underlying blocker** (dynamic Str allocation) so the + LLM can decide whether to wait for the future milestone or work + around (no current workaround: there's no other Float→Str path). + +The reach itself (looking at `ail builtins`, finding +`float_to_str : (Float) -> Str`, writing +`(do io/print_str (app float_to_str 3.14))`) was natural — exactly +the shape an LLM produces for "print this Float as a labeled +string". The deferred-codegen state is communicated cleanly enough +that the LLM gets a useful signal without reaching for the source. + +- **Recommended action:** `carry-on`. This is the model for how + other "type-installed but codegen-deferred" features should + diagnose themselves. + +### [working] W2 — DESIGN.md §"Float semantics" is sufficient to guide the natural-reach for `is_nan` + +DESIGN.md line 2034-2037: + +> - `is_nan` (`fcmp uno double %x, %x`) returns `true` iff `x` is +> NaN. Bit-pattern-based NaN detection without dependence on the +> payload bits. + +Plus line 2079-2081: + +> Use ordering operators (`<`, `>`, ...) and `is_nan` to +> discriminate Floats. + +Reading these alone (no compiler peek), an LLM-author writing +example 3 reaches for `is_nan` directly, never considers the +ill-fated `(== x x)` form, and produces a working classifier. +The `nan` / `inf` / `neg_inf` constants are also discoverable +from the same section (line 2135-2137), and `ail builtins` +confirms them as bare-value globals — they were used as test +inputs in the iteration of example 3, where the same probe +showed they propagate correctly through `is_nan`, `<`, `>`. + +- **Recommended action:** `carry-on`. + +### [friction] F1 — `io/print_float` strips trailing `.0`, making float output indistinguishable from int output + +Example 3 stdout includes `2` for `(/ 6.0 3.0)`, not `2.0`. The +spec/JOURNAL describes the printer as "shortest round-trippable +decimal" (Floats.2 entry, line 13548-13549) but that property +applies to the surface printer (`Literal::Float` → surface +`"2.0"`). The runtime path `io/print_float` lowers via +`printf("%g\n", v)` (Floats.4 entry, line 13571), and `%g` strips +trailing zeros and the trailing decimal point. + +Consequences: + +1. The stdout of `io/print_float v` for an integer-valued `v` is + indistinguishable from `io/print_int (float_to_int_truncate v)`. + For an LLM author writing a test that asserts on stdout, the + round-trip property "the text I see is unambiguously a Float" + silently breaks. +2. The asymmetry between the surface printer (always emits `.0`) + and the runtime printer (sometimes doesn't) is a hidden + contract the LLM-author has to learn from running the program, + not from DESIGN.md. + +The deeper issue: DESIGN.md says nothing about the format of +`io/print_float` output. It is silent on whether the contract is +"shortest round-trippable" (matching the surface printer) or +"`%g`-style". An LLM-author who writes a test asserting `2.0` in +stdout has no DESIGN.md anchor to consult; running the test is +the only way to find out. + +- **Recommended action:** `plan` (tidy iteration). Either (a) + switch `io/print_float` to a shortest-round-trippable format + (matches the surface printer, removes the asymmetry, makes + `print_float` output unambiguous as a Float), or (b) document + the `%g`-style format explicitly in DESIGN.md §"Float + semantics" so the contract is at least nameable. Option (a) is + the LLM-utility-aligned choice: the LLM-author benefit from + unambiguous output exceeds the cost of a more verbose format + string for integer-valued Floats. (Note: `float_to_str`, + when it ships its codegen, faces the *same* design choice — + pinning the format on `io/print_float` first preserves the + freedom to make `float_to_str` match.) + +### [spec_gap] G1 — DESIGN.md is silent on the NaN sign-bit / payload visible to `io/print_float` + +Example 3 stdout shows `-nan` for `(/ 0.0 0.0)`. DESIGN.md line +2057-2060 explicitly leaves the NaN payload unspecified: + +> The exact NaN bit pattern produced by an op. Any quiet NaN bit +> pattern is conformant; `0.0 / 0.0` may produce +> `0x7ff8000000000000` on one target and a different qNaN on +> another. + +The reading taken: since the payload (and sign bit) of the qNaN +produced by `0.0/0.0` is unspecified, the *printed form* of that +qNaN is also unspecified. The compiler's `printf("%g\n", v)` +exposes the sign bit (`-nan` vs `nan`) — that's a downstream +consequence of "NaN bit pattern unspecified". + +The other plausible reading: DESIGN.md guarantees the constant +`nan` (line 2135) has bits `7ff8000000000000` (no sign bit set, so +prints as `nan`), AND that `is_nan` correctly identifies any qNaN +regardless of sign — but does NOT promise that the printed form +of an op-produced NaN is normalised to `nan` (no sign). + +The picked reading (current behaviour) is internally consistent +with A5's "NaN payload unspecified". But an LLM-author who reads +DESIGN.md, sees the `nan` constant prints as `nan`, and assumes +all NaN values print the same way will be surprised by `-nan` on +ops that produce NaN. **Both readings are equally plausible from +the text.** + +- **Recommended action:** `tighten DESIGN.md`. One additional + sentence in §"Float semantics" Unspecified-list: "The textual + form of a NaN value emitted by `io/print_float` follows + `printf("%g")` and may include a sign bit (`-nan`); `is_nan` is + the only reliable NaN test." Or, if the team prefers a + guarantee, normalise NaN printing in the runtime to `nan` and + add the corresponding Guaranteed-list entry. Either pin + removes the ambiguity. + +### [working] W3 — Mixed `Int + Float` produces a clear, actionable diagnostic + +Probe (`/tmp/probe_mixed.ailx`): + +``` +(do io/print_float (app + 1 1.5)) +``` + +`ail check` reports: + +``` +error: [type-mismatch] main: type mismatch: expected Int, got Float +``` + +The diagnostic doesn't *suggest* `int_to_float`, but the LLM-author +reading "expected Int, got Float" will think "I need to coerce +the Int" and either (a) write `1.0` instead of `1` (the natural +fix here) or (b) reach for `int_to_float`. Both work. The +diagnostic doesn't lie about the type system: `+ : forall a. (a, a) +-> a` unifies under one type variable; mixing rejects. + +- **Recommended action:** `carry-on`. A future polish iteration + could add a "did you mean to coerce one side?" hint, but for + the milestone scope the diagnostic is sufficient. + +## Recommendation summary + +| Finding | Class | Action | +|---|---|---| +| B1 — Pattern::Lit::Float not rejected | bug | `debug` — RED test pinning the missing `CheckError::FloatPatternNotAllowed`, then fix in `crates/ailang-check/`. Alternative: if option (b) is now preferred, run `brainstorm` to amend DESIGN.md and JOURNAL. | +| W1 — `float_to_str` deferred-codegen diagnostic | working | carry-on | +| W2 — `is_nan` discoverability | working | carry-on | +| F1 — `io/print_float` strips `.0` | friction | `plan` (tidy iteration) — switch to shortest-round-trippable, or document `%g` contract in DESIGN.md | +| G1 — NaN sign-bit print form unspecified | spec_gap | `tighten DESIGN.md` — one sentence in Unspecified-list, OR runtime normalisation + Guaranteed-list addition | +| W3 — mixed Int+Float diagnostic | working | carry-on | diff --git a/examples/fieldtest/floats_1_newton_sqrt.ail.json b/examples/fieldtest/floats_1_newton_sqrt.ail.json new file mode 100644 index 0000000..43e75fd --- /dev/null +++ b/examples/fieldtest/floats_1_newton_sqrt.ail.json @@ -0,0 +1 @@ +{"defs":[{"body":{"cond":{"args":[{"name":"k","t":"var"},{"lit":{"kind":"int","value":0},"t":"lit"}],"fn":{"name":"==","t":"var"},"t":"app"},"else":{"args":[{"name":"n","t":"var"},{"args":[{"lit":{"bits":"3fe0000000000000","kind":"float"},"t":"lit"},{"args":[{"name":"x","t":"var"},{"args":[{"name":"n","t":"var"},{"name":"x","t":"var"}],"fn":{"name":"/","t":"var"},"t":"app"}],"fn":{"name":"+","t":"var"},"t":"app"}],"fn":{"name":"*","t":"var"},"t":"app"},{"args":[{"name":"k","t":"var"},{"lit":{"kind":"int","value":1},"t":"lit"}],"fn":{"name":"-","t":"var"},"t":"app"}],"fn":{"name":"newton_iter","t":"var"},"t":"app","tail":true},"t":"if","then":{"name":"x","t":"var"}},"doc":"Recurse k times applying x' = 0.5 * (x + n/x).","kind":"fn","name":"newton_iter","params":["n","x","k"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"Float"},{"k":"con","name":"Float"},{"k":"con","name":"Int"}],"ret":{"k":"con","name":"Float"}}},{"body":{"args":[{"name":"n","t":"var"},{"name":"n","t":"var"},{"lit":{"kind":"int","value":20},"t":"lit"}],"fn":{"name":"newton_iter","t":"var"},"t":"app"},"doc":"20-step Newton iteration starting from x0 = n.","kind":"fn","name":"sqrt","params":["n"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"Float"}],"ret":{"k":"con","name":"Float"}}},{"body":{"args":[{"args":[{"lit":{"bits":"4000000000000000","kind":"float"},"t":"lit"}],"fn":{"name":"sqrt","t":"var"},"t":"app"}],"op":"io/print_float","t":"do"},"kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[],"name":"floats_1_newton_sqrt","schema":"ailang/v0"} \ No newline at end of file diff --git a/examples/fieldtest/floats_1_newton_sqrt.ailx b/examples/fieldtest/floats_1_newton_sqrt.ailx new file mode 100644 index 0000000..1d11b27 --- /dev/null +++ b/examples/fieldtest/floats_1_newton_sqrt.ailx @@ -0,0 +1,38 @@ +; Fieldtest — Floats milestone, axis 1: numerical computation. +; +; Newton's method for sqrt(N): x_{k+1} = 0.5 * (x_k + N / x_k). +; Iterate a fixed 20 times — well past convergence for double-precision +; from a starting guess of N (always positive). Prints the result of +; sqrt(2.0). Expected stdout (one line): approximately 1.41421356... +; +; Exercises: Float literals, polymorphic +, *, /, recursion with +; Float-typed accumulator, io/print_float. + +(module floats_1_newton_sqrt + + (fn newton_iter + (doc "Recurse k times applying x' = 0.5 * (x + n/x).") + (type + (fn-type + (params (con Float) (con Float) (con Int)) + (ret (con Float)))) + (params n x k) + (body + (if (app == k 0) + x + (tail-app newton_iter + n + (app * 0.5 (app + x (app / n x))) + (app - k 1))))) + + (fn sqrt + (doc "20-step Newton iteration starting from x0 = n.") + (type (fn-type (params (con Float)) (ret (con Float)))) + (params n) + (body (app newton_iter n n 20))) + + (fn main + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body + (do io/print_float (app sqrt 2.0))))) diff --git a/examples/fieldtest/floats_2_average_int_list.ail.json b/examples/fieldtest/floats_2_average_int_list.ail.json new file mode 100644 index 0000000..0041518 --- /dev/null +++ b/examples/fieldtest/floats_2_average_int_list.ail.json @@ -0,0 +1 @@ +{"defs":[{"ctors":[{"fields":[],"name":"Nil"},{"fields":[{"k":"con","name":"Int"},{"k":"con","name":"IntList"}],"name":"Cons"}],"kind":"type","name":"IntList"},{"body":{"arms":[{"body":{"name":"acc","t":"var"},"pat":{"ctor":"Nil","fields":[],"p":"ctor"}},{"body":{"args":[{"name":"t","t":"var"},{"args":[{"name":"acc","t":"var"},{"name":"h","t":"var"}],"fn":{"name":"+","t":"var"},"t":"app"}],"fn":{"name":"sum_acc","t":"var"},"t":"app","tail":true},"pat":{"ctor":"Cons","fields":[{"name":"h","p":"var"},{"name":"t","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"doc":"Tail-recursive sum with accumulator.","kind":"fn","name":"sum_acc","params":["xs","acc"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"IntList"},{"k":"con","name":"Int"}],"ret":{"k":"con","name":"Int"}}},{"body":{"arms":[{"body":{"name":"acc","t":"var"},"pat":{"ctor":"Nil","fields":[],"p":"ctor"}},{"body":{"args":[{"name":"t","t":"var"},{"args":[{"name":"acc","t":"var"},{"lit":{"kind":"int","value":1},"t":"lit"}],"fn":{"name":"+","t":"var"},"t":"app"}],"fn":{"name":"count_acc","t":"var"},"t":"app","tail":true},"pat":{"ctor":"Cons","fields":[{"p":"wild"},{"name":"t","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"doc":"Tail-recursive length with accumulator.","kind":"fn","name":"count_acc","params":["xs","acc"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"IntList"},{"k":"con","name":"Int"}],"ret":{"k":"con","name":"Int"}}},{"body":{"args":[{"args":[{"args":[{"name":"xs","t":"var"},{"lit":{"kind":"int","value":0},"t":"lit"}],"fn":{"name":"sum_acc","t":"var"},"t":"app"}],"fn":{"name":"int_to_float","t":"var"},"t":"app"},{"args":[{"args":[{"name":"xs","t":"var"},{"lit":{"kind":"int","value":0},"t":"lit"}],"fn":{"name":"count_acc","t":"var"},"t":"app"}],"fn":{"name":"int_to_float","t":"var"},"t":"app"}],"fn":{"name":"/","t":"var"},"t":"app"},"doc":"Mean as Float = sum / count, both promoted via int_to_float.","kind":"fn","name":"mean","params":["xs"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"IntList"}],"ret":{"k":"con","name":"Float"}}},{"body":{"args":[{"args":[{"args":[{"lit":{"kind":"int","value":1},"t":"lit"},{"args":[{"lit":{"kind":"int","value":2},"t":"lit"},{"args":[{"lit":{"kind":"int","value":3},"t":"lit"},{"args":[{"lit":{"kind":"int","value":4},"t":"lit"},{"args":[{"lit":{"kind":"int","value":6},"t":"lit"},{"args":[],"ctor":"Nil","t":"ctor","type":"IntList"}],"ctor":"Cons","t":"ctor","type":"IntList"}],"ctor":"Cons","t":"ctor","type":"IntList"}],"ctor":"Cons","t":"ctor","type":"IntList"}],"ctor":"Cons","t":"ctor","type":"IntList"}],"ctor":"Cons","t":"ctor","type":"IntList"}],"fn":{"name":"mean","t":"var"},"t":"app"}],"op":"io/print_float","t":"do"},"kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[],"name":"floats_2_average_int_list","schema":"ailang/v0"} \ No newline at end of file diff --git a/examples/fieldtest/floats_2_average_int_list.ailx b/examples/fieldtest/floats_2_average_int_list.ailx new file mode 100644 index 0000000..95d0517 --- /dev/null +++ b/examples/fieldtest/floats_2_average_int_list.ailx @@ -0,0 +1,67 @@ +; Fieldtest — Floats milestone, axis 2: Int/Float interop. +; +; Compute the arithmetic mean of an Int list and print it as a Float. +; Sum is Int (no overflow risk for the inputs we use); count is Int; +; the divide must happen in Float space so we get a fractional result. +; +; This is the canonical task that exercises int_to_float at both +; converted operands. The literal `2` cannot be shared with Float +; arithmetic, so the natural shape is: +; (/ (int_to_float sum) (int_to_float count)) +; +; Inputs: [1, 2, 3, 4, 6]; sum=16, count=5; mean=3.2. +; Expected stdout: 3.2 + +(module floats_2_average_int_list + + (data IntList + (ctor Nil) + (ctor Cons (con Int) (con IntList))) + + (fn sum_acc + (doc "Tail-recursive sum with accumulator.") + (type + (fn-type + (params (con IntList) (con Int)) + (ret (con Int)))) + (params xs acc) + (body + (match xs + (case (pat-ctor Nil) acc) + (case (pat-ctor Cons h t) + (tail-app sum_acc t (app + acc h)))))) + + (fn count_acc + (doc "Tail-recursive length with accumulator.") + (type + (fn-type + (params (con IntList) (con Int)) + (ret (con Int)))) + (params xs acc) + (body + (match xs + (case (pat-ctor Nil) acc) + (case (pat-ctor Cons _ t) + (tail-app count_acc t (app + acc 1)))))) + + (fn mean + (doc "Mean as Float = sum / count, both promoted via int_to_float.") + (type (fn-type (params (con IntList)) (ret (con Float)))) + (params xs) + (body + (app / + (app int_to_float (app sum_acc xs 0)) + (app int_to_float (app count_acc xs 0))))) + + (fn main + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body + (do io/print_float + (app mean + (term-ctor IntList Cons 1 + (term-ctor IntList Cons 2 + (term-ctor IntList Cons 3 + (term-ctor IntList Cons 4 + (term-ctor IntList Cons 6 + (term-ctor IntList Nil))))))))))) diff --git a/examples/fieldtest/floats_3_safe_division.ail.json b/examples/fieldtest/floats_3_safe_division.ail.json new file mode 100644 index 0000000..1db2cff --- /dev/null +++ b/examples/fieldtest/floats_3_safe_division.ail.json @@ -0,0 +1 @@ +{"defs":[{"body":{"cond":{"args":[{"name":"x","t":"var"}],"fn":{"name":"is_nan","t":"var"},"t":"app"},"else":{"cond":{"args":[{"name":"x","t":"var"},{"lit":{"bits":"7fe1ccf385ebc8a0","kind":"float"},"t":"lit"}],"fn":{"name":">","t":"var"},"t":"app"},"else":{"cond":{"args":[{"name":"x","t":"var"},{"lit":{"bits":"ffe1ccf385ebc8a0","kind":"float"},"t":"lit"}],"fn":{"name":"<","t":"var"},"t":"app"},"else":{"lit":{"kind":"int","value":1},"t":"lit"},"t":"if","then":{"lit":{"kind":"int","value":0},"t":"lit"}},"t":"if","then":{"lit":{"kind":"int","value":0},"t":"lit"}},"t":"if","then":{"lit":{"kind":"int","value":-1},"t":"lit"}},"doc":"-1=NaN, 0=infinite, 1=finite. Uses is_nan + abs > huge.","kind":"fn","name":"classify","params":["x"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"Float"}],"ret":{"k":"con","name":"Int"}}},{"body":{"lhs":{"args":[{"args":[{"lit":{"bits":"4018000000000000","kind":"float"},"t":"lit"},{"lit":{"bits":"4008000000000000","kind":"float"},"t":"lit"}],"fn":{"name":"/","t":"var"},"t":"app"}],"op":"io/print_float","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"args":[{"lit":{"bits":"4018000000000000","kind":"float"},"t":"lit"},{"lit":{"bits":"4008000000000000","kind":"float"},"t":"lit"}],"fn":{"name":"/","t":"var"},"t":"app"}],"fn":{"name":"classify","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"lit":{"bits":"3ff0000000000000","kind":"float"},"t":"lit"},{"lit":{"bits":"0000000000000000","kind":"float"},"t":"lit"}],"fn":{"name":"/","t":"var"},"t":"app"}],"op":"io/print_float","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"args":[{"lit":{"bits":"3ff0000000000000","kind":"float"},"t":"lit"},{"lit":{"bits":"0000000000000000","kind":"float"},"t":"lit"}],"fn":{"name":"/","t":"var"},"t":"app"}],"fn":{"name":"classify","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"lit":{"bits":"0000000000000000","kind":"float"},"t":"lit"},{"lit":{"bits":"0000000000000000","kind":"float"},"t":"lit"}],"fn":{"name":"/","t":"var"},"t":"app"}],"op":"io/print_float","t":"do"},"rhs":{"args":[{"args":[{"args":[{"lit":{"bits":"0000000000000000","kind":"float"},"t":"lit"},{"lit":{"bits":"0000000000000000","kind":"float"},"t":"lit"}],"fn":{"name":"/","t":"var"},"t":"app"}],"fn":{"name":"classify","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"t":"seq"},"t":"seq"},"t":"seq"},"t":"seq"},"t":"seq"},"kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[],"name":"floats_3_safe_division","schema":"ailang/v0"} \ No newline at end of file diff --git a/examples/fieldtest/floats_3_safe_division.ailx b/examples/fieldtest/floats_3_safe_division.ailx new file mode 100644 index 0000000..29512e0 --- /dev/null +++ b/examples/fieldtest/floats_3_safe_division.ailx @@ -0,0 +1,55 @@ +; Fieldtest — Floats milestone, axis 3: NaN / Inf / is_nan handling. +; +; safe_div(a, b) returns a/b when b != 0; falls through to the IEEE +; result otherwise. The fixture exercises four cases: +; 1) 6.0 / 3.0 -> 2.0 (normal) +; 2) 1.0 / 0.0 -> +inf (use is_nan to confirm finite-vs-NaN) +; 3) 0.0 / 0.0 -> NaN (is_nan should report true) +; 4) (- 1.0 1.0) / 0.0 -> NaN (subexpr drives same) +; +; classify(x) returns: +; -1 if x is NaN +; 0 if x is +inf or -inf (we test via x > / x < -) +; 1 otherwise +; +; The subtle point: the IEEE-correct way to test for NaN is `is_nan`, +; NOT `(== x x)` (which is false for NaN — but the LLM author who +; reaches for `==` first will get the right answer by accident here, +; only because the natural reading of the operator doesn't apply). +; The DESIGN.md says explicitly to use `is_nan`. +; +; Expected stdout (one per line): +; 2.0 ; 6/3 +; 1 ; classify(2.0) -> normal +; inf ; 1/0 +; 0 ; classify(1/0) -> infinite +; nan ; 0/0 +; -1 ; classify(0/0) -> NaN +; +; (`io/print_float` prints "%g\n", so inf prints as "inf", NaN as "nan".) + +(module floats_3_safe_division + + (fn classify + (doc "-1=NaN, 0=infinite, 1=finite. Uses is_nan + abs > huge.") + (type (fn-type (params (con Float)) (ret (con Int)))) + (params x) + (body + (if (app is_nan x) + -1 + (if (app > x 1.0e308) + 0 + (if (app < x -1.0e308) + 0 + 1))))) + + (fn main + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body + (seq (do io/print_float (app / 6.0 3.0)) + (seq (do io/print_int (app classify (app / 6.0 3.0))) + (seq (do io/print_float (app / 1.0 0.0)) + (seq (do io/print_int (app classify (app / 1.0 0.0))) + (seq (do io/print_float (app / 0.0 0.0)) + (do io/print_int (app classify (app / 0.0 0.0))))))))))) diff --git a/examples/fieldtest/floats_4_float_to_str_reach.ail.json b/examples/fieldtest/floats_4_float_to_str_reach.ail.json new file mode 100644 index 0000000..6e97ec5 --- /dev/null +++ b/examples/fieldtest/floats_4_float_to_str_reach.ail.json @@ -0,0 +1 @@ +{"defs":[{"body":{"args":[{"args":[{"lit":{"bits":"40091eb851eb851f","kind":"float"},"t":"lit"}],"fn":{"name":"float_to_str","t":"var"},"t":"app"}],"op":"io/print_str","t":"do"},"kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[],"name":"floats_4_float_to_str_reach","schema":"ailang/v0"} \ No newline at end of file diff --git a/examples/fieldtest/floats_4_float_to_str_reach.ailx b/examples/fieldtest/floats_4_float_to_str_reach.ailx new file mode 100644 index 0000000..615031a --- /dev/null +++ b/examples/fieldtest/floats_4_float_to_str_reach.ailx @@ -0,0 +1,26 @@ +; Fieldtest — Floats milestone, axis 4: reach for float_to_str. +; +; Natural task: build a label like "result = " by concatenating +; a Str prefix with the float's text. The LLM-author who looks at +; `ail builtins` sees `float_to_str : (Float) -> Str`, reaches for it, +; and gets a codegen-deferred error per DESIGN.md §"Float semantics": +; +; `float_to_str` (Float → Str) is type-installed but codegen- +; deferred to a follow-up milestone: ... Calling it typechecks but +; produces a structured `CodegenError::Internal`. +; +; This fixture pins the reach-and-bounce. Expected: `ail check` +; succeeds; `ail build` fails with a clear, structured codegen error +; that names `float_to_str` and points at the missing runtime path. +; +; (No string-concat builtin exists either; we just print the result of +; float_to_str directly via io/print_str. The first failure point is +; codegen, before any concat would be needed.) + +(module floats_4_float_to_str_reach + + (fn main + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body + (do io/print_str (app float_to_str 3.14)))))