fix(boot): the DJF "fix" was a parse error — BootTidal.hs could not load at all
Mid-rehearsal, PLN reported the three DJ filters still misbehaving after
yesterday's #48 bidirectional rewrite. He described three symptoms:
C1 centre->0 : no LPF effect on d3 (hats)
C1 centre->0 : kick goes from TOTALLY high-passed to normal
C1 centre->127: d3 high-passes
Those fingerprint the *previous* (2026-07-25) definition exactly —
`gHPF ch = (# hpf (range 20 8000 ch))`, one-directional, no lpf term:
at centre hpf=4010Hz (kick gutted), at 0 hpf=20Hz (normal), at max
hpf=8000Hz. Four observations, four matches. So the running ghci was not
executing the new code. The question was why.
Root cause: the new gDJF was written as a right section containing another
operator of the same fixity —
gDJF ch = (# lpf (...) # hpf (...)) -- `#` is infixl 9
which is a hard Haskell parse error, not a subtle type slip. GHC:
"The operator '#' of a section must have lower precedence than that of the
operand". One bad line takes down the entire enclosing `let` block, i.e.
EVERY g* helper — gF1/2/3, gMask, gMute*, gPanic, the drum helpers.
What made it dangerous rather than merely broken: nothing surfaced it. The
live ghci still held the older definitions from before the edit, so the rig
kept making sound and behaving plausibly while the file on disk was
unloadable. The failure was scheduled for the next cold boot — i.e. on
stage at OPAL. Same shape as #21's mute-bomb: a definition that lives only
in a running process is not a verified definition.
Fix: express it as two composed sections, which is also semantically
cleaner (two independent unions rather than one nested):
gDJF ch = (# lpf (...)) . (# hpf (...))
The filter math itself was correct and is unchanged — verified at three
points: v=0 -> lpf 180 / hpf 20 (low-pass closed to subbass), v=0.5 ->
lpf 20000 / hpf 20 (both wide open = true bypass at the centre detent),
v=1 -> lpf 20000 / hpf 8000 (high-pass climbed to superhigh). Applied to
both BootTidal.hs and the live/lib/prelude.tidal rescue sheet, which had
inherited the identical broken form.
Also adds tools/check-boot.sh so this class of bug cannot ship again: it
extracts the helper let-block, dedents it into a real module, stubs the two
stream-bound names it closes over (p, setI), and runs `ghc -fno-code`.
Types only — no codegen, no stream, no port 6010 grab, safe to run mid-set.
Validation, both directions:
- real BootTidal.hs -> OK, exit 0 (whole 164-431 block typechecks
against tidal-1.9.5 / ghc 9.4.7)
- bug reintroduced -> FAIL, exit 1, pointing at the exact line
That negative test earned its keep immediately: the first version of the
guard used `ghc | grep -q` under `set -o pipefail`, where a failing ghc
makes the pipeline non-zero regardless of grep's verdict, silently
inverting the check. It reported OK on a file it had just proven broken.
Now it runs ghc once and tests the captured exit code. A guard that has
never seen the bug it targets is a hypothesis, not a guard.
Showing
tools/check-boot.sh
0 → 100644