feat(course): audit fixes and twelve extensions (Modules 7b, 12, 13)

Audit pass:
- Frame rule counts frames since the last move, so every step lasts
  exactly movesEvery(score) frames, also right after a speed-up
- clampTime + MAX_GAP_MS: a hidden tab no longer replays the whole gap
- Pong Restart keeps held keys
- Fact corrections (mulberry32 output coverage, Haskell IO, uniform)
- New fixture cases from a mutation analysis

Extensions (all three tracks, shared fixtures, course text):
- Turn queue (Exercise 10.6), persisted high score (10.7),
  replay viewer (10.8), play() end-to-end helper (9.5)
- Optional State (4.7) and optional Module 7b, stack-safe IO
- Module 12, property-based testing with generators, shrinking,
  invariants and a model-based oracle
- Module 13, Conway's Life with a Click event
- JS type checking via jsconfig, JSDoc and types.d.ts
- Maintainer tools: scripts/mutate.mjs, scripts/typecheck-js.sh,
  Playwright end-to-end suite (e2e/, scripts/e2e.sh)

Docs: docs/contract.md is the canonical contract; specs and build log
under docs/superpowers.

Verified: JS 728, Scala 689, F# 691 unit tests; stubs and progression
M01-M13 and M07b; 176 end-to-end tests on Chromium and WebKit.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Fable 5.2 committed 2026-11-11 12:05:29 +01:00
1 parent 876f5aabb1
commit edd57f0148
253 files changed
+21753 -503

No files matched your search

+262
View File
@@ -0,0 +1,262 @@
package snake
import SafeIO.*
class M07bSafeIOSuite extends munit.FunSuite:
private def rectAt(x: Int): Shape = Shape.Rect(x, 0, 1, 1, "#000000")
private val r1 = rectAt(0)
private val t1 = Shape.Text(1, 2, "hi", "#ffffff", 16, "left")
private def drawOp(shape: Shape): Op = Op.Draw(shape)
private val seedOp: Op = Op.RandomSeed
private def boom[A, B](a: A): B = throw new RuntimeException("boom")
private val N = 100000
// The value of a RandomSeed answer (the tests' interpreters always answer it with a Seed).
private def seedOf(r: OpResult): Long = r match
case OpResult.Seed(value) => value
case _ => -1L // Done, or Module 10's Best: never the answer to a RandomSeed
// Which constructor a program is. Functions cannot be compared, so the tests check a node's shape by its tag and
// then observe the program through `trace`. (A tag, not the program itself: printing a 100 000-deep chain would
// overflow the stack.)
private def tag(p: SafeIO[Any]): String = p match
case Pure(_) => "Pure"
case Effect(_, _) => "Effect"
case FlatMap(_, _) => "FlatMap"
// --- building -------------------------------------------------------------------------------------------------
test("pure builds a Pure") { assertEquals(pure(3), Pure(3)) }
test("effect(op) is an Effect whose continuation is pure") {
effect(seedOp) match
case Effect(op, k) =>
assertEquals(op, seedOp)
assertEquals(k(OpResult.Seed(5)), Pure(OpResult.Seed(5)))
case other => fail(s"expected an Effect, got ${tag(other)}")
}
test("flatMap only builds a FlatMap node") {
val m = pure(1)
val p = m.flatMap(n => pure(n + 1))
p match
case FlatMap(io, _) => assertEquals[SafeIO[Any], SafeIO[Any]](io, m)
case other => fail(s"expected a FlatMap, got ${tag(other)}")
assertEquals(trace(p, 0), Traced(2, Nil))
}
test("building flatMap(pure(1), throwingF) does not throw") {
assertEquals(tag(pure(1).flatMap(boom[Int, SafeIO[Int]])), "FlatMap")
}
test("building map(io, throwingF) does not throw") {
assertEquals(tag(effect(seedOp).map(boom[OpResult, Int])), "FlatMap")
}
test("tracing map(io, throwingF) runs f and throws") {
interceptMessage[RuntimeException]("boom")(trace(effect(seedOp).map(boom[OpResult, Int]), 0))
}
// --- step: the three rewrite rules ----------------------------------------------------------------------------
// An Effect that reads the seed and produces it as a Long.
private val readSeed: SafeIO[Long] = Effect(seedOp, r => Pure(seedOf(r)))
test("step leaves a Pure unchanged") {
val p = pure(1)
assertEquals(step(p), p)
}
test("step leaves an Effect unchanged") {
val p = effect(seedOp)
assertEquals(step(p), p)
}
test("step of FlatMap(Pure(a), f) is f(a)") {
assertEquals(step(pure(3).flatMap(n => pure(n * 2))), Pure(6))
}
test("step of FlatMap(Effect(op, k), f) is an Effect with the same op that continues with FlatMap(k(r), f)") {
step(readSeed.flatMap(n => pure(n * 2))) match
case Effect(op, k) =>
assertEquals(op, seedOp)
val next = k(OpResult.Seed(5))
next match
case FlatMap(io, _) => assertEquals[SafeIO[Any], SafeIO[Any]](io, Pure(5L))
case other => fail(s"expected a FlatMap, got ${tag(other)}")
assertEquals(trace(next, 0), Traced(10L, Nil))
case other => fail(s"expected an Effect, got ${tag(other)}")
}
test("step re-associates FlatMap(FlatMap(m, g), f) into FlatMap(m, x => FlatMap(g(x), f))") {
val m = readSeed
val s = step(m.flatMap(n => pure(n + 1)).flatMap(n => pure(n * 2)))
s match
case FlatMap(io, _) => assertEquals[SafeIO[Any], SafeIO[Any]](io, m)
case other => fail(s"expected a FlatMap, got ${tag(other)}")
assertEquals(trace(s, 4), Traced(10L, List(seedOp)))
}
test("step does not call the functions it re-associates") {
assertEquals(tag(step(effect(seedOp).flatMap(boom[OpResult, SafeIO[Int]]).flatMap(boom[Int, SafeIO[Int]]))), "FlatMap")
}
test("step of a left-nested chain is one rewrite, not a whole walk") {
// After one step, the top is still a FlatMap: step did constant work, the rest is left for later steps.
val chain = (1 to N).foldLeft(pure(0))((acc, _) => acc.flatMap(n => pure(n + 1)))
assertEquals(tag(step(chain)), "FlatMap")
}
// --- the interpreter ------------------------------------------------------------------------------------------
test("trace of pure(3) has result 3 and no ops") {
assertEquals(trace(pure(3), 0), Traced(3, Nil))
}
test("trace answers RandomSeed with the seed and Draw with Done") {
assertEquals(
trace(effect(seedOp).flatMap(s => effect(drawOp(r1)).map(_ => s)), 9),
Traced(OpResult.Seed(9), List(seedOp, drawOp(r1)))
)
assertEquals(trace(effect(drawOp(r1)), 9), Traced(OpResult.Done, List(drawOp(r1))))
}
test("trace refuses operations of later modules") {
intercept[IllegalArgumentException](trace(effect(Op.LoadBest), 0))
}
private def drawing(shape: Shape): SafeIO[Unit] = effect(drawOp(shape)).map(_ => ())
test("sequence of nothing has no ops") {
assertEquals(trace(sequence(Nil), 0), Traced((), Nil))
}
test("sequence runs effects left to right") {
assertEquals(
trace(sequence(List(effect(seedOp).map(_ => ()), drawing(t1), drawing(r1))), 0).ops,
List(seedOp, drawOp(t1), drawOp(r1))
)
}
// --- stack safety ---------------------------------------------------------------------------------------------
private def leftDraws(n: Int): SafeIO[Any] =
(0 until n).foldLeft[SafeIO[Any]](pure(()))((acc, i) => acc.flatMap(_ => effect(drawOp(rectAt(i)))))
private def rightDraws(n: Int): SafeIO[Any] =
(0 until n).foldRight[SafeIO[Any]](pure(()))((i, rest) => effect(drawOp(rectAt(i))).flatMap(_ => rest))
test("100 000 left-nested flatMaps of draws run, in order") {
val Traced(result, ops) = trace(leftDraws(N), 0)
assertEquals(result, OpResult.Done)
assertEquals(ops.length, N)
assertEquals((ops.head, ops.last), (drawOp(rectAt(0)), drawOp(rectAt(N - 1))))
}
test("100 000 right-nested flatMaps of draws run, in order") {
val ops = trace(rightDraws(N), 0).ops
assertEquals(ops.length, N)
assertEquals((ops.head, ops.last), (drawOp(rectAt(0)), drawOp(rectAt(N - 1))))
}
test("100 000 left-nested maps run without any op") {
assertEquals(trace((1 to N).foldLeft(pure(0))((acc, _) => acc.map(_ + 1)), 0), Traced(N, Nil))
}
test("sequence of 100 000 draws runs") {
assertEquals(trace(sequence(List.tabulate(N)(i => drawing(rectAt(i)))), 0).ops.length, N)
}
test("a while loop over step is a complete interpreter for 100 000 left-nested flatMaps") {
// What an impure runtime would do (a loop is allowed there): rewrite until the top is Pure or Effect, perform.
var performed = List.empty[Op]
var current = leftDraws(N)
while tag(current) != "Pure" do
current = step(current)
current match
case Effect(op, k) =>
performed = op :: performed
current = k(OpResult.Done)
case _ => ()
assertEquals(performed.length, N)
}
// --- the monad laws, observed through trace -------------------------------------------------------------------
private val programs: List[(String, () => SafeIO[Long])] = List(
("a pure value", () => pure(4L)),
("one effect", () => readSeed),
("a draw followed by reading the seed", () => effect(drawOp(r1)).flatMap(_ => readSeed))
)
private def f(n: Long): SafeIO[Long] = effect(drawOp(rectAt(n.toInt))).flatMap(_ => pure(n * 2))
private def g(n: Long): SafeIO[Long] = effect(seedOp).map(s => seedOf(s) + n)
programs.foreach { (name, program) =>
test(s"left identity holds for $name") {
val a = trace(program(), 5).result
assertEquals(trace(pure(a).flatMap(f), 5), trace(f(a), 5))
}
test(s"right identity holds for $name") {
assertEquals(trace(program().flatMap(pure), 5), trace(program(), 5))
}
test(s"associativity holds for $name") {
assertEquals(trace(program().flatMap(f).flatMap(g), 5), trace(program().flatMap(x => f(x).flatMap(g)), 5))
}
}
// --- the same programs under Module 7's IO --------------------------------------------------------------------
// Each program is written once against this interface and built with both IOs.
private trait Builder[F[_]]:
def pure[A](a: A): F[A]
def effect(op: Op): F[OpResult]
def map[A, B](io: F[A])(f: A => B): F[B]
def flatMap[A, B](io: F[A])(f: A => F[B]): F[B]
def sequence(ios: List[F[Unit]]): F[Unit]
def trace[A](io: F[A], seed: Long): Traced[A]
def perform(op: Op): F[Unit] = map(effect(op))(_ => ())
private object Safe extends Builder[SafeIO]:
def pure[A](a: A): SafeIO[A] = SafeIO.pure(a)
def effect(op: Op): SafeIO[OpResult] = SafeIO.effect(op)
def map[A, B](io: SafeIO[A])(f: A => B): SafeIO[B] = io.map(f)
def flatMap[A, B](io: SafeIO[A])(f: A => SafeIO[B]): SafeIO[B] = io.flatMap(f)
def sequence(ios: List[SafeIO[Unit]]): SafeIO[Unit] = SafeIO.sequence(ios)
def trace[A](io: SafeIO[A], seed: Long): Traced[A] = SafeIO.trace(io, seed)
private object Simple extends Builder[IO]:
def pure[A](a: A): IO[A] = IO.pure(a)
def effect(op: Op): IO[OpResult] = IO.effect(op)
def map[A, B](io: IO[A])(f: A => B): IO[B] = io.map(f)
def flatMap[A, B](io: IO[A])(f: A => IO[B]): IO[B] = io.flatMap(f)
def sequence(ios: List[IO[Unit]]): IO[Unit] = IO.sequence(ios)
def trace[A](io: IO[A], seed: Long): Traced[A] = IO.trace(io, seed)
// A program for any Builder, traced with seed 7.
private trait Shared:
def traced[F[_]](M: Builder[F]): Traced[?]
private val shared: List[(String, Shared)] = List(
("seed, then a draw that depends on it", new Shared:
def traced[F[_]](M: Builder[F]): Traced[?] =
M.trace(M.flatMap(M.map(M.effect(seedOp))(seedOf))(s => M.map(M.effect(drawOp(rectAt((s % 20).toInt))))(_ => s)), 7)
),
("a sequence of three effects", new Shared:
def traced[F[_]](M: Builder[F]): Traced[?] =
M.trace(M.sequence(List(M.perform(drawOp(t1)), M.perform(seedOp), M.perform(drawOp(r1)))), 7)
),
("200 left-nested flatMaps", new Shared:
def traced[F[_]](M: Builder[F]): Traced[?] =
M.trace((0 until 200).foldLeft(M.pure(0))((acc, i) => M.flatMap(acc)(n => M.map(M.effect(drawOp(rectAt(i))))(_ => n + 1))), 7)
),
("nested on both sides", new Shared:
def traced[F[_]](M: Builder[F]): Traced[?] =
val left = M.flatMap(M.map(M.effect(seedOp))(seedOf))(s => M.flatMap(M.effect(drawOp(r1)))(_ => M.pure(s + 1)))
M.trace(M.flatMap(left)(n => M.map(M.sequence(List(M.perform(drawOp(t1)), M.perform(seedOp))))(_ => n * 3)), 7)
)
)
shared.foreach { (name, program) =>
test(s"trace equals Module 7's trace for $name") {
assertEquals(program.traced(Safe), program.traced(Simple))
}
}