Files
klammertext/tst/recursion_test.sh
Andy Kopra 240cff4278 An output policy for the three commands, and @cond as a true special form
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.
2026-08-16 01:37:59 +02:00

140 lines
4.6 KiB
Bash
Executable File

#!/bin/bash
#
# recursion_test.sh — The klammer application recursion guard.
#
# Before the guard, a klammer that applied itself -- directly or through a
# cycle -- descended until the C++ stack was exhausted. The process died with
# SIGSEGV: no message, no location, no indication of which klammer was at
# fault, and a core dump. For a language whose premise is that users define
# their own klammers, that was the worst available failure mode.
#
# Machine::apply_klammer() now carries a depth guard (Depth_guard in
# mac/machine.cpp) that raises a Recursion_error naming the klammer and its
# location. Separately, the top-level fixed-point loop in Machine::apply()
# ends when a pass applies no klammer -- rather than when the katom list stops
# growing -- and exceeding its round limit is an error rather than a message
# followed by rendering a document with live klammers still in it.
#
# These are engine tests: no klammer set is loaded (--klammersets none) and every klammer
# used is defined inline as a fixture.
#
# Usage: ./recursion_test.sh
# Exit code: 0 if all tests pass, 1 otherwise.
PASS=0
FAIL=0
KTEXT=ktext
K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set}
red=$'\033[31m'
green=$'\033[32m'
bold=$'\033[1m'
reset=$'\033[0m'
# check_error TEST_NAME PATTERN KTEXT_ARGS...
# Runs ktext, expects a NONZERO exit status and PATTERN in the message.
# A signal death (exit >= 128) is called out separately: that is the exact
# regression this suite exists to prevent, and reporting it as "some error"
# would hide it.
check_error() {
local test_name="$1"
local pattern="$2"
shift 2
local output status
output=$("$KTEXT" "$@" 2>&1)
status=$?
if [ $status -ge 128 ]; then
echo "${red}FAIL${reset} $test_name — ktext died from signal $((status - 128))"
FAIL=$((FAIL + 1))
return
fi
if [ $status -eq 0 ]; then
echo "${red}FAIL${reset} $test_name — expected an error but ktext succeeded"
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 error to contain [$pattern]"
echo " output: $(echo "$output" | head -4)"
FAIL=$((FAIL + 1))
fi
}
# check_eq TEST_NAME EXPECTED KTEXT_ARGS...
check_eq() {
local test_name="$1"
local expected="$2"
shift 2
local output status
output=$("$KTEXT" "$@" 2>/dev/null)
status=$?
output=$(printf '%s' "$output" | tr -d '\n' | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')
if [ $status -ne 0 ]; then
echo "${red}FAIL${reset} $test_name — ktext exited $status"
FAIL=$((FAIL + 1))
return
fi
if [ "$output" = "$expected" ]; then
echo "${green}PASS${reset} $test_name"
PASS=$((PASS + 1))
else
echo "${red}FAIL${reset} $test_name"
echo " expected: [$expected]"
echo " got: [$output]"
FAIL=$((FAIL + 1))
fi
}
echo "${bold}Klammer recursion guard tests${reset}"
echo "============================="
echo
# --- Non-termination is an error, not a crash ---
check_error " 1. direct self-recursion is caught" \
"does not terminate" \
--klammersets none -s '@@f : x @f@ @@ @f@' -d
check_error " 2. the offending klammer is named" \
'applying "f"' \
--klammersets none -s '@@f : x @f@ @@ @f@' -d
check_error " 3. mutual recursion is caught" \
"does not terminate" \
--klammersets none -s '@@a : ( @b@ ) @@ @@b : [ @a@ ] @@ @a@' -d
check_error " 4. self-recursion through an argument is caught" \
"does not terminate" \
--klammersets none -s '@@w t : < *t* > @@ @@r : @w @r@ @ @@ @r@' -d
# --- Terminating nesting is untouched ---
check_eq " 5. deep but finite nesting still reduces" \
"<<<<<x>>>>>" \
--klammersets none -s '@@w t : <*t*> @@ @w @w @w @w @w x @ @ @ @ @' -d
check_eq " 6. a chain of klammers generating klammers reduces" \
"END" \
--klammersets none -s '@@k1 : @k2@ @@ @@k2 : @k3@ @@ @@k3 : @k4@ @@ @@k4 : @k5@ @@ @@k5 : @k6@ @@ @@k6 : @k7@ @@ @@k7 : @k8@ @@ @@k8 : END @@ @k1@' -d
# --- The fixed point ends on "nothing was applied", not "nothing was added" ---
#
# A klammer whose body is empty reduces without adding katoms. Under the old
# size-comparison test such a klammer looked like no progress at all.
check_eq " 7. a klammer with an empty body reduces" \
"a b" \
--klammersets none -s '@@nothing : @@ a @nothing@ b' -d
echo
echo "============================="
echo "Results: ${green}$PASS passed${reset}, ${red}$FAIL failed${reset}"
[ $FAIL -eq 0 ]