fieldtest(series-step1): 5 examples, 5 findings (1 bug, 1 friction, 1 spec-gap)
Post-ship field test of the shipped `series` library extension as a downstream Form-A author working from the public surface only. Five fixtures under examples/fieldtest/ across the construction/auto-import, ring-buffer/financial-indexing, len/total_count bookkeeping, param-in error-quality, and operator-naming axes; spec at docs/specs/0067-fieldtest-series-step1.md. Wins (carry-on): auto-import works with no `(import series)`; the financial index (0 = newest) maps directly onto the bounded-buffer mental model and stays correct after multiple wraps; the param-in diagnostic for a disallowed element type names the type, the type-variable, the qualified type, and the allowed set. Findings routed: - [bug] An owned ADT threaded through tail-recursion whose base case neither returns nor consumes it is never dropped — a general drop-soundness leak (a plain `Box` control with no Series/RawBuf also leaks). Series surfaces it heavily on the whitepaper's headline streaming pattern, and the committed reference examples/series_sma.ail itself leaks (live=8). Minimal repro: series-step1_5 (live=4). - [friction] `ail describe ... Series.push` (the canonical type-scoped form) reports module-not-found; only `series.push` / bare `push` resolve, and no command lists a type's op set. - [spec_gap] referencing a kernel base type (`RawBuf`) from a user ADT ctor field requires full qualification (`raw_buf.RawBuf`) while bare `Series` works in type-position via auto-import — the asymmetry is undocumented. The fixtures that leak still produce correct output; the leak is a separate observable under AILANG_RC_STATS and an orthogonal, pre-existing bug, not a Series defect. refs #61
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
# Fieldtest — series-step1 — 2026-06-02
|
||||
|
||||
**Status:** Draft — awaiting orchestrator triage
|
||||
**Author:** fieldtester (dispatched by fieldtest skill)
|
||||
|
||||
## Scope
|
||||
|
||||
The `Series a` kernel-tier library extension shipped at `35a9bc6`
|
||||
(model-0007 reconciliation at `73fbb24`). It is a bounded ring buffer
|
||||
with financial-style indexing (index 0 = newest), built as PURE AILang
|
||||
over `RawBuf` — zero Rust intercepts, zero `(intrinsic)` markers. The
|
||||
element type is restricted to `{Int, Float}` via `param-in` (`Bool`
|
||||
deferred, #61). It is auto-imported like a built-in (no `(import series)`
|
||||
needed). Public type-scoped API: `(new Series (con T) lookback)`,
|
||||
`Series.push : (own (Series a), own a) -> own (Series a)`,
|
||||
`Series.at : (borrow (Series a), Int) -> own a` (0 = newest),
|
||||
`Series.len : (borrow (Series a)) -> Int` (live count, saturates at
|
||||
lookback), `Series.total_count : (borrow (Series a)) -> Int` (monotone
|
||||
total pushes).
|
||||
|
||||
This field test probed five axes: construction/auto-import discovery,
|
||||
ring-buffer + financial-indexing ergonomics, len/total_count bookkeeping
|
||||
with warmup, `param-in` error quality, and operator-naming friction.
|
||||
|
||||
The build exercised was the HEAD working tree, invoked via
|
||||
`cargo run -q --bin ail -- build --alloc=rc <fixture>` (debug profile);
|
||||
each produced native binary was then run directly, and re-run under
|
||||
`AILANG_RC_STATS=1` for drop soundness.
|
||||
|
||||
## Examples
|
||||
|
||||
### examples/fieldtest/series-step1_1_rolling_max.ail — rolling max over the last-3 window
|
||||
- Streams 6 Ints into a size-3 Series and, after each push, prints the
|
||||
max of the live window by scanning `Series.at s i` for `i` in
|
||||
`[0, Series.len s)`.
|
||||
- Fits the ring-buffer/financial-indexing + warmup axes: it proves
|
||||
eviction (a large old value scrolled out of the window stops winning)
|
||||
and handles warmup by bounding the scan at the live count.
|
||||
- Outcome: checked, built, ran. Stdout `4 4 7 7 7 3` — exactly expected,
|
||||
including the eviction proof (max drops from 7 to 3 when 7 is evicted).
|
||||
**Leaks `live=8` under `AILANG_RC_STATS` (see finding [bug]).**
|
||||
|
||||
### examples/fieldtest/series-step1_2_last_n_events.ail — last-N events ring, oldest-first dump
|
||||
- Pushes 7 Ints into a size-3 Series, then dumps the live window
|
||||
oldest-first by walking `Series.at` from `len-1` down to `0`, and
|
||||
reports `total_count` (7) and `len` (3).
|
||||
- Fits the wrap-boundary off-by-one axis (the buffer wrapped 2+ times
|
||||
before the dump) and the len-vs-total_count bookkeeping axis.
|
||||
- Outcome: checked, built, ran. Stdout `50 60 70 7 3` — correct;
|
||||
financial indexing maps correctly after multiple wraps; `total_count`
|
||||
did not saturate, `len` did. **Leak-clean (`live=0`).**
|
||||
|
||||
### examples/fieldtest/series-step1_3_warmup_rolling_sum.ail — Float warmup-gated rolling sum
|
||||
- Streams 6 Floats into a size-4 Series; emits the rolling sum only once
|
||||
`Series.len s == 4` (warmup suppressed for the first 3 pushes).
|
||||
- Fits the Float-element + warmup/bookkeeping axes; pins the reading that
|
||||
"full window" is gated on the saturating `len`, not `total_count`.
|
||||
- Outcome: checked, built, ran. Stdout `20.0 28.0 36.0` — correct, with
|
||||
warmup suppressed and correct eviction. **Leaks `live=8` (same bug).**
|
||||
|
||||
### examples/fieldtest/series-step1_4_paramin_str_reject.ail — param-in Str element (MUST FAIL)
|
||||
- A single push of a `Str` into `(con Series (con Str))`. The only defect
|
||||
is the disallowed element type.
|
||||
- Fits the `param-in` error-quality axis.
|
||||
- Outcome: `ail check` rejects with exit code 1 and a high-quality
|
||||
diagnostic (see finding [working]). A `(con Bool)` probe produced the
|
||||
same clean diagnostic.
|
||||
|
||||
### examples/fieldtest/series-step1_5_drop_leak_repro.ail — minimal drop-leak reproduction
|
||||
- Smallest form of the leak: push two Ints into a size-3 Series via
|
||||
`tail-app` recursion whose base case returns Unit.
|
||||
- Outcome: checked, built, ran. Stdout `done`; **`live=4` under
|
||||
`AILANG_RC_STATS`**. This is the reduced repro for the [bug] below.
|
||||
|
||||
## Findings
|
||||
|
||||
### [working] Construction, auto-import, and financial indexing are correct and ergonomic
|
||||
- Examples 1, 2, 3.
|
||||
- All three reach for `(new Series (con T) lookback)` with **no
|
||||
`(import series)`** — auto-import works as the whitepaper promises.
|
||||
`Series.push` / `Series.at` / `Series.len` / `Series.total_count`
|
||||
resolve type-scoped at the call site with no qualifier. Financial
|
||||
indexing (0 = newest) is intuitive: walking `[0, len)` gives newest→
|
||||
oldest, walking `len-1` down to `0` gives oldest→newest, and the index
|
||||
maps correctly even after the buffer wraps 2+ times (example 2). All
|
||||
three programs produced exactly the predicted output on the first run.
|
||||
- This is a clear win: the bounded-buffer mental model the LLM author
|
||||
reaches for maps directly onto the API, and eviction/warmup are handled
|
||||
by the `len`/`at` pair without any off-by-one surprise.
|
||||
- Recommended downstream action: carry-on.
|
||||
|
||||
### [working] `param-in` diagnostic is exactly what the ledger promised
|
||||
- Example 4 (plus a `(con Bool)` probe).
|
||||
- Verbatim:
|
||||
`error: [param-not-in-restricted-set] main: type-arg `Str` is not in
|
||||
the restricted set for type-variable `a` of `series.Series` (allowed:
|
||||
one of {Float, Int})`. Exit code 1. The `Bool` element produced the
|
||||
identical shape with `Bool` substituted.
|
||||
- Names the offending type, the type-variable, the fully-qualified type
|
||||
`series.Series`, and the allowed set — matching the
|
||||
`ParamNotInRestrictedSet` contract in model 0007 §param-in. Actionable
|
||||
and precise.
|
||||
- Recommended downstream action: carry-on.
|
||||
|
||||
### [bug] Owned Series threaded through a tail-recursive stream loop leaks the RawBuf slab
|
||||
- Examples 1, 3, 5; also the committed reference `examples/series_sma.ail`
|
||||
(`live=8`).
|
||||
- Under `AILANG_RC_STATS=1`, the canonical streaming pattern the
|
||||
whitepaper itself uses leaks heap allocations:
|
||||
- `series_sma` (committed reference): `allocs=19 frees=11 live=8`
|
||||
- `series-step1_1_rolling_max`: `live=8`
|
||||
- `series-step1_3_warmup_rolling_sum`: `live=8`
|
||||
- `series-step1_5_drop_leak_repro` (minimal): `allocs=7 frees=3 live=4`
|
||||
- The trigger is an `(own (con Series ...))` value threaded through
|
||||
`tail-app` recursion whose base case returns a non-Series type (Unit)
|
||||
and does not explicitly consume the owned Series. The intermediate
|
||||
push results and/or the final Series alive at the base-case arm are
|
||||
never dropped. A `let s_new (Series.push s v)` binding adds to the leak
|
||||
vs. passing the push directly as the tail-app argument
|
||||
(`live=4` vs `live=2` in the minimal probe).
|
||||
- **Not Series-specific.** A control probe wrapping a plain heap
|
||||
`IntList` in a user `Box` ADT and threading it through the identical
|
||||
tail-recursion pattern also leaks (`live=7`). The root is general
|
||||
drop soundness for an owned heap value reaching a base case that
|
||||
neither returns nor consumes it; Series merely surfaces it heavily
|
||||
(RawBuf slab + wrapper) and on the whitepaper's headline pattern. The
|
||||
`Series` module source itself is pure AILang and behaves correctly —
|
||||
example 2 (Series *returned* from the recursion and dropped at the
|
||||
caller's scope close) is `live=0`.
|
||||
- One-line repro:
|
||||
`ail build --alloc=rc examples/fieldtest/series-step1_5_drop_leak_repro.ail -o /tmp/r && AILANG_RC_STATS=1 /tmp/r`
|
||||
→ `ailang_rc_stats: allocs=7 frees=3 live=4`.
|
||||
- Recommended downstream action: debug (RED-first). Note for the
|
||||
debugger: probe whether this is the same own-param-at-non-consuming-
|
||||
base-case drop family seen in prior cutover work, not a Series codegen
|
||||
issue. The contradiction with model 0007's "dropping the Window frees
|
||||
the slab / leak-clean" claim should be resolved by fixing the drop,
|
||||
not by softening the doc.
|
||||
|
||||
### [friction] `ail describe` does not resolve the canonical type-scoped `Series.<op>` form
|
||||
- Discovery axis (probed via the CLI, not a fixture).
|
||||
- `ail describe --workspace examples/series_sma.ail Series.push` and
|
||||
`... Series.at` both fail with `Error: no module `Series` in workspace`.
|
||||
The same ops resolve via the module-scoped `series.push` or the bare
|
||||
`push`. `describe --workspace ... Series` prints only the `Series`
|
||||
TypeDef (with `param-in`), not its operations.
|
||||
- This is friction, not a bug against a written contract: model 0007 §1
|
||||
declares type-scoped access the *canonical* form for type-associated
|
||||
operations and names `TypeScopedReceiverNotAType` for a bad receiver —
|
||||
but `describe` treats `Series.push` as a module path and reports a
|
||||
module-not-found error even though `Series` IS a known type. A
|
||||
downstream LLM author who has just written `(app Series.push s v)` and
|
||||
wants to confirm its signature will type `describe ... Series.push`
|
||||
first (the canonical form) and hit a dead end, then has to know to fall
|
||||
back to `series.push` or bare `push`. Discovery of the op *set* for a
|
||||
type is also absent: neither `describe Series` nor `builtins` lists the
|
||||
five Series ops (`builtins` covers only the 20 primitive operations).
|
||||
- Recommended downstream action: plan (tidy iteration) — make
|
||||
`describe`'s name resolver try the type-scoped branch (mirroring the
|
||||
call-site resolver in model 0007 §1), and consider surfacing a type's
|
||||
home-module ops when `describe`-ing the type itself.
|
||||
|
||||
### [spec_gap] Whitepaper §Series substrate writes a ctor-field mode that does not parse; correct field form for a Series-like wrapper is undocumented
|
||||
- Surfaced while drafting (carried over from the raw-buf fieldtest
|
||||
`examples/fieldtest/rbx_1_float_window_stats.ail`, re-confirmed this
|
||||
cycle as still the live behaviour).
|
||||
- Model 0007 §"Series — the library extension" shows the storage ctor
|
||||
field as `(con RawBuf a)` with a comment "no mode: modes live on fn
|
||||
params/ret, not on ADT fields" — which is correct and parses. But the
|
||||
two-tier §"Library extensions" prose and the §Series-substrate framing
|
||||
elsewhere lead an author to try `(own (RawBuf a))` in a ctor field (a
|
||||
surface parse error) or a bare unqualified `(con RawBuf (con T))` (a
|
||||
type-mismatch: `RawBuf` vs `raw_buf.RawBuf`) when writing their own
|
||||
Series-like wrapper ADT. The only form that checks for a *user* module
|
||||
is the fully-qualified `(con raw_buf.RawBuf (con T))`.
|
||||
- The reading I picked: a downstream author wrapping a kernel base type
|
||||
in their own ADT field must fully-qualify it (`raw_buf.RawBuf`), even
|
||||
though the *kernel-tier* `series` module uses the bare `a`-parametrised
|
||||
`(con RawBuf a)` form (which works because `series` is itself
|
||||
kernel-tier and the element is a type variable, not a concrete bare
|
||||
ctor ref). Another equally plausible reading: bare `(con RawBuf (con T))`
|
||||
should resolve via the same kernel auto-import that makes
|
||||
`(con Series (con Float))` work bare at a call site — the asymmetry
|
||||
(bare `Series` type-position OK, bare `RawBuf` ctor-field not OK) is
|
||||
not stated anywhere in the public surface.
|
||||
- Recommended downstream action: ratify / tighten the design ledger —
|
||||
document, in model 0007 or contract 0002, the rule for referencing a
|
||||
kernel base type from a user ADT ctor field (must fully-qualify; bare
|
||||
works only inside a kernel-tier module or in type-position via
|
||||
auto-import), so the asymmetry is no longer a guess.
|
||||
|
||||
## Recommendation summary
|
||||
|
||||
| Finding | Class | Action |
|
||||
|---|---|---|
|
||||
| Construction / auto-import / financial indexing correct | working | carry-on |
|
||||
| `param-in` diagnostic quality | working | carry-on |
|
||||
| Owned Series tail-recursion leaks RawBuf slab | bug | debug |
|
||||
| `describe` does not resolve type-scoped `Series.<op>` | friction | plan |
|
||||
| Whitepaper ctor-field mode / bare-RawBuf-in-field asymmetry | spec_gap | ratify / tighten ledger |
|
||||
@@ -0,0 +1,79 @@
|
||||
; Fieldtest series-step1.1 — ring-buffer + financial-indexing + warmup.
|
||||
;
|
||||
; Task an LLM author is naturally given: "stream a sequence of Int
|
||||
; readings into a window of size 3 and, after each push, print the
|
||||
; maximum of the values currently in the window." This is the canonical
|
||||
; bounded-buffer streaming task: the window only ever holds the last 3
|
||||
; readings, so the running max must respect eviction (a large old value
|
||||
; that has scrolled out of the window must NOT keep winning).
|
||||
;
|
||||
; Exercises:
|
||||
; - (new Series (con Int) 3) + auto-import (no (import series))
|
||||
; - Series.push threading the owned Series through the stream
|
||||
; - Series.at with financial indexing (0 = newest) — we scan i in
|
||||
; [0, Series.len) so warmup (fewer than 3 elements) is handled by
|
||||
; reading only the live count, never a stale slot.
|
||||
; - Series.len as the live-count bound (saturates at lookback=3).
|
||||
;
|
||||
; Stream: 4, 1, 7, 2, 3, 1
|
||||
; after 4 -> window {4} max 4
|
||||
; after 1 -> window {4,1} max 4
|
||||
; after 7 -> window {4,1,7} max 7
|
||||
; after 2 -> window {1,7,2} max 7 (4 evicted)
|
||||
; after 3 -> window {7,2,3} max 7
|
||||
; after 1 -> window {2,3,1} max 3 (7 evicted) <-- the eviction proof
|
||||
; Expected stdout (one per line): 4 4 7 7 7 3
|
||||
|
||||
(module series-step1_1_rolling_max
|
||||
|
||||
(data IntList
|
||||
(ctor INil)
|
||||
(ctor ICons (con Int) (con IntList)))
|
||||
|
||||
; Max over the live window: scan indices 0..len, return the largest.
|
||||
(fn window_max_step
|
||||
(type (fn-type
|
||||
(params (borrow (con Series (con Int))) (own (con Int)) (own (con Int)) (own (con Int)))
|
||||
(ret (own (con Int)))))
|
||||
(params s len i acc)
|
||||
(body
|
||||
(if (app eq i len)
|
||||
acc
|
||||
(tail-app window_max_step s len
|
||||
(app + i 1)
|
||||
(if (app gt (app Series.at s i) acc) (app Series.at s i) acc)))))
|
||||
|
||||
(fn window_max
|
||||
(type (fn-type
|
||||
(params (borrow (con Series (con Int))))
|
||||
(ret (own (con Int)))))
|
||||
(params s)
|
||||
(body (app window_max_step s (app Series.len s) 0 0)))
|
||||
|
||||
(fn run_stream
|
||||
(type (fn-type
|
||||
(params (own (con Series (con Int))) (own (con IntList)))
|
||||
(ret (own (con Unit))) (effects IO)))
|
||||
(params s input)
|
||||
(body
|
||||
(match input
|
||||
(case (pat-ctor INil) (do io/print_str ""))
|
||||
(case (pat-ctor ICons v rest)
|
||||
(let s_new (app Series.push s v)
|
||||
(seq
|
||||
(seq (app print (app window_max s_new)) (do io/print_str "\n"))
|
||||
(tail-app run_stream s_new rest)))))))
|
||||
|
||||
(fn main
|
||||
(type (fn-type (params) (ret (own (con Unit))) (effects IO)))
|
||||
(params)
|
||||
(body
|
||||
(let s (new Series (con Int) 3)
|
||||
(app run_stream s
|
||||
(term-ctor IntList ICons 4
|
||||
(term-ctor IntList ICons 1
|
||||
(term-ctor IntList ICons 7
|
||||
(term-ctor IntList ICons 2
|
||||
(term-ctor IntList ICons 3
|
||||
(term-ctor IntList ICons 1
|
||||
(term-ctor IntList INil))))))))))))
|
||||
@@ -0,0 +1,75 @@
|
||||
; Fieldtest series-step1.2 — financial-indexing off-by-one at the wrap
|
||||
; boundary + len-vs-total_count bookkeeping.
|
||||
;
|
||||
; Task: "keep the last 3 event codes seen on a stream; after the whole
|
||||
; stream, dump the surviving window oldest-first, then report how many
|
||||
; events were seen in total." This is the classic 'ring of last N' task.
|
||||
;
|
||||
; The interesting part is the DUMP: index 0 is the newest, so to print
|
||||
; oldest-first I walk i from (len - 1) down to 0. After the buffer has
|
||||
; wrapped more than once (total >> lookback), the financial index must
|
||||
; still map correctly onto the underlying slot — this is the wrap-boundary
|
||||
; off-by-one trap the axis calls out.
|
||||
;
|
||||
; total_count is the monotone "events ever seen" (does NOT saturate);
|
||||
; len is the live window size (saturates at lookback=3). The task needs
|
||||
; BOTH: len to bound the dump walk, total_count for the summary.
|
||||
;
|
||||
; Stream of 7 events: 10 20 30 40 50 60 70 (lookback 3)
|
||||
; After all 7 pushes the window holds the newest 3: 50 60 70.
|
||||
; Dumped oldest-first: 50, 60, 70.
|
||||
; total_count = 7, len = 3.
|
||||
; Expected stdout (one per line): 50 60 70 then "total=" path -> 7 and 3.
|
||||
; Concretely the lines are: 50 60 70 7 3
|
||||
|
||||
(module series-step1_2_last_n_events
|
||||
|
||||
(data IntList
|
||||
(ctor INil)
|
||||
(ctor ICons (con Int) (con IntList)))
|
||||
|
||||
; Push every element of the list into the series, returning the series.
|
||||
(fn fill
|
||||
(type (fn-type
|
||||
(params (own (con Series (con Int))) (own (con IntList)))
|
||||
(ret (own (con Series (con Int))))))
|
||||
(params s input)
|
||||
(body
|
||||
(match input
|
||||
(case (pat-ctor INil) s)
|
||||
(case (pat-ctor ICons v rest)
|
||||
(tail-app fill (app Series.push s v) rest)))))
|
||||
|
||||
; Print the live window oldest-first: walk i from (len-1) down to 0.
|
||||
; index 0 = newest, index (len-1) = oldest live element.
|
||||
(fn dump_step
|
||||
(type (fn-type
|
||||
(params (borrow (con Series (con Int))) (own (con Int)))
|
||||
(ret (own (con Unit))) (effects IO)))
|
||||
(params s i)
|
||||
(body
|
||||
(if (app lt i 0)
|
||||
(do io/print_str "")
|
||||
(seq
|
||||
(seq (app print (app Series.at s i)) (do io/print_str "\n"))
|
||||
(tail-app dump_step s (app - i 1))))))
|
||||
|
||||
(fn main
|
||||
(type (fn-type (params) (ret (own (con Unit))) (effects IO)))
|
||||
(params)
|
||||
(body
|
||||
(let s0 (new Series (con Int) 3)
|
||||
(let s (app fill s0
|
||||
(term-ctor IntList ICons 10
|
||||
(term-ctor IntList ICons 20
|
||||
(term-ctor IntList ICons 30
|
||||
(term-ctor IntList ICons 40
|
||||
(term-ctor IntList ICons 50
|
||||
(term-ctor IntList ICons 60
|
||||
(term-ctor IntList ICons 70
|
||||
(term-ctor IntList INil)))))))))
|
||||
(seq
|
||||
(app dump_step s (app - (app Series.len s) 1))
|
||||
(seq
|
||||
(seq (app print (app Series.total_count s)) (do io/print_str "\n"))
|
||||
(seq (app print (app Series.len s)) (do io/print_str "\n")))))))))
|
||||
@@ -0,0 +1,82 @@
|
||||
; Fieldtest series-step1.3 — Float element + warmup-aware emission using
|
||||
; the len/total_count bookkeeping distinction.
|
||||
;
|
||||
; Task: "compute a rolling sum over a window of 4 Float samples, but only
|
||||
; emit a value once the window is actually full (warmup suppressed)." A
|
||||
; downstream author reaches for the bookkeeping fields to gate emission:
|
||||
; emit iff the live count has reached the lookback. This is the warmup
|
||||
; pattern, but gated on `Series.len` (the saturating live count) rather
|
||||
; than `Series.total_count` (the monotone total) — both are offered and
|
||||
; the author must pick the right one.
|
||||
;
|
||||
; Using `len` as the gate means: emit the first time len hits 4, and on
|
||||
; every push after that (len stays saturated at 4). Using total_count
|
||||
; would behave identically here BUT the author who wants "full window"
|
||||
; semantics should reach for len; this example pins that reading.
|
||||
;
|
||||
; Stream: 2.0 4.0 6.0 8.0 10.0 12.0 (lookback 4)
|
||||
; push 2.0 len 1 (warmup, suppressed)
|
||||
; push 4.0 len 2 (warmup, suppressed)
|
||||
; push 6.0 len 3 (warmup, suppressed)
|
||||
; push 8.0 len 4 -> sum {2,4,6,8} = 20.0
|
||||
; push 10.0 len 4 -> sum {4,6,8,10} = 28.0 (2.0 evicted)
|
||||
; push 12.0 len 4 -> sum {6,8,10,12} = 36.0 (4.0 evicted)
|
||||
; Expected stdout (one per line): 20.0 28.0 36.0
|
||||
|
||||
(module series-step1_3_warmup_rolling_sum
|
||||
|
||||
(data FloatList
|
||||
(ctor FNil)
|
||||
(ctor FCons (con Float) (con FloatList)))
|
||||
|
||||
(fn sum_window_step
|
||||
(type (fn-type
|
||||
(params (borrow (con Series (con Float))) (own (con Int)) (own (con Int)) (own (con Float)))
|
||||
(ret (own (con Float)))))
|
||||
(params s len i acc)
|
||||
(body
|
||||
(if (app eq i len)
|
||||
acc
|
||||
(tail-app sum_window_step s len
|
||||
(app + i 1)
|
||||
(app + acc (app Series.at s i))))))
|
||||
|
||||
; Emit the rolling sum only once the window is full (len == lookback=4).
|
||||
(fn emit_if_full
|
||||
(type (fn-type
|
||||
(params (borrow (con Series (con Float))))
|
||||
(ret (own (con Unit))) (effects IO)))
|
||||
(params s)
|
||||
(body
|
||||
(if (app eq (app Series.len s) 4)
|
||||
(seq (app print (app sum_window_step s (app Series.len s) 0 0.0))
|
||||
(do io/print_str "\n"))
|
||||
(do io/print_str ""))))
|
||||
|
||||
(fn run_stream
|
||||
(type (fn-type
|
||||
(params (own (con Series (con Float))) (own (con FloatList)))
|
||||
(ret (own (con Unit))) (effects IO)))
|
||||
(params s input)
|
||||
(body
|
||||
(match input
|
||||
(case (pat-ctor FNil) (do io/print_str ""))
|
||||
(case (pat-ctor FCons v rest)
|
||||
(let s_new (app Series.push s v)
|
||||
(seq (app emit_if_full s_new)
|
||||
(tail-app run_stream s_new rest)))))))
|
||||
|
||||
(fn main
|
||||
(type (fn-type (params) (ret (own (con Unit))) (effects IO)))
|
||||
(params)
|
||||
(body
|
||||
(let s (new Series (con Float) 4)
|
||||
(app run_stream s
|
||||
(term-ctor FloatList FCons 2.0
|
||||
(term-ctor FloatList FCons 4.0
|
||||
(term-ctor FloatList FCons 6.0
|
||||
(term-ctor FloatList FCons 8.0
|
||||
(term-ctor FloatList FCons 10.0
|
||||
(term-ctor FloatList FCons 12.0
|
||||
(term-ctor FloatList FNil)))))))))))
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
; Fieldtest series-step1.4 (MUST FAIL) — param-in error quality for a
|
||||
; disallowed element type.
|
||||
;
|
||||
; Task framing: an author who has used Series for Int/Float reaches for
|
||||
; "a ring of the last N log lines" — i.e. (con Series (con Str)). The
|
||||
; element type Str is NOT in Series's param-in set {Int, Float}, so this
|
||||
; must be rejected. The axis question is the QUALITY of the diagnostic:
|
||||
; - does it name Series and the offending type Str?
|
||||
; - does it state the allowed set?
|
||||
; - does it point at a location?
|
||||
;
|
||||
; This fixture is otherwise valid: a single push of a Str literal into a
|
||||
; Series of Str. The ONLY defect is the element type.
|
||||
;
|
||||
; Expected: `ail check` rejects. The whitepaper (0007 §param-in) names the
|
||||
; diagnostic ParamNotInRestrictedSet, "naming the offending type and the
|
||||
; allowed set." We record what actually surfaces.
|
||||
|
||||
(module series-step1_4_paramin_str_reject
|
||||
|
||||
(fn main
|
||||
(type (fn-type (params) (ret (own (con Unit))) (effects IO)))
|
||||
(params)
|
||||
(body
|
||||
(let s (new Series (con Str) 3)
|
||||
(let s2 (app Series.push s "hello")
|
||||
(do io/print_str "unreachable\n"))))))
|
||||
@@ -0,0 +1,55 @@
|
||||
; Fieldtest series-step1.5 (LEAK REPRO) — owned Series threaded through a
|
||||
; tail-recursive stream loop is not dropped, leaking the RawBuf slab.
|
||||
;
|
||||
; This is the minimal form of a leak that surfaces on the CANONICAL
|
||||
; streaming pattern the whitepaper itself uses (design/models/0007 §Series,
|
||||
; the SMA worked example, and examples/series_sma.ail): a `run`-style fn
|
||||
; takes (own (con Series ...)) plus a worklist, and on each element does
|
||||
; (Series.push s v) then tail-recurses; the base case returns Unit.
|
||||
;
|
||||
; Under AILANG_RC_STATS the program reports `live > 0` (here live=4):
|
||||
; the intermediate Series values produced by push, and/or the final Series
|
||||
; alive at the base-case arm, are never freed. The committed reference
|
||||
; example examples/series_sma.ail exhibits the same leak (live=8), as do
|
||||
; series-step1_1_rolling_max.ail (live=8) and
|
||||
; series-step1_3_warmup_rolling_sum.ail (live=8).
|
||||
;
|
||||
; The leak is NOT present when the owned Series is RETURNED from the
|
||||
; recursion (ret own Series) and dropped at the final caller's scope close
|
||||
; (see series-step1_2_last_n_events.ail, which is live=0): the trigger is
|
||||
; an owned Series reaching a base case whose return type is not Series and
|
||||
; that does not explicitly consume it.
|
||||
;
|
||||
; This fixture is the smallest reproduction: push two Ints into a size-3
|
||||
; Series via tail recursion, base case prints "done".
|
||||
;
|
||||
; Expected stdout: "done".
|
||||
; Observed under AILANG_RC_STATS: `ailang_rc_stats: allocs=7 frees=3 live=4`.
|
||||
|
||||
(module series-step1_5_drop_leak_repro
|
||||
|
||||
(data IntList
|
||||
(ctor INil)
|
||||
(ctor ICons (con Int) (con IntList)))
|
||||
|
||||
(fn run
|
||||
(type (fn-type
|
||||
(params (own (con Series (con Int))) (own (con IntList)))
|
||||
(ret (own (con Unit))) (effects IO)))
|
||||
(params s input)
|
||||
(body
|
||||
(match input
|
||||
(case (pat-ctor INil) (do io/print_str "done\n"))
|
||||
(case (pat-ctor ICons v rest)
|
||||
(let s_new (app Series.push s v)
|
||||
(tail-app run s_new rest))))))
|
||||
|
||||
(fn main
|
||||
(type (fn-type (params) (ret (own (con Unit))) (effects IO)))
|
||||
(params)
|
||||
(body
|
||||
(let s (new Series (con Int) 3)
|
||||
(app run s
|
||||
(term-ctor IntList ICons 1
|
||||
(term-ctor IntList ICons 2
|
||||
(term-ctor IntList INil))))))))
|
||||
Reference in New Issue
Block a user