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:
117
mac/machine.cpp
117
mac/machine.cpp
@@ -25,6 +25,56 @@ Machine::Machine()
|
||||
*/
|
||||
}
|
||||
|
||||
// Klammer application recursion guard.
|
||||
//
|
||||
// Applying a klammer expands its body, which is processed and applied in
|
||||
// turn (apply_klammer -> process_katoms -> apply -> apply_klammer), so a
|
||||
// klammer that reaches itself -- directly (@@f : x @f@ @@) or through a
|
||||
// cycle -- descends without bound. Before this guard the descent simply
|
||||
// exhausted the C++ stack: SIGSEGV, no message, no location.
|
||||
//
|
||||
// The counter is a translation-unit static rather than a Machine member for
|
||||
// two reasons: recursion can cross Machine instances (Eval::eval builds a
|
||||
// sub-Machine to re-read an @eval result, and that sub-Machine applies
|
||||
// klammers on the same C++ stack), and keeping it out of Machine avoids
|
||||
// changing the class layout shared with the dlopened sks/document.so.
|
||||
//
|
||||
// The limit bounds the C++ stack, not the language: it is far above any
|
||||
// plausible nesting depth in a document, and reaching it means a klammer
|
||||
// does not terminate. NOTE: not thread-safe; if input files are ever
|
||||
// processed in parallel this needs to become thread_local.
|
||||
namespace {
|
||||
|
||||
constexpr int max_apply_depth = 200;
|
||||
int apply_depth = 0;
|
||||
|
||||
// Rounds of the top-level fixed-point loop (see Machine::apply below). The
|
||||
// former limit of 5 was a silent truncation; it is now an error, so it is set
|
||||
// well clear of any legitimate chain of klammers generating klammers.
|
||||
constexpr int apply_round_limit = 100;
|
||||
|
||||
class Depth_guard
|
||||
{
|
||||
public:
|
||||
Depth_guard(const std::string& name, const Locator& loc)
|
||||
{
|
||||
if (apply_depth >= max_apply_depth) {
|
||||
std::stringstream ss {};
|
||||
ss << "Klammer application nested more than " << max_apply_depth
|
||||
<< " levels deep while applying " << q_(name) << ".\n"
|
||||
<< "A klammer that applies itself, directly or through a cycle "
|
||||
<< "of klammers, does not terminate.";
|
||||
throw Recursion_error(ss.str(), loc, false);
|
||||
}
|
||||
++apply_depth;
|
||||
}
|
||||
~Depth_guard() { --apply_depth; }
|
||||
Depth_guard(const Depth_guard&) = delete;
|
||||
Depth_guard& operator=(const Depth_guard&) = delete;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void Machine::process_eval_katoms(katom_list& katoms)
|
||||
{
|
||||
(void)K::log(3);
|
||||
@@ -88,6 +138,35 @@ bool is_true(const std::string& s)
|
||||
return s == "True" || s == "true" || s == "1";
|
||||
}
|
||||
|
||||
// @cond's predicate relation is currently partial in effect: is_true()
|
||||
// recognizes three strings as true and treats EVERYTHING else as false, so a
|
||||
// misspelled state variable, a "TRUE", a "yes", or a Python traceback all
|
||||
// silently select the false branch.
|
||||
//
|
||||
// What the truth values should be is an open language-policy question (see
|
||||
// notes/Klammertext_improvements.md, "The @cond predicate relation"), so the
|
||||
// semantics here is deliberately unchanged. What is added is visibility: a
|
||||
// predicate outside the provisionally recognized sets below is reported, with
|
||||
// its value and location, so the cases can be found in real documents while
|
||||
// the policy is decided. The recognized false set carries no semantics -- it
|
||||
// exists only to keep the diagnostic quiet for values that plainly mean false.
|
||||
bool is_recognized_predicate(const std::string& s)
|
||||
{
|
||||
return s.empty()
|
||||
|| s == "True" || s == "true" || s == "1"
|
||||
|| s == "False" || s == "false" || s == "0";
|
||||
}
|
||||
|
||||
void warn_unrecognized_predicate(const std::string& predicate, const Locator& loc)
|
||||
{
|
||||
if (is_recognized_predicate(predicate)) return;
|
||||
std::stringstream ss {};
|
||||
ss << "The @cond predicate " << q_(predicate)
|
||||
<< " is not a recognized truth value, so the false branch was taken.\n"
|
||||
<< " Recognized: true, True, 1 (true); false, False, 0, empty (false).";
|
||||
warning(ss.str(), loc);
|
||||
}
|
||||
|
||||
void Machine::process_cond_katoms(katom_list& katoms)
|
||||
{
|
||||
if (std::find_if(katoms.begin(), katoms.end(), begin_cond) != katoms.end()) {
|
||||
@@ -104,6 +183,7 @@ void Machine::process_cond_katoms(katom_list& katoms)
|
||||
check_bar_count(begin, bars.size());
|
||||
auto bar_1 = bars[0];
|
||||
std::string predicate = to_string(begin + 1, bar_1, true);
|
||||
warn_unrecognized_predicate(predicate, begin->m_loc);
|
||||
katom_list true_clause {};
|
||||
katom_list false_clause {};
|
||||
if (bars.size() == 2) {
|
||||
@@ -460,6 +540,7 @@ katom_list Machine::apply_klammer(
|
||||
Klammer& klammer, const std::string& target, katom_iter arguments_begin, katom_iter arguments_end)
|
||||
{
|
||||
(void)K::log(3, "argument substitution", *arguments_begin, *(arguments_end - 1));
|
||||
Depth_guard depth_guard(klammer.m_name, arguments_begin->m_loc);
|
||||
m_state.replace("K_loc", arguments_begin->m_loc.str(), false);
|
||||
auto [positional, optional, rest] =
|
||||
argument_split(arguments_begin + 1, arguments_end - 1, klammer.m_parameters.m_positional.size());
|
||||
@@ -563,10 +644,11 @@ void Machine::apply_klammer_registry(
|
||||
katoms.insert(end, applied_katoms.begin(), applied_katoms.end());
|
||||
}
|
||||
|
||||
void Machine::apply(
|
||||
int Machine::apply(
|
||||
Klammer_registry& klammer_registry, katom_list& katoms, const std::string& target)
|
||||
{
|
||||
(void)K::log(3, "Klammer_registry");
|
||||
int applied = 0;
|
||||
for (const auto& [op, cl] : find_spans(
|
||||
katoms, begin_klammer_apply, end_klammer_apply, true, command_name)) {
|
||||
auto [begin, end] = find_span_katoms(katoms, op, cl);
|
||||
@@ -574,7 +656,9 @@ void Machine::apply(
|
||||
klammer_name_from_katom(begin->m_text, begin->m_loc),
|
||||
target, begin->m_loc);
|
||||
apply_klammer_registry(klammer_registry, katoms, target, begin, end);
|
||||
++applied;
|
||||
}
|
||||
return applied;
|
||||
}
|
||||
|
||||
std::string Machine::run_phase_functions()
|
||||
@@ -622,7 +706,6 @@ void Machine::escape_target_characters(const Target& target, katom_list& katoms)
|
||||
std::string Machine::apply(const std::string& target_name, bool final_processing, bool escape_characters)
|
||||
{
|
||||
(void)K::log(3, "top level");
|
||||
int recursive_limit = 5;
|
||||
m_state.set("K_target", target_name);
|
||||
m_state.subst(m_katoms.begin(), m_katoms.end());
|
||||
|
||||
@@ -634,20 +717,26 @@ std::string Machine::apply(const std::string& target_name, bool final_processing
|
||||
if (escape_characters)
|
||||
escape_target_characters(target, m_katoms);
|
||||
|
||||
// Reduce to a fixed point. A pass reports how many klammers it applied;
|
||||
// the loop ends when a pass applies none. (It formerly ended when the
|
||||
// katom list stopped GROWING, which is not the same thing: 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 the unreduced document -- silently emitting a document with
|
||||
// live klammers still in it is worse than not emitting one. Runaway
|
||||
// recursion is caught earlier and more precisely by the depth guard in
|
||||
// apply_klammer(); this limit only bounds the number of ROUNDS, which is
|
||||
// the length of a chain of klammers that generate further klammers.
|
||||
int apply_count = 0;
|
||||
auto katom_size = m_katoms.size();
|
||||
while (true) {
|
||||
apply(m_klammers, m_katoms, target_name);
|
||||
|
||||
if (m_katoms.size() == katom_size) {
|
||||
break;
|
||||
while (apply(m_klammers, m_katoms, target_name) > 0) {
|
||||
if (++apply_count > apply_round_limit) {
|
||||
std::stringstream ss {};
|
||||
ss << "Klammer application did not reach a fixed point after "
|
||||
<< apply_round_limit << " rounds.\n"
|
||||
<< "Each round applies every klammer present; a klammer whose "
|
||||
<< "result contains further klammers starts another round.";
|
||||
throw Recursion_error(ss.str(), Locator(), false);
|
||||
}
|
||||
if (++apply_count > recursive_limit) {
|
||||
msg() << red << "Error: Recursive limit ("
|
||||
<< recursive_limit << ") reached\n" << black;
|
||||
break;
|
||||
}
|
||||
katom_size = m_katoms.size();
|
||||
}
|
||||
m_result = to_string(m_katoms.begin(), m_katoms.end());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user