Recursion guard and static klammer checking

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>
This commit is contained in:
2026-08-01 15:41:43 +02:00
parent 55a99c7eeb
commit 4306dcd490
11 changed files with 849 additions and 29 deletions

View File

@@ -1,5 +1,6 @@
#include <iostream>
#include "error.h"
#include "check.h"
#include "command.h"
#include "log.h"
#include "argv.h"
@@ -25,6 +26,9 @@ int main(int argc, char* argv[])
"pathname", "", "'word'");
args.flag("d", "Display the output to the screen, rather than writing files.");
args.flag("m", "Show the Klammermachine state at the beginning of processing.");
args.flag("check", "Check every klammer application in the input and in the "
"body of every defined klammer, report all problems found, and exit "
"without producing output.");
args.opt("v", "'verbosity'", "degree", "0", "'verbosity'");
if (show_usage(argc, argv)) {
@@ -52,8 +56,13 @@ int main(int argc, char* argv[])
parse_args(input_filenames, args.as_string("t"),
expand_tilde(args.as_string("o")), args.as_bool("d"));
// --check produces no output, so it needs neither a target nor -d.
// With no -t it checks every defined target, which is the more useful
// default here: "does this document hold together at all?"
bool check_only = args.as_bool("check");
if (*(output_filename.end() - 1) == '*'
&& !display_only) {
&& !display_only && !check_only) {
throw Argument_error(
"You must specify an output target or "
"display the results with the \"-d\" flag.",
@@ -106,6 +115,14 @@ int main(int argc, char* argv[])
std::cout << M << "\n";
}
// --check reports statically, before anything is applied: all problems
// at once, including ones in @cond branches that are not selected and
// in klammer bodies that this render would never reach.
if (check_only) {
int errors = report_diagnostics(check_machine(M, target), std::cout);
return errors > 0 ? 1 : 0;
}
std::string result = trim(M.apply(target));
if (display_only && !result.empty()) {
std::cout << result << "\n";