A klammer that reaches itself, directly or through a cycle, expanded until the C++ stack was exhausted: the process died from SIGSEGV with no message and no location. The former limit guarded only the top-level fixed-point iteration, never the descent through klammer application. A depth guard now raises a recursion error naming the klammer and where it was applied. The same loop's termination test moves from "the katom list stopped growing" to "a pass applied no klammer", since a klammer whose body expands to nothing is a reduction that adds no katoms; exceeding the round limit is now an error rather than a message followed by rendering a document with live klammers still in it. ktext --check locates every klammer application written in a document or in a klammer body and checks name existence, argument count, option names, and target coverage without applying anything, reporting all problems at once. This is possible because Klammertext has no catcodes: katom structure is fixed when a file is read, so a klammer body has a determinate shape before it is expanded. The check therefore reaches what the engine cannot -- the branch of a @cond that is not selected, and bodies a given render never enters. @cond's set of truth values is an open language question, so its meaning is unchanged here; an unrecognized predicate now warns, giving its value and location. tst/ gains recursion_test.sh (7 cases) and check_test.sh (19 cases), and this snapshot's test Makefile is generated from the shipped suite list so the two cannot drift apart. (from dev c27e63802406) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
140 lines
4.5 KiB
Bash
Executable File
140 lines
4.5 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 (-k 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" \
|
|
-k none -s '@@f : x @f@ @@ @f@' -d
|
|
|
|
check_error " 2. the offending klammer is named" \
|
|
'applying "f"' \
|
|
-k none -s '@@f : x @f@ @@ @f@' -d
|
|
|
|
check_error " 3. mutual recursion is caught" \
|
|
"does not terminate" \
|
|
-k none -s '@@a : ( @b@ ) @@ @@b : [ @a@ ] @@ @a@' -d
|
|
|
|
check_error " 4. self-recursion through an argument is caught" \
|
|
"does not terminate" \
|
|
-k none -s '@@w t : < *t* > @@ @@r : @w @r@ @ @@ @r@' -d
|
|
|
|
# --- Terminating nesting is untouched ---
|
|
|
|
check_eq " 5. deep but finite nesting still reduces" \
|
|
"<<<<<x>>>>>" \
|
|
-k none -s '@@w t : <*t*> @@ @w @w @w @w @w x @ @ @ @ @' -d
|
|
|
|
check_eq " 6. a chain of klammers generating klammers reduces" \
|
|
"END" \
|
|
-k 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" \
|
|
-k none -s '@@nothing : @@ a @nothing@ b' -d
|
|
|
|
echo
|
|
echo "============================="
|
|
echo "Results: ${green}$PASS passed${reset}, ${red}$FAIL failed${reset}"
|
|
[ $FAIL -eq 0 ]
|