Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions projects/aether-lang-c3d8/JOURNAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,34 @@ compile the same optimized core — and the equivalence checks prove it preserve

## Ideas / backlog

### Aether 30.2 — a standard library worth the name (planned + shipped this session)

30.0 and 30.1 made Aether *pleasant to write* (comprehensions, ranges, sections); this makes it
*productive* to write, by giving it the list vocabulary every functional programmer expects. All
fifteen functions are written **in Aether itself** on top of the existing primitives — so they cost
the type checker and the three backends nothing new, and the differential suite covers them exactly
like user code. They pair directly with what just shipped: `sort (concatMap (fn x -> [x, 0 - x]) [1 .. n])`,
`partition (> 0)`, `takeWhile (< limit)`, `zipWith (+)`.

- [x] **Transformers:** `zipWith f xs ys`, `concatMap f xs` (the list-monad bind), `scanl f z xs`
(foldl keeping every running accumulator), `iterate n f x` (a bounded orbit `[x, f x, f (f x), …]`).
- [x] **Splitters:** `takeWhile p`, `dropWhile p`, `span p` (their pair), `partition p` (order
preserved in both halves).
- [x] **Ordering:** `insert x xs` (into a sorted list) and `sort` (ascending insertion sort) — both
polymorphic on the structural `≤`, so `sort ["pear", "apple"]` works as readily as on ints.
- [x] **Reducers:** `product`, `maximum`, `minimum` (partial on `[]`, like `head`), `last`, `init`.
- [x] **Verification:** a new 200-program **stdlib fuzzer** (`stdlibFuzz.ts`) generating random calls
across all eleven order-sensitive combinators, each checked against an **independent TypeScript
reference** *and* the JS backend (300/300 across four seeds); a 15-case in-app `stdlib` battery
(suite 182 → 197, WASM ≡ VM 166 → 181); a gallery example; About/stdlib updates. Every existing
suite (property suite, optimizer/SpecConstr/SROA/comprehension/section fuzzers) stays green.
- [ ] **A `Maybe`/`Option` in the prelude** would unlock the total reducers (`headOpt`, `find`,
`lookup`, `mapMaybe`) — it needs a prelude *type* declaration, not just `let` defs, so it's a small
extension to how `PRELUDE_DEFS` are wired.
- [ ] **`sortBy` / `groupBy` / `nub`** once a comparator/eq-callback convention is settled, and a
merge sort behind `sort` for the log-linear guarantee on long lists.
- [ ] **`foldr1`/`foldl1`/`scanr`** to round out the fold family.

### Aether 30.1 — operator sections, point-free style (+ a real JS-backend bug this caught) (planned + shipped this session)

With comprehensions and ranges in (30.0), the next everyday convenience the language lacked was
Expand Down Expand Up @@ -2243,13 +2271,29 @@ finds `sum x`, `abs x`, `max x 0` (clamp), `reverse x`, `map (fn h -> h + h) x`,

- list: `map filter foldl foldr length append reverse sum range enumFromTo enumFromThenTo take drop elem all any concat zip replicate`
(`enumFromTo`/`enumFromThenTo` back the `[a .. b]` / `[a, s .. b]` range literals)
- list (30.2): `zipWith concatMap takeWhile dropWhile span partition scanl insert sort product maximum minimum last init iterate`
- combinators: `subtract flip` (companions to operator sections — `(subtract n)` is the section `-` can't give)
- string: `strlen toUpper toLower chars join parseInt` (+ `show`, `^`)
- primitives: `head tail empty print sqrt sin cos floor toFloat pi abs min max`
- operators: `+ - * / % | +. -. *. /. | == != < > <= >= | && || ! | :: ++ ^ | |> | ;`

## Session log

- 2026-07-24 (claude): **Aether 30.2 — a standard library worth the name.** After making Aether
pleasant to write (30.0 comprehensions/ranges, 30.1 sections), this makes it productive by adding
the list vocabulary every functional programmer expects — **fifteen** combinators, all written **in
Aether itself** so they cost the type checker and the three backends nothing new: transformers
(`zipWith`, `concatMap`, `scanl`, `iterate`), splitters (`takeWhile`, `dropWhile`, `span`,
`partition`), ordering (`insert`, `sort` — polymorphic on structural `≤`, so `sort ["pear","apple"]`
works), and reducers (`product`, `maximum`, `minimum`, `last`, `init`). They compose directly with
the new syntax: `sort (concatMap (fn x -> [x, 0 - x]) [1 .. n])`, `partition (> 0)`,
`zipWith (+)`. **Verification:** a new 200-program **stdlib fuzzer** (`stdlibFuzz.ts`) generating
random calls across all eleven order-sensitive combinators, each checked against an **independent
TypeScript reference** *and* the JS backend (300/300 across four seeds); a 15-case in-app `stdlib`
battery (suite **182 → 197**, WASM ≡ VM **166 → 181**); a gallery example; About/stdlib updates. No
regression — the property suite (12/12), the optimizer/SpecConstr/SROA fuzzers, and the
comprehension/section fuzzers all stay green. Full CI gate green.

- 2026-07-24 (claude): **Aether 30.1 — operator sections (+ a real JS-backend bug the differential
suite caught).** After 30.0's comprehensions, the next everyday convenience missing was **operator
sections**. Shipped, again as pure parser sugar desugared in `parseParen` to a lambda (nothing
Expand Down
25 changes: 25 additions & 0 deletions projects/aether-lang-c3d8/src/examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1682,6 +1682,31 @@ let people = [ { name = "Ada", age = 36 }, { name = "Alan", age = 41 }, { name =
, names people
, map (subtract 1) [10, 20, 30]
)`,
},
{
id: 'stdlib-tour',
title: 'Standard-library tour',
blurb: 'sort, partition, span, scanl, zipWith, takeWhile — the 30.2 list toolkit, point-free.',
visual: false,
code: `// Aether 30.2 — a real list library, all written in Aether itself, and all
// happy to be driven by sections and comprehensions.

let xs = [ 5, 3, 8, 1, 9, 2, 7, 4, 6 ] in

// running sums, the sorted list, and a split into evens / odds
let sums = scanl (+) 0 xs in
let sorted = sort xs in
let evens = partition (fn x -> x % 2 == 0) xs in

// combine two lists element-wise; keep a prefix; grab the extremes
let dots = zipWith ( * ) [1, 2, 3] [10, 20, 30] in
let small = takeWhile (< 9) sorted in
let bounds = (minimum xs, maximum xs) in

// it all composes: reflect [1..4] through zero, then sort the 8 points
let mirror = sort (concatMap (fn x -> [x, 0 - x]) [1 .. 4]) in

(sums, sorted, evens, dots, small, bounds, mirror)`,
},
{
id: 'comprehension-garden',
Expand Down
92 changes: 92 additions & 0 deletions projects/aether-lang-c3d8/src/lang/prelude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,4 +331,96 @@ export const PRELUDE_DEFS: PreludeDef[] = [
doc: 'replicate n x — a list of n copies of x',
src: 'fn n x -> if n <= 0 then [] else x :: replicate (n - 1) x',
},

// ---- Aether 30.2 — a richer list toolkit (all written in Aether itself) ----
{
name: 'zipWith',
recursive: true,
doc: 'zipWith f xs ys — combine two lists element-wise with f',
src: 'fn f xs ys -> if empty xs then [] else if empty ys then [] else f (head xs) (head ys) :: zipWith f (tail xs) (tail ys)',
},
{
name: 'concatMap',
recursive: true,
doc: 'concatMap f xs — map f then flatten (the list-monad bind)',
src: 'fn f xs -> if empty xs then [] else append (f (head xs)) (concatMap f (tail xs))',
},
{
name: 'takeWhile',
recursive: true,
doc: 'takeWhile p xs — the longest prefix whose elements all satisfy p',
src: 'fn p xs -> if empty xs then [] else if p (head xs) then head xs :: takeWhile p (tail xs) else []',
},
{
name: 'dropWhile',
recursive: true,
doc: 'dropWhile p xs — drop the longest prefix satisfying p, keep the rest',
src: 'fn p xs -> if empty xs then [] else if p (head xs) then dropWhile p (tail xs) else xs',
},
{
name: 'span',
recursive: false,
doc: 'span p xs — (takeWhile p xs, dropWhile p xs)',
src: 'fn p xs -> (takeWhile p xs, dropWhile p xs)',
},
{
name: 'partition',
recursive: true,
doc: 'partition p xs — (elements where p holds, elements where it doesn’t), order preserved',
src: 'fn p xs -> if empty xs then ([], []) else match partition p (tail xs) with (yes, no) -> if p (head xs) then (head xs :: yes, no) else (yes, head xs :: no)',
},
{
name: 'scanl',
recursive: true,
doc: 'scanl f z xs — like foldl, but keeping every intermediate accumulator (starts with z)',
src: 'fn f z xs -> z :: (if empty xs then [] else scanl f (f z (head xs)) (tail xs))',
},
{
name: 'insert',
recursive: true,
doc: 'insert x xs — insert x into an already-sorted list, keeping it sorted',
src: 'fn x xs -> if empty xs then [x] else if x <= head xs then x :: xs else head xs :: insert x (tail xs)',
},
{
name: 'sort',
recursive: false,
doc: 'sort xs — ascending insertion sort (works on any comparable element)',
src: 'foldr insert []',
},
{
name: 'product',
recursive: false,
doc: 'product xs — the product of an Int list',
src: 'foldl (fn a x -> a * x) 1',
},
{
name: 'maximum',
recursive: false,
doc: 'maximum xs — the largest element (partial: undefined on [])',
src: 'fn xs -> foldl max (head xs) (tail xs)',
},
{
name: 'minimum',
recursive: false,
doc: 'minimum xs — the smallest element (partial: undefined on [])',
src: 'fn xs -> foldl min (head xs) (tail xs)',
},
{
name: 'last',
recursive: true,
doc: 'last xs — the final element (partial: undefined on [])',
src: 'fn xs -> if empty (tail xs) then head xs else last (tail xs)',
},
{
name: 'init',
recursive: true,
doc: 'init xs — every element but the last (partial: undefined on [])',
src: 'fn xs -> if empty (tail xs) then [] else head xs :: init (tail xs)',
},
{
name: 'iterate',
recursive: true,
doc: 'iterate n f x — the n-element list [x, f x, f (f x), …]',
src: 'fn n f x -> if n <= 0 then [] else x :: iterate (n - 1) f (f x)',
},
]
176 changes: 176 additions & 0 deletions projects/aether-lang-c3d8/src/lang/stdlibFuzz.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// Aether — an in-browser DIFFERENTIAL FUZZER for the 30.2 list standard library.
//
// The new combinators (`sort`, `partition`, `span`, `takeWhile`/`dropWhile`,
// `scanl`, `zipWith`, `insert`, `product`, `maximum`/`minimum`, …) are written in
// Aether itself and compiled into every program. This harness generates hundreds
// of random calls to them and proves, for each:
//
// • the compiled program's VM result equals an INDEPENDENT reference (the same
// operation computed directly here in TypeScript), and
// • that value re-appears from the JavaScript backend (VM ≡ JS).
//
// So the library isn't merely "tested" — every call is checked against a second,
// separate implementation of what it should mean, on two backends. Deterministic
// given the seed; pure logic, so it also runs head-less under Node.

import { runPipeline } from './pipeline.ts'
import { compileToJs, runJsModule } from './jsBackend.ts'
import { valueToString } from './values.ts'

export interface StdlibFuzzResult {
total: number
passed: number
/** distinct combinators exercised across the batch */
covered: number
failures: { code: string; detail: string }[]
}

type Rng = () => number
function makeRng(seed: number): Rng {
let s = seed >>> 0
return () => {
s = (s * 1664525 + 1013904223) >>> 0
return s / 4294967296
}
}
const int = (rng: Rng, lo: number, hi: number): number => lo + (Math.floor(rng() * (hi - lo + 1)) | 0)
const randList = (rng: Rng, maxLen: number, hi: number): number[] => {
const n = int(rng, 0, maxLen)
const xs: number[] = []
for (let i = 0; i < n; i++) xs.push(int(rng, 0, hi))
return xs
}

const fmtList = (xs: number[]): string => `[${xs.join(', ')}]`
const fmtLit = (xs: number[]): string => `[${xs.join(', ')}]`
const pairLL = (a: number[], b: number[]): string => `(${fmtList(a)}, ${fmtList(b)})`

// each op: build the Aether source and the reference string, from fresh randoms
const OPS: readonly ((rng: Rng) => { code: string; expected: string })[] = [
// sort
(rng) => {
const xs = randList(rng, 8, 9)
return { code: `sort ${fmtLit(xs)}`, expected: fmtList([...xs].sort((a, b) => a - b)) }
},
// insert into a sorted list
(rng) => {
const xs = randList(rng, 7, 9).sort((a, b) => a - b)
const k = int(rng, 0, 9)
const out = [...xs]
let i = 0
while (i < out.length && out[i] < k) i++
out.splice(i, 0, k)
return { code: `insert ${k} ${fmtLit(xs)}`, expected: fmtList(out) }
},
// partition on parity
(rng) => {
const xs = randList(rng, 8, 9)
return {
code: `partition (fn x -> x % 2 == 0) ${fmtLit(xs)}`,
expected: pairLL(xs.filter((x) => x % 2 === 0), xs.filter((x) => x % 2 !== 0)),
}
},
// span (< k)
(rng) => {
const xs = randList(rng, 8, 9)
const k = int(rng, 0, 9)
let i = 0
while (i < xs.length && xs[i] < k) i++
return { code: `span (< ${k}) ${fmtLit(xs)}`, expected: pairLL(xs.slice(0, i), xs.slice(i)) }
},
// takeWhile (< k)
(rng) => {
const xs = randList(rng, 8, 9)
const k = int(rng, 0, 9)
let i = 0
while (i < xs.length && xs[i] < k) i++
return { code: `takeWhile (< ${k}) ${fmtLit(xs)}`, expected: fmtList(xs.slice(0, i)) }
},
// dropWhile (< k)
(rng) => {
const xs = randList(rng, 8, 9)
const k = int(rng, 0, 9)
let i = 0
while (i < xs.length && xs[i] < k) i++
return { code: `dropWhile (< ${k}) ${fmtLit(xs)}`, expected: fmtList(xs.slice(i)) }
},
// scanl (+) z
(rng) => {
const xs = randList(rng, 7, 6)
const z = int(rng, 0, 5)
const acc: number[] = [z]
let s = z
for (const x of xs) {
s += x
acc.push(s)
}
return { code: `scanl (+) ${z} ${fmtLit(xs)}`, expected: fmtList(acc) }
},
// zipWith (+)
(rng) => {
const xs = randList(rng, 8, 9)
const ys = randList(rng, 8, 9)
const n = Math.min(xs.length, ys.length)
const out: number[] = []
for (let i = 0; i < n; i++) out.push(xs[i] + ys[i])
return { code: `zipWith (+) ${fmtLit(xs)} ${fmtLit(ys)}`, expected: fmtList(out) }
},
// product
(rng) => {
const xs = randList(rng, 6, 5)
return { code: `product ${fmtLit(xs)}`, expected: String(xs.reduce((a, b) => a * b, 1)) }
},
// maximum of a guaranteed-non-empty list
(rng) => {
const xs = randList(rng, 7, 20)
xs.push(int(rng, 0, 20))
return { code: `maximum ${fmtLit(xs)}`, expected: String(Math.max(...xs)) }
},
// minimum of a guaranteed-non-empty list
(rng) => {
const xs = randList(rng, 7, 20)
xs.push(int(rng, 0, 20))
return { code: `minimum ${fmtLit(xs)}`, expected: String(Math.min(...xs)) }
},
]

export function runStdlibFuzz(runs = 200, seed = 0x57d11b): StdlibFuzzResult {
const rng = makeRng(seed)
let passed = 0
const usedOps = new Set<number>()
const failures: { code: string; detail: string }[] = []

for (let i = 0; i < runs; i++) {
const opIdx = int(rng, 0, OPS.length - 1)
usedOps.add(opIdx)
const { code, expected } = OPS[opIdx](rng)

let vm: string
let coreAst
try {
const r = runPipeline(code, { execute: true })
if (r.error) {
if (failures.length < 8) failures.push({ code, detail: `${r.error.stage}: ${r.error.message}` })
continue
}
coreAst = r.coreAst
vm = r.run?.result ? valueToString(r.run.result) : '()'
} catch (e) {
if (failures.length < 8) failures.push({ code, detail: `threw: ${String(e)}` })
continue
}
if (vm !== expected) {
if (failures.length < 8) failures.push({ code, detail: `VM ${vm} ≠ reference ${expected}` })
continue
}
try {
const js = runJsModule(compileToJs(coreAst!).full)
if (js.error === null && js.result === vm) passed++
else if (failures.length < 8) failures.push({ code, detail: `JS ${js.error ?? js.result} ≠ VM ${vm}` })
} catch (e) {
if (failures.length < 8) failures.push({ code, detail: `JS threw: ${String(e)}` })
}
}

return { total: runs, passed, covered: usedOps.size, failures }
}
Loading