#!/bin/bash # # check_test.sh — Static checking of klammer applications ("kdiag --check"). # # The engine applies klammers as it meets them, so it can only complain about # what it reaches. Two things it therefore never reaches: # # * the branch of a @cond that is not selected. @cond is a non-strict # special form, so an undefined klammer or a wrong argument count sitting # in the unselected branch is invisible -- and stays invisible until the # day the predicate flips. # * a klammer body that this render does not use, including a body defined # for a target other than the one being built. # # Klammertext can check both without running anything, because katom structure # is fixed when a file is read: there are no catcodes, so nothing later can # change how text already read divides into katoms. Every application written # literally in a document or a klammer body can be located and checked against # the registry. That is what check_machine() (mac/check.cpp) does and what # these tests pin down. # # What the checker deliberately does not see is also tested: klammers produced # by @eval at run time, and the interiors of @eval/@read argument spans and of # literal parameters, which are code, filenames, and raw text -- not # applications. # # Engine tier: kdiag loads no klammerset, so every klammer is defined inline. # The one case that needs the SKS reads it explicitly with "@read" -- which is # how a klammerset reaches kdiag at all: the symbol mechanism is deliberately # absent there, so that kdiag still works when symbol resolution is what broke. # # Usage: ./check_test.sh # Exit code: 0 if all tests pass, 1 otherwise. PASS=0 FAIL=0 KDIAG=kdiag K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set} red=$'\033[31m' green=$'\033[32m' bold=$'\033[1m' reset=$'\033[0m' # check_finds TEST_NAME PATTERN KDIAG_ARGS... # --check must exit nonzero and report PATTERN. check_finds() { local test_name="$1" local pattern="$2" shift 2 local output status output=$("$KDIAG" --process --check "$@" 2>&1) status=$? if [ $status -eq 0 ]; then echo "${red}FAIL${reset} $test_name — expected a diagnostic, got none" FAIL=$((FAIL + 1)) return fi if echo "$output" | grep -qF "$pattern"; then echo "${green}PASS${reset} $test_name" PASS=$((PASS + 1)) else echo "${red}FAIL${reset} $test_name — expected report to contain [$pattern]" echo " output: $(echo "$output" | head -4)" FAIL=$((FAIL + 1)) fi } # check_clean TEST_NAME KDIAG_ARGS... # --check must exit 0 and report no diagnostics. check_clean() { local test_name="$1" shift local output status output=$("$KDIAG" --process --check "$@" 2>&1) status=$? if [ $status -eq 0 ] && echo "$output" | grep -q "0 diagnostics"; then echo "${green}PASS${reset} $test_name" PASS=$((PASS + 1)) else echo "${red}FAIL${reset} $test_name — expected a clean check" echo " output: $(echo "$output" | head -4)" FAIL=$((FAIL + 1)) fi } # check_count TEST_NAME N KDIAG_ARGS... # --check must report exactly N diagnostics. check_count() { local test_name="$1" local expected="$2" shift 2 local output got output=$("$KDIAG" --process --check "$@" 2>&1) got=$(echo "$output" | sed -nE 's/^([0-9]+) diagnostics?,.*/\1/p') if [ "$got" = "$expected" ]; then echo "${green}PASS${reset} $test_name" PASS=$((PASS + 1)) else echo "${red}FAIL${reset} $test_name — expected $expected diagnostics, got ${got:-none}" echo " output: $(echo "$output" | head -4)" FAIL=$((FAIL + 1)) fi } GREET='@@greet name : Hello, *name*. @@' echo "${bold}Static klammer checking tests${reset}" echo "=============================" echo # --- What the renderer cannot reach --- check_finds " 1. undefined klammer in an unselected @cond branch (in a body)" \ "@nosuch is not defined" \ "@@pick p : @cond *p* | @nosuch x @ | ok @ @@ @pick false @" check_finds " 2. wrong arity in an unselected @cond branch (in a body)" \ "is given 3" \ "$GREET @@pick p : @cond *p* | @greet a | b | c @ | ok @ @@ @pick false @" # A top-level @cond branch is now checked like any other. It was the one # documented gap in the checker: a @cond written at the top level of a DOCUMENT # was resolved when the file was READ, so its unselected branch was gone before # anything could look at it. Resolving @cond at APPLICATION time instead # (notes/Klammertext_improvements.md §4.2, decided 2026-08-15) closed it -- the # branches survive read, so both are visible to a pass that applies nothing. check_finds " 2a. a top-level @cond branch is checked too" \ "@nosuch is not defined" \ "@cond false | @nosuch x @ | ok @" check_finds " 2b. ... including the branch that WOULD be selected" \ "@nosuch is not defined" \ "@cond true | @nosuch x @ | ok @" check_finds " 3. undefined klammer in a body that is never applied" \ "@nosuch is not defined" \ "@@unused : @nosuch x @ @@ nothing applies it" check_finds " 4. the body it was found in is named" \ "in body of @unused" \ "@@unused : @nosuch x @ @@ nothing applies it" # --- Arity --- check_finds " 5. too few positional arguments" \ "needs 2 positional arguments but is given 1" \ '@@pair a | b : *a**b* @@ @pair x @' check_finds " 6. too many positional arguments" \ "takes 1 positional argument but is given 2" \ "$GREET @greet a | b @" check_finds " 7. undefined optional argument" \ 'has no optional argument ":nope"' \ "$GREET @greet a :nope 1 @" check_finds " 8. the accepted optional arguments are listed" \ "It accepts: :loud" \ '@@greet name :loud : *name* @@ @greet a :nope 1 @' check_clean " 9. a rest argument accepts extra positional arguments" \ '@@many a | rest.rest : *a* @@ @many x | y | z @' # --- Nesting. A bar or an option name belonging to a nested klammer is not # this klammer's; the checker counts at depth 0, as @cond does. --- check_clean "10. nested klammer's bars are not counted as the outer's" \ '@@frac a | b : *a*/*b* @@ @@one x : [*x*] @@ @one @frac 1 | 2 @ @' check_clean "11. nested klammer's option name is not counted as the outer's" \ '@@inner a :flag : *a* @@ @@outer x : [*x*] @@ @outer @inner q :flag y @ @' # --- Target coverage --- check_finds "12. klammer not defined for a target" \ 'is not defined for the target "tex"' \ '@@@target html | HTML output @@@ @@@target tex | TeX output @@@ @@only.html : H @@ @only@' check_clean "13. defined for every target is clean" \ '@@@target html | HTML output @@@ @@@target tex | TeX output @@@ @@both : B @@ @both@' # --- What the checker deliberately does not see --- # In a body, so the @eval is not evaluated at read time: what is being tested # is that the checker does not read the eval's ARGUMENT as an application. check_clean "14. @eval argument content is code, not applications" \ '@@w : @eval len("@nosuch") @ @@' check_clean "15. a literal parameter's content is raw text" \ '@@lit t.literal : *t* @@ @lit @nosuch x @ lit@' # --- Reporting --- check_count "16. a target-independent fault is reported once, not per target" \ 1 \ '@@@target html | HTML output @@@ @@@target tex | TeX output @@@ @@g : @nosuch@ @@' check_clean "17. a correct document checks clean" \ "$GREET @greet World @" # An absolute path: "make -C tst test" runs from tst/, and @read resolves # against the current directory. check_clean "18. the Standard Klammer Set checks clean" \ "@read $K/sks/sks.k @" echo echo "=============================" echo "Results: ${green}$PASS passed${reset}, ${red}$FAIL failed${reset}" [ $FAIL -eq 0 ]