#!/usr/bin/env bash
# check-boot.sh — typecheck BootTidal.hs WITHOUT booting anything.
#
# Why this exists (2026-07-27, mid-rehearsal):
# a DJF "fix" shipped to BootTidal.hs as  (# lpf .. # hpf ..)  — a right
# section containing another operator of the same fixity, which is a hard
# Haskell PARSE ERROR (`#` is infixl 9). One bad line took the whole `let`
# block down, i.e. EVERY g* helper. And nothing surfaced it: the running
# ghci still held the old definitions, so the rig kept making sound while
# the file on disk was unloadable. The bug would only have detonated at the
# next boot — on stage.
#
# The lesson is the rig's recurring one: a definition that lives only in a
# running process is not verified. Prove it structurally, cheaply, cold.
#
# Method: extract the big helper `let` block, dedent it into a real module,
# stub the few stream-bound names (p, setI) it closes over, and run
# `ghc -fno-code`. Types only — no code generated, no stream started, no
# port 6010 grabbed. Safe to run mid-set.
set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BOOT="${1:-$ROOT/BootTidal.hs}"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT

# Locate the helper block: the `:{ let ...` that defines the g* helpers,
# found by content rather than hardcoded line numbers so edits don't rot it.
START="$(grep -n '^let -- DPV specific parameters' "$BOOT" | cut -d: -f1)"
if [ -z "$START" ]; then
  echo "check-boot: FAIL — could not locate the helper let-block in $BOOT" >&2
  exit 2
fi
END="$(awk -v s="$START" 'NR>s && /^:\}/ {print NR-1; exit}' "$BOOT")"
if [ -z "$END" ]; then
  echo "check-boot: FAIL — no closing :} after line $START" >&2
  exit 2
fi

{
  echo '{-# LANGUAGE OverloadedStrings #-}'
  echo '{-# OPTIONS_GHC -Wno-missing-signatures -Wno-name-shadowing #-}'
  echo 'module BootCheck where'
  echo 'import Sound.Tidal.Context'
  echo 'import Data.Char (toLower)'
  # Names the block closes over from earlier :{ :} blocks (stream-bound).
  echo 'p :: Int -> ControlPattern -> IO ()'
  echo 'p = undefined'
  echo 'setI :: String -> Pattern Int -> IO ()'
  echo 'setI = undefined'
  sed -n "${START},${END}p" "$BOOT" | sed -e '1s/^let //' -e 's/^    //'
} > "$WORK/BootCheck.hs"

echo "check-boot: typechecking $BOOT lines $START-$END ..."
# NB: run ghc ONCE and capture, rather than piping into `grep -q`. Under
# `set -o pipefail` a failing ghc makes the whole pipeline non-zero no matter
# what grep decides, which silently inverts the test — this guard shipped
# broken exactly that way and only its own negative test caught it.
set +e
OUT="$(ghc -fno-code -package tidal "$WORK/BootCheck.hs" 2>&1)"
RC=$?
set -e
if [ "$RC" -ne 0 ]; then
  echo "" >&2
  printf '%s\n' "$OUT" | sed "s#$WORK/BootCheck.hs#<let-block>#" >&2
  echo "" >&2
  echo "check-boot: FAIL — BootTidal.hs would NOT load. Do not boot." >&2
  exit 1
fi
echo "check-boot: OK — helper block typechecks against tidal-1.9.5."

# --- Pass 2: the #55 control-seed block ---------------------------------
# Seeding runs at boot and silences nothing if it fails, but a type error in
# it aborts the rest of the boot file. Check the seed list's shape and the
# streamSetF signature it depends on.
SEED_START="$(grep -n '^let _seed = concat' "$BOOT" | cut -d: -f1)"
if [ -n "$SEED_START" ]; then
  SEED_END="$(awk -v s="$SEED_START" 'NR>=s && /\] :: \[\(String, Double\)\]/ {print NR; exit}' "$BOOT")"
  {
    echo 'module SeedCheck where'
    echo 'import Sound.Tidal.Context'
    # Pin the exact signature the seed block calls through `setF`.
    echo 'sigF :: Stream -> String -> Pattern Double -> IO ()'
    echo 'sigF = streamSetF'
    sed -n "${SEED_START},${SEED_END}p" "$BOOT" | sed '1s/^let //'
    echo 'useSeed :: Stream -> IO ()'
    echo 'useSeed t = mapM_ (\(k, v) -> streamSetF t k (pure v)) _seed'
  } > "$WORK/SeedCheck.hs"
  set +e
  SOUT="$(ghc -fno-code -package tidal "$WORK/SeedCheck.hs" 2>&1)"
  SRC=$?
  set -e
  if [ "$SRC" -ne 0 ]; then
    echo "" >&2
    printf '%s\n' "$SOUT" | sed "s#$WORK/SeedCheck.hs#<seed-block>#" >&2
    echo "check-boot: FAIL — the #55 control-seed block does not typecheck." >&2
    exit 1
  fi
  echo "check-boot: OK — #55 control-seed block typechecks."

  # --- Pass 3: RUN the helpers against an EMPTY control map (#55) ----------
  # The regression test that tonight's bug needed and nobody had. Typechecking
  # cannot catch it: `gF1 $ sound "bd"` is perfectly well-typed while emitting
  # ZERO events, because an untouched "^NN" yields nothing and `#` cannot emit
  # without a right-hand value. The only mechanical check is to QUERY the
  # pattern with no controls set and assert events come out.
  #
  # This is a real execution, not a typecheck — but it is pure pattern
  # evaluation: no stream, no scsynth, no MIDI, no ports. Safe mid-set.
  {
    echo '{-# LANGUAGE OverloadedStrings #-}'
    echo '{-# OPTIONS_GHC -Wno-missing-signatures -Wno-name-shadowing -Wno-type-defaults #-}'
    echo 'module Main where'
    echo 'import Sound.Tidal.Context'
    echo 'import qualified Data.Map.Strict as M'
    echo 'import System.Exit (exitFailure)'
    echo 'import Data.IORef'
    echo 'p :: Int -> ControlPattern -> IO ()'
    echo 'p = undefined'
    echo 'setI :: String -> Pattern Int -> IO ()'
    echo 'setI = undefined'
    sed -n "${START},${END}p" "$BOOT" | sed -e '1s/^let //' -e 's/^    //'
    cat <<'HS'

-- Query with a DELIBERATELY EMPTY controls map: the state of a fresh boot
-- before any knob has been moved.
audible :: String -> ControlPattern -> IORef Bool -> IO ()
audible label pat failed = do
  let evs = query pat (State (Arc 0 1) M.empty)
  if null evs
    then do putStrLn ("  FAIL " ++ label ++ " -> SILENT with an empty control map")
            writeIORef failed True
    else putStrLn ("  ok   " ++ label ++ " -> " ++ show (length evs) ++ " event(s)")

main :: IO ()
main = do
  failed <- newIORef False
  putStrLn "  (untouched-control audibility, #55)"
  mapM_ (\(l, f) -> audible l (f (sound "bd")) failed)
    [ ("gF1   ", gF1), ("gF2   ", gF2), ("gF3   ", gF3)
    , ("gMask ", gMask)
    , ("gMute1", gMute1), ("gMute2", gMute2), ("gMute3", gMute3)
    , ("gM1   ", gM1), ("gM2   ", gM2), ("gM3   ", gM3)
    , ("gPanic", gPanic)
    ]
  -- Gain paths: silent here means the global fader zeroes the whole rig.
  audible "midiGdef" (sound "bd" # midiGdef) failed
  -- A composed stack is the realistic shape a track actually uses.
  audible "gF1.gM2 " (gF1 (gM2 (sound "bd"))) failed
  bad <- readIORef failed
  if bad then exitFailure else return ()
HS
  } > "$WORK/RunCheck.hs"
  set +e
  ROUT="$(ghc -package tidal -package containers -o "$WORK/runcheck" \
            -outputdir "$WORK/out" "$WORK/RunCheck.hs" 2>&1)"
  RRC=$?
  set -e
  if [ "$RRC" -ne 0 ]; then
    echo "" >&2
    printf '%s\n' "$ROUT" | sed "s#$WORK/RunCheck.hs#<run-check>#" >&2
    echo "check-boot: FAIL — could not build the untouched-control test." >&2
    exit 1
  fi
  set +e
  "$WORK/runcheck"
  RUNRC=$?
  set -e
  if [ "$RUNRC" -ne 0 ]; then
    echo "check-boot: FAIL — helpers go SILENT with an untouched controller." >&2
    echo "            This is the #55 mute-bomb. Streams will have no sound on" >&2
    echo "            a fresh boot until every knob is physically moved." >&2
    exit 1
  fi
  echo "check-boot: OK — all g* helpers audible with an untouched controller."
else
  echo "check-boot: WARN — no #55 control-seed block found (untouched knobs" >&2
  echo "            will silence their streams; see BootTidal.hs history)." >&2
fi
