#!/bin/bash # # target_list_test.sh — A klammer definition may name several targets. # # @@table.html,tex :: @@ # # One body, several targets. The list is SURFACE SYNTAX: the registry makes # one definition per target named, so nothing downstream of registration knows # a list was written. That is what these tests pin down — in particular that # each member goes through the redefinition transition table on its own, so a # list overlapping an existing definition is decided per target rather than # all-or-nothing. # # Why the feature exists: a klammer whose body is an `@eval` cannot have its # coverage derived (deciding which targets a Python function answers for is # undecidable), so its targets must be DECLARED. Writing one definition per # target would then duplicate the body. See notes/target_coverage.md. # # Two patterns are involved and must not be confused (mac/ktype.h): # definition_begin_name — runs through commas; used ONLY for "@@" # definition_name — no comma; klammer applications, option names, # "*arg*" variables, and the closing delimiters # Case 14 guards the second: a comma next to an application is writer text. # # These are engine tests, so they use --klammersets none and define their own targets # inline: a target is a Machine construct (@@@target), not owned by any # klammer set. # # Usage: ./target_list_test.sh (needs KLAMMERTEXT_HOME set; ktext on PATH) # Exit code: 0 if all tests pass, 1 otherwise. PASS=0 FAIL=0 KTEXT=ktext K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set} ERR=/tmp/target_list_test_err.$$ red=$'\033[31m' green=$'\033[32m' bold=$'\033[1m' reset=$'\033[0m' # ta, tb, tc are independent; td INCLUDES tc, so tc "provides" td and a # definition for tc is copied to td (mac/klammer_registry.cpp). TARGETS='@@@target ta | Target A @@@ @@@target tb | Target B @@@ @@@target tc | Target C @@@ @@@target td | Target D :includes tc @@@ ' trim() { awk '{ sub(/[ \t\r]+$/, "") } { line[NR]=$0 } END { f=1; while (f<=NR && line[f]=="") f++; l=NR; while (l>=1 && line[l]=="") l--; for (i=f;i<=l;i++) print line[i] }'; } # check_eq NAME TARGET EXPECTED SRC — exit 0, stdout==EXPECTED, no warning. check_eq() { local name="$1" target="$2" expected="$3" src="$4" local out status err out=$("$KTEXT" --klammersets none -s "$TARGETS$src" -t "$target" -d 2>"$ERR"); status=$? err=$(cat "$ERR") out=$(printf '%s' "$out" | trim) if [ $status -ne 0 ]; then echo "${red}FAIL${reset} $name — ktext exited $status" echo " stderr: $(echo "$err" | head -2)"; FAIL=$((FAIL+1)); return fi if printf '%s' "$err" | grep -qiF "warning"; then echo "${red}FAIL${reset} $name — unexpected warning" echo " stderr: $(echo "$err" | head -2)"; FAIL=$((FAIL+1)); return fi if [ "$out" = "$expected" ]; then echo "${green}PASS${reset} $name"; PASS=$((PASS+1)) else echo "${red}FAIL${reset} $name" echo " expected: [$expected]"; echo " got: [$out]"; FAIL=$((FAIL+1)) fi } # check_warn NAME TARGET EXPECTED SRC — exit 0, stdout==EXPECTED, the override # SILENT at the default verbosity and REPORTED at "-v 1" (2026-08-15; see # tst/deftype_test.sh's check_warn for why it is no longer a warning). check_warn() { local name="$1" target="$2" expected="$3" src="$4" local out status err out=$("$KTEXT" --klammersets none -s "$TARGETS$src" -t "$target" -d 2>"$ERR"); status=$? out=$(printf '%s' "$out" | trim) if [ $status -ne 0 ]; then echo "${red}FAIL${reset} $name — ktext exited $status"; FAIL=$((FAIL+1)); return fi if [ -s "$ERR" ]; then echo "${red}FAIL${reset} $name — the default run was not silent: $(head -1 "$ERR")" FAIL=$((FAIL+1)); return fi "$KTEXT" --klammersets none -s "$TARGETS$src" -t "$target" -d -v 1 >/dev/null 2>"$ERR" err=$(cat "$ERR") if ! printf '%s' "$err" | grep -qiF "overridden"; then echo "${red}FAIL${reset} $name — no override reported at -v 1"; FAIL=$((FAIL+1)); return fi if [ "$out" = "$expected" ]; then echo "${green}PASS${reset} $name"; PASS=$((PASS+1)) else echo "${red}FAIL${reset} $name" echo " expected: [$expected]"; echo " got: [$out]"; FAIL=$((FAIL+1)) fi } # check_error NAME TARGET PATTERN SRC — nonzero exit and PATTERN in the message. check_error() { local name="$1" target="$2" pattern="$3" src="$4" local out status out=$("$KTEXT" --klammersets none -s "$TARGETS$src" -t "$target" -d 2>&1); status=$? if [ $status -eq 0 ]; then echo "${red}FAIL${reset} $name — expected an error but ktext succeeded"; FAIL=$((FAIL+1)); return fi if echo "$out" | grep -qF "$pattern"; then echo "${green}PASS${reset} $name"; PASS=$((PASS+1)) else echo "${red}FAIL${reset} $name — expected error to contain [$pattern]" echo " output: $(echo "$out" | head -3)"; FAIL=$((FAIL+1)) fi } echo "${bold}Klammer definition target-list tests${reset}" echo "====================================" echo # --- One body, several targets --- echo "-- the list defines each target named --" check_eq " 1. first member gets the body" ta "x B y" \ '@@f.k : a test @@ @@f.ta,tb :: B @@ x @f@ y' check_eq " 2. second member gets the body" tb "x B y" \ '@@f.k : a test @@ @@f.ta,tb :: B @@ x @f@ y' check_error " 3. a target NOT in the list is undefined" tc "not defined for target" \ '@@f.k : a test @@ @@f.ta,tb :: B @@ x @f@ y' check_eq " 4. three members, last one" tc "x B y" \ '@@f.k : a test @@ @@f.ta,tb,tc :: B @@ x @f@ y' check_eq " 5. parameters are inherited from .k for every member" tb "x [q] y" \ '@@f.k s : a test @@ @@f.ta,tb :: [*s*] @@ x @f q @ y' check_eq " 6. a list member propagates to a target that includes it" td "x B y" \ '@@f.k : a test @@ @@f.ta,tc :: B @@ x @f@ y' # --- Malformed lists --- echo echo "-- malformed lists --" check_error " 7. an unknown target in the list" ta "is not defined" \ '@@f.ta,nosuch : B @@' check_error " 8. a target named twice" ta "more than once" \ '@@f.ta,ta : B @@' check_error " 9. \".k\" in a list" ta "in a list of targets" \ '@@f.k,ta : a test @@' check_error "10. \".o\" in a list" ta "in a list of targets" \ '@@f.o,ta : an option set @@' # "*" is the general target written out: an assertion that the klammer works # for EVERY target, including ones not yet defined. In a list it is either # redundant or a misunderstanding. check_error "10a. \"*\" in a list" ta 'names "*" in a list of targets' \ '@@f.*,ta : b @@' check_eq "10b. \"*\" alone defines every target" tb "x body" \ '@@f.* : body @@ x @f@' check_eq "10c. ... including one it does not name" ta "x body" \ '@@f.* : body @@ x @f@' check_error "11. a trailing comma" ta "is not correctly defined" \ '@@f.ta, : B @@' check_error "12. an empty member" ta "is not correctly defined" \ '@@f.ta,,tb : B @@' # --- The transition table applies per member, not to the list --- echo echo "-- redefinition, decided per member --" check_error "13. create + list create collides on the shared member" ta "already defined" \ '@@f.ta : one @@ @@f.ta,tb : two @@ x @f@' # The list's default is silently ignored for ta (which already has a create) # and creates tb. This is the case a whole-list decision would get wrong. check_eq "14. existing create + list default: existing kept" ta "x one" \ '@@f.ta : one @@ @@f.ta,tb :::: two @@ x @f@' check_eq "15. existing create + list default: other member defined" tb "x two" \ '@@f.ta : one @@ @@f.ta,tb :::: two @@ x @f@' check_eq "16. list default + create for one member: replaced" ta "x real" \ '@@f.ta,tb :::: def @@ @@f.ta : real @@ x @f@' check_eq "17. list default + create for one member: other keeps default" tb "x def" \ '@@f.ta,tb :::: def @@ @@f.ta : real @@ x @f@' check_warn "18. list override warns and replaces (first member)" ta "x over" \ '@@f.k : a test @@ @@f.ta :: one @@ @@f.tb :: two @@ @@f.ta,tb ::: over @@ x @f@' check_warn "19. list override warns and replaces (second member)" tb "x over" \ '@@f.k : a test @@ @@f.ta :: one @@ @@f.tb :: two @@ @@f.ta,tb ::: over @@ x @f@' # --- The comma is a target separator ONLY after "@@" --- echo echo "-- a comma elsewhere is writer text --" check_eq "20. a comma in a body is text" ta "x a,b" \ '@@f.ta : a,b @@ x @f@' check_eq "21. a comma after an argument substitution" ta "x q,tail" \ '@@f.ta s : *s*,tail @@ x @f q @' check_eq "22. a comma after an application" ta "x q, y" \ '@@f.ta s : *s* @@ x @f q @, y' check_eq "23. a general definition is unaffected" ta "x B" \ '@@f : B @@ x @f@' # --- Diagnostics count what was WRITTEN, not how often it registered --- echo echo "-- one definition, several registrations --" # tc provides td, so "@@f.tc : ..." registers twice from one line. Before # this was grouped, the message said "2 definitions" and then printed the one # line the author wrote twice, sending them to look for a second. check_error "24. the count is of written definitions" ta \ "and a definition that declares its own parameters" \ '@@f.k s : d @@ @@f.tc : [*s*] @@' check_error "25. ... and the location names its targets" ta "(targets tc, td)" \ '@@f.k s : d @@ @@f.tc : [*s*] @@' # Two definitions in two places are still two. check_error "26. distinct places still count separately" ta \ "2 definitions that declare their own parameters" \ '@@f.k s : d @@ @@f.ta : a*s* @@ @@f.tb : b*s* @@' # The remedy, named in the message rather than implied. check_error "27. the message says what to write instead" ta \ 'Write "::" instead of ":"' \ '@@f.k s : d @@ @@f.tc : [*s*] @@' rm -f "$ERR" echo echo "====================================" echo "Results: ${PASS} passed, ${FAIL} failed" [ "$FAIL" -eq 0 ] || exit 1 exit 0