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>
142 lines
5.9 KiB
C++
142 lines
5.9 KiB
C++
#include <iostream>
|
|
#include "error.h"
|
|
#include "check.h"
|
|
#include "command.h"
|
|
#include "log.h"
|
|
#include "argv.h"
|
|
#include "file.h"
|
|
#include "util.h"
|
|
#include "machine.h"
|
|
#include "show.h"
|
|
#include "target_registry.h"
|
|
#include "klammerset_registry.h"
|
|
|
|
int main(int argc, char* argv[])
|
|
{
|
|
try {
|
|
set_verbose_level(argc, argv);
|
|
Argv args {};
|
|
args.req("filenames", "Input files in Klammertext format. ", "'list'");
|
|
args.opt("s", "Text processed before input files.", "input-string", "", "'text'");
|
|
args.opt("t","Output target; default is general (unspecified)", "target",
|
|
Target_registry::general_name, "'word'");
|
|
args.opt("o", "Output basename; meaning and default defined by target.", "basename",
|
|
"", "'word'");
|
|
args.opt("k", "Klammerset symbol (resolved on the klammerset search path) or the pathname of a klammerset definition file; default is the Standard Klammer Set. With a value of \"none\", no klammerset is loaded.",
|
|
"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)) {
|
|
args.usage(file_basename(argv[0]));
|
|
exit(1);
|
|
}
|
|
args.parse(argc, argv);
|
|
if (verbose_level > 0) {
|
|
args.describe();
|
|
}
|
|
std::string input_text = args.as_string("s");
|
|
// argv boundaries are authoritative (a shell-quoted "my file.kt" is
|
|
// one element); group_filename_tokens adds the standalone-"/" list
|
|
// form, the existence rescue for unquoted spaces, and ~ expansion.
|
|
std::vector<std::string> input_filenames =
|
|
group_filename_tokens(args.as_vector("filenames"));
|
|
|
|
if (input_text.empty() && input_filenames.empty()) {
|
|
throw Argument_error(
|
|
"You must specify input filenames and/or text", Locator());
|
|
}
|
|
|
|
auto [target, output_dir, output_basename, output_filename,
|
|
write_files, display_only] =
|
|
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 && !check_only) {
|
|
throw Argument_error(
|
|
"You must specify an output target or "
|
|
"display the results with the \"-d\" flag.",
|
|
Locator());
|
|
}
|
|
|
|
Machine M;
|
|
M.m_state.open_frame("ktext");
|
|
M.m_state.set("K_target", target);
|
|
M.m_state.set("K_output_dir", output_dir);
|
|
M.m_state.set("K_output_basename", output_basename);
|
|
M.m_state.set("K_stdout_only", display_only ? "true" : "false");
|
|
// The list form uses the standalone-"/" separator (filenames may
|
|
// contain spaces); K_input_filename is the single root input file.
|
|
M.m_state.set("K_input_filenames", join(input_filenames, " / "));
|
|
M.m_state.set("K_input_filename",
|
|
input_filenames.empty() ? "" : input_filenames[0]);
|
|
M.m_state.set("K_verbose_level", std::to_string(verbose_level));
|
|
|
|
if (!input_filenames.empty()) {
|
|
M.m_state.set(
|
|
"K_input_dir",absolute_pathname(file_directory(input_filenames[0])));
|
|
} else {
|
|
M.m_state.set("K_input_dir", fs::current_path().string());
|
|
}
|
|
|
|
std::string klammerset_filename = args.as_string("k");
|
|
if (klammerset_filename != "none") {
|
|
if (klammerset_filename.empty()) {
|
|
klammerset_filename = M.m_state.value("KLAMMERTEXT_HOME") + "/sks/sks.k";
|
|
} else if (is_klammerset_symbol(klammerset_filename)) {
|
|
// A bare symbol resolves on the klammerset search path;
|
|
// the local stage is the input document's directory (the
|
|
// cwd when the input is a string).
|
|
klammerset_filename = resolve_klammerset_symbol(
|
|
klammerset_filename, M.m_state.value("K_input_dir"), Locator()).string();
|
|
}
|
|
K::log(1, "Reading klammerset filename: " + klammerset_filename);
|
|
M.read(fs::path(klammerset_filename));
|
|
}
|
|
|
|
if (!input_text.empty()) {
|
|
M.read(input_text + "\n");
|
|
}
|
|
for (auto p : input_filenames) {
|
|
M.read(fs::path(p));
|
|
}
|
|
|
|
if (args.as_bool("m")) {
|
|
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";
|
|
} else if (!result.empty()) {
|
|
msg() << "Output filename: " << output_filename << "\n";
|
|
string_to_file(output_filename, result + "\n");
|
|
(void)K::log(1, "Wrote file: " + output_filename);
|
|
}
|
|
}
|
|
catch (Error& err) {
|
|
err.print_message();
|
|
std::cout << "\n";
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|