A snapshot of the development tree. The substantial changes since the last one:
COMMAND OUTPUT POLICY. The three commands display text in exactly three cases,
and each owns a stream: LOGGING under "-v" greater than 0 and an ERROR before
termination go to STDERR; OUTPUT THE USER ASKED FOR goes to STDOUT. For ktext
that output is a document, so "ktext doc.kt -d | ..." is now safe -- logging
used to share the stream and land inside the document. A bare command prints
its usage and succeeds rather than failing. Colour is emitted only to a
terminal, per stream, and NO_COLOR is honoured.
"-v 1" reports every decision whose outcome you could not have read off your own
input: the klammerset that was loaded and from which file, a font's directory, a
":files" name's file, how "-o" was expanded. Higher levels are the trace.
The commands no longer warn and continue: an anomaly is an error, described with
its location. Two exceptions remain, each for a stated reason -- a condition
that is expected and temporary by design, and a judgment that is a heuristic
rather than exact.
@cond IS NOW A TRUE SPECIAL FORM, resolved at APPLICATION time rather than when
the file is read. Two consequences for a writer:
* a state variable reaches the predicate. "@@@state Flag :value true @@@
@cond *Flag* | T | F @" renders "T"; it used to see the literal "*Flag*" and
silently take the false branch. The document now behaves like a klammer
body, whose arguments are bound before its conditionals are decided.
* nothing in a discarded branch happens -- it is not read, not evaluated, not
expanded. An @eval in the branch not taken used to run anyway.
Its predicate relation is total and strict: true, True, 1; false, False, 0, and
empty; anything else is an error at the @cond rather than silently false.
@eval REACHING OUTSIDE. ":shell" and ":haskell" now keep the command's standard
error out of the document (it appears under "-v 1") and treat a nonzero exit as
an error naming what the command reported. A command that exits nonzero on
purpose -- "grep" finding no match -- says so with "|| true".
KLAMMER SETS. Several combine: "--klammersets a b c" loads all three in the
order given, sharing one namespace, with the definition modes deciding
collisions. "none" means none and may not be combined with other symbols. A
klammerset with symbol X is declared in a file X/X.k, which is what lets two
sets require the same third set without loading it twice.
TESTS. Four new suites: the kdiag command's interface, the @eval primitive's
contract with the outside world, and verbosity at both tiers. Three suites
that could not run on macOS at all now do.
Assembled from dev commit 6c8ee6c22fca.
312 lines
15 KiB
Bash
Executable File
312 lines
15 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# kdiag_test.sh — the kdiag command's interface.
|
|
#
|
|
# kdiag is the PROGRAMMER's command in the 2026-08-14 three-command redesign:
|
|
# ktext renders a document for an author, kdesc describes a klammer set for a
|
|
# designer, kdiag dissects katoms and the Machine. It had no suite of its own,
|
|
# which is how four defects survived in it -- including a "--check" that
|
|
# reported "0 diagnostics, 0 errors" for every input there is, while every
|
|
# other suite stayed green. An absent test is what let that read as a pass, so
|
|
# each of the four is pinned here by OUTCOME.
|
|
#
|
|
# Its peculiarities, and why each is what it is:
|
|
#
|
|
# * Its input positional is OPTIONAL, so "kdiag --machine" answers a question
|
|
# about the Machine rather than about a document.
|
|
# * --klammer registers the @@ tier and --system the @@@ tier; --process does
|
|
# both. A registered definition's katoms are CONSUMED, so they show up
|
|
# only under --replaced -- their disappearance is the evidence.
|
|
# * Extraction is TOLERANT here and nowhere else. The tiers are ordered (a
|
|
# klammer names a target), so running one without the other leaves
|
|
# definitions that cannot be registered. In kdiag that is not an error:
|
|
# the definition is skipped and its katoms stay visible, which IS the
|
|
# report. ktext must keep throwing on the same input, and the contrast is
|
|
# asserted, not assumed.
|
|
# * It deliberately has NO --klammersets. A klammerset arrives the way
|
|
# anything else does, "@read sks/sks.k @": a debugger must not depend on
|
|
# the machinery it debugs, so kdiag still runs when symbol resolution is
|
|
# what broke.
|
|
#
|
|
# Engine tier: no klammerset is loaded, so every fixture defines what it needs
|
|
# inline. The one case that wants the SKS reads it by path.
|
|
#
|
|
# Usage: ./kdiag_test.sh (needs KLAMMERTEXT_HOME set; kdiag and ktext on PATH)
|
|
# Exit code: 0 if all tests pass, 1 otherwise.
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
KDIAG=kdiag
|
|
KTEXT=ktext
|
|
K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set}
|
|
|
|
red=$'\033[31m'
|
|
green=$'\033[32m'
|
|
bold=$'\033[1m'
|
|
reset=$'\033[0m'
|
|
|
|
plain() { printf '%s' "$1" | sed 's/\x1b\[[0-9;]*m//g'; }
|
|
|
|
# GNU coreutils' "timeout" is NOT present on macOS, and Homebrew's is named
|
|
# "gtimeout", so a bare "timeout" made this suite fail WHOLESALE there -- every
|
|
# case, because the command never ran at all (found on Olion, 2026-08-16). The
|
|
# guard is a safety net against a hung command, not part of what is being
|
|
# tested, so it is optional: bound the command where the tool exists, run it
|
|
# directly where it does not.
|
|
if command -v timeout >/dev/null 2>&1; then
|
|
limited() { timeout 60 "$@"; }
|
|
elif command -v gtimeout >/dev/null 2>&1; then
|
|
limited() { gtimeout 60 "$@"; }
|
|
else
|
|
limited() { "$@"; }
|
|
fi
|
|
|
|
# run KDIAG_ARGS... — sets OUT (colour codes stripped) and STATUS. The status
|
|
# must be taken from the command itself: a pipeline inside a command
|
|
# substitution reports the SED's status, so "kdiag | plain" would call every
|
|
# failure a success. Strip afterwards instead.
|
|
OUT=''
|
|
STATUS=0
|
|
run() {
|
|
OUT=$(limited "$@" 2>&1); STATUS=$?
|
|
OUT=$(plain "$OUT")
|
|
}
|
|
|
|
# A signal death is not an error exit: 128+n. Report it distinctly, so a
|
|
# crash can never be read as a reported error.
|
|
died_by_signal() { [ "$1" -gt 128 ]; }
|
|
|
|
pass() { echo "${green}PASS${reset} $1"; PASS=$((PASS + 1)); }
|
|
fail() { echo "${red}FAIL${reset} $1"; [ -n "$2" ] && echo " $2"; FAIL=$((FAIL + 1)); }
|
|
|
|
# shows NAME PATTERN KDIAG_ARGS... — exits 0 and the output matches PATTERN.
|
|
shows() {
|
|
local name="$1" pattern="$2"; shift 2
|
|
run "$KDIAG" "$@"
|
|
if died_by_signal "$STATUS"; then
|
|
fail "$name" "died by signal $((STATUS - 128))"; return
|
|
fi
|
|
if [ "$STATUS" -ne 0 ]; then
|
|
fail "$name" "exit $STATUS: $(printf '%s' "$OUT" | head -2)"; return
|
|
fi
|
|
if printf '%s' "$OUT" | grep -Eq -- "$pattern"; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "no match for [$pattern]; got: $(printf '%s' "$OUT" | head -2)"
|
|
fi
|
|
}
|
|
|
|
# absent NAME PATTERN KDIAG_ARGS... — exits 0 and the output does NOT match.
|
|
absent() {
|
|
local name="$1" pattern="$2"; shift 2
|
|
run "$KDIAG" "$@"
|
|
if died_by_signal "$STATUS"; then
|
|
fail "$name" "died by signal $((STATUS - 128))"; return
|
|
fi
|
|
if [ "$STATUS" -ne 0 ]; then
|
|
fail "$name" "exit $STATUS: $(printf '%s' "$OUT" | head -2)"; return
|
|
fi
|
|
if printf '%s' "$OUT" | grep -Eq -- "$pattern"; then
|
|
fail "$name" "unexpected match [$pattern]"
|
|
else
|
|
pass "$name"
|
|
fi
|
|
}
|
|
|
|
# reports NAME PATTERN KDIAG_ARGS... — NONZERO exit, no signal, PATTERN shown.
|
|
reports() {
|
|
local name="$1" pattern="$2"; shift 2
|
|
run "$KDIAG" "$@"
|
|
if died_by_signal "$STATUS"; then
|
|
fail "$name" "died by signal $((STATUS - 128))"; return
|
|
fi
|
|
if [ "$STATUS" -eq 0 ]; then
|
|
fail "$name" "expected a nonzero exit, got 0"; return
|
|
fi
|
|
if printf '%s' "$OUT" | grep -Eq -- "$pattern"; then
|
|
pass "$name"
|
|
else
|
|
fail "$name" "no match for [$pattern]; got: $(printf '%s' "$OUT" | head -2)"
|
|
fi
|
|
}
|
|
|
|
# A klammer definition and an application of it, with no target named: the
|
|
# general target needs no @@@target, so the @@ tier can be exercised alone.
|
|
GREET='@@greet name : Hello, *name*. @@ @greet World @'
|
|
|
|
# Input that declares its own target AND a klammer for it. The two tiers are
|
|
# ordered, so this is what tells --klammer and --system apart.
|
|
OWNTARGET='@@@target foo | Foo output @@@ @@bar.foo : B @@ text'
|
|
|
|
# A file for --read to pull in.
|
|
READFIXTURE=$(mktemp /tmp/kdiag_read.XXXXXX)
|
|
printf 'READFILE\n' > "$READFIXTURE"
|
|
trap 'rm -f "$READFIXTURE"' EXIT
|
|
|
|
echo "${bold}kdiag interface tests${reset}"
|
|
echo "====================="
|
|
echo
|
|
|
|
echo "-- the input positional is optional --"
|
|
# The Machine's initial state is a question about the Machine, and the one
|
|
# thing a programmer wants before feeding it anything.
|
|
shows " 1. --machine with no input at all" 'Machine' --machine
|
|
shows " 2. ... and shows the built-in argtypes" 'Argtypes \([0-9]+\)' --machine
|
|
shows " 3. ... and the pseudo-targets" 'Targets \([0-9]+\)' --machine
|
|
shows " 4. an input is still accepted" '⟨@i' --type '@i x @'
|
|
# Listed under "Arguments:", so it must not be repeated under "Options:".
|
|
usage=$(plain "$(limited "$KDIAG" 2>&1)")
|
|
n_input=$(printf '%s\n' "$usage" | grep -c '\[<input>\]')
|
|
if [ "$n_input" = "2" ]; then
|
|
pass " 5. usage lists [<input>] once as a positional (plus the Usage: line)"
|
|
else
|
|
fail " 5. usage lists [<input>] $n_input times, expected 2" \
|
|
"$(printf '%s\n' "$usage" | grep -n '\[<input>\]')"
|
|
fi
|
|
|
|
echo
|
|
echo "-- registration: one flag per definition tier --"
|
|
# A registered definition is CONSUMED. Its disappearance from the katom
|
|
# display is the evidence that it registered, and --replaced brings it back.
|
|
absent " 6. --klammer consumes the @@ definition" '⟨@@greet⟩' --klammer "$GREET"
|
|
shows " 7. ... and --replaced shows it again" '⟨@@greet⟩' --klammer --replaced "$GREET"
|
|
shows " 8. ... and the klammer is in the registry" 'Klammers \(1\)' --klammer --machine "$GREET"
|
|
absent " 9. --system consumes the @@@ definition" '⟨@@@target⟩' --system "$OWNTARGET"
|
|
shows "10. ... and the target is in the registry" 'foo → ' --system --machine "$OWNTARGET"
|
|
shows "11. --process registers both tiers" 'Klammers \(1\)' --process --machine "$OWNTARGET"
|
|
absent "12. ... and consumes both" '⟨@@' --process "$OWNTARGET"
|
|
|
|
echo
|
|
echo "-- tolerant extraction: kdiag only --"
|
|
# --klammer alone cannot register @@bar.foo, because the target "foo" it names
|
|
# is declared by the @@@ tier this run did not process. Skipping it is the
|
|
# report, not a failure -- the definition's katoms stay on screen.
|
|
shows "13. --klammer alone does not fail on an unregisterable klammer" \
|
|
'⟨@@bar.foo⟩' --klammer "$OWNTARGET"
|
|
absent "14. ... and does not register it either" \
|
|
'Klammers \(1\)' --klammer --machine "$OWNTARGET"
|
|
shows "15. --system alone leaves the @@ katoms visible" \
|
|
'⟨@@bar.foo⟩' --system "$OWNTARGET"
|
|
# The contrast. In ktext the same input is a genuine error: a document naming
|
|
# an undefined target would otherwise render wrongly and silently.
|
|
# The pattern must sit on ONE line of the message: an error is wrapped for the
|
|
# terminal, so "is not defined" can arrive with a newline inside it and a
|
|
# line-oriented grep will never see it.
|
|
run "$KTEXT" --klammersets none -s '@@bar.nosuchtarget : B @@' -d
|
|
if [ "$STATUS" -ne 0 ] && printf '%s' "$OUT" | grep -q 'target "nosuchtarget"'; then
|
|
pass "16. ktext still THROWS on a klammer naming an undefined target"
|
|
else
|
|
fail "16. ktext accepted an undefined target (exit $STATUS)" \
|
|
"$(printf '%s' "$OUT" | head -2)"
|
|
fi
|
|
|
|
echo
|
|
echo "-- --check --"
|
|
# The regression: check_machine() read machine.m_katoms, which read() fills and
|
|
# process() -- how kdiag builds its katoms -- does not. So it had nothing to
|
|
# check and said so, for every input there is. These two are the guard: a
|
|
# clean check must be able to fail.
|
|
shows "17. a correct input checks clean" '0 diagnostics' --process --check "$GREET"
|
|
reports "18. an undefined klammer is found" '@nosuch is not defined' \
|
|
--process --check '@@g : @nosuch x @ @@'
|
|
reports "19. ... and the body it sits in is named" 'in body of @g' \
|
|
--process --check '@@g : @nosuch x @ @@'
|
|
reports "20. a wrong argument count is found" 'is given 2' \
|
|
--process --check "@@greet name : Hello, *name*. @@ @greet a | b @"
|
|
# Without --klammer/--system/--process nothing is registered, so every
|
|
# application is undefined. Reporting 80 spurious errors without saying why
|
|
# would be worse than the old silence; the hint is part of the report.
|
|
reports "21. --check alone explains why nothing is registered" \
|
|
'No klammers are registered' --check "$GREET"
|
|
reports "22. ... and names the way to fix it" '--process' --check "$GREET"
|
|
# --machine must print even when the check failed: the state shown last has to
|
|
# reflect everything that happened, so the nonzero exit waits for it.
|
|
reports "23. --machine still prints after a failed check" \
|
|
'Machine' --process --check --machine '@nosuch x @'
|
|
|
|
echo
|
|
echo "-- a klammerset reaches kdiag only as input --"
|
|
# No --klammersets, deliberately: a debugger must not depend on the machinery
|
|
# it debugs. The flag must be REJECTED, not silently ignored.
|
|
reports "24. --klammersets is not a kdiag argument" 'argument error' \
|
|
--klammersets sks '@i x @'
|
|
# ...so the advice on an undefined-klammer error must not offer it. It did
|
|
# until 2026-08-15: the one remedy the error suggested was a flag this command
|
|
# rejects. A wrong hint is worse than none, because it is followed.
|
|
#
|
|
# The trigger is a GENERATOR: an @eval whose result holds a klammer. A klammer
|
|
# written literally in the input is only displayed, never applied, so nothing
|
|
# reaches the catch block -- which is also why this advice went years unread.
|
|
UNDEF='@eval chr(64)+"nosuch 7 "+chr(64) @'
|
|
reports "24a. the error advice names a way kdiag has" '@read sks/sks\.k @' \
|
|
--process "$UNDEF"
|
|
# absent_in_error NAME PATTERN ARGS... -- the pattern is missing whatever the
|
|
# exit status; these run on a path that exits 1 by design.
|
|
absent_in_error() {
|
|
local name="$1" pattern="$2"; shift 2
|
|
run "$KDIAG" "$@"
|
|
if printf '%s' "$OUT" | grep -Eq -- "$pattern"; then
|
|
fail "$name" "unexpected match [$pattern]"
|
|
else
|
|
pass "$name"
|
|
fi
|
|
}
|
|
absent_in_error "24b. ... and not the flag it rejects" '\-\-klammersets' \
|
|
--process "$UNDEF"
|
|
# The advice is appended to the description, which is then justified to 80
|
|
# columns -- without a blank line between them the two ran together into one
|
|
# word ("...unspecified targetkdiag loads no klammerset...").
|
|
absent_in_error "24c. ... and does not run onto the message" 'target[a-z]' \
|
|
--process "$UNDEF"
|
|
shows "25. @read loads the SKS instead" 'Klammers \([0-9][0-9]+\)' \
|
|
"@read $K/sks/sks.k @" --process --machine
|
|
shows "26. ... and the SKS then checks clean" '0 diagnostics' \
|
|
"@read $K/sks/sks.k @" --process --check
|
|
# ...and the clean result above is a real one: the same input with a fault
|
|
# added must still be caught, or case 26 says nothing.
|
|
reports "27. ... while a fault added to it is still caught" '@nosuchklammer is not defined' \
|
|
"@read $K/sks/sks.k @ @@bad : @nosuchklammer x @ @@" --process --check
|
|
|
|
echo
|
|
echo "-- the katom display flags --"
|
|
shows "28. --type subscripts the katom type" '⟨@i.⟩' --type '@i x @'
|
|
shows "29. --index subscripts the list index" '⟨@i.' --index '@i x @'
|
|
shows "30. --spans shows span endpoints" '⟨@i.⟩.*⟨@.⟩' --spans '@i x @'
|
|
shows "31. --args parses positional arguments" 'required' --args --pos 2 'a | b | c'
|
|
shows "32. --pos sets how many are positional" 'rest: c' --args --pos 2 'a | b | c'
|
|
shows "33. --ignore removes removed text" '^a b' --ignore 'a #[gone]# b'
|
|
shows "34. --all shows what was removed" '⟨gone⟩' --ignore --all 'a #[gone]# b'
|
|
shows "35. --ws applies whitespace operators" 'ab' --ws 'a#- b'
|
|
# A DECOMPOSED sequence: the base letter followed by U+0308 COMBINING
|
|
# DIAERESIS, not the precomposed U+00FC. The diacritic FOLLOWS the base
|
|
# letter -- that is the design of the ^ codes -- so the two spellings look
|
|
# identical in an editor and only one of them matches.
|
|
shows "36. --nonascii decodes to a combining sequence" 'ü' --nonascii '^u"'
|
|
shows "37. --literal marks a literal span" 'x' --literal "^'x'^"
|
|
# Evaluating needs a target: Eval::eval re-reads its result under K_target, and
|
|
# kdiag has no target argument, so with the variable unset EVERY @eval died
|
|
# with an argument error naming something the user never wrote. A command that
|
|
# specifies no target evaluates under the GENERAL target (Andy, 2026-08-15),
|
|
# which is also what kdiag means -- it loads no klammerset, so nothing
|
|
# target-specific is in scope. All three eval modes are checked, because the
|
|
# failure was in the shared read-back and not in any one of them.
|
|
shows "38. --eval evaluates a Python expression" '^4$' --eval '@eval 2 + 2 @'
|
|
shows "38a. ... and --process does too" '^4$' --process '@eval 2 + 2 @'
|
|
shows "38b. ... in :shell mode" '^hi$' --process '@eval :shell echo hi @'
|
|
shows "38c. ... and a module.function auto-imports" '^3$' --process '@eval len("abc") @'
|
|
shows "38d. --read reads a file" 'READFILE' --read "@read $READFIXTURE @"
|
|
shows "39. --cond selects a branch" 'yes' --cond '@cond true | yes | no @'
|
|
shows "40. --rewrite reports a rewrite" '.' --rewrite '@i x @'
|
|
shows "41. --text shows text katoms" 'x' --text '@i x @'
|
|
shows "42. --ignored shows ignored katoms" '.' --ignore --ignored 'a #[gone]# b'
|
|
|
|
echo
|
|
echo "-- verbosity is about processing --"
|
|
shows "43. -v takes a value" '.' -v 1 '@i x @'
|
|
|
|
echo
|
|
echo "====================="
|
|
echo "Results: ${PASS} passed, ${FAIL} failed"
|
|
[ "$FAIL" -eq 0 ] || exit 1
|
|
exit 0
|