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.
This commit is contained in:
@@ -41,7 +41,7 @@ are regenerated on each release — patches cannot be merged directly.
|
|||||||
Report problems (or send patches) to the author; accepted changes are
|
Report problems (or send patches) to the author; accepted changes are
|
||||||
applied to the development tree and appear in a following snapshot.
|
applied to the development tree and appear in a following snapshot.
|
||||||
|
|
||||||
This snapshot was assembled from development commit `46f54080bd9a`.
|
This snapshot was assembled from development commit `6c8ee6c22fca`.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
174
com/kdesc.cpp
174
com/kdesc.cpp
@@ -16,18 +16,18 @@ static void font_usage()
|
|||||||
{
|
{
|
||||||
std::cout <<
|
std::cout <<
|
||||||
"Font maintenance commands:\n"
|
"Font maintenance commands:\n"
|
||||||
" --font List the available fonts\n"
|
" --font List the available fonts\n"
|
||||||
" --font list The same\n"
|
" --font list The same\n"
|
||||||
" --font samples <output-dir> Write a sample page of the available fonts\n"
|
" --font samples <output-dir> Write a sample page of the available fonts\n"
|
||||||
" to <output-dir>/index.html\n"
|
" to <output-dir>/index.html\n"
|
||||||
" --font samples <input-dir> <output-dir>\n"
|
" --font samples <input-dir> <output-dir>\n"
|
||||||
" Write a sample page for the (not yet\n"
|
" Write a sample page for the (not yet\n"
|
||||||
" installed) font files under <input-dir>\n"
|
" installed) font files under <input-dir>\n"
|
||||||
" --font install <input-dir> Install the font files found under\n"
|
" --font install <input-dir> Install the font files found under\n"
|
||||||
" <input-dir> into the user font directory\n"
|
" <input-dir> into the user font directory\n"
|
||||||
" --font install <input-dir> <output-dir>\n"
|
" --font install <input-dir> <output-dir>\n"
|
||||||
" Install into <output-dir> instead\n"
|
" Install into <output-dir> instead\n"
|
||||||
" --font help This description\n"
|
" --font help This description\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Fonts are searched in the directories of the KLAMMERTEXT_FONTS\n"
|
"Fonts are searched in the directories of the KLAMMERTEXT_FONTS\n"
|
||||||
"environment variable (colon-separated; default $HOME/.klammertext/fonts)\n"
|
"environment variable (colon-separated; default $HOME/.klammertext/fonts)\n"
|
||||||
@@ -38,11 +38,11 @@ static void coverage_usage()
|
|||||||
{
|
{
|
||||||
std::cout <<
|
std::cout <<
|
||||||
"Coverage commands:\n"
|
"Coverage commands:\n"
|
||||||
" --coverage Report the targets each klammer covers\n"
|
" --coverage Report the targets each klammer covers\n"
|
||||||
" --coverage all Also name the file each klammer is written\n"
|
" --coverage all Also name the file each klammer is written\n"
|
||||||
" in, and show every \"needs attention\"\n"
|
" in, and show every \"needs attention\"\n"
|
||||||
" category, including the empty ones\n"
|
" category, including the empty ones\n"
|
||||||
" --coverage help This description\n"
|
" --coverage help This description\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Coverage is a fact about a klammer: the targets it can render to. It is\n"
|
"Coverage is a fact about a klammer: the targets it can render to. It is\n"
|
||||||
"DERIVED where the definitions determine it (a general body of klammer\n"
|
"DERIVED where the definitions determine it (a general body of klammer\n"
|
||||||
@@ -60,10 +60,9 @@ static void katom_usage()
|
|||||||
{
|
{
|
||||||
std::cout <<
|
std::cout <<
|
||||||
"Katom commands:\n"
|
"Katom commands:\n"
|
||||||
" --katoms List the katom types\n"
|
" --katoms List the katom types\n"
|
||||||
" --katoms full The same, with the regex each type is\n"
|
" --katoms full The list, with the regex syntax used for a march\n"
|
||||||
" matched by\n"
|
" --katoms help This description\n";
|
||||||
" --katoms help This description\n";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The words of a variadic option, with the empty ones dropped.
|
// The words of a variadic option, with the empty ones dropped.
|
||||||
@@ -82,33 +81,47 @@ static void klammerset_usage()
|
|||||||
{
|
{
|
||||||
std::cout <<
|
std::cout <<
|
||||||
"Klammerset commands:\n"
|
"Klammerset commands:\n"
|
||||||
" --klammerset List the klammersets on the search path\n"
|
" --klammersets List the klammersets on the search path\n"
|
||||||
" --klammerset list The same\n"
|
" --klammersets <symbols> Load the klammersets specified by <symbols>\n"
|
||||||
" --klammerset help This description\n"
|
" --klammersets help This description\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
"Without an argument, all available klammersets are listed. With one or more\n"
|
||||||
|
"klammerset symbols, those klammersets are loaded for analysis by the -k, -t,\n"
|
||||||
|
"--argtypes, --optionsets, and --coverage arguments.\n\n"
|
||||||
|
"SEVERAL SETS COMBINE. The symbols are a list, loaded in the order given, and\n"
|
||||||
|
"the klammers of all of them share ONE flat namespace -- membership in a set is\n"
|
||||||
|
"provenance, not containment. Where two sets define the same klammer, the\n"
|
||||||
|
"definition modes decide: \":\" is an error if the name is already defined,\n"
|
||||||
|
"\":::\" replaces the earlier definition (with a warning), and \"::::\" yields to\n"
|
||||||
|
"any later one. So a set of house overrides is loaded after the set it\n"
|
||||||
|
"adjusts, and a set of defaults before it.\n\n"
|
||||||
|
"The Standard Klammer Set is loaded by default. If the <symbols> argument is\n"
|
||||||
|
"\"none\", no klammerset is loaded.\n\n"
|
||||||
"A klammerset symbol x names the declaration file x/x.k, searched for in:\n"
|
"A klammerset symbol x names the declaration file x/x.k, searched for in:\n"
|
||||||
"the current directory (for ktext, the input document's directory), the\n"
|
"1) the current directory (for ktext, the input document's directory); 2) the\n"
|
||||||
"KLAMMERTEXT_KLAMMERSETS directories (colon-separated; default\n"
|
"KLAMMERTEXT_KLAMMERSETS directories (colon-separated; the default is\n"
|
||||||
"$HOME/.klammertext/klammersets), and $KLAMMERTEXT_HOME. The first hit\n"
|
"$HOME/.klammertext/klammersets); and 3) $KLAMMERTEXT_HOME. The first hit\n"
|
||||||
"wins, so a document-local klammerset shadows an installed one, which\n"
|
"wins, so a document-local klammerset shadows an installed one, which\n"
|
||||||
"shadows a distributed one.\n";
|
"shadows a distributed one.\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
static void klammerset_command(const strings_t& words)
|
static void klammerset_command(Machine& machine, const strings_t& words)
|
||||||
{
|
{
|
||||||
std::string verb = words.empty() ? "list" : words[0];
|
if (words.empty()) {
|
||||||
if (verb == "list") {
|
|
||||||
std::cout << boldblack << "Klammersets\n" << black
|
std::cout << boldblack << "Klammersets\n" << black
|
||||||
<< describe_klammerset_search(fs::current_path().string());
|
<< describe_klammerset_search(fs::current_path().string());
|
||||||
} else if (verb == "help") {
|
} else if (words[0] == "help") {
|
||||||
klammerset_usage();
|
klammerset_usage();
|
||||||
} else {
|
} else {
|
||||||
std::cout << "Unrecognized klammerset command: --klammerset "
|
// "none" is not filtered out here: load_klammersets() is the single
|
||||||
<< join(words, " ") << "\n\n";
|
// place that decides what the word means, including that it may not be
|
||||||
klammerset_usage();
|
// combined with other symbols. Deciding it twice is how kdesc came to
|
||||||
|
// accept "--klammersets none sks" and silently load neither.
|
||||||
|
load_klammersets(machine, words);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static void font_command(const strings_t& words)
|
static void font_command(const strings_t& words)
|
||||||
{
|
{
|
||||||
std::string verb = words.empty() ? "list" : words[0];
|
std::string verb = words.empty() ? "list" : words[0];
|
||||||
@@ -147,21 +160,24 @@ int main(int argc, char* argv[])
|
|||||||
// regexes and the coverage file column are words of their own options
|
// regexes and the coverage file column are words of their own options
|
||||||
// rather than verbosity levels.
|
// rather than verbosity levels.
|
||||||
args.var("k", "Show klammers. With <text>, only those whose name or description contains <text>.", "text");
|
args.var("k", "Show klammers. With <text>, only those whose name or description contains <text>.", "text");
|
||||||
args.flag("t", "Show the targets for Klammertext output");
|
|
||||||
args.flag("c", "Show the codes for accented vowels and other special characters");
|
args.flag("c", "Show the codes for accented vowels and other special characters");
|
||||||
args.opt("i", "Input filename. If given, it is used instead of the SKS.", "filename", "", "'text'");
|
args.flag("t", "Show the targets for Klammertext output");
|
||||||
|
args.opt("i", "Input filename to analyze containing klammer and other definitions", "filename", "", "'text'");
|
||||||
|
args.var("klammersets", "List the available klammersets, or load one or more for analysis. Enter \"--klammersets help\" for details.", "symbols");
|
||||||
args.var("font", "List installed fonts. Enter \"--font help\" for font maintenance commands.");
|
args.var("font", "List installed fonts. Enter \"--font help\" for font maintenance commands.");
|
||||||
args.flag("argtypes", "Show the klammer argument types");
|
args.flag("argtypes", "Show the klammer argument types");
|
||||||
args.var("katoms", "Show the katom types. Enter \"--katoms help\" for details.");
|
args.var("katoms", "Show the katom types. Enter \"--katoms help\" for details.");
|
||||||
args.flag("rewrite", "Show the katom rewrite patterns");
|
args.flag("rewrite", "Show the katom rewrite patterns");
|
||||||
args.flag("optionsets", "Show the option sets declared by the input");
|
args.flag("optionsets", "Show the option sets declared by the input");
|
||||||
args.var("coverage", "Show the targets each klammer covers. Enter \"--coverage help\" for details.");
|
args.var("coverage", "Show the targets each klammer covers. Enter \"--coverage help\" for details.");
|
||||||
args.var("klammerset", "List the klammersets on the search path. Enter \"--klammerset help\" for details.");
|
|
||||||
args.opt("v", "'verbosity'", "n", "0", "'verbosity'");
|
args.opt("v", "'verbosity'", "n", "0", "'verbosity'");
|
||||||
|
|
||||||
if (show_usage(argc, argv)) {
|
if (show_usage(argc, argv)) {
|
||||||
|
// A bare command is a REQUEST for information, not a failure:
|
||||||
|
// usage goes to stdout (it is the result being asked for) and the
|
||||||
|
// exit status is 0, so "kdesc && echo ok" reports what happened.
|
||||||
args.usage(file_basename(argv[0]));
|
args.usage(file_basename(argv[0]));
|
||||||
exit(1);
|
exit(0);
|
||||||
}
|
}
|
||||||
auto p = [&](const std::string& name) { return args.get(name) == "true"; };
|
auto p = [&](const std::string& name) { return args.get(name) == "true"; };
|
||||||
|
|
||||||
@@ -208,11 +224,45 @@ int main(int argc, char* argv[])
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The --klammerset subcommands operate on the search path
|
Machine M;
|
||||||
// (filesystem enumeration) and load no klammer set.
|
// Same rule as kdiag: a command that specifies no target evaluates
|
||||||
if (args.given("klammerset")) {
|
// under the GENERAL target (Andy, 2026-08-15). kdesc has no target
|
||||||
klammerset_command(option_words(args, "klammerset"));
|
// argument either -- it describes what a klammer set provides for ALL
|
||||||
return 0;
|
// targets -- and a designer's ".k" holding a top-level @eval hit the
|
||||||
|
// identical "Variable "K_target" not defined".
|
||||||
|
M.m_state.set("K_target", Target_registry::general_name);
|
||||||
|
|
||||||
|
strings_t input_filenames = resolve_filename_list(args.get("i"));
|
||||||
|
if (verbose_level > 0 && !input_filenames.empty()) {
|
||||||
|
std::cout << "input_filenames: " << input_filenames << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --klammersets both lists and loads: with no words it enumerates the
|
||||||
|
// search path, with symbols it loads them for the analyses below.
|
||||||
|
if (args.given("klammersets")) {
|
||||||
|
klammerset_command(M, option_words(args, "klammersets"));
|
||||||
|
} else { // By default, SKS is loaded for analysis
|
||||||
|
M.read(fs::path(M.m_state.value("KLAMMERTEXT_HOME") + "/sks/sks.k"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot BEFORE the input is read. The distinction is not "declared
|
||||||
|
// by a klammerset" but "loaded as context": an input file may itself
|
||||||
|
// declare @@@klammerset -- that is how a designer writes one -- and
|
||||||
|
// its klammers are still the ones being asked about.
|
||||||
|
strings_t context_files = M.m_klammersets.loaded_files();
|
||||||
|
|
||||||
|
for (auto fname : input_filenames) {
|
||||||
|
if (verbose_level > 0) {
|
||||||
|
std::cout << "Read " << fname << "\n";
|
||||||
|
}
|
||||||
|
M.read(fs::path(absolute_pathname(fname)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --coverage is validated here, before a klammer set is read, so a
|
// --coverage is validated here, before a klammer set is read, so a
|
||||||
@@ -236,41 +286,31 @@ int main(int argc, char* argv[])
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Machine M;
|
|
||||||
|
|
||||||
strings_t input_filenames = resolve_filename_list(args.get("i"));
|
strings_t own_klammers_only {};
|
||||||
if (verbose_level > 0) {
|
if (!input_filenames.empty()) {
|
||||||
std::cout << "input_filenames: " << input_filenames << "\n";
|
own_klammers_only = context_files;
|
||||||
}
|
|
||||||
if (input_filenames.empty()) {
|
|
||||||
M.read(fs::path(M.m_state.value("KLAMMERTEXT_HOME") + "/sks/sks.k"));
|
|
||||||
} else {
|
|
||||||
for (auto fname : input_filenames) {
|
|
||||||
if (is_klammerset_symbol(fname)) {
|
|
||||||
// A bare symbol resolves on the klammerset search
|
|
||||||
// path; kdesc has no input document, so the local
|
|
||||||
// stage is the cwd.
|
|
||||||
fname = resolve_klammerset_symbol(
|
|
||||||
fname, fs::current_path().string(), Locator()).string();
|
|
||||||
}
|
|
||||||
if (verbose_level > 0) {
|
|
||||||
std::cout << "Read " << fname << "\n";
|
|
||||||
}
|
|
||||||
M.read(fs::path(absolute_pathname(fname)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (p("t")) {
|
if (p("t")) {
|
||||||
std::cout << boldblack << "Targets\n" << black << M.m_targets.describe(2, true);
|
std::cout << boldblack << "Targets\n" << black
|
||||||
|
<< M.m_targets.describe(2, true, own_klammers_only);
|
||||||
}
|
}
|
||||||
|
|
||||||
// "-k <text>" searches names AND descriptions, case-insensitively,
|
// "-k <text>" searches names AND descriptions, case-insensitively,
|
||||||
// with whitespace collapsed on both sides. An empty listing for a
|
// with whitespace collapsed on both sides. An empty listing for a
|
||||||
// search that was actually made is reported: silence would read as a
|
// search that was actually made is reported: silence would read as a
|
||||||
// broken command. It is not an error -- finding nothing is a result.
|
// broken command. It is not an error -- finding nothing is a result.
|
||||||
|
// With an input file, the klammersets are loaded so the input can be
|
||||||
|
// ANALYSED against them, but they are not what the user is asking
|
||||||
|
// about: a designer wants the klammers this file defines, and a reader
|
||||||
|
// of an unfamiliar document wants the custom klammers it carries. So
|
||||||
|
// the listing is restricted to klammers with a definition outside the
|
||||||
|
// klammerset files. Without -i there is nothing else to show, so the
|
||||||
|
// klammerset itself is the answer and no filter applies.
|
||||||
if (args.given("k")) {
|
if (args.given("k")) {
|
||||||
std::string search = join(option_words(args, "k"), " ");
|
std::string search = join(option_words(args, "k"), " ");
|
||||||
std::string listing = M.m_klammers.describe(2, search);
|
std::string listing = M.m_klammers.describe(2, search, own_klammers_only);
|
||||||
if (listing.empty() && !search.empty()) {
|
if (listing.empty() && !search.empty()) {
|
||||||
std::cout << "No klammer names or descriptions contained "
|
std::cout << "No klammer names or descriptions contained "
|
||||||
<< q_(collapse_whitespace(search)) << ".\n";
|
<< q_(collapse_whitespace(search)) << ".\n";
|
||||||
@@ -280,15 +320,15 @@ int main(int argc, char* argv[])
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (p("optionsets")) {
|
if (p("optionsets")) {
|
||||||
std::cout << boldblack << "Option sets\n" << black << M.m_option_sets.describe(2);
|
std::cout << boldblack << "Option sets\n" << black
|
||||||
|
<< M.m_option_sets.describe(2, own_klammers_only);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Analysis only: klammer_coverage() reads the registry and modifies
|
// Analysis only: klammer_coverage() reads the registry and modifies
|
||||||
// nothing, so what a document renders to is unaffected by asking.
|
// nothing, so what a document renders to is unaffected by asking.
|
||||||
if (args.given("coverage")) {
|
if (args.given("coverage")) {
|
||||||
report_coverage(M, klammer_coverage(M), coverage_all, std::cout);
|
report_coverage(M, klammer_coverage(M, own_klammers_only), coverage_all, std::cout);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Error& e) {
|
catch (Error& e) {
|
||||||
// Nonzero, as ktext does: a command that prints an error and exits 0
|
// Nonzero, as ktext does: a command that prints an error and exits 0
|
||||||
|
|||||||
118
com/kdiag.cpp
118
com/kdiag.cpp
@@ -3,6 +3,7 @@
|
|||||||
#include <map>
|
#include <map>
|
||||||
|
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
#include "check.h"
|
||||||
#include "argument_set.h"
|
#include "argument_set.h"
|
||||||
#include "argv.h"
|
#include "argv.h"
|
||||||
#include "argument.h"
|
#include "argument.h"
|
||||||
@@ -19,7 +20,11 @@ int main(int argc, char* argv[])
|
|||||||
set_verbose_level(argc, argv);
|
set_verbose_level(argc, argv);
|
||||||
|
|
||||||
Argv args {};
|
Argv args {};
|
||||||
args.req("input", "Klammertext input text", "'text'");
|
// Optional: "kdiag --machine" with no input shows the Machine's initial
|
||||||
|
// state, which is a question about the machine rather than about any
|
||||||
|
// document -- and the one thing a programmer wants before feeding it
|
||||||
|
// anything.
|
||||||
|
args.req("input", "Klammertext input text", "'text'", false);
|
||||||
args.flag("type", "Show katom types in subscript");
|
args.flag("type", "Show katom types in subscript");
|
||||||
args.flag("index", "Show the list index of the katom");
|
args.flag("index", "Show the list index of the katom");
|
||||||
args.flag("text", "Show text katoms with selected attributes");
|
args.flag("text", "Show text katoms with selected attributes");
|
||||||
@@ -37,13 +42,27 @@ int main(int argc, char* argv[])
|
|||||||
args.flag("literal", "Process literal: ^'...'^");
|
args.flag("literal", "Process literal: ^'...'^");
|
||||||
args.flag("ignore", "Process ignored: #, ##, #[...]#");
|
args.flag("ignore", "Process ignored: #, ##, #[...]#");
|
||||||
args.flag("ws", "Process whitespace: #-, #+, #/");
|
args.flag("ws", "Process whitespace: #-, #+, #/");
|
||||||
args.flag("klammer", "Process klammer definitions: @@<name> ... @@");
|
args.flag("klammer", "Register klammer definitions: @@<name> ... @@. Their katoms "
|
||||||
args.flag("process", "Process all");
|
"are consumed, so they appear only with --replaced.");
|
||||||
|
args.flag("system", "Register machine definitions: @@@target, @@@argtype, @@@state, "
|
||||||
|
"@@@klammerset. Their katoms are consumed, so they appear only "
|
||||||
|
"with --replaced. The two tiers are ORDERED: a klammer names a "
|
||||||
|
"target, so --klammer alone cannot register a klammer whose "
|
||||||
|
"target the input itself declares. That is not an error here: "
|
||||||
|
"the definition is skipped and its katoms stay visible, which is "
|
||||||
|
"the report.");
|
||||||
|
args.flag("process", "Process all, both tiers of definition included");
|
||||||
|
args.flag("check", "Check every klammer application in the input and in the "
|
||||||
|
"body of every defined klammer");
|
||||||
|
args.flag("machine", "Show the state of the Klammermachine after all processing");
|
||||||
args.opt("v", "'verbosity'", "degree", "0", "'verbosity'");
|
args.opt("v", "'verbosity'", "degree", "0", "'verbosity'");
|
||||||
|
|
||||||
if (show_usage(argc, argv)) {
|
if (show_usage(argc, argv)) {
|
||||||
|
// A bare command is a REQUEST for information, not a failure:
|
||||||
|
// usage goes to stdout (it is the result being asked for) and the
|
||||||
|
// exit status is 0, so "kdiag && echo ok" reports what happened.
|
||||||
args.usage(file_basename(argv[0]));
|
args.usage(file_basename(argv[0]));
|
||||||
exit(1);
|
exit(0);
|
||||||
}
|
}
|
||||||
auto p = [&](const std::string& name) { return args.get(name) == "true"; };
|
auto p = [&](const std::string& name) { return args.get(name) == "true"; };
|
||||||
|
|
||||||
@@ -57,19 +76,48 @@ int main(int argc, char* argv[])
|
|||||||
show_rewrite_rules = true;
|
show_rewrite_rules = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// kdiag deliberately has no --klammersets: a klammerset is reached here
|
||||||
|
// the way the input reaches anything else, with "@read sks/sks.k @".
|
||||||
|
// The symbol mechanism is a higher-level convenience, and a debugger
|
||||||
|
// must not depend on the machinery it debugs -- if symbol resolution
|
||||||
|
// breaks, kdiag must still run, and the two paths to the same file can
|
||||||
|
// then be compared.
|
||||||
Machine machine;
|
Machine machine;
|
||||||
|
// kdiag has no target argument, and evaluating needs one: Eval::eval
|
||||||
|
// re-reads its result under K_target, so with the variable unset every
|
||||||
|
// @eval died with "Variable "K_target" not defined" -- an error naming
|
||||||
|
// something the user never wrote. A command that does not specify a
|
||||||
|
// target evaluates under the GENERAL target (Andy, 2026-08-15), which
|
||||||
|
// is also what kdiag means: it loads no klammerset, so nothing
|
||||||
|
// target-specific is in scope to begin with.
|
||||||
|
machine.m_state.set("K_target", Target_registry::general_name);
|
||||||
|
machine.m_state.set("K_input_dir", fs::current_path().string());
|
||||||
std::string input = args.as_string("input");
|
std::string input = args.as_string("input");
|
||||||
std::string command = construct_command_pathname(argv[0]);
|
std::string command = construct_command_pathname(argv[0]);
|
||||||
katom_list katoms {};
|
// ONE authoritative katom list. process() returns the katoms but
|
||||||
if (p("process")) {
|
// registers nothing -- read() is what appends to m_katoms and extracts
|
||||||
katoms = machine.process(input, command);
|
// the definitions -- so kdiag does that itself, in the order read()
|
||||||
} else {
|
// uses, and then displays m_katoms. Displaying its own copy instead
|
||||||
katoms = machine.process(
|
// would show the definition katoms unconsumed, because extraction
|
||||||
//args.as_string("input"), construct_command_pathname(argv[0]),
|
// marks them "replaced" in the Machine's list, not in a copy.
|
||||||
input, command,
|
bool all = p("process");
|
||||||
p("nonascii"), p("literal"), p("ignore"), p("ws"), p("klammer"),
|
katom_list processed = all
|
||||||
p("eval"), p("cond"), p("read"));
|
? machine.process(input, command)
|
||||||
}
|
: machine.process(input, command,
|
||||||
|
p("nonascii"), p("literal"), p("ignore"), p("ws"),
|
||||||
|
p("klammer"), p("eval"), p("cond"), p("read"));
|
||||||
|
machine.m_katoms.insert(
|
||||||
|
machine.m_katoms.end(), processed.begin(), processed.end());
|
||||||
|
// Machine definitions first: a klammer definition names a target, so
|
||||||
|
// the target must be registered before the klammer that uses it.
|
||||||
|
// Tolerant: a definition that cannot be registered because the other
|
||||||
|
// tier was not run is skipped, not fatal -- its katoms stay visible.
|
||||||
|
// kdiag dissects structure and typing, which means allowing modes of
|
||||||
|
// processing that would be errors in ktext.
|
||||||
|
if (all || p("system")) machine.extract_machine_definitions(true);
|
||||||
|
if (all || p("klammer")) machine.extract_klammer_definitions(true);
|
||||||
|
katom_list& katoms = machine.m_katoms;
|
||||||
|
|
||||||
if (p("spans")) {
|
if (p("spans")) {
|
||||||
describe_spans(katoms);
|
describe_spans(katoms);
|
||||||
} else {
|
} else {
|
||||||
@@ -79,7 +127,9 @@ int main(int argc, char* argv[])
|
|||||||
if (p("text")) std::cout << kall;
|
if (p("text")) std::cout << kall;
|
||||||
if (p("all")) std::cout << kall << kreplaced << kignored;
|
if (p("all")) std::cout << kall << kreplaced << kignored;
|
||||||
if (p("replaced")) std::cout << kreplaced;
|
if (p("replaced")) std::cout << kreplaced;
|
||||||
std::cout << katoms << "\n";
|
if (!p("check")) {
|
||||||
|
std::cout << katoms << "\n";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (p("args")) {
|
if (p("args")) {
|
||||||
@@ -105,11 +155,45 @@ int main(int argc, char* argv[])
|
|||||||
std::cout << std::right << std::setw(label_width) << "rest:" << " " << rest << "\n";
|
std::cout << std::right << std::setw(label_width) << "rest:" << " " << rest << "\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
int check_errors = 0;
|
||||||
|
if (p("check")) {
|
||||||
|
check_errors = report_diagnostics(check_machine(machine, katoms, "*"), std::cout);
|
||||||
|
// The commonest cause of a wall of "not defined" is that nothing
|
||||||
|
// was registered. The advice belongs here rather than in the
|
||||||
|
// catch block: --check REPORTS, it does not throw, so an exception
|
||||||
|
// handler never sees this case. The condition is the CAUSE, not a
|
||||||
|
// proxy for it.
|
||||||
|
if (check_errors > 0 && machine.m_klammers.m_klammers.empty()) {
|
||||||
|
std::cout << "\nNo klammers are registered, so every application is "
|
||||||
|
"undefined.\nkdiag loads no klammerset: read one in the input, "
|
||||||
|
"as \"@read sks/sks.k @\",\nand add --process (or --system "
|
||||||
|
"--klammer --read) to register what it defines.\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Last, and after everything else: the state shown is the result of
|
||||||
|
// whatever the other arguments did. This is why --check defers its
|
||||||
|
// exit status rather than returning as soon as it finds errors.
|
||||||
|
if (p("machine")) {
|
||||||
|
std::cout << machine;
|
||||||
|
}
|
||||||
|
if (check_errors > 0) {
|
||||||
|
std::cout << black;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Error& e) {
|
catch (Error& e) {
|
||||||
|
// The advice must name a way in that kdiag actually has. It named
|
||||||
|
// "--klammersets sks" until 2026-08-15, a flag the 2026-08-14 redesign
|
||||||
|
// deliberately removed from this command -- a debugger must not depend
|
||||||
|
// on the machinery it debugs -- so the one remedy offered was one that
|
||||||
|
// errors. Same wording as the --check hint above, for the same reason.
|
||||||
std::string advice = "";
|
std::string advice = "";
|
||||||
if (e.m_type == "target")
|
if (e.m_type == "target" || e.m_type == "definition") {
|
||||||
advice = "To include the Standard Klammer Set, add flag \"--sks\".";
|
advice = "kdiag loads no klammerset: read one in the input, as "
|
||||||
|
"\"@read sks/sks.k @\", and add --process to register what it "
|
||||||
|
"defines.";
|
||||||
|
}
|
||||||
e.print_message(advice);
|
e.print_message(advice);
|
||||||
// Nonzero, as ktext does: a command that prints an error and exits 0
|
// Nonzero, as ktext does: a command that prints an error and exits 0
|
||||||
// reports success, and a script cannot tell the difference.
|
// reports success, and a script cannot tell the difference.
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include "error.h"
|
#include "error.h"
|
||||||
#include "check.h"
|
|
||||||
#include "command.h"
|
#include "command.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "argv.h"
|
#include "argv.h"
|
||||||
@@ -22,18 +21,20 @@ int main(int argc, char* argv[])
|
|||||||
Target_registry::general_name, "'word'");
|
Target_registry::general_name, "'word'");
|
||||||
args.opt("o", "Output basename; meaning and default defined by target.", "basename",
|
args.opt("o", "Output basename; meaning and default defined by target.", "basename",
|
||||||
"", "'word'");
|
"", "'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.",
|
args.flag("d", "Display the output to the screen, rather than writing a file.");
|
||||||
"pathname", "", "'word'");
|
args.opt("klammersets", "Klammersets to load, as a list of symbols resolved on the "
|
||||||
args.flag("d", "Display the output to the screen, rather than writing files.");
|
"klammerset search path. Several combine: they are loaded in the order given "
|
||||||
args.flag("m", "Show the Klammermachine state at the beginning of processing.");
|
"and share one namespace. The default is the Standard Klammer Set; "
|
||||||
args.flag("check", "Check every klammer application in the input and in the "
|
"\"none\" loads none.",
|
||||||
"body of every defined klammer, report all problems found, and exit "
|
"symbols", "", "'list'");
|
||||||
"without producing output.");
|
|
||||||
args.opt("v", "'verbosity'", "degree", "0", "'verbosity'");
|
args.opt("v", "'verbosity'", "degree", "0", "'verbosity'");
|
||||||
|
|
||||||
if (show_usage(argc, argv)) {
|
if (show_usage(argc, argv)) {
|
||||||
|
// A bare command is a REQUEST for information, not a failure:
|
||||||
|
// usage goes to stdout (it is the result being asked for) and the
|
||||||
|
// exit status is 0, so "ktext && echo ok" reports what happened.
|
||||||
args.usage(file_basename(argv[0]));
|
args.usage(file_basename(argv[0]));
|
||||||
exit(1);
|
exit(0);
|
||||||
}
|
}
|
||||||
args.parse(argc, argv);
|
args.parse(argc, argv);
|
||||||
if (verbose_level > 0) {
|
if (verbose_level > 0) {
|
||||||
@@ -56,19 +57,6 @@ int main(int argc, char* argv[])
|
|||||||
parse_args(input_filenames, args.as_string("t"),
|
parse_args(input_filenames, args.as_string("t"),
|
||||||
expand_tilde(args.as_string("o")), args.as_bool("d"));
|
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;
|
Machine M;
|
||||||
M.m_state.open_frame("ktext");
|
M.m_state.open_frame("ktext");
|
||||||
M.m_state.set("K_target", target);
|
M.m_state.set("K_target", target);
|
||||||
@@ -89,20 +77,7 @@ int main(int argc, char* argv[])
|
|||||||
M.m_state.set("K_input_dir", fs::current_path().string());
|
M.m_state.set("K_input_dir", fs::current_path().string());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string klammerset_filename = args.as_string("k");
|
load_klammersets(M, word_split(args.as_string("klammersets")));
|
||||||
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()) {
|
if (!input_text.empty()) {
|
||||||
M.read(input_text + "\n");
|
M.read(input_text + "\n");
|
||||||
@@ -111,30 +86,17 @@ int main(int argc, char* argv[])
|
|||||||
M.read(fs::path(p));
|
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));
|
std::string result = trim(M.apply(target));
|
||||||
if (display_only && !result.empty()) {
|
if (display_only && !result.empty()) {
|
||||||
std::cout << result << "\n";
|
std::cout << result << "\n";
|
||||||
} else if (!result.empty()) {
|
} else if (!result.empty()) {
|
||||||
msg() << "Output filename: " << output_filename << "\n";
|
|
||||||
string_to_file(output_filename, result + "\n");
|
string_to_file(output_filename, result + "\n");
|
||||||
(void)K::log(1, "Wrote file: " + output_filename);
|
(void)K::log(1, "Wrote file: " + output_filename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Error& err) {
|
catch (Error& err) {
|
||||||
err.print_message();
|
err.print_message();
|
||||||
std::cout << "\n";
|
std::cerr << "\n";
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -67,7 +67,15 @@ std::string pylist(strings_t words)
|
|||||||
{
|
{
|
||||||
std::transform(words.begin(), words.end(), words.begin(),
|
std::transform(words.begin(), words.end(), words.begin(),
|
||||||
[](const std::string& s) { return pyformat_string({s}); });
|
[](const std::string& s) { return pyformat_string({s}); });
|
||||||
return "[" + join(words, ", ") + "]";
|
// std::string(...) on the leading literal, not "[" + ... : a literal on the
|
||||||
|
// LEFT of + selects operator+(const char*, string&&), which is insert(0,
|
||||||
|
// ...) -- it shifts the whole string to make room. The explicit temporary
|
||||||
|
// selects the append overload instead. Cheaper, and it silences a gcc-12
|
||||||
|
// -Wrestrict FALSE POSITIVE (it reports a 2**63-byte overlap at a negative
|
||||||
|
// offset, from value-range propagation failing to bound size() after
|
||||||
|
// inlining; g++-15 and clang-18 do not). Verified 2 -> 0 warnings on
|
||||||
|
// gcc 12.2.0 with the container's flags.
|
||||||
|
return std::string("[") + join(words, ", ") + "]";
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string pyformat_list(const strings_t& value)
|
std::string pyformat_list(const strings_t& value)
|
||||||
|
|||||||
40
mac/argv.cpp
40
mac/argv.cpp
@@ -111,15 +111,18 @@ void Argv::flag(const std::string& name, const std::string& desc)
|
|||||||
update_width(arg);
|
update_width(arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Argv::req(const std::string& name, const std::string& desc, const std::string& regex_pattern)
|
void Argv::req(const std::string& name, const std::string& desc,
|
||||||
|
const std::string& regex_pattern, bool required)
|
||||||
{
|
{
|
||||||
(void)K::log(2, name, desc, regex_pattern);
|
(void)K::log(2, name, desc, regex_pattern);
|
||||||
Arg arg {};
|
Arg arg {};
|
||||||
arg.m_type = "req";
|
// "posopt" is a positional that may be absent. It stays in m_req_names so
|
||||||
|
// it keeps its POSITION; only its absence is tolerated.
|
||||||
|
arg.m_type = required ? "req" : "posopt";
|
||||||
arg.m_name = name;
|
arg.m_name = name;
|
||||||
arg.m_desc = get_regex_desc(desc);
|
arg.m_desc = get_regex_desc(desc);
|
||||||
arg.make_regex(regex_pattern);
|
arg.make_regex(regex_pattern);
|
||||||
arg.m_syntax = "<" + name + ">";
|
arg.m_syntax = required ? "<" + name + ">" : "[<" + name + ">]";
|
||||||
m_args[name] = arg;
|
m_args[name] = arg;
|
||||||
m_names.push_back(name);
|
m_names.push_back(name);
|
||||||
m_req_names.push_back(name);
|
m_req_names.push_back(name);
|
||||||
@@ -191,17 +194,26 @@ void Argv::usage_line(Arg arg)
|
|||||||
<< wrap_around(arg.m_desc, m_syntax_size + 6) << "\n";
|
<< wrap_around(arg.m_desc, m_syntax_size + 6) << "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A positional argument, whether or not it may be absent. Both kinds are
|
||||||
|
// listed under "Arguments:" and must therefore be skipped when the options are
|
||||||
|
// listed -- testing only for "req" printed an optional positional twice, once
|
||||||
|
// in each section (kdiag's "[<input>]").
|
||||||
|
static bool is_positional(const std::string& type)
|
||||||
|
{
|
||||||
|
return type == "req" || type == "posopt";
|
||||||
|
}
|
||||||
|
|
||||||
void Argv::usage(const std::string& command)
|
void Argv::usage(const std::string& command)
|
||||||
{
|
{
|
||||||
std::cout << "\nUsage: " << command << " ";
|
std::cout << "\nUsage: " << command << " ";
|
||||||
for (const std::string& name : m_req_names) {
|
for (const std::string& name : m_req_names) {
|
||||||
std::cout << "<" + name + "> ";
|
std::cout << m_args[name].m_syntax << " ";
|
||||||
}
|
}
|
||||||
if (m_opt_names.size() + m_flag_names.size() > 5) {
|
if (m_opt_names.size() + m_flag_names.size() > 5) {
|
||||||
std::cout << "[<optional-arguments>]\n";
|
std::cout << "[<optional-arguments>]\n";
|
||||||
} else {
|
} else {
|
||||||
for (const std::string& name : m_names) {
|
for (const std::string& name : m_names) {
|
||||||
if (m_args[name].m_type == "req") {
|
if (is_positional(m_args[name].m_type)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
std::cout << "[" + m_args[name].m_syntax + "] ";
|
std::cout << "[" + m_args[name].m_syntax + "] ";
|
||||||
@@ -222,7 +234,7 @@ void Argv::usage(const std::string& command)
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& name : m_names) {
|
for (const auto& name : m_names) {
|
||||||
if (m_args[name].m_type == "req") {
|
if (is_positional(m_args[name].m_type)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
usage_line(m_args[name]);
|
usage_line(m_args[name]);
|
||||||
@@ -350,6 +362,12 @@ void Argv::parse_positional(const std::string& command, //strings_t words,
|
|||||||
auto arg = m_args[req];
|
auto arg = m_args[req];
|
||||||
auto [substring, rest, found] = regex_split_prefix(arg.m_rgx, pos_args);
|
auto [substring, rest, found] = regex_split_prefix(arg.m_rgx, pos_args);
|
||||||
if (!found) {
|
if (!found) {
|
||||||
|
// An optional positional simply stays empty; only a required one
|
||||||
|
// is an error.
|
||||||
|
if (arg.m_type == "posopt") {
|
||||||
|
named_args[req] = "";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
std::stringstream ss {};
|
std::stringstream ss {};
|
||||||
ss << "The argument " << q_(req) << " was not found in:\n " << command;
|
ss << "The argument " << q_(req) << " was not found in:\n " << command;
|
||||||
throw Argument_error(ss.str(), Locator(), false);
|
throw Argument_error(ss.str(), Locator(), false);
|
||||||
@@ -409,7 +427,8 @@ void Argv::check_required(
|
|||||||
|
|
||||||
if (required > given) {
|
if (required > given) {
|
||||||
std::string missing = m_req_names[required - given - 1];
|
std::string missing = m_req_names[required - given - 1];
|
||||||
if (given == 0 && m_args[missing].m_rgx_symbol == "'list'") {
|
if (given == 0 && (m_args[missing].m_rgx_symbol == "'list'"
|
||||||
|
|| m_args[missing].m_type == "posopt")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
std::stringstream ss {};
|
std::stringstream ss {};
|
||||||
@@ -583,7 +602,8 @@ int Argv::as_verbosity(const std::string& name)
|
|||||||
std::string Argv::as_string(const std::string& name)
|
std::string Argv::as_string(const std::string& name)
|
||||||
{
|
{
|
||||||
(void)K::log(2, name);
|
(void)K::log(2, name);
|
||||||
return get(name);
|
auto result = get(name);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
strings_t Argv::as_vector(const std::string& name)
|
strings_t Argv::as_vector(const std::string& name)
|
||||||
@@ -622,7 +642,7 @@ std::pair<std::string, strings_t> Argv::as_input(const std::string& name, bool a
|
|||||||
return { input_text, input_filenames };
|
return { input_text, input_filenames };
|
||||||
}
|
}
|
||||||
|
|
||||||
void Argv::describe()
|
void Argv::describe(std::ostream& os)
|
||||||
{
|
{
|
||||||
std::size_t width = std::accumulate(
|
std::size_t width = std::accumulate(
|
||||||
m_names.begin(), m_names.end(), 0,
|
m_names.begin(), m_names.end(), 0,
|
||||||
@@ -642,6 +662,6 @@ void Argv::describe()
|
|||||||
// whose whole job is to display what Argv parsed, was silent. The
|
// whose whole job is to display what Argv parsed, was silent. The
|
||||||
// callers already decide whether to call it (the commands gate it on
|
// callers already decide whether to call it (the commands gate it on
|
||||||
// verbose_level > 0), so it prints unconditionally here.
|
// verbose_level > 0), so it prints unconditionally here.
|
||||||
std::cout << ss.str() << "\n";
|
os << ss.str() << "\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
mac/argv.h
14
mac/argv.h
@@ -6,6 +6,7 @@
|
|||||||
#include <set>
|
#include <set>
|
||||||
#include <ranges>
|
#include <ranges>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <iostream>
|
||||||
#include <regex>
|
#include <regex>
|
||||||
|
|
||||||
inline std::map<std::string, std::string> regex_symbols {
|
inline std::map<std::string, std::string> regex_symbols {
|
||||||
@@ -51,7 +52,12 @@ class Argv
|
|||||||
public:
|
public:
|
||||||
static std::string delimiter;
|
static std::string delimiter;
|
||||||
void flag(const std::string& name, const std::string& desc);
|
void flag(const std::string& name, const std::string& desc);
|
||||||
void req(const std::string& name, const std::string& desc, const std::string& regex_pattern="'text'");
|
// A positional argument. `required` false makes it OPTIONAL: the command
|
||||||
|
// may be run without it, and its value is then empty. kdiag uses that for
|
||||||
|
// its input text, so that "kdiag --machine" can show the Machine's initial
|
||||||
|
// state -- a question about the machine, not about any document.
|
||||||
|
void req(const std::string& name, const std::string& desc,
|
||||||
|
const std::string& regex_pattern="'text'", bool required = true);
|
||||||
void opt(const std::string& name, const std::string& desc="", const std::string& parameter="", const std::string& default_value="", const std::string& regex_pattern="text");
|
void opt(const std::string& name, const std::string& desc="", const std::string& parameter="", const std::string& default_value="", const std::string& regex_pattern="text");
|
||||||
// A variadic option: --name collects every following word up to the
|
// A variadic option: --name collects every following word up to the
|
||||||
// next -/-- token (zero or more). get(name) returns the words
|
// next -/-- token (zero or more). get(name) returns the words
|
||||||
@@ -99,7 +105,11 @@ public:
|
|||||||
|
|
||||||
void usage_line(Arg arg);
|
void usage_line(Arg arg);
|
||||||
void usage(const std::string& command_name);
|
void usage(const std::string& command_name);
|
||||||
void describe();
|
// The default is stderr: the three commands call this under "-v 1", and
|
||||||
|
// logging never shares stdout with the command's result (a ktext document
|
||||||
|
// may be piped). argv_test passes std::cout, because there the parsed
|
||||||
|
// arguments ARE the result.
|
||||||
|
void describe(std::ostream& os = std::cerr);
|
||||||
// void describe(Argv original);
|
// void describe(Argv original);
|
||||||
|
|
||||||
bool is_flag(const std::string& name) const {
|
bool is_flag(const std::string& name) const {
|
||||||
|
|||||||
@@ -235,7 +235,8 @@ void Checker::check_list(
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
std::vector<Diagnostic> check_machine(Machine& machine, const std::string& target)
|
std::vector<Diagnostic> check_machine(Machine& machine, const katom_list& document,
|
||||||
|
const std::string& target)
|
||||||
{
|
{
|
||||||
std::vector<Diagnostic> diagnostics {};
|
std::vector<Diagnostic> diagnostics {};
|
||||||
Checker checker(machine, diagnostics);
|
Checker checker(machine, diagnostics);
|
||||||
@@ -253,7 +254,7 @@ std::vector<Diagnostic> check_machine(Machine& machine, const std::string& targe
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& t : targets) {
|
for (const auto& t : targets) {
|
||||||
checker.check_list(machine.m_katoms, t, "document");
|
checker.check_list(document, t, "document");
|
||||||
for (const auto& [name, klammer] : machine.m_klammers.m_klammers) {
|
for (const auto& [name, klammer] : machine.m_klammers.m_klammers) {
|
||||||
auto body = klammer.m_body.find(t);
|
auto body = klammer.m_body.find(t);
|
||||||
if (body == klammer.m_body.end()) continue;
|
if (body == klammer.m_body.end()) continue;
|
||||||
|
|||||||
12
mac/check.h
12
mac/check.h
@@ -5,6 +5,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "locator.h"
|
#include "locator.h"
|
||||||
|
#include "util.h" // katom_list
|
||||||
|
|
||||||
class Machine;
|
class Machine;
|
||||||
|
|
||||||
@@ -58,7 +59,16 @@ struct Diagnostic
|
|||||||
// (Target_registry::general_name) checks every defined target. Diagnostics
|
// (Target_registry::general_name) checks every defined target. Diagnostics
|
||||||
// accumulate: checking never stops at the first failure, because the point is
|
// accumulate: checking never stops at the first failure, because the point is
|
||||||
// to see all of them at once.
|
// to see all of them at once.
|
||||||
std::vector<Diagnostic> check_machine(Machine& machine, const std::string& target);
|
// `document` is the katom list to check. It is a parameter rather than being
|
||||||
|
// taken from machine.m_katoms because the caller is the one that has it, and
|
||||||
|
// the two are not always the same list: Machine::read() fills m_katoms, but
|
||||||
|
// Machine::process() -- which is how kdiag builds its katoms -- does not.
|
||||||
|
// Reading m_katoms therefore checked an EMPTY document under kdiag and
|
||||||
|
// reported "0 diagnostics, 0 errors" for anything, which is worse than not
|
||||||
|
// checking: it reports success for input it never examined. Passing the list
|
||||||
|
// makes that mistake impossible to write.
|
||||||
|
std::vector<Diagnostic> check_machine(Machine& machine, const katom_list& document,
|
||||||
|
const std::string& target);
|
||||||
|
|
||||||
// Print diagnostics, grouped in the order found, and return the number of
|
// Print diagnostics, grouped in the order found, and return the number of
|
||||||
// errors (warnings do not count). Used by "ktext --check".
|
// errors (warnings do not count). Used by "ktext --check".
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "argv.h"
|
#include "argv.h"
|
||||||
#include "file.h"
|
#include "file.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
|
#include "machine.h"
|
||||||
|
|
||||||
using namespace std::string_literals;
|
using namespace std::string_literals;
|
||||||
|
|
||||||
@@ -109,3 +110,48 @@ parse_args(
|
|||||||
}
|
}
|
||||||
return {output_target, output_dir, output_basename, output_filename, write_files, display_only};
|
return {output_target, output_dir, output_basename, output_filename, write_files, display_only};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void load_klammersets(Machine& machine, const strings_t& symbols)
|
||||||
|
{
|
||||||
|
// "none" is the absence of a klammerset, not the name of one, so it says
|
||||||
|
// nothing about what should be loaded ALONGSIDE it: written first it would
|
||||||
|
// silently discard the rest of the list, written last it would be looked up
|
||||||
|
// as a symbol and reported missing. Both readings are wrong, so neither is
|
||||||
|
// guessed at -- "none" is accepted only alone.
|
||||||
|
if (is_in("none"s, symbols) && symbols.size() > 1) {
|
||||||
|
throw Klammerset_error(
|
||||||
|
"The klammerset \"none\" cannot be combined with other klammersets: "
|
||||||
|
"it means that none is loaded. Give \"none\" alone, or name only the "
|
||||||
|
"klammersets to load.",
|
||||||
|
Locator());
|
||||||
|
}
|
||||||
|
if (symbols.empty()) {
|
||||||
|
// Which klammerset was loaded, and from where, is a DERIVED value: the
|
||||||
|
// user wrote no --klammersets at all and got the SKS from
|
||||||
|
// $KLAMMERTEXT_HOME. Only the symbol branch below reported its
|
||||||
|
// resolution, so the commonest case -- the default -- said nothing.
|
||||||
|
std::string sks = machine.m_state.value("KLAMMERTEXT_HOME") + "/sks/sks.k";
|
||||||
|
(void)K::log(1, "Klammerset (default): " + sks);
|
||||||
|
machine.read(fs::path(sks));
|
||||||
|
} else if (symbols[0] == "none") {
|
||||||
|
(void)K::log(1, "Klammersets: none");
|
||||||
|
} else {
|
||||||
|
// The whitespace-delimited words in symbols must be klammerset symbols:
|
||||||
|
for (const auto& symbol : symbols) {
|
||||||
|
// Same guard as ":requires" (Machine::load_klammerset_files): a set
|
||||||
|
// named twice on the command line is loaded once.
|
||||||
|
if (machine.m_klammersets.has(symbol)) {
|
||||||
|
(void)K::log(1, "Klammerset \"" + symbol + "\": already loaded, skipped");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::string klammerset_filename = resolve_klammerset_symbol(
|
||||||
|
symbol, machine.m_state.value("K_input_dir"), Locator()).string();
|
||||||
|
// Symbol -> file is the search path's answer, and the search
|
||||||
|
// path has three stages with shadowing: the file it landed on is
|
||||||
|
// exactly what a user cannot read off "--klammersets x".
|
||||||
|
(void)K::log(1, "Klammerset \"" + symbol + "\": " + klammerset_filename);
|
||||||
|
machine.read(fs::path(klammerset_filename));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "file.h"
|
#include "file.h"
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
|
class Machine;
|
||||||
|
|
||||||
void set_verbose_level(int argc, char* argv[]);
|
void set_verbose_level(int argc, char* argv[]);
|
||||||
bool show_usage(int argc, char* argv[]);
|
bool show_usage(int argc, char* argv[]);
|
||||||
@@ -15,3 +18,5 @@ std::tuple<std::string, std::string, std::string, std::string, bool, bool>
|
|||||||
parse_args(
|
parse_args(
|
||||||
const std::vector<std::string>& input_filenames, std::string target,
|
const std::vector<std::string>& input_filenames, std::string target,
|
||||||
std::string output_basename, bool display_only);
|
std::string output_basename, bool display_only);
|
||||||
|
|
||||||
|
void load_klammersets(Machine& machine, const strings_t& symbols);
|
||||||
|
|||||||
@@ -131,7 +131,8 @@ std::string short_path(const std::string& path)
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
std::vector<Klammer_coverage> klammer_coverage(const Machine& machine)
|
std::vector<Klammer_coverage> klammer_coverage(const Machine& machine,
|
||||||
|
const strings_t& defined_outside)
|
||||||
{
|
{
|
||||||
target_set all_targets {};
|
target_set all_targets {};
|
||||||
for (const auto& t : machine.m_targets.user_defined()) {
|
for (const auto& t : machine.m_targets.user_defined()) {
|
||||||
@@ -188,9 +189,15 @@ std::vector<Klammer_coverage> klammer_coverage(const Machine& machine)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass 3: classify and collect.
|
// Pass 3: classify and collect. The filter applies HERE and not earlier:
|
||||||
|
// passes 1 and 2 must see every klammer, because a derived coverage is the
|
||||||
|
// intersection of what the called klammers cover and those are mostly the
|
||||||
|
// klammerset's.
|
||||||
std::vector<Klammer_coverage> result {};
|
std::vector<Klammer_coverage> result {};
|
||||||
for (const auto& [name, klammer] : machine.m_klammers.m_klammers) {
|
for (const auto& [name, klammer] : machine.m_klammers.m_klammers) {
|
||||||
|
if (!defined_outside.empty() && !klammer.defined_outside(defined_outside)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const Written& w = written.at(name);
|
const Written& w = written.at(name);
|
||||||
Klammer_coverage kc {};
|
Klammer_coverage kc {};
|
||||||
kc.m_name = name;
|
kc.m_name = name;
|
||||||
@@ -274,8 +281,9 @@ void report_coverage(const Machine& machine,
|
|||||||
strings_t targets = machine.m_targets.user_defined();
|
strings_t targets = machine.m_targets.user_defined();
|
||||||
os << "Klammer coverage\n"
|
os << "Klammer coverage\n"
|
||||||
<< "================\n\n"
|
<< "================\n\n"
|
||||||
<< coverage.size() << " klammers, " << targets.size()
|
<< coverage.size() << " " << plural("klammer", static_cast<int>(coverage.size()))
|
||||||
<< " targets: " << join(targets, " ") << "\n";
|
<< ", " << targets.size() << " " << plural("target", static_cast<int>(targets.size()))
|
||||||
|
<< ": " << join(targets, " ") << "\n";
|
||||||
|
|
||||||
auto of_kind = [&coverage](coverage_t kind) {
|
auto of_kind = [&coverage](coverage_t kind) {
|
||||||
std::vector<const Klammer_coverage*> result {};
|
std::vector<const Klammer_coverage*> result {};
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "util.h" // strings_t
|
||||||
|
|
||||||
class Machine;
|
class Machine;
|
||||||
|
|
||||||
// Target coverage: which targets a klammer can actually render to.
|
// Target coverage: which targets a klammer can actually render to.
|
||||||
@@ -91,7 +93,13 @@ struct Klammer_coverage
|
|||||||
|
|
||||||
// Compute the coverage of every klammer the machine has loaded. Analysis
|
// Compute the coverage of every klammer the machine has loaded. Analysis
|
||||||
// only: nothing in the Machine is modified.
|
// only: nothing in the Machine is modified.
|
||||||
std::vector<Klammer_coverage> klammer_coverage(const Machine& machine);
|
// `defined_outside` restricts the RESULT to klammers with a definition from a
|
||||||
|
// file not in the list. The ANALYSIS still sees every klammer: a general
|
||||||
|
// body's coverage is the intersection of what the klammers it calls cover, and
|
||||||
|
// most of those come from the klammerset. So the klammersets are loaded, used
|
||||||
|
// to derive, and left out of the report -- which is what "kdesc -i" wants.
|
||||||
|
std::vector<Klammer_coverage> klammer_coverage(const Machine& machine,
|
||||||
|
const strings_t& defined_outside = {});
|
||||||
|
|
||||||
// The report behind "kdesc --coverage". `full` ("--coverage all") adds the
|
// The report behind "kdesc --coverage". `full` ("--coverage all") adds the
|
||||||
// source-file column and shows every "Needs attention" category, including
|
// source-file column and shows every "Needs attention" category, including
|
||||||
|
|||||||
@@ -15,9 +15,14 @@ bool display_source(const std::string& filename_arg)
|
|||||||
|
|
||||||
void Error::print_message(const std::string& epilog)
|
void Error::print_message(const std::string& epilog)
|
||||||
{
|
{
|
||||||
if (epilog != "")
|
// The separator is not cosmetic: the description ends with a sentence, and
|
||||||
//desc += " " + epilog + "\n";
|
// appending the advice straight onto it ran the two together into one word
|
||||||
m_desc += epilog + "\n";
|
// ("...for an unspecified targetkdiag loads no klammerset..."). m_desc is
|
||||||
|
// then justified to 80 columns, so the break has to be a blank line rather
|
||||||
|
// than a single newline, which justify() would fold back into the flow.
|
||||||
|
if (epilog != "") {
|
||||||
|
m_desc += "\n\n" + epilog + "\n";
|
||||||
|
}
|
||||||
if (m_just)
|
if (m_just)
|
||||||
m_desc = justify(m_desc, 80, 0);
|
m_desc = justify(m_desc, 80, 0);
|
||||||
std::cerr << "\n" << red << command_name << " (" << m_type << " error)";
|
std::cerr << "\n" << red << command_name << " (" << m_type << " error)";
|
||||||
@@ -31,5 +36,4 @@ void Error::print_message(const std::string& epilog)
|
|||||||
std::cerr << ", character " << m_loc.m_chr + 1;
|
std::cerr << ", character " << m_loc.m_chr + 1;
|
||||||
}
|
}
|
||||||
std::cerr << ":\n\n" << m_desc << reset << "\n";
|
std::cerr << ":\n\n" << m_desc << reset << "\n";
|
||||||
std::cout << reset;
|
|
||||||
}
|
}
|
||||||
|
|||||||
138
mac/eval.cpp
138
mac/eval.cpp
@@ -1,4 +1,5 @@
|
|||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
#include "error.h"
|
||||||
#include "eval.h"
|
#include "eval.h"
|
||||||
#include "eval_python.h"
|
#include "eval_python.h"
|
||||||
#include "eval_cpp.h"
|
#include "eval_cpp.h"
|
||||||
@@ -9,13 +10,51 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
|
||||||
|
// Run a shell command and return its STANDARD OUTPUT, which becomes document
|
||||||
|
// text. Two things the first version did not do, both of them the same defect
|
||||||
|
// as a msg() on the wrong stream -- output nobody chose to see, and a failure
|
||||||
|
// nobody was told about:
|
||||||
|
//
|
||||||
|
// * stderr went straight to the user's terminal, unattributed. It is not the
|
||||||
|
// command's output in any of the three policy categories (CLAUDE.md,
|
||||||
|
// "Command output policy"): it belongs to a subprocess a klammer invoked, at
|
||||||
|
// a location the Locator can name. It is captured here and reported at
|
||||||
|
// "-v 1" -- a derived value, the sort of thing "-v 1" exists for.
|
||||||
|
// * the exit status was discarded, so a command that failed contributed its
|
||||||
|
// partial output (or nothing) to the document and said nothing. A nonzero
|
||||||
|
// status is now an error, with the status, the command, and whatever the
|
||||||
|
// command said on stderr. Some commands exit nonzero without failing --
|
||||||
|
// "grep" finding no match is the usual one -- so the message names the
|
||||||
|
// explicit way to say that was intended, "cmd || true".
|
||||||
|
//
|
||||||
|
// tex_to_pdf() in sks/document/document.cpp is the same pattern for xelatex:
|
||||||
|
// capture, then decide what to report.
|
||||||
std::string shell(State state, std::string command, Locator loc)
|
std::string shell(State state, std::string command, Locator loc)
|
||||||
{
|
{
|
||||||
command = state.subst(command);
|
command = state.subst(command);
|
||||||
FILE* pipe = popen(command.c_str(), "r");
|
|
||||||
|
// popen gives one pipe, so stderr goes to a temporary file. mkstemp rather
|
||||||
|
// than a constructed name: several ktext runs may share /tmp.
|
||||||
|
std::string err_path = (fs::temp_directory_path() / "ktext_shell_XXXXXX").string();
|
||||||
|
std::vector<char> err_template(err_path.begin(), err_path.end());
|
||||||
|
err_template.push_back('\0');
|
||||||
|
int err_fd = mkstemp(err_template.data());
|
||||||
|
if (err_fd == -1) {
|
||||||
|
throw Environment_error("Could not create a temporary file for the "
|
||||||
|
"command's error output", loc, false);
|
||||||
|
}
|
||||||
|
close(err_fd);
|
||||||
|
err_path = err_template.data();
|
||||||
|
|
||||||
|
// The braces keep the redirection outside the writer's command, so a
|
||||||
|
// command containing its own pipeline or redirection still works.
|
||||||
|
std::string wrapped = "{ " + command + " ; } 2>" + q_(err_path);
|
||||||
|
FILE* pipe = popen(wrapped.c_str(), "r");
|
||||||
if (!pipe) {
|
if (!pipe) {
|
||||||
throw Parsing_error(
|
fs::remove(err_path);
|
||||||
|
throw Environment_error(
|
||||||
"Could not run command:\n" + command, loc, false);
|
"Could not run command:\n" + command, loc, false);
|
||||||
}
|
}
|
||||||
char buffer[128];
|
char buffer[128];
|
||||||
@@ -23,8 +62,37 @@ std::string shell(State state, std::string command, Locator loc)
|
|||||||
while (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
|
while (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
|
||||||
result += buffer;
|
result += buffer;
|
||||||
}
|
}
|
||||||
pclose(pipe);
|
int status = pclose(pipe);
|
||||||
// std::cout << "Command output:\n" << result << "\n";
|
std::string error_output = trim_right(string_from_file(err_path));
|
||||||
|
fs::remove(err_path);
|
||||||
|
|
||||||
|
int exit_status = WIFEXITED(status) ? WEXITSTATUS(status) : -1;
|
||||||
|
if (exit_status != 0) {
|
||||||
|
std::stringstream ss {};
|
||||||
|
ss << "The shell command failed";
|
||||||
|
if (WIFEXITED(status)) {
|
||||||
|
ss << " (exit status " << exit_status << ")";
|
||||||
|
} else if (WIFSIGNALED(status)) {
|
||||||
|
ss << " (killed by signal " << WTERMSIG(status) << ")";
|
||||||
|
}
|
||||||
|
ss << ":\n " << command;
|
||||||
|
if (!error_output.empty()) {
|
||||||
|
ss << "\nIt reported:\n " << error_output;
|
||||||
|
}
|
||||||
|
// Do not echo the command back into the suggestion: appending to an
|
||||||
|
// arbitrary command can produce nonsense ("exit 3 || true" cannot
|
||||||
|
// work, since exit terminates before || is reached).
|
||||||
|
ss << "\nIf a nonzero status is expected -- \"grep\" finding no match, "
|
||||||
|
"say -- end the command with \"|| true\" to say so.";
|
||||||
|
// "environment", not "parsing": the failure is OUTSIDE Klammertext,
|
||||||
|
// in the command the document invoked -- the same class as a missing
|
||||||
|
// xelatex or an unset environment variable.
|
||||||
|
throw Environment_error(ss.str(), loc, false);
|
||||||
|
}
|
||||||
|
if (!error_output.empty()) {
|
||||||
|
(void)K::log(1, "shell command wrote to stderr:", command,
|
||||||
|
"\n " + error_output);
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,13 +105,34 @@ bool is_haskell_file(const std::string& text)
|
|||||||
return trimmed.substr(trimmed.size() - 3) == ".hs";
|
return trimmed.substr(trimmed.size() - 3) == ".hs";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run a Haskell program with runghc and return its STANDARD OUTPUT, which
|
||||||
|
// becomes document text. Same rule as shell() above, and it was broken the
|
||||||
|
// same way: the command ran with "2>&1", so on a SUCCESSFUL run everything the
|
||||||
|
// program (or GHC) wrote to stderr was merged into the result and became part
|
||||||
|
// of the document. A program printing a progress note or a warning silently
|
||||||
|
// contributed it to the output.
|
||||||
|
//
|
||||||
|
// A compile error was already reported rather than swallowed -- the exit status
|
||||||
|
// was checked -- so what changes here is the successful case, the error's type,
|
||||||
|
// and where a failure's detail comes from.
|
||||||
std::string run_haskell(const std::string& hsfile, Locator loc)
|
std::string run_haskell(const std::string& hsfile, Locator loc)
|
||||||
{
|
{
|
||||||
std::string command = "runghc " + hsfile + " 2>&1";
|
std::string err_path = (fs::temp_directory_path() / "ktext_haskell_XXXXXX").string();
|
||||||
|
std::vector<char> err_template(err_path.begin(), err_path.end());
|
||||||
|
err_template.push_back('\0');
|
||||||
|
int err_fd = mkstemp(err_template.data());
|
||||||
|
if (err_fd == -1) {
|
||||||
|
throw Environment_error("Could not create a temporary file for runghc's "
|
||||||
|
"error output", loc, false);
|
||||||
|
}
|
||||||
|
close(err_fd);
|
||||||
|
err_path = err_template.data();
|
||||||
|
|
||||||
|
std::string command = "runghc " + q_(hsfile) + " 2>" + q_(err_path);
|
||||||
FILE* pipe = popen(command.c_str(), "r");
|
FILE* pipe = popen(command.c_str(), "r");
|
||||||
if (!pipe) {
|
if (!pipe) {
|
||||||
throw Parsing_error(
|
fs::remove(err_path);
|
||||||
"Could not run runghc.", loc, false);
|
throw Environment_error("Could not run runghc.", loc, false);
|
||||||
}
|
}
|
||||||
char buffer[128];
|
char buffer[128];
|
||||||
std::string result = "";
|
std::string result = "";
|
||||||
@@ -51,18 +140,41 @@ std::string run_haskell(const std::string& hsfile, Locator loc)
|
|||||||
result += buffer;
|
result += buffer;
|
||||||
}
|
}
|
||||||
int status = pclose(pipe);
|
int status = pclose(pipe);
|
||||||
|
std::string error_output = trim_right(string_from_file(err_path));
|
||||||
|
fs::remove(err_path);
|
||||||
|
|
||||||
if (status != 0) {
|
int exit_status = WIFEXITED(status) ? WEXITSTATUS(status) : -1;
|
||||||
throw Parsing_error(
|
if (exit_status != 0) {
|
||||||
"Haskell evaluation failed:\n" + result, loc, false);
|
std::stringstream ss {};
|
||||||
|
ss << "The Haskell program failed";
|
||||||
|
if (WIFEXITED(status)) {
|
||||||
|
ss << " (exit status " << exit_status << ")";
|
||||||
|
} else if (WIFSIGNALED(status)) {
|
||||||
|
ss << " (killed by signal " << WTERMSIG(status) << ")";
|
||||||
|
}
|
||||||
|
ss << ".";
|
||||||
|
if (!error_output.empty()) {
|
||||||
|
ss << "\nrunghc reported:\n" << error_output;
|
||||||
|
}
|
||||||
|
// A compile error arrives here, and so does a program that ran and then
|
||||||
|
// exited nonzero; the message does not guess which, it shows what
|
||||||
|
// runghc said.
|
||||||
|
throw Environment_error(ss.str(), loc, false);
|
||||||
|
}
|
||||||
|
if (!error_output.empty()) {
|
||||||
|
// GHC's warnings, and anything the program itself wrote to stderr.
|
||||||
|
// Not document text -- reported at "-v 1", like a shell command's.
|
||||||
|
(void)K::log(1, "runghc wrote to stderr:\n " + error_output);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string haskell(State state, std::string code, Locator loc)
|
std::string haskell(State state, std::string code, Locator loc)
|
||||||
{
|
{
|
||||||
if (system("which runghc > /dev/null 2>&1") != 0) {
|
if (system("command -v runghc > /dev/null 2>&1") != 0) {
|
||||||
throw Parsing_error(
|
// "environment", not "parsing": a missing external command is the same
|
||||||
|
// class as a missing xelatex, and "parsing error" misdescribes it.
|
||||||
|
throw Environment_error(
|
||||||
"@eval with the :haskell argument requires runghc, which was not found in PATH.\n"
|
"@eval with the :haskell argument requires runghc, which was not found in PATH.\n"
|
||||||
"Install it using GHCup; see https://www.haskell.org/ghcup/install/.",
|
"Install it using GHCup; see https://www.haskell.org/ghcup/install/.",
|
||||||
loc, false);
|
loc, false);
|
||||||
@@ -80,7 +192,7 @@ std::string haskell(State state, std::string code, Locator loc)
|
|||||||
tmppath.push_back('\0');
|
tmppath.push_back('\0');
|
||||||
int fd = mkstemp(tmppath.data());
|
int fd = mkstemp(tmppath.data());
|
||||||
if (fd < 0) {
|
if (fd < 0) {
|
||||||
throw Parsing_error(
|
throw Environment_error(
|
||||||
"Could not create temporary file for Haskell evaluation.", loc, false);
|
"Could not create temporary file for Haskell evaluation.", loc, false);
|
||||||
}
|
}
|
||||||
std::string hsfile = std::string(tmppath.data()) + ".hs";
|
std::string hsfile = std::string(tmppath.data()) + ".hs";
|
||||||
|
|||||||
@@ -209,12 +209,13 @@ std::string Eval_python::eval(std::string code)
|
|||||||
{
|
{
|
||||||
(void)K::log(3, code);
|
(void)K::log(3, code);
|
||||||
code = m_machine.m_state.subst(code, true);
|
code = m_machine.m_state.subst(code, true);
|
||||||
/*
|
// Kept as a record of a value worth watching; line comments rather than a
|
||||||
msg() << "\n"
|
// block, so the msg() guard can see it is inert (doc/check_output_policy.sh
|
||||||
<< std::string(80, '-') << "\n"
|
// is line-based and cannot tell it is inside a /* */).
|
||||||
<< code << "\n"
|
// msg() << "\n"
|
||||||
<< std::string(80, '-') << "\n";
|
// << std::string(80, '-') << "\n"
|
||||||
*/
|
// << code << "\n"
|
||||||
|
// << std::string(80, '-') << "\n";
|
||||||
if (std::regex_search(code, statement_delimiter)) {
|
if (std::regex_search(code, statement_delimiter)) {
|
||||||
return eval_statements(code);
|
return eval_statements(code);
|
||||||
} else {
|
} else {
|
||||||
@@ -229,7 +230,7 @@ std::string Eval_python::eval_katom_list(
|
|||||||
katom_iter code_begin = begin + 1;
|
katom_iter code_begin = begin + 1;
|
||||||
katom_iter code_end = end - 1;
|
katom_iter code_end = end - 1;
|
||||||
std::string code_result = eval(as_string(code_begin, code_end, true));
|
std::string code_result = eval(as_string(code_begin, code_end, true));
|
||||||
msg() << "code_result: " << code_result << "\n";
|
(void)K::log(3, "code_result:", code_result);
|
||||||
katom_list code_katoms = m_machine.process(code_result, command_name);
|
katom_list code_katoms = m_machine.process(code_result, command_name);
|
||||||
for (auto kiter = code_begin; kiter < code_end; kiter++) {
|
for (auto kiter = code_begin; kiter < code_end; kiter++) {
|
||||||
kiter->m_type = katom_t::replaced;
|
kiter->m_type = katom_t::replaced;
|
||||||
|
|||||||
31
mac/file.cpp
31
mac/file.cpp
@@ -160,6 +160,7 @@ strings_t group_filename_tokens(const strings_t& tokens, const std::string& base
|
|||||||
// name exists. A name that never resolves is kept as given, so the
|
// name exists. A name that never resolves is kept as given, so the
|
||||||
// missing-file error downstream reports what the user wrote.
|
// missing-file error downstream reports what the user wrote.
|
||||||
size_t i = 0;
|
size_t i = 0;
|
||||||
|
bool reported = false; // at most one "tried" report per list
|
||||||
while (i < tokens.size()) {
|
while (i < tokens.size()) {
|
||||||
if (filename_exists(tokens[i], base_dir)) {
|
if (filename_exists(tokens[i], base_dir)) {
|
||||||
result.push_back(tokens[i]);
|
result.push_back(tokens[i]);
|
||||||
@@ -173,8 +174,13 @@ strings_t group_filename_tokens(const strings_t& tokens, const std::string& base
|
|||||||
acc += " " + tokens[j];
|
acc += " " + tokens[j];
|
||||||
++j;
|
++j;
|
||||||
if (filename_exists(acc, base_dir)) {
|
if (filename_exists(acc, base_dir)) {
|
||||||
std::cerr << command_name << ": interpreting \""
|
// A successful regrouping is a DERIVED value -- the
|
||||||
<< acc << "\" as one filename\n";
|
// command completed a name the input left ambiguous -- so
|
||||||
|
// it is reported at "-v 1" and not at every run. Silence
|
||||||
|
// is safe here because a rescue only succeeds when the
|
||||||
|
// joined name names an existing file: a mistyped name
|
||||||
|
// fails rather than resolving to something else.
|
||||||
|
K::log(1, "interpreting", q_(acc), "as one filename");
|
||||||
result.push_back(acc);
|
result.push_back(acc);
|
||||||
i = j;
|
i = j;
|
||||||
found = true;
|
found = true;
|
||||||
@@ -182,6 +188,27 @@ strings_t group_filename_tokens(const strings_t& tokens, const std::string& base
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!found) {
|
if (!found) {
|
||||||
|
// Nothing resolved. The groupings that were TRIED are what
|
||||||
|
// the user needs at verbosity 0, because the downstream error
|
||||||
|
// names only the first token and cannot say why the others
|
||||||
|
// were not joined to it. Recorded on the name so the caller
|
||||||
|
// can report them with the missing-file error.
|
||||||
|
strings_t tried {};
|
||||||
|
std::string acc2 = tokens[i];
|
||||||
|
tried.push_back(q_(acc2));
|
||||||
|
for (size_t k = i + 1; k < tokens.size(); ++k) {
|
||||||
|
acc2 += " " + tokens[k];
|
||||||
|
tried.push_back(q_(acc2));
|
||||||
|
}
|
||||||
|
// Once: the first unresolved token's list already shows
|
||||||
|
// every joining from that point on, and one report per
|
||||||
|
// leftover word buries it.
|
||||||
|
if (tried.size() > 1 && !reported) {
|
||||||
|
std::cerr << command_name << ": no file matches "
|
||||||
|
<< q_(tokens[i]) << "; tried "
|
||||||
|
<< join(tried, ", ") << "\n";
|
||||||
|
reported = true;
|
||||||
|
}
|
||||||
result.push_back(tokens[i]);
|
result.push_back(tokens[i]);
|
||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -221,6 +221,11 @@ Resolved_font resolve_font(const std::string& family_name)
|
|||||||
Resolved_font font = resolve_in_directory(family_name, dir_name, base);
|
Resolved_font font = resolve_in_directory(family_name, dir_name, base);
|
||||||
if (!font.family_name.empty()) {
|
if (!font.family_name.empty()) {
|
||||||
extract_font_metrics(font);
|
extract_font_metrics(font);
|
||||||
|
// A DERIVED value: an installed font may shadow a default one, and
|
||||||
|
// which directory won is not readable from ":serif_font Whatever".
|
||||||
|
// The commonest font question -- "why does it not look like the
|
||||||
|
// one I installed?" -- is answered by this line.
|
||||||
|
(void)K::log(1, "Font \"" + family_name + "\": " + base + "/" + dir_name);
|
||||||
return font;
|
return font;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ katom_list make_katoms_from_word(std::string s, const std::string& source_desc,
|
|||||||
for (const auto& [desc, rgx, expanded] : katom_rewrite_rules) {
|
for (const auto& [desc, rgx, expanded] : katom_rewrite_rules) {
|
||||||
std::string modified = expand_compound_katom(s, rgx.m_regex, expanded);
|
std::string modified = expand_compound_katom(s, rgx.m_regex, expanded);
|
||||||
if (dbg) {
|
if (dbg) {
|
||||||
msg() << "Rewrite: " << desc << " " << rgx.m_pattern << " " << expanded << "\n";
|
msg() << "Rewrite: " << desc << " " << rgx.m_pattern << " " << expanded << "\n"; // output-policy-ok (dbg)
|
||||||
}
|
}
|
||||||
if (modified != s) {
|
if (modified != s) {
|
||||||
auto parts = word_split(modified);
|
auto parts = word_split(modified);
|
||||||
@@ -118,11 +118,22 @@ void warn_unparsed_katoms(katom_list& katoms, bool warn)
|
|||||||
if (k.m_type != katom_t::ignored
|
if (k.m_type != katom_t::ignored
|
||||||
&& k.m_type != katom_t::replaced
|
&& k.m_type != katom_t::replaced
|
||||||
&& k.m_type != katom_t::literal) {
|
&& k.m_type != katom_t::literal) {
|
||||||
std::cerr << command_name
|
// One of the two surviving WARNINGS (the commands otherwise treat
|
||||||
<< " [warning]: Word not parsed in "
|
// an anomaly as an error and stop -- see the output policy in
|
||||||
<< k.m_loc.m_filename << ", line " << k.m_loc.m_line << ":\n"
|
// CLAUDE.md). It stays a warning because the judgment is a
|
||||||
<< " " << k.m_text << "\n"
|
// HEURISTIC, not exact: the @eval read-back legitimately fails to
|
||||||
<< "To include a special character (@, |, #, and ^), put \"^\" before it.\n";
|
// parse raw target markup, which is why m_warn_unparsed exists. A
|
||||||
|
// heuristic that halts turns every false positive into a blocker on
|
||||||
|
// a document that would otherwise render. Promote it to an error
|
||||||
|
// when the judgment becomes exact.
|
||||||
|
//
|
||||||
|
// Through warning(), not a raw std::cerr, so every warning the
|
||||||
|
// commands issue has one format: it said "[warning]" where
|
||||||
|
// everything else says "(warning)".
|
||||||
|
warning("Word not parsed:\n " + k.m_text
|
||||||
|
+ "\nTo include a special character (@, |, #, and ^), put "
|
||||||
|
"\"^\" before it.",
|
||||||
|
k.m_loc);
|
||||||
k.m_unparsed = false;
|
k.m_unparsed = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,7 +142,7 @@ void warn_unparsed_katoms(katom_list& katoms, bool warn)
|
|||||||
katom_list split_into_katoms(std::string s, const std::string& source, int source_line)
|
katom_list split_into_katoms(std::string s, const std::string& source, int source_line)
|
||||||
{
|
{
|
||||||
if (dbg) {
|
if (dbg) {
|
||||||
msg() << "Make katoms: " << s << "<\n";
|
msg() << "Make katoms: " << s << "<\n"; // output-policy-ok (dbg)
|
||||||
}
|
}
|
||||||
// const std::string middle_dot { "\u00B7" };
|
// const std::string middle_dot { "\u00B7" };
|
||||||
s = string_replace(s, "\r", "");
|
s = string_replace(s, "\r", "");
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ public:
|
|||||||
, m_initial_type(other.m_initial_type)
|
, m_initial_type(other.m_initial_type)
|
||||||
, m_display(other.m_display)
|
, m_display(other.m_display)
|
||||||
, m_unparsed(other.m_unparsed)
|
, m_unparsed(other.m_unparsed)
|
||||||
|
, m_deferred(other.m_deferred)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
// Copy assignment operator
|
// Copy assignment operator
|
||||||
@@ -38,6 +39,7 @@ public:
|
|||||||
m_initial_type = other.m_initial_type;
|
m_initial_type = other.m_initial_type;
|
||||||
m_display = other.m_display;
|
m_display = other.m_display;
|
||||||
m_unparsed = other.m_unparsed;
|
m_unparsed = other.m_unparsed;
|
||||||
|
m_deferred = other.m_deferred;
|
||||||
}
|
}
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
@@ -74,6 +76,13 @@ public:
|
|||||||
// The warning is deferred to warn_unparsed_katoms(), after removal and
|
// The warning is deferred to warn_unparsed_katoms(), after removal and
|
||||||
// literal marking, so removed text (comments, #[...]# blocks) never warns.
|
// literal marking, so removed text (comments, #[...]# blocks) never warns.
|
||||||
bool m_unparsed {};
|
bool m_unparsed {};
|
||||||
|
// Inside an unresolved @cond span, and therefore INERT: the passes with
|
||||||
|
// observable effects -- @eval and @read -- skip it, so a branch that is
|
||||||
|
// never selected never runs anything. @cond is a non-strict special form
|
||||||
|
// (doc/cond_evaluation_order.md), and this flag is the mechanism; the flag
|
||||||
|
// is not the semantics. Cleared on the selected branch when the @cond is
|
||||||
|
// resolved in the apply fold, which then processes that branch normally.
|
||||||
|
bool m_deferred {};
|
||||||
};
|
};
|
||||||
|
|
||||||
bool active(const std::vector<Katom>& katoms);
|
bool active(const std::vector<Katom>& katoms);
|
||||||
|
|||||||
@@ -430,13 +430,16 @@ std::string parameter_signature(const Parameter_set& parameters)
|
|||||||
};
|
};
|
||||||
bool first = true;
|
bool first = true;
|
||||||
for (const auto& pos : parameters.m_positional) {
|
for (const auto& pos : parameters.m_positional) {
|
||||||
add(first ? typed(pos) : "| " + typed(pos));
|
// std::string(...) on the leading literal: see the note in
|
||||||
|
// argtype.cpp::pylist -- append rather than insert-at-front, and it
|
||||||
|
// avoids a gcc-12 -Wrestrict false positive.
|
||||||
|
add(first ? typed(pos) : std::string("| ") + typed(pos));
|
||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
for (const auto& rest : parameters.m_rest)
|
for (const auto& rest : parameters.m_rest)
|
||||||
add(typed(rest));
|
add(typed(rest));
|
||||||
for (const auto& opt : parameters.m_optional)
|
for (const auto& opt : parameters.m_optional)
|
||||||
add(":" + typed(opt)
|
add(std::string(":") + typed(opt)
|
||||||
+ (opt.m_default.empty() ? "" : " " + opt.m_default));
|
+ (opt.m_default.empty() ? "" : " " + opt.m_default));
|
||||||
return result.empty() ? "(no parameters)" : result;
|
return result.empty() ? "(no parameters)" : result;
|
||||||
}
|
}
|
||||||
@@ -555,6 +558,16 @@ void show_args(const std::string& label_text, const std::vector<Argument>& argum
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Klammer::defined_outside(const strings_t& files) const
|
||||||
|
{
|
||||||
|
for (const auto& def : m_defs) {
|
||||||
|
if (!is_in(def.loc.m_filename, files)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
strings_t Klammer::get_target_names() const
|
strings_t Klammer::get_target_names() const
|
||||||
{
|
{
|
||||||
strings_t names {};
|
strings_t names {};
|
||||||
|
|||||||
@@ -76,6 +76,12 @@ public:
|
|||||||
if (p.m_argtype.m_name == "literal") return true;
|
if (p.m_argtype.m_name == "literal") return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// True when at least one definition of this klammer came from a file
|
||||||
|
// OUTSIDE the given list -- that is, the input defined it rather than a
|
||||||
|
// klammerset supplying it. "At least one" is deliberate: a klammer the
|
||||||
|
// input REDEFINES (":::") came originally from the klammerset, and a
|
||||||
|
// reader of that input needs to know it was changed.
|
||||||
|
bool defined_outside(const strings_t& files) const;
|
||||||
strings_t get_target_names() const;
|
strings_t get_target_names() const;
|
||||||
strings_t get_locations();
|
strings_t get_locations();
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,14 @@ void Klammer_registry::add(
|
|||||||
std::string msg = result.message;
|
std::string msg = result.message;
|
||||||
msg = string_replace(msg, "NAME", q_(name_target));
|
msg = string_replace(msg, "NAME", q_(name_target));
|
||||||
msg = string_replace(msg, "AT", at_desc);
|
msg = string_replace(msg, "AT", at_desc);
|
||||||
warning(msg, begin->m_loc);
|
// An override is not a warning: ":::" exists to override, and
|
||||||
|
// a klammer set a user did not write may be overridden by
|
||||||
|
// design (TODO #33). A warning nobody can act on is not a
|
||||||
|
// warning. It IS worth reporting at "-v 1", because the
|
||||||
|
// definition being replaced usually lives in another file, in
|
||||||
|
// another klammerset, which the user cannot see from what they
|
||||||
|
// wrote (Andy, 2026-08-15).
|
||||||
|
(void)K::log(1, msg + " at " + begin->m_loc.desc());
|
||||||
}
|
}
|
||||||
m_klammers[klammer_name].remove_target_definition(target_name);
|
m_klammers[klammer_name].remove_target_definition(target_name);
|
||||||
}
|
}
|
||||||
@@ -172,7 +179,8 @@ std::string Klammer_registry::instance_list(int margin) const
|
|||||||
return ss.str();
|
return ss.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Klammer_registry::describe(int margin, const std::string& search) const
|
std::string Klammer_registry::describe(int margin, const std::string& search,
|
||||||
|
const strings_t& defined_outside) const
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
strings_t names {};
|
strings_t names {};
|
||||||
@@ -182,6 +190,9 @@ std::string Klammer_registry::describe(int margin, const std::string& search) co
|
|||||||
std::string result;
|
std::string result;
|
||||||
std::string query = collapse_whitespace(search);
|
std::string query = collapse_whitespace(search);
|
||||||
for (const auto& [name, k] : m_klammers) {
|
for (const auto& [name, k] : m_klammers) {
|
||||||
|
if (!defined_outside.empty() && !k.defined_outside(defined_outside)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!query.empty() &&
|
if (!query.empty() &&
|
||||||
!contains_fold(name, query) &&
|
!contains_fold(name, query) &&
|
||||||
!contains_fold(collapse_whitespace(k.description_text()), query)) {
|
!contains_fold(collapse_whitespace(k.description_text()), query)) {
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ public:
|
|||||||
// sides (a description written across several lines in a ".k" file must
|
// sides (a description written across several lines in a ".k" file must
|
||||||
// still match a phrase typed on one). Empty when nothing matched, which
|
// still match a phrase typed on one). Empty when nothing matched, which
|
||||||
// is how the caller knows to say so.
|
// is how the caller knows to say so.
|
||||||
std::string describe(int margin=0, const std::string& search="") const;
|
// `defined_outside` restricts the listing to klammers with at least one
|
||||||
|
// definition from a file not in the list -- the input's own klammers,
|
||||||
|
// with the loaded klammersets' left out.
|
||||||
|
std::string describe(int margin=0, const std::string& search="",
|
||||||
|
const strings_t& defined_outside={}) const;
|
||||||
|
|
||||||
std::map<std::string, Klammer> m_klammers {};
|
std::map<std::string, Klammer> m_klammers {};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "util.h" // strings_t
|
||||||
|
|
||||||
#include "locator.h"
|
#include "locator.h"
|
||||||
|
|
||||||
// A Klammerset is the formal construct declared by the @@@klammerset system
|
// A Klammerset is the formal construct declared by the @@@klammerset system
|
||||||
@@ -25,4 +27,11 @@ public:
|
|||||||
std::vector<std::string> m_requires {}; // klammerset declaration files, loaded first
|
std::vector<std::string> m_requires {}; // klammerset declaration files, loaded first
|
||||||
std::vector<std::string> m_files {}; // definition files, loaded in list order
|
std::vector<std::string> m_files {}; // definition files, loaded in list order
|
||||||
Locator m_loc {}; // the declaring file; relative names resolve against it
|
Locator m_loc {}; // the declaring file; relative names resolve against it
|
||||||
|
// Every file this set actually read, as a canonical path: the declaring
|
||||||
|
// file itself (a set may define klammers before or after its declaration)
|
||||||
|
// followed by the resolved :requires and :files. m_files holds the names
|
||||||
|
// as WRITTEN, which cannot be compared against a definition's location;
|
||||||
|
// this holds what was opened. It is what lets a display tell a klammer
|
||||||
|
// that came from a klammerset apart from one the input defined.
|
||||||
|
strings_t m_loaded_files {};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ std::optional<Klammerset> Klammerset_registry::add(
|
|||||||
|
|
||||||
std::string symbol = values["symbol"];
|
std::string symbol = values["symbol"];
|
||||||
check_symbol(symbol, begin->m_loc);
|
check_symbol(symbol, begin->m_loc);
|
||||||
|
check_declaring_file(symbol, begin->m_loc);
|
||||||
if (has(symbol)) {
|
if (has(symbol)) {
|
||||||
(void)K::log(2, "Klammerset \"" + symbol + "\" is already loaded; declaration skipped");
|
(void)K::log(2, "Klammerset \"" + symbol + "\" is already loaded; declaration skipped");
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
@@ -76,6 +77,45 @@ void Klammerset_registry::check_symbol(const std::string& symbol, const Locator&
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A klammerset that can be NAMED must live where its name says: symbol X is
|
||||||
|
// declared in X/X.k (Andy, 2026-08-15). The convention already governed
|
||||||
|
// symbol RESOLUTION; making it a requirement of the declaration too is what
|
||||||
|
// turns the symbol into a function of the path -- and that is what lets the
|
||||||
|
// already-loaded guard run BEFORE a file is read rather than after, which is
|
||||||
|
// the whole of the diamond-":requires" fix. Without it the symbol is known
|
||||||
|
// only once the file has been parsed, and by then a second read has already
|
||||||
|
// re-executed the declaring file's own definitions.
|
||||||
|
//
|
||||||
|
// It also continues the reasoning behind symbols-only on the command line
|
||||||
|
// (2026-08-14): if every klammerset is reachable by symbol the set of all of
|
||||||
|
// them is ENUMERABLE; if every declaration is in X/X.k each one is also
|
||||||
|
// IDENTIFIABLE from where it sits.
|
||||||
|
//
|
||||||
|
// EXEMPT: a declaration that is not in a ".k" file at all. A document may
|
||||||
|
// declare a klammerset -- that is how a designer writes one, and kdesc's
|
||||||
|
// provenance filter depends on it -- and such a set is local to the document:
|
||||||
|
// nothing can ":requires" it, so it has no identity to protect.
|
||||||
|
void Klammerset_registry::check_declaring_file(
|
||||||
|
const std::string& symbol, const Locator& loc) const
|
||||||
|
{
|
||||||
|
fs::path declaring(loc.m_filename);
|
||||||
|
if (declaring.extension() != ".k") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (declaring.stem() == symbol && declaring.parent_path().filename() == symbol) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw Klammerset_error(
|
||||||
|
"The klammerset \"" + symbol + "\" must be declared in a file named \""
|
||||||
|
+ symbol + "/" + symbol + ".k\", but this declaration is in \""
|
||||||
|
+ declaring.filename().string() + "\" (in a directory named \""
|
||||||
|
+ declaring.parent_path().filename().string() + "\"). A klammerset that "
|
||||||
|
"can be named is identified by where it is, so that a symbol names one "
|
||||||
|
"file and one file declares one symbol. A klammerset local to a document "
|
||||||
|
"may be declared in the document itself.",
|
||||||
|
loc);
|
||||||
|
}
|
||||||
|
|
||||||
bool Klammerset_registry::has(const std::string& symbol) const
|
bool Klammerset_registry::has(const std::string& symbol) const
|
||||||
{
|
{
|
||||||
return m_klammersets.count(symbol) > 0;
|
return m_klammersets.count(symbol) > 0;
|
||||||
@@ -119,13 +159,28 @@ std::vector<std::string> klammerset_search_dirs(const std::string& local_dir)
|
|||||||
if (!local_dir.empty()) {
|
if (!local_dir.empty()) {
|
||||||
push_unique(local_dir);
|
push_unique(local_dir);
|
||||||
}
|
}
|
||||||
std::string paths {};
|
// Read ONCE per process, not once per resolution. A search path decides
|
||||||
const char* env = std::getenv("KLAMMERTEXT_KLAMMERSETS");
|
// WHICH FILE a symbol means -- an identity question, not a value one -- so
|
||||||
if (env && *env) {
|
// it must not move while a document is being processed: if it did, "which
|
||||||
paths = env;
|
// klammerset is X" would depend on evaluation order and "kdesc
|
||||||
} else if (const char* home = std::getenv("HOME"); home && *home) {
|
// --klammersets" could not be a complete answer, which is the reason the
|
||||||
paths = std::string(home) + "/.klammertext/klammersets";
|
// command line takes symbols only.
|
||||||
}
|
//
|
||||||
|
// It could move. The embedded Python shares the process, so an @eval
|
||||||
|
// doing os.environ[...] = ... changed what a later getenv here returned;
|
||||||
|
// a document could extend its own search path between one declaration and
|
||||||
|
// the next. Nothing intended that -- it fell out of a fresh getenv and an
|
||||||
|
// in-process interpreter.
|
||||||
|
static const std::string paths = [] {
|
||||||
|
const char* env = std::getenv("KLAMMERTEXT_KLAMMERSETS");
|
||||||
|
if (env && *env) {
|
||||||
|
return std::string(env);
|
||||||
|
}
|
||||||
|
if (const char* home = std::getenv("HOME"); home && *home) {
|
||||||
|
return std::string(home) + "/.klammertext/klammersets";
|
||||||
|
}
|
||||||
|
return std::string();
|
||||||
|
}();
|
||||||
std::stringstream ss(paths);
|
std::stringstream ss(paths);
|
||||||
std::string dir;
|
std::string dir;
|
||||||
while (std::getline(ss, dir, ':')) {
|
while (std::getline(ss, dir, ':')) {
|
||||||
@@ -153,7 +208,7 @@ fs::path resolve_klammerset_symbol(
|
|||||||
"The klammerset \"" + symbol + "\" was not found. A symbol x names the "
|
"The klammerset \"" + symbol + "\" was not found. A symbol x names the "
|
||||||
"declaration file x/x.k in one of the search directories: "
|
"declaration file x/x.k in one of the search directories: "
|
||||||
+ join(dirs, ", ")
|
+ join(dirs, ", ")
|
||||||
+ ". Enter \"kdesc --klammerset\" to list the available klammersets.",
|
+ ". Enter \"kdesc --klammersets\" to list the available klammersets.",
|
||||||
loc);
|
loc);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,3 +278,21 @@ std::string Klammerset_registry::describe(int margin, bool long_format) const
|
|||||||
}
|
}
|
||||||
return ss.str();
|
return ss.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Klammerset_registry::set_loaded_files(const std::string& symbol, const strings_t& files)
|
||||||
|
{
|
||||||
|
auto it = m_klammersets.find(symbol);
|
||||||
|
if (it != m_klammersets.end()) {
|
||||||
|
it->second.m_loaded_files = files;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
strings_t Klammerset_registry::loaded_files() const
|
||||||
|
{
|
||||||
|
strings_t result {};
|
||||||
|
for (const auto& [symbol, klammerset] : m_klammersets) {
|
||||||
|
result.insert(result.end(),
|
||||||
|
klammerset.m_loaded_files.begin(), klammerset.m_loaded_files.end());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,12 +19,25 @@ public:
|
|||||||
// (typically reached through :requires) is skipped, not an error.
|
// (typically reached through :requires) is skipped, not an error.
|
||||||
std::optional<Klammerset> add(std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end, std::vector<Katom>& katoms);
|
std::optional<Klammerset> add(std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end, std::vector<Katom>& katoms);
|
||||||
void check_symbol(const std::string& symbol, const Locator& loc) const;
|
void check_symbol(const std::string& symbol, const Locator& loc) const;
|
||||||
|
// Symbol X must be declared in X/X.k, so that the symbol is a function of
|
||||||
|
// the path and the already-loaded guard can run before a file is read.
|
||||||
|
void check_declaring_file(const std::string& symbol, const Locator& loc) const;
|
||||||
|
|
||||||
bool has(const std::string& symbol) const;
|
bool has(const std::string& symbol) const;
|
||||||
Klammerset get(const std::string& symbol, const Locator& loc) const;
|
Klammerset get(const std::string& symbol, const Locator& loc) const;
|
||||||
|
|
||||||
std::string describe(int margin=2, bool long_format=false) const;
|
std::string describe(int margin=2, bool long_format=false) const;
|
||||||
|
|
||||||
|
// Record what a set actually opened (see Klammerset::m_loaded_files).
|
||||||
|
void set_loaded_files(const std::string& symbol, const strings_t& files);
|
||||||
|
|
||||||
|
// Every file every registered klammerset read. A klammer whose
|
||||||
|
// definitions all come from these was supplied by a klammerset; one with
|
||||||
|
// a definition elsewhere was defined by the input -- which is the
|
||||||
|
// distinction "kdesc -i" needs, since it loads a klammerset in order to
|
||||||
|
// analyse a file without wanting to LIST the klammerset's own klammers.
|
||||||
|
strings_t loaded_files() const;
|
||||||
|
|
||||||
std::map<std::string, Klammerset> m_klammersets {};
|
std::map<std::string, Klammerset> m_klammersets {};
|
||||||
std::vector<std::string> m_symbols {};
|
std::vector<std::string> m_symbols {};
|
||||||
Parameter_set m_parameters {};
|
Parameter_set m_parameters {};
|
||||||
@@ -59,5 +72,5 @@ fs::path resolve_klammerset_symbol(
|
|||||||
|
|
||||||
// The symbols available on the search path, with provenance; a symbol
|
// The symbols available on the search path, with provenance; a symbol
|
||||||
// found again in a later directory is marked as shadowed. For
|
// found again in a later directory is marked as shadowed. For
|
||||||
// kdesc --klammerset.
|
// kdesc --klammersets.
|
||||||
std::string describe_klammerset_search(const std::string& local_dir, int margin=2);
|
std::string describe_klammerset_search(const std::string& local_dir, int margin=2);
|
||||||
|
|||||||
14
mac/log.cpp
14
mac/log.cpp
@@ -52,20 +52,20 @@ std::ostream& operator<<(std::ostream& os, const log_arg& arg)
|
|||||||
return os;
|
return os;
|
||||||
}
|
}
|
||||||
|
|
||||||
void log_indent(const std::string& filename, const std::string& color)
|
void log_indent(const std::string& filename, const Color& color)
|
||||||
{
|
{
|
||||||
std::cout
|
std::cerr
|
||||||
<< color
|
<< color
|
||||||
<< std::setfill(' ')
|
<< std::setfill(' ')
|
||||||
<< std::setw(22-static_cast<int>(std::size(filename)))
|
<< std::setw(22-static_cast<int>(std::size(filename)))
|
||||||
<< std::right;
|
<< std::right;
|
||||||
}
|
}
|
||||||
|
|
||||||
void log_filepos(const std::string& filename, int line, const std::string& color)
|
void log_filepos(const std::string& filename, int line, const Color& color)
|
||||||
{
|
{
|
||||||
// if (show_verbose_location) {
|
// if (show_verbose_location) {
|
||||||
log_indent(filename, color);
|
log_indent(filename, color);
|
||||||
std::cout
|
std::cerr
|
||||||
<< "[" << filename << ":"
|
<< "[" << filename << ":"
|
||||||
<< std::setw(3) << std::setfill('0')
|
<< std::setw(3) << std::setfill('0')
|
||||||
<< line << "] " << reset;
|
<< line << "] " << reset;
|
||||||
@@ -91,7 +91,7 @@ std::string prettify_name(const std::string& s)
|
|||||||
void display_location(int log_level, std::source_location location)
|
void display_location(int log_level, std::source_location location)
|
||||||
{
|
{
|
||||||
if (verbose_level >= log_level) {
|
if (verbose_level >= log_level) {
|
||||||
std::string color = blue; // cyan;
|
Color color = blue; // cyan;
|
||||||
if (log_level == 1) {
|
if (log_level == 1) {
|
||||||
color = magenta;
|
color = magenta;
|
||||||
}
|
}
|
||||||
@@ -100,9 +100,9 @@ void display_location(int log_level, std::source_location location)
|
|||||||
location.line(), color);
|
location.line(), color);
|
||||||
}
|
}
|
||||||
if (verbose_level > 3) {
|
if (verbose_level > 3) {
|
||||||
std::cout << location.function_name();
|
std::cerr << location.function_name();
|
||||||
} else if (verbose_level > 1) {
|
} else if (verbose_level > 1) {
|
||||||
std::cout << prettify_name(location.function_name());
|
std::cerr << prettify_name(location.function_name());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
mac/log.h
12
mac/log.h
@@ -26,14 +26,18 @@ struct log
|
|||||||
log(int log_level, Ts&&... ts, const std::source_location& location = std::source_location::current()) {
|
log(int log_level, Ts&&... ts, const std::source_location& location = std::source_location::current()) {
|
||||||
if (verbose_level >= log_level) {
|
if (verbose_level >= log_level) {
|
||||||
display_location(log_level, location);
|
display_location(log_level, location);
|
||||||
|
// stderr, always. Logging is category 1 (see the output policy in
|
||||||
|
// CLAUDE.md); stdout belongs to the command's RESULT, and for
|
||||||
|
// ktext that result is a document a user may pipe. Writing a log
|
||||||
|
// line to stdout put it inside the document.
|
||||||
if (verbose_level > 1 && sizeof...(ts) > 0) {
|
if (verbose_level > 1 && sizeof...(ts) > 0) {
|
||||||
std::cout << ": ";
|
std::cerr << ": ";
|
||||||
} else if (verbose_level == 1) {
|
} else if (verbose_level == 1) {
|
||||||
std::cout << command_name << ": ";
|
std::cerr << command_name << ": ";
|
||||||
}
|
}
|
||||||
if (log_level > 0) {
|
if (log_level > 0) {
|
||||||
((std::cout << std::forward<Ts>(ts) << " "), ...);
|
((std::cerr << std::forward<Ts>(ts) << " "), ...);
|
||||||
std::cout << '\n';
|
std::cerr << '\n';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
225
mac/machine.cpp
225
mac/machine.cpp
@@ -81,6 +81,9 @@ void Machine::process_eval_katoms(katom_list& katoms)
|
|||||||
if (std::find_if(katoms.begin(), katoms.end(), begin_eval) != katoms.end()) {
|
if (std::find_if(katoms.begin(), katoms.end(), begin_eval) != katoms.end()) {
|
||||||
for (const auto& [op, cl] : find_spans(katoms, level_increase, level_decrease, true, "eval")) {
|
for (const auto& [op, cl] : find_spans(katoms, level_increase, level_decrease, true, "eval")) {
|
||||||
auto [begin, end] = find_span_katoms(katoms, op, cl);
|
auto [begin, end] = find_span_katoms(katoms, op, cl);
|
||||||
|
// Inert: inside a @cond branch that has not been selected. An
|
||||||
|
// @eval in a discarded branch must not run.
|
||||||
|
if (begin->m_deferred) continue;
|
||||||
if (begin_eval(*begin)) {
|
if (begin_eval(*begin)) {
|
||||||
Eval E(*this, begin->m_loc);
|
Eval E(*this, begin->m_loc);
|
||||||
katom_list eval_katoms = E.eval(begin, end);
|
katom_list eval_katoms = E.eval(begin, end);
|
||||||
@@ -138,18 +141,22 @@ bool is_true(const std::string& s)
|
|||||||
return s == "True" || s == "true" || s == "1";
|
return s == "True" || s == "true" || s == "1";
|
||||||
}
|
}
|
||||||
|
|
||||||
// @cond's predicate relation is currently partial in effect: is_true()
|
// @cond's predicate relation is TOTAL AND STRICT (Andy, 2026-08-15, deciding
|
||||||
// recognizes three strings as true and treats EVERYTHING else as false, so a
|
// notes/Klammertext_improvements.md §4.1): there is a defined true set, a
|
||||||
// misspelled state variable, a "TRUE", a "yes", or a Python traceback all
|
// defined false set, and anything else is an error at the @cond.
|
||||||
// silently select the false branch.
|
|
||||||
//
|
//
|
||||||
// What the truth values should be is an open language-policy question (see
|
// It was partial in effect until then -- is_true() recognized three strings and
|
||||||
// notes/Klammertext_improvements.md, "The @cond predicate relation"), so the
|
// treated EVERYTHING else as false, so a misspelled state variable, a "TRUE", a
|
||||||
// semantics here is deliberately unchanged. What is added is visibility: a
|
// "yes", or a Python traceback all silently selected the false branch. The
|
||||||
// predicate outside the provisionally recognized sets below is reported, with
|
// 2026-08-01 work made that visible with a warning while the policy was
|
||||||
// its value and location, so the cases can be found in real documents while
|
// undecided; the warning found nothing in the SKS, which is the evidence that
|
||||||
// the policy is decided. The recognized false set carries no semantics -- it
|
// the corpus uses well-formed predicates and that the blast radius is small.
|
||||||
// exists only to keep the diagnostic quiet for values that plainly mean false.
|
//
|
||||||
|
// Empty stays in the FALSE set, and deliberately: an optional argument that
|
||||||
|
// was not written substitutes as empty, so "absent means false" is what
|
||||||
|
// carries the "@cond *opt* | ... @" idiom. The entangled sub-question in
|
||||||
|
// §4.1 -- whether empty means false or means "not supplied" -- is answered
|
||||||
|
// "false" by that use, not left open.
|
||||||
bool is_recognized_predicate(const std::string& s)
|
bool is_recognized_predicate(const std::string& s)
|
||||||
{
|
{
|
||||||
return s.empty()
|
return s.empty()
|
||||||
@@ -157,25 +164,71 @@ bool is_recognized_predicate(const std::string& s)
|
|||||||
|| s == "False" || s == "false" || s == "0";
|
|| s == "False" || s == "false" || s == "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
void warn_unrecognized_predicate(const std::string& predicate, const Locator& loc)
|
void check_predicate(const std::string& predicate, const Locator& loc)
|
||||||
{
|
{
|
||||||
if (is_recognized_predicate(predicate)) return;
|
if (is_recognized_predicate(predicate)) return;
|
||||||
std::stringstream ss {};
|
std::stringstream ss {};
|
||||||
ss << "The @cond predicate " << q_(predicate)
|
ss << "The @cond predicate " << q_(predicate) << " is not a truth value. "
|
||||||
<< " is not a recognized truth value, so the false branch was taken.\n"
|
<< "Recognized: true, True, 1 (true); false, False, 0, and empty (false). "
|
||||||
<< " Recognized: true, True, 1 (true); false, False, 0, empty (false).";
|
<< "A value outside these is an error rather than false, so a misspelled "
|
||||||
warning(ss.str(), loc);
|
<< "variable or a failed @eval cannot silently select a branch.";
|
||||||
|
throw Argument_error(ss.str(), loc);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Machine::process_cond_katoms(katom_list& katoms)
|
// Mark the interior of every @cond span INERT. Runs during process_katoms,
|
||||||
|
// before the passes with observable effects, so that @eval and @read inside a
|
||||||
|
// branch do nothing until a branch is selected -- which is the non-strictness
|
||||||
|
// doc/cond_evaluation_order.md already specifies ("with side-effecting
|
||||||
|
// @read/@eval, wrong ... must not read the missing file") and which @eval did
|
||||||
|
// not honour: an @eval in a discarded branch used to run, because eval swept
|
||||||
|
// the list before cond did.
|
||||||
|
//
|
||||||
|
// The delimiters themselves stay unmarked, so the span is still found later.
|
||||||
|
// Nested @cond spans are marked by the enclosing one and become live only when
|
||||||
|
// the branch holding them is selected and processed.
|
||||||
|
void Machine::mark_cond_content(katom_list& katoms)
|
||||||
{
|
{
|
||||||
|
(void)K::log(4);
|
||||||
|
if (std::find_if(katoms.begin(), katoms.end(), begin_cond) == katoms.end()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const auto& [op, cl] : find_spans(katoms, level_increase, level_decrease, true, "cond")) {
|
||||||
|
auto [begin, end] = find_span_katoms(katoms, op, cl);
|
||||||
|
if (!begin_cond(*begin)) continue;
|
||||||
|
// Only the BRANCHES are inert. The predicate is always evaluated --
|
||||||
|
// that is what a conditional is -- so marking from "begin + 1" would
|
||||||
|
// stop "@cond @eval 1==1 @ | yes | no @" from ever computing its own
|
||||||
|
// predicate. Mark from the first depth-0 bar onward.
|
||||||
|
std::vector<katom_iter> bars = cond_separator_bars(begin, end);
|
||||||
|
if (bars.empty()) continue; // malformed; reported when it resolves
|
||||||
|
for (auto k = bars[0] + 1; k < end - 1; ++k) {
|
||||||
|
k->m_deferred = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the @cond spans in `katoms`, at APPLICATION time. Returns the number
|
||||||
|
// resolved, so the caller's fixed point accounts for them.
|
||||||
|
//
|
||||||
|
// The selected branch is spliced and then processed exactly as a klammer body
|
||||||
|
// is (process_katoms + apply, the two lines apply_klammer already uses): that
|
||||||
|
// is what makes the document behave like a function body whose state variables
|
||||||
|
// are its arguments -- they are bound by the substitution at the top of
|
||||||
|
// Machine::apply, BEFORE any conditional in the document is decided. Resolving
|
||||||
|
// @cond at read time meant a top-level "@cond *Flag*" saw the literal "*Flag*".
|
||||||
|
int Machine::resolve_cond_katoms(katom_list& katoms, const std::string& target)
|
||||||
|
{
|
||||||
|
int resolved = 0;
|
||||||
if (std::find_if(katoms.begin(), katoms.end(), begin_cond) != katoms.end()) {
|
if (std::find_if(katoms.begin(), katoms.end(), begin_cond) != katoms.end()) {
|
||||||
(void)K::log(3);
|
(void)K::log(3);
|
||||||
//for (auto [op, cl] : find_spans(katoms, begin_cond, end_apply, true, "cond")) {
|
|
||||||
for (const auto& [op, cl] : find_spans(katoms, level_increase, level_decrease, true, "cond")) {
|
for (const auto& [op, cl] : find_spans(katoms, level_increase, level_decrease, true, "cond")) {
|
||||||
auto [begin, end] = find_span_katoms(katoms, op, cl);
|
auto [begin, end] = find_span_katoms(katoms, op, cl);
|
||||||
// msg() << "find_spans: " << std::pair(begin, end) << "\n";
|
|
||||||
if (begin_cond(*begin)) {
|
if (begin_cond(*begin)) {
|
||||||
|
// A @cond nested inside an unresolved outer @cond is still
|
||||||
|
// inert; the outer one will process it when its branch is
|
||||||
|
// selected. Without this an inner branch would be decided
|
||||||
|
// before it is known whether it is reached at all.
|
||||||
|
if (begin->m_deferred) continue;
|
||||||
// Delimit @cond's arguments by the bars at depth 0 within the
|
// Delimit @cond's arguments by the bars at depth 0 within the
|
||||||
// span, so that bars belonging to nested klammers are not
|
// span, so that bars belonging to nested klammers are not
|
||||||
// mistaken for @cond's own separators (see cond_separator_bars).
|
// mistaken for @cond's own separators (see cond_separator_bars).
|
||||||
@@ -183,7 +236,7 @@ void Machine::process_cond_katoms(katom_list& katoms)
|
|||||||
check_bar_count(begin, bars.size());
|
check_bar_count(begin, bars.size());
|
||||||
auto bar_1 = bars[0];
|
auto bar_1 = bars[0];
|
||||||
std::string predicate = to_string(begin + 1, bar_1, true);
|
std::string predicate = to_string(begin + 1, bar_1, true);
|
||||||
warn_unrecognized_predicate(predicate, begin->m_loc);
|
check_predicate(predicate, begin->m_loc);
|
||||||
katom_list true_clause {};
|
katom_list true_clause {};
|
||||||
katom_list false_clause {};
|
katom_list false_clause {};
|
||||||
if (bars.size() == 2) {
|
if (bars.size() == 2) {
|
||||||
@@ -199,11 +252,22 @@ void Machine::process_cond_katoms(katom_list& katoms)
|
|||||||
// (@cond is a non-strict special form).
|
// (@cond is a non-strict special form).
|
||||||
katom_list result = is_true(predicate)
|
katom_list result = is_true(predicate)
|
||||||
? trim_whitespace(true_clause) : trim_whitespace(false_clause);
|
? trim_whitespace(true_clause) : trim_whitespace(false_clause);
|
||||||
|
// The selected branch becomes live: clear the inert flag and
|
||||||
|
// give it the same processing a klammer body gets. The
|
||||||
|
// unselected branch is discarded still inert, so nothing in it
|
||||||
|
// ever ran.
|
||||||
|
for (Katom& k : result) {
|
||||||
|
k.m_deferred = false;
|
||||||
|
}
|
||||||
|
process_katoms(result, command_name);
|
||||||
|
apply(m_klammers, result, target);
|
||||||
std::for_each(begin, end, mark_as_replaced);
|
std::for_each(begin, end, mark_as_replaced);
|
||||||
katoms.insert(end, result.begin(), result.end());
|
katoms.insert(end, result.begin(), result.end());
|
||||||
|
++resolved;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -339,8 +403,15 @@ void Machine::process_katoms(
|
|||||||
if (ignore) mark_ignored_katoms(katoms);
|
if (ignore) mark_ignored_katoms(katoms);
|
||||||
if (whitespace) process_whitespace_modifiers(katoms);
|
if (whitespace) process_whitespace_modifiers(katoms);
|
||||||
if (klammers) process_klammer_katoms(katoms);
|
if (klammers) process_klammer_katoms(katoms);
|
||||||
|
// @cond is no longer RESOLVED here -- it is resolved in the apply fold
|
||||||
|
// (see mark_cond_content / resolve_cond_katoms). What happens here is the
|
||||||
|
// marking that makes its branches inert, and it must run BEFORE the eval
|
||||||
|
// and read passes, which are the ones with observable effects. Leaving it
|
||||||
|
// where process_cond_katoms used to sit -- after eval -- kept the old
|
||||||
|
// "eval in a discarded branch runs anyway" behaviour, since the marking
|
||||||
|
// arrived too late to stop it.
|
||||||
|
if (cond) mark_cond_content(katoms);
|
||||||
if (eval) process_eval_katoms(katoms);
|
if (eval) process_eval_katoms(katoms);
|
||||||
if (cond) process_cond_katoms(katoms);
|
|
||||||
if (read) expand_read_katoms(
|
if (read) expand_read_katoms(
|
||||||
katoms, source,
|
katoms, source,
|
||||||
nonascii, literal, ignore, whitespace, klammers, eval, cond, read);
|
nonascii, literal, ignore, whitespace, klammers, eval, cond, read);
|
||||||
@@ -402,6 +473,8 @@ void Machine::expand_read_katoms(
|
|||||||
(void)K::log(3);
|
(void)K::log(3);
|
||||||
for (const auto& [op, cl] : find_spans(katoms, begin_apply, end_apply, true, "read")) {
|
for (const auto& [op, cl] : find_spans(katoms, begin_apply, end_apply, true, "read")) {
|
||||||
auto [begin, end] = find_span_katoms(katoms, op, cl);
|
auto [begin, end] = find_span_katoms(katoms, op, cl);
|
||||||
|
// Inert: see the same guard in process_eval_katoms.
|
||||||
|
if (begin->m_deferred) continue;
|
||||||
if (begin_read(*begin)) {
|
if (begin_read(*begin)) {
|
||||||
std::string read_filename = to_string(begin + 1, end - 1, true);
|
std::string read_filename = to_string(begin + 1, end - 1, true);
|
||||||
|
|
||||||
@@ -441,7 +514,27 @@ void Machine::expand_read_katoms(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void Machine::extract_machine_definitions()
|
// One @@@ declaration. Factored out so the tolerant and strict paths share
|
||||||
|
// it; `rescan` is set when a klammerset has spliced files into the stream,
|
||||||
|
// which invalidates the caller's span list.
|
||||||
|
void Machine::add_machine_definition(
|
||||||
|
const std::string& name, katom_iter begin, katom_iter end, bool& rescan)
|
||||||
|
{
|
||||||
|
if (name == "@@@target") {
|
||||||
|
m_targets.add(begin, end, m_katoms);
|
||||||
|
} else if (name == "@@@argtype") {
|
||||||
|
m_argtypes.add(begin, end, m_katoms);
|
||||||
|
} else if (name == "@@@state") {
|
||||||
|
m_state.parse_state_katoms(begin, end, m_katoms);
|
||||||
|
} else if (name == "@@@klammerset") {
|
||||||
|
if (auto klammerset = m_klammersets.add(begin, end, m_katoms)) {
|
||||||
|
load_klammerset_files(*klammerset, end);
|
||||||
|
rescan = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Machine::extract_machine_definitions(bool tolerant)
|
||||||
{
|
{
|
||||||
(void)K::log(3);
|
(void)K::log(3);
|
||||||
if (m_katoms.empty()) {
|
if (m_katoms.empty()) {
|
||||||
@@ -456,20 +549,23 @@ void Machine::extract_machine_definitions()
|
|||||||
rescan = false;
|
rescan = false;
|
||||||
for (const auto& [op, cl] : find_spans(m_katoms, begin_machine_def, end_machine_def, true, command_name)) {
|
for (const auto& [op, cl] : find_spans(m_katoms, begin_machine_def, end_machine_def, true, command_name)) {
|
||||||
auto [begin, end] = find_span_katoms(m_katoms, op, cl);
|
auto [begin, end] = find_span_katoms(m_katoms, op, cl);
|
||||||
//std::string name = trim_char(begin->m_text, '@');
|
|
||||||
std::string name = begin->m_text;
|
std::string name = begin->m_text;
|
||||||
if (name == "@@@target") {
|
if (tolerant) {
|
||||||
m_targets.add(begin, end, m_katoms);
|
// kdiag: a declaration that cannot be carried out -- a
|
||||||
} else if (name == "@@@argtype") {
|
// @@@klammerset naming a file that is not there, say -- is
|
||||||
m_argtypes.add(begin, end, m_katoms);
|
// skipped rather than fatal. Its span stays unreplaced, so
|
||||||
} else if (name == "@@@state") {
|
// the katoms remain visible and report the failure themselves.
|
||||||
m_state.parse_state_katoms(begin, end, m_katoms);
|
try {
|
||||||
} else if (name == "@@@klammerset") {
|
add_machine_definition(name, begin, end, rescan);
|
||||||
if (auto klammerset = m_klammersets.add(begin, end, m_katoms)) {
|
} catch (Error& e) {
|
||||||
load_klammerset_files(*klammerset, end);
|
K::log(1, "Machine definition not registered: " + e.m_desc);
|
||||||
rescan = true;
|
continue;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
add_machine_definition(name, begin, end, rescan);
|
||||||
|
}
|
||||||
|
if (rescan) {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -491,17 +587,39 @@ void Machine::load_klammerset_files(const Klammerset& klammerset, katom_iter ins
|
|||||||
// entries are always filenames (this set's own definition files).
|
// entries are always filenames (this set's own definition files).
|
||||||
std::vector<std::string> filenames {};
|
std::vector<std::string> filenames {};
|
||||||
for (const auto& required : klammerset.m_requires) {
|
for (const auto& required : klammerset.m_requires) {
|
||||||
if (is_klammerset_symbol(required)) {
|
std::string path = is_klammerset_symbol(required)
|
||||||
filenames.push_back(
|
? resolve_klammerset_symbol(required, base_dir, klammerset.m_loc).string()
|
||||||
resolve_klammerset_symbol(required, base_dir, klammerset.m_loc).string());
|
: required;
|
||||||
} else {
|
// ALREADY LOADED? Decided BEFORE the file is opened. The guard used
|
||||||
filenames.push_back(required);
|
// to sit in Klammerset_registry::add, which only runs once the file has
|
||||||
|
// been read and its declaration reached -- by which time the declaring
|
||||||
|
// file's OWN definitions have been re-executed. Two sets requiring a
|
||||||
|
// third therefore died on "Target ... is already defined", pointing at
|
||||||
|
// a line the author wrote once.
|
||||||
|
//
|
||||||
|
// Deciding it here is possible only because a klammerset symbol X is
|
||||||
|
// declared in X/X.k, so the symbol is a function of the path: it is
|
||||||
|
// known for a ":requires" written as a filename just as much as for one
|
||||||
|
// written as a symbol. That is what the X/X.k requirement bought.
|
||||||
|
std::string symbol = is_klammerset_symbol(required)
|
||||||
|
? required : fs::path(path).stem().string();
|
||||||
|
if (m_klammersets.has(symbol)) {
|
||||||
|
(void)K::log(1, "Klammerset \"" + symbol + "\": already loaded, skipped");
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
filenames.push_back(path);
|
||||||
}
|
}
|
||||||
filenames.insert(filenames.end(), klammerset.m_files.begin(), klammerset.m_files.end());
|
filenames.insert(filenames.end(), klammerset.m_files.begin(), klammerset.m_files.end());
|
||||||
|
|
||||||
// Collect all files into one list and insert once: insert_at is
|
// Collect all files into one list and insert once: insert_at is
|
||||||
// invalidated by the first insertion into m_katoms.
|
// invalidated by the first insertion into m_katoms.
|
||||||
|
// Recorded on the set: what it actually opened, canonical, so a display
|
||||||
|
// can tell its klammers from the input's. The declaring file counts --
|
||||||
|
// definitions may sit before or after the declaration in it.
|
||||||
|
strings_t loaded_files {};
|
||||||
|
if (fs::exists(declaring)) {
|
||||||
|
loaded_files.push_back(fs::canonical(declaring).string());
|
||||||
|
}
|
||||||
katom_list loaded {};
|
katom_list loaded {};
|
||||||
for (const auto& filename : filenames) {
|
for (const auto& filename : filenames) {
|
||||||
fs::path pathname = resolve_relative_to(filename, base);
|
fs::path pathname = resolve_relative_to(filename, base);
|
||||||
@@ -512,6 +630,7 @@ void Machine::load_klammerset_files(const Klammerset& klammerset, katom_iter ins
|
|||||||
klammerset.m_loc);
|
klammerset.m_loc);
|
||||||
}
|
}
|
||||||
pathname = fs::canonical(pathname);
|
pathname = fs::canonical(pathname);
|
||||||
|
loaded_files.push_back(pathname.string());
|
||||||
m_state.add_search_dir(pathname.parent_path().string());
|
m_state.add_search_dir(pathname.parent_path().string());
|
||||||
std::string text = trim_right(string_from_file(pathname.string()));
|
std::string text = trim_right(string_from_file(pathname.string()));
|
||||||
katom_list ks = katomize(line_split(text), pathname);
|
katom_list ks = katomize(line_split(text), pathname);
|
||||||
@@ -519,6 +638,9 @@ void Machine::load_klammerset_files(const Klammerset& klammerset, katom_iter ins
|
|||||||
loaded.insert(loaded.end(), ks.begin(), ks.end());
|
loaded.insert(loaded.end(), ks.begin(), ks.end());
|
||||||
}
|
}
|
||||||
m_katoms.insert(insert_at, loaded.begin(), loaded.end());
|
m_katoms.insert(insert_at, loaded.begin(), loaded.end());
|
||||||
|
// The registry holds the registered copy; the parameter is a const
|
||||||
|
// reference to it, so the record goes back through the registry.
|
||||||
|
m_klammersets.set_loaded_files(klammerset.m_symbol, loaded_files);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register one "@@...@@" definition. An ".o" target declares an option set
|
// Register one "@@...@@" definition. An ".o" target declares an option set
|
||||||
@@ -541,19 +663,29 @@ void Machine::add_definition(katom_list& katoms, const Katom& op, const Katom& c
|
|||||||
|
|
||||||
void Machine::extract_klammer_definitions(katom_list katoms)
|
void Machine::extract_klammer_definitions(katom_list katoms)
|
||||||
{
|
{
|
||||||
fmsg() << katoms << "\n";
|
// fmsg() << katoms << "\n";
|
||||||
(void)K::log(3);
|
(void)K::log(3, katoms);
|
||||||
for (const auto& [op, cl] : find_spans(katoms, begin_klammer_def, end_klammer_def, true, command_name)) {
|
for (const auto& [op, cl] : find_spans(katoms, begin_klammer_def, end_klammer_def, true, command_name)) {
|
||||||
add_definition(katoms, op, cl);
|
add_definition(katoms, op, cl);
|
||||||
}
|
}
|
||||||
m_klammers.rationalize(m_targets);
|
m_klammers.rationalize(m_targets);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Machine::extract_klammer_definitions()
|
void Machine::extract_klammer_definitions(bool tolerant)
|
||||||
{
|
{
|
||||||
(void)K::log(3);
|
(void)K::log(3);
|
||||||
for (const auto& [op, cl] : find_spans(m_katoms, begin_klammer_def, end_klammer_def, true, command_name)) {
|
for (const auto& [op, cl] : find_spans(m_katoms, begin_klammer_def, end_klammer_def, true, command_name)) {
|
||||||
add_definition(m_katoms, op, cl);
|
if (tolerant) {
|
||||||
|
// add_definition marks the span replaced only on success, so a
|
||||||
|
// skipped definition keeps its katoms and stays visible.
|
||||||
|
try {
|
||||||
|
add_definition(m_katoms, op, cl);
|
||||||
|
} catch (Error& e) {
|
||||||
|
K::log(1, "Definition not registered: " + e.m_desc);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
add_definition(m_katoms, op, cl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
m_klammers.rationalize(m_targets);
|
m_klammers.rationalize(m_targets);
|
||||||
}
|
}
|
||||||
@@ -677,7 +809,12 @@ int Machine::apply(
|
|||||||
Klammer_registry& klammer_registry, katom_list& katoms, const std::string& target)
|
Klammer_registry& klammer_registry, katom_list& katoms, const std::string& target)
|
||||||
{
|
{
|
||||||
(void)K::log(3, "Klammer_registry");
|
(void)K::log(3, "Klammer_registry");
|
||||||
int applied = 0;
|
// @cond is a special form handled here rather than by the klammer loop
|
||||||
|
// below: its span head is a cond_begin, which begin_klammer_apply does not
|
||||||
|
// match. Resolving first means a klammer revealed by the selected branch
|
||||||
|
// is applied in this same pass. The count is returned with the klammer
|
||||||
|
// applications, so the caller's fixed point iterates while either happens.
|
||||||
|
int applied = resolve_cond_katoms(katoms, target);
|
||||||
for (const auto& [op, cl] : find_spans(
|
for (const auto& [op, cl] : find_spans(
|
||||||
katoms, begin_klammer_apply, end_klammer_apply, true, command_name)) {
|
katoms, begin_klammer_apply, end_klammer_apply, true, command_name)) {
|
||||||
auto [begin, end] = find_span_katoms(katoms, op, cl);
|
auto [begin, end] = find_span_katoms(katoms, op, cl);
|
||||||
|
|||||||
@@ -46,7 +46,14 @@ public:
|
|||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
void process_cond_katoms(std::vector<Katom>& katoms);
|
// @cond is resolved at APPLICATION time, not read time (Andy, 2026-08-15,
|
||||||
|
// deciding notes/Klammertext_improvements.md §4.2). mark_cond_content()
|
||||||
|
// runs during process_katoms and makes every branch inert;
|
||||||
|
// resolve_cond_katoms() runs inside the apply fold, selects a branch, and
|
||||||
|
// processes only that one. The document then behaves like a klammer body:
|
||||||
|
// its state variables are bound before its conditionals are decided.
|
||||||
|
void mark_cond_content(std::vector<Katom>& katoms);
|
||||||
|
int resolve_cond_katoms(std::vector<Katom>& katoms, const std::string& target);
|
||||||
void process_eval_katoms(std::vector<Katom>& katoms);
|
void process_eval_katoms(std::vector<Katom>& katoms);
|
||||||
void mark_literal_klammer_content(std::vector<Katom>& katoms);
|
void mark_literal_klammer_content(std::vector<Katom>& katoms);
|
||||||
void escape_target_characters(const Target& target, std::vector<Katom>& katoms);
|
void escape_target_characters(const Target& target, std::vector<Katom>& katoms);
|
||||||
@@ -73,8 +80,19 @@ public:
|
|||||||
void expand_constant_klammers(katom_list& katoms, const Katom& op, const Katom& cl);
|
void expand_constant_klammers(katom_list& katoms, const Katom& op, const Katom& cl);
|
||||||
void add_definition(katom_list& katoms, const Katom& op, const Katom& cl);
|
void add_definition(katom_list& katoms, const Katom& op, const Katom& cl);
|
||||||
void load_klammerset_files(const Klammerset& klammerset, katom_iter insert_at);
|
void load_klammerset_files(const Klammerset& klammerset, katom_iter insert_at);
|
||||||
void extract_machine_definitions();
|
// `tolerant` is for kdiag ONLY. It dissects the structure and typing of
|
||||||
void extract_klammer_definitions();
|
// the katom list, and allows modes of processing that would be errors in
|
||||||
|
// ktext: run one definition tier without the other and a definition may
|
||||||
|
// be unregisterable (a klammer names a target that "--system" would have
|
||||||
|
// registered). Tolerant then SKIPS that definition instead of throwing,
|
||||||
|
// leaving its katoms unconsumed and therefore visible -- which is the
|
||||||
|
// report. ktext must keep throwing: there, a document naming an
|
||||||
|
// undefined target is a genuine error, and skipping it silently would
|
||||||
|
// render a wrong document.
|
||||||
|
void add_machine_definition(const std::string& name, katom_iter begin,
|
||||||
|
katom_iter end, bool& rescan);
|
||||||
|
void extract_machine_definitions(bool tolerant = false);
|
||||||
|
void extract_klammer_definitions(bool tolerant = false);
|
||||||
void extract_klammer_definitions(katom_list katoms);
|
void extract_klammer_definitions(katom_list katoms);
|
||||||
void update_state(const std::map<std::string, std::string>& arg_map);
|
void update_state(const std::map<std::string, std::string>& arg_map);
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ void Option_set_registry::add(
|
|||||||
throw Definition_error(message, begin->m_loc);
|
throw Definition_error(message, begin->m_loc);
|
||||||
}
|
}
|
||||||
if (result.warn) {
|
if (result.warn) {
|
||||||
warning(message, begin->m_loc);
|
// Reported, not warned: see the same case in klammer_registry.cpp.
|
||||||
|
(void)K::log(1, message + " at " + begin->m_loc.desc());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,11 +138,15 @@ std::string Option_set_registry::available() const
|
|||||||
return m_names.empty() ? "(none are declared)" : join(m_names, ", ");
|
return m_names.empty() ? "(none are declared)" : join(m_names, ", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Option_set_registry::describe(int margin) const
|
std::string Option_set_registry::describe(int margin, const strings_t& defined_outside) const
|
||||||
{
|
{
|
||||||
std::string result {};
|
std::string result {};
|
||||||
for (const auto& name : m_names) {
|
for (const auto& name : m_names) {
|
||||||
result += m_option_sets.at(name).describe(margin) + "\n";
|
const Option_set& set = m_option_sets.at(name);
|
||||||
|
if (!defined_outside.empty() && is_in(set.m_loc.m_filename, defined_outside)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result += set.describe(margin) + "\n";
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ public:
|
|||||||
const Option_set& get(const std::string& name, const Locator& loc) const;
|
const Option_set& get(const std::string& name, const Locator& loc) const;
|
||||||
void add_user(const std::string& set_name, const std::string& klammer_name);
|
void add_user(const std::string& set_name, const std::string& klammer_name);
|
||||||
|
|
||||||
std::string describe(int margin = 2) const;
|
// `defined_outside`: list only sets declared in a file not in the list --
|
||||||
|
// the input's own, with the loaded klammersets' left out. See
|
||||||
|
// Klammer::defined_outside.
|
||||||
|
std::string describe(int margin = 2, const strings_t& defined_outside = {}) const;
|
||||||
// "a, b, c" -- the sets that exist, for a diagnostic about one that does not.
|
// "a, b, c" -- the sets that exist, for a diagnostic about one that does not.
|
||||||
std::string available() const;
|
std::string available() const;
|
||||||
|
|
||||||
|
|||||||
34
mac/show.cpp
34
mac/show.cpp
@@ -1,5 +1,8 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
#include <iterator>
|
#include <iterator>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
#include "ktype.h"
|
#include "ktype.h"
|
||||||
#include "show.h"
|
#include "show.h"
|
||||||
@@ -7,6 +10,37 @@
|
|||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "file.h"
|
#include "file.h"
|
||||||
|
|
||||||
|
bool color_enabled(const std::ostream& os)
|
||||||
|
{
|
||||||
|
// Function-local statics: isatty is asked once per descriptor, on first
|
||||||
|
// use, so there is no static-initialization-order question and no syscall
|
||||||
|
// per colour written.
|
||||||
|
static const bool no_color = [] {
|
||||||
|
const char* v = std::getenv("NO_COLOR");
|
||||||
|
return v != nullptr && v[0] != '\0';
|
||||||
|
}();
|
||||||
|
static const bool tty_out = isatty(fileno(stdout)) != 0;
|
||||||
|
static const bool tty_err = isatty(fileno(stderr)) != 0;
|
||||||
|
if (no_color) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (&os == &std::cout) {
|
||||||
|
return tty_out;
|
||||||
|
}
|
||||||
|
if (&os == &std::cerr || &os == &std::clog) {
|
||||||
|
return tty_err;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ostream& operator<<(std::ostream& os, const Color& color)
|
||||||
|
{
|
||||||
|
if (color_enabled(os)) {
|
||||||
|
os << color.code();
|
||||||
|
}
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
std::ostream& msg(Locator loc)
|
std::ostream& msg(Locator loc)
|
||||||
{
|
{
|
||||||
std::cout << blue << loc.abbrev(false) << black << " ";
|
std::cout << blue << loc.abbrev(false) << black << " ";
|
||||||
|
|||||||
51
mac/show.h
51
mac/show.h
@@ -30,17 +30,46 @@ const std::string left_square_bracket { "\u2045" };
|
|||||||
const std::string right_square_bracket { "\u2046" };
|
const std::string right_square_bracket { "\u2046" };
|
||||||
const std::string check { "\u2713" };
|
const std::string check { "\u2713" };
|
||||||
|
|
||||||
const std::string black("\033[0;30m");
|
// A colour is a STREAM-AWARE object, not a string, because the same name is
|
||||||
const std::string boldblack("\033[1;30m");
|
// written to two streams whose destinations differ: "ktext -d > doc.txt" has a
|
||||||
const std::string green("\033[0;32m");
|
// file on stdout and a terminal on stderr at the same time, so one global
|
||||||
const std::string boldgreen("\033[1;32m");
|
// on/off cannot be right for both. Inserting a Color emits its escape
|
||||||
const std::string cyan("\033[0;36m");
|
// sequence only when THAT stream is a terminal, so escape codes can never
|
||||||
const std::string blue("\033[0;34m");
|
// reach a redirected document, a pipe, or a captured log.
|
||||||
const std::string boldblue("\033[1;34m");
|
//
|
||||||
const std::string magenta("\033[0;35m");
|
// Streams other than std::cout/std::cerr -- a stringstream building an error
|
||||||
const std::string red("\033[31m");
|
// message, a file -- are never coloured. That is the conservative reading and
|
||||||
const std::string yellow("\033[0;33m");
|
// it also stops escapes from being baked into strings whose length is then
|
||||||
const std::string reset("\033[0m");
|
// measured (abbreviate() in util.cpp counted them).
|
||||||
|
class Color {
|
||||||
|
public:
|
||||||
|
explicit Color(const std::string& code) : m_code(code) {}
|
||||||
|
// The raw sequence, for the rare place that needs a string rather than an
|
||||||
|
// insertion. Prefer inserting the Color: this form cannot be suppressed.
|
||||||
|
const std::string& code() const { return m_code; }
|
||||||
|
private:
|
||||||
|
std::string m_code {};
|
||||||
|
};
|
||||||
|
|
||||||
|
// True when colour should be emitted on this stream: it is a terminal and the
|
||||||
|
// NO_COLOR environment variable is unset. NO_COLOR (https://no-color.org) is
|
||||||
|
// honoured for ANY non-empty value, which is what the convention specifies; it
|
||||||
|
// is the escape hatch for the case isatty cannot see, such as a pager or a CI
|
||||||
|
// capture that does want, or does not want, colour.
|
||||||
|
bool color_enabled(const std::ostream& os);
|
||||||
|
std::ostream& operator<<(std::ostream& os, const Color& color);
|
||||||
|
|
||||||
|
const Color black("\033[0;30m");
|
||||||
|
const Color boldblack("\033[1;30m");
|
||||||
|
const Color green("\033[0;32m");
|
||||||
|
const Color boldgreen("\033[1;32m");
|
||||||
|
const Color cyan("\033[0;36m");
|
||||||
|
const Color blue("\033[0;34m");
|
||||||
|
const Color boldblue("\033[1;34m");
|
||||||
|
const Color magenta("\033[0;35m");
|
||||||
|
const Color red("\033[31m");
|
||||||
|
const Color yellow("\033[0;33m");
|
||||||
|
const Color reset("\033[0m");
|
||||||
|
|
||||||
const auto seqout = [](auto x) { std::cout << "seq: " << x << "\n"; };
|
const auto seqout = [](auto x) { std::cout << "seq: " << x << "\n"; };
|
||||||
//const auto mapout = [](auto m) { auto const& key std::cout << m.first << sp_arrow << m.second << "\n"; };
|
//const auto mapout = [](auto m) { auto const& key std::cout << m.first << sp_arrow << m.second << "\n"; };
|
||||||
|
|||||||
@@ -136,8 +136,19 @@ Var State::get(const std::string& name, bool error_if_not_defined, const Locator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (error_if_not_defined) {
|
if (error_if_not_defined) {
|
||||||
msg() << describe();
|
// Which frames were searched belongs IN the error, not printed beside
|
||||||
throw Argument_error("Variable " + q_(name) + " not defined", loc);
|
// it. This was a msg() dumping the whole state to stdout before the
|
||||||
|
// throw -- scaffolding, on the wrong stream, and separated from the
|
||||||
|
// message it was explaining. The frame NAMES are the useful part: a
|
||||||
|
// variable missing because the expected frame was never opened looks
|
||||||
|
// exactly like one that was never set.
|
||||||
|
strings_t frame_names {};
|
||||||
|
for (const auto& f : m_frames) {
|
||||||
|
frame_names.push_back(f.m_name);
|
||||||
|
}
|
||||||
|
throw Argument_error(
|
||||||
|
"Variable " + q_(name) + " not defined (searched: "
|
||||||
|
+ join(frame_names, ", ") + ")", loc);
|
||||||
} else {
|
} else {
|
||||||
return Var();
|
return Var();
|
||||||
}
|
}
|
||||||
@@ -187,7 +198,7 @@ void State::subst(katom_iter begin, katom_iter end)
|
|||||||
//auto [var, found] = get(match[1]);
|
//auto [var, found] = get(match[1]);
|
||||||
auto var_value = value(match[2], true, begin->m_loc);
|
auto var_value = value(match[2], true, begin->m_loc);
|
||||||
if (var_value != klammerstate::no_value) {
|
if (var_value != klammerstate::no_value) {
|
||||||
msg() << "Found subst: " << match[1] << sp_arrow << var_value << "\n";
|
(void)K::log(3, "Found subst:", match[1].str(), var_value);
|
||||||
std::stringstream ss {};
|
std::stringstream ss {};
|
||||||
ss << match[1] << var_value << match[3];
|
ss << match[1] << var_value << match[3];
|
||||||
ki->m_text = ss.str(); // match[1] + var_value + match[3];
|
ki->m_text = ss.str(); // match[1] + var_value + match[3];
|
||||||
|
|||||||
@@ -16,12 +16,15 @@ std::string Target_registry::optionset_name = "o";
|
|||||||
Target_registry::Target_registry()
|
Target_registry::Target_registry()
|
||||||
: m_parameters(Parameter_set("name | desc :after_apply :after_write :includes :escape | transforms.rest"))
|
: m_parameters(Parameter_set("name | desc :after_apply :after_write :includes :escape | transforms.rest"))
|
||||||
{
|
{
|
||||||
|
// Registration order is display order. The two that declare an interface
|
||||||
|
// come first -- "k" a klammer's, "o" an option set's -- and then "*", the
|
||||||
|
// targets themselves beginning after it.
|
||||||
Target k(declare_name, "Description of parameters and klammer result", Locator());
|
Target k(declare_name, "Description of parameters and klammer result", Locator());
|
||||||
Target general(general_name, "Every target: written \".*\" to declare a klammer works for all of them, or implied when a definition names no target", Locator());
|
|
||||||
Target option_set(optionset_name, "Declaration of an option set: parameters shared by klammers", Locator());
|
Target option_set(optionset_name, "Declaration of an option set: parameters shared by klammers", Locator());
|
||||||
|
Target general(general_name, "All targets", Locator());
|
||||||
add(k);
|
add(k);
|
||||||
add(general);
|
|
||||||
add(option_set);
|
add(option_set);
|
||||||
|
add(general);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Target_registry::add(Target target)
|
void Target_registry::add(Target target)
|
||||||
@@ -122,8 +125,18 @@ std::vector<std::string> Target_registry::applicable() const
|
|||||||
&& name != Target_registry::optionset_name; });
|
&& name != Target_registry::optionset_name; });
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Target_registry::describe(int margin, bool long_format) const
|
std::string Target_registry::describe(int margin, bool long_format,
|
||||||
|
const strings_t& defined_outside) const
|
||||||
{
|
{
|
||||||
|
// The built-in pseudo-targets belong to the language rather than to any
|
||||||
|
// klammerset, so a filtered listing keeps them.
|
||||||
|
auto shown = [&](const std::string& name) {
|
||||||
|
if (defined_outside.empty()) return true;
|
||||||
|
if (name == declare_name || name == general_name || name == optionset_name) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !is_in(m_targets.at(name).m_loc.m_filename, defined_outside);
|
||||||
|
};
|
||||||
std::string tab(margin, ' ');
|
std::string tab(margin, ' ');
|
||||||
std::stringstream ss {};
|
std::stringstream ss {};
|
||||||
std::vector<std::string> descs {};
|
std::vector<std::string> descs {};
|
||||||
@@ -133,6 +146,7 @@ std::string Target_registry::describe(int margin, bool long_format) const
|
|||||||
auto name_width = max_length(m_names);
|
auto name_width = max_length(m_names);
|
||||||
auto desc_width = max_length(descs);
|
auto desc_width = max_length(descs);
|
||||||
for (const std::string& name : m_names) {
|
for (const std::string& name : m_names) {
|
||||||
|
if (!shown(name)) continue;
|
||||||
const Target& t = m_targets.at(name);
|
const Target& t = m_targets.at(name);
|
||||||
if (long_format) {
|
if (long_format) {
|
||||||
ss << tab << std::setfill(' ') << std::setw(name_width) << std::right << name << " "
|
ss << tab << std::setfill(' ') << std::setw(name_width) << std::right << name << " "
|
||||||
|
|||||||
@@ -38,7 +38,11 @@ public:
|
|||||||
std::vector<std::string> user_defined() const;
|
std::vector<std::string> user_defined() const;
|
||||||
std::vector<std::string> applicable() const;
|
std::vector<std::string> applicable() const;
|
||||||
|
|
||||||
std::string describe(int margin=2, bool long_format=false) const;
|
// `defined_outside`: list only targets declared in a file not in the list.
|
||||||
|
// The three pseudo-targets (k, o, *) are built in and always shown -- they
|
||||||
|
// are the language's, not any klammerset's.
|
||||||
|
std::string describe(int margin=2, bool long_format=false,
|
||||||
|
const strings_t& defined_outside = {}) const;
|
||||||
|
|
||||||
//Argtype_registry m_argtypes {};
|
//Argtype_registry m_argtypes {};
|
||||||
std::map<std::string, Target> m_targets {};
|
std::map<std::string, Target> m_targets {};
|
||||||
|
|||||||
12
mac/util.cpp
12
mac/util.cpp
@@ -114,7 +114,7 @@ std::string regex_escape(const std::string& s)
|
|||||||
strings_t regex_split(const std::string& s, const std::regex& re, bool trim_parts)
|
strings_t regex_split(const std::string& s, const std::regex& re, bool trim_parts)
|
||||||
{
|
{
|
||||||
strings_t result = {};
|
strings_t result = {};
|
||||||
if (s.size() == 0) {
|
if (s.empty()) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
auto it = std::sregex_token_iterator(s.begin(), s.end(), re, -1);
|
auto it = std::sregex_token_iterator(s.begin(), s.end(), re, -1);
|
||||||
@@ -419,8 +419,14 @@ std::string abbrev(const std::string& s, unsigned int max_length, bool remove_ne
|
|||||||
result = std::regex_replace(result, std::regex("\n"), broken_bar);
|
result = std::regex_replace(result, std::regex("\n"), broken_bar);
|
||||||
}
|
}
|
||||||
int suffix_size = 8;
|
int suffix_size = 8;
|
||||||
std::string ellipsis { red + "[...]" + black };
|
// Plain, deliberately. This string is spliced into another and shown
|
||||||
int prefix_end = max_length - suffix_size - ellipsis.size();
|
// later, on a stream this function cannot know -- so a colour baked in
|
||||||
|
// here could reach a pipe or a file. It also fixes an arithmetic
|
||||||
|
// error that the colours caused: ellipsis.size() counted the 14
|
||||||
|
// characters of the two escape sequences as visible width, so the
|
||||||
|
// abbreviation was cut 14 characters shorter than max_length asked.
|
||||||
|
std::string ellipsis { "[...]" };
|
||||||
|
int prefix_end = max_length - suffix_size - static_cast<int>(ellipsis.size());
|
||||||
result = result.replace(result.begin() + prefix_end,
|
result = result.replace(result.begin() + prefix_end,
|
||||||
result.end() - suffix_size,
|
result.end() - suffix_size,
|
||||||
ellipsis);
|
ellipsis);
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ def is_comment(s):
|
|||||||
return s.strip().startswith("//")
|
return s.strip().startswith("//")
|
||||||
|
|
||||||
def split_blocks(s):
|
def split_blocks(s):
|
||||||
kutil.msg()
|
|
||||||
blocks = []
|
blocks = []
|
||||||
in_code = True
|
in_code = True
|
||||||
block = ""
|
block = ""
|
||||||
@@ -53,7 +52,6 @@ def split_blocks(s):
|
|||||||
return blocks
|
return blocks
|
||||||
|
|
||||||
def get_lines(comment, code, n):
|
def get_lines(comment, code, n):
|
||||||
kutil.msg()
|
|
||||||
lines = code.split("\n")
|
lines = code.split("\n")
|
||||||
if len(lines) < n:
|
if len(lines) < n:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
@@ -64,7 +62,6 @@ def get_lines(comment, code, n):
|
|||||||
return ("\n".join(lines[:n]), "\n".join(lines[n:]))
|
return ("\n".join(lines[:n]), "\n".join(lines[n:]))
|
||||||
|
|
||||||
def parse_blocks(blocks):
|
def parse_blocks(blocks):
|
||||||
kutil.msg()
|
|
||||||
box_comment_rgx = re.compile("\\s*//(\\d+)\\s+.*", re.S)
|
box_comment_rgx = re.compile("\\s*//(\\d+)\\s+.*", re.S)
|
||||||
i = 0
|
i = 0
|
||||||
block_pairs = []
|
block_pairs = []
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ std::string document(Machine& machine)
|
|||||||
// width check (tex_width_check() in table.py) left in the xelatex log,
|
// width check (tex_width_check() in table.py) left in the xelatex log,
|
||||||
// followed by a short :column_width primer. Plain line scanning -- no
|
// followed by a short :column_width primer. Plain line scanning -- no
|
||||||
// std::regex over the (arbitrarily large) log text.
|
// std::regex over the (arbitrarily large) log text.
|
||||||
|
// One of the two surviving WARNINGS (see the output policy in CLAUDE.md; the
|
||||||
|
// other is warn_unparsed_katoms). It stays a warning because the judgment is
|
||||||
|
// a HEURISTIC and a question of layout QUALITY rather than well-formedness:
|
||||||
|
// the check carries a 2pt tolerance for exactly-full tables, and a document
|
||||||
|
// with one over-wide table still renders. Halting would turn a false positive
|
||||||
|
// into a blocker. Promote it to an error when the measurement is exact.
|
||||||
static void warn_wide_tables(const std::string& xelatex_log)
|
static void warn_wide_tables(const std::string& xelatex_log)
|
||||||
{
|
{
|
||||||
bool any = false;
|
bool any = false;
|
||||||
|
|||||||
@@ -149,12 +149,19 @@ fs::path parse_input_filename(const std::string& s, const std::string& input_dir
|
|||||||
// (K_input_dir), so a document renders identically wherever ktext is
|
// (K_input_dir), so a document renders identically wherever ktext is
|
||||||
// run from; then the legacy kt/ subdirectory; a name found in neither
|
// run from; then the legacy kt/ subdirectory; a name found in neither
|
||||||
// is returned as given (cwd-relative) and errors downstream.
|
// is returned as given (cwd-relative) and errors downstream.
|
||||||
|
// Which of the three a name landed on is a DERIVED value -- the ".kt" may
|
||||||
|
// have been supplied, and the directory certainly was -- so "-v 1" reports
|
||||||
|
// it. A ":files chapter1" that quietly found kt/chapter1.kt rather than
|
||||||
|
// the file beside the document is exactly what the author cannot see.
|
||||||
fs::path in_input_dir = fs::path(input_dir) / p;
|
fs::path in_input_dir = fs::path(input_dir) / p;
|
||||||
if (file_exists(in_input_dir.string())) {
|
if (file_exists(in_input_dir.string())) {
|
||||||
|
(void)K::log(1, "Input file \"" + s + "\": " + in_input_dir.string());
|
||||||
return in_input_dir;
|
return in_input_dir;
|
||||||
}
|
}
|
||||||
fs::path in_kt_dir = fs::path(input_dir) / "kt" / p;
|
fs::path in_kt_dir = fs::path(input_dir) / "kt" / p;
|
||||||
if (file_exists(in_kt_dir.string())) {
|
if (file_exists(in_kt_dir.string())) {
|
||||||
|
(void)K::log(1, "Input file \"" + s + "\": " + in_kt_dir.string()
|
||||||
|
+ " (found in the kt/ subdirectory)");
|
||||||
return in_kt_dir;
|
return in_kt_dir;
|
||||||
}
|
}
|
||||||
return p;
|
return p;
|
||||||
@@ -171,7 +178,10 @@ void write_file(const std::string& filename, const std::string& contents, bool w
|
|||||||
if (write_p) {
|
if (write_p) {
|
||||||
string_to_file(filename, contents);
|
string_to_file(filename, contents);
|
||||||
} else {
|
} else {
|
||||||
msg() << "Output filename: " << filename << "\n";
|
// Not writing: name the file that would have been written. A
|
||||||
|
// derived value, so "-v 1" -- and never on stdout, which carries
|
||||||
|
// the document itself.
|
||||||
|
(void)K::log(1, "Output filename:", filename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -190,7 +190,9 @@ std::string Document_class::create_html_output_directories()
|
|||||||
m_cache_dir = cache_directory("_html_pages");
|
m_cache_dir = cache_directory("_html_pages");
|
||||||
if (m_no_cache && fs::exists(m_cache_dir)) {
|
if (m_no_cache && fs::exists(m_cache_dir)) {
|
||||||
for (auto& entry : std::filesystem::directory_iterator(m_cache_dir)) {
|
for (auto& entry : std::filesystem::directory_iterator(m_cache_dir)) {
|
||||||
msg() << " Removing cache directory: " << entry << "\n";
|
// What ":cache false" actually removed: a derived consequence of
|
||||||
|
// an option, so "-v 1".
|
||||||
|
(void)K::log(1, "Removing cache directory:", entry.path().string());
|
||||||
std::filesystem::remove_all(entry);
|
std::filesystem::remove_all(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -696,12 +698,16 @@ std::string Document_class::make_single_html_page(const std::string& output_dire
|
|||||||
std::string Document_class::make_html_navigation_structure(
|
std::string Document_class::make_html_navigation_structure(
|
||||||
const std::string& output_directory, std::vector<Heading> headings)
|
const std::string& output_directory, std::vector<Heading> headings)
|
||||||
{
|
{
|
||||||
msg() << "Navigation format\n";
|
// Which structure path was taken is a DERIVED fact -- ":structure book"
|
||||||
|
// chose it -- so it belongs at "-v 1". It was a msg(), which printed it
|
||||||
|
// to stdout on every book render, ahead of the document.
|
||||||
|
(void)K::log(1, "Navigation format");
|
||||||
std::vector<std::string> pages_basenames {};
|
std::vector<std::string> pages_basenames {};
|
||||||
|
|
||||||
for (auto [basename, page_text] : m_file_components) {
|
for (auto [basename, page_text] : m_file_components) {
|
||||||
pages_basenames.push_back(basename);
|
pages_basenames.push_back(basename);
|
||||||
msg() << basename << ": " << abbrev(page_text, 96) << "\n";
|
// A trace of the work, not a decision: verbosity 3.
|
||||||
|
(void)K::log(3, basename + ": " + abbrev(page_text, 96));
|
||||||
}
|
}
|
||||||
std::string pages_basenames_filename = m_cache_dir + "/_pages_basenames.js";
|
std::string pages_basenames_filename = m_cache_dir + "/_pages_basenames.js";
|
||||||
write_basenames_js_file(pages_basenames_filename, pages_basenames);
|
write_basenames_js_file(pages_basenames_filename, pages_basenames);
|
||||||
|
|||||||
@@ -71,17 +71,13 @@ namespace latex {
|
|||||||
std::string test = trim(title + subtitle + author + date + version);
|
std::string test = trim(title + subtitle + author + date + version);
|
||||||
std::stringstream ss {};
|
std::stringstream ss {};
|
||||||
if (!test.empty()) {
|
if (!test.empty()) {
|
||||||
// ss << Pagestyle?
|
ss << "\\thispagestyle{empty}\n";
|
||||||
std::string full_title = title;
|
ss << title_line(title, "1.8", "6pt")
|
||||||
if (!full_title.empty() && !subtitle.empty()) {
|
<< title_line(subtitle, "1.5", "16pt", true)
|
||||||
full_title += " --- " + subtitle;
|
|
||||||
}
|
|
||||||
ss << title_line(full_title, "1.8", "8pt")
|
|
||||||
<< title_line(author)
|
<< title_line(author)
|
||||||
<< title_line(date)
|
<< title_line(date)
|
||||||
<< title_line(version)
|
<< title_line(version)
|
||||||
<< "\n";
|
<< "\n";
|
||||||
|
|
||||||
}
|
}
|
||||||
return ss.str();
|
return ss.str();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
.PHONY: test
|
.PHONY: test
|
||||||
test:
|
test:
|
||||||
./cond_test.sh
|
./cond_test.sh
|
||||||
|
./eval_test.sh
|
||||||
./recursion_test.sh
|
./recursion_test.sh
|
||||||
./check_test.sh
|
./check_test.sh
|
||||||
./deftype_test.sh
|
./deftype_test.sh
|
||||||
@@ -22,5 +23,7 @@ test:
|
|||||||
./target_list_test.sh
|
./target_list_test.sh
|
||||||
./coverage_test.sh
|
./coverage_test.sh
|
||||||
./command_option_test.sh
|
./command_option_test.sh
|
||||||
|
./verbosity_test.sh
|
||||||
./kdesc_test.sh
|
./kdesc_test.sh
|
||||||
|
./kdiag_test.sh
|
||||||
./editor_test.sh
|
./editor_test.sh
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
# would take "some words" as the value and the alone value would never be
|
# would take "some words" as the value and the alone value would never be
|
||||||
# reached. That is a definition-time error.
|
# reached. That is a definition-time error.
|
||||||
#
|
#
|
||||||
# Engine tier: these tests run with -k none and define their own argtypes
|
# Engine tier: these tests run with --klammersets none and define their own argtypes
|
||||||
# and klammers inline, so they do not depend on the Standard Klammer Set.
|
# and klammers inline, so they do not depend on the Standard Klammer Set.
|
||||||
#
|
#
|
||||||
# Usage: ./alone_test.sh (LSan suppressions come from env/runtime.env)
|
# Usage: ./alone_test.sh (LSan suppressions come from env/runtime.env)
|
||||||
@@ -114,29 +114,29 @@ echo
|
|||||||
check_eq \
|
check_eq \
|
||||||
" 1. name absent — the default" \
|
" 1. name absent — the default" \
|
||||||
"[0]" \
|
"[0]" \
|
||||||
-k none -s "$DEPTH $DK @d@" -d
|
--klammersets none -s "$DEPTH $DK @d@" -d
|
||||||
|
|
||||||
check_eq \
|
check_eq \
|
||||||
" 2. name written alone — the type's alone value" \
|
" 2. name written alone — the type's alone value" \
|
||||||
"[3]" \
|
"[3]" \
|
||||||
-k none -s "$DEPTH $DK @d :n @" -d
|
--klammersets none -s "$DEPTH $DK @d :n @" -d
|
||||||
|
|
||||||
check_eq \
|
check_eq \
|
||||||
" 3. name written with a value — that value" \
|
" 3. name written with a value — that value" \
|
||||||
"[7]" \
|
"[7]" \
|
||||||
-k none -s "$DEPTH $DK @d :n 7 @" -d
|
--klammersets none -s "$DEPTH $DK @d :n 7 @" -d
|
||||||
|
|
||||||
# --- A type with an alone value but no default ---
|
# --- A type with an alone value but no default ---
|
||||||
|
|
||||||
check_eq \
|
check_eq \
|
||||||
" 4. no default declared — absent is empty" \
|
" 4. no default declared — absent is empty" \
|
||||||
"[]" \
|
"[]" \
|
||||||
-k none -s "$MARK $MK @m@" -d
|
--klammersets none -s "$MARK $MK @m@" -d
|
||||||
|
|
||||||
check_eq \
|
check_eq \
|
||||||
" 5. no default declared — alone still applies" \
|
" 5. no default declared — alone still applies" \
|
||||||
"[*]" \
|
"[*]" \
|
||||||
-k none -s "$MARK $MK @m :c @" -d
|
--klammersets none -s "$MARK $MK @m :c @" -d
|
||||||
|
|
||||||
# --- The alone value belongs to the type, so every parameter of that
|
# --- The alone value belongs to the type, so every parameter of that
|
||||||
# type gets it, and a parameter default does not disturb it ---
|
# type gets it, and a parameter default does not disturb it ---
|
||||||
@@ -144,73 +144,73 @@ check_eq \
|
|||||||
check_eq \
|
check_eq \
|
||||||
" 6. two parameters of one type share the alone value" \
|
" 6. two parameters of one type share the alone value" \
|
||||||
"[3][3]" \
|
"[3][3]" \
|
||||||
-k none -s "$DEPTH @@d2 :a.depth :b.depth : [*a*][*b*] @@ @d2 :a :b @" -d
|
--klammersets none -s "$DEPTH @@d2 :a.depth :b.depth : [*a*][*b*] @@ @d2 :a :b @" -d
|
||||||
|
|
||||||
check_eq \
|
check_eq \
|
||||||
" 7. a parameter default overrides the type default, not the alone value" \
|
" 7. a parameter default overrides the type default, not the alone value" \
|
||||||
"[5]|[3]" \
|
"[5]|[3]" \
|
||||||
-k none -s "$DEPTH @@d3 :n.depth 5 : [*n*] @@ @d3@|@d3 :n @" -d
|
--klammersets none -s "$DEPTH @@d3 :n.depth 5 : [*n*] @@ @d3@|@d3 :n @" -d
|
||||||
|
|
||||||
# --- bool: the convention that a bare boolean option means true ---
|
# --- bool: the convention that a bare boolean option means true ---
|
||||||
|
|
||||||
check_eq \
|
check_eq \
|
||||||
" 8. bool written alone is true" \
|
" 8. bool written alone is true" \
|
||||||
"[true]" \
|
"[true]" \
|
||||||
-k none -s '@@b :f.bool : [*f*] @@ @b :f @' -d
|
--klammersets none -s '@@b :f.bool : [*f*] @@ @b :f @' -d
|
||||||
|
|
||||||
check_eq \
|
check_eq \
|
||||||
" 9. bool written with false stays false" \
|
" 9. bool written with false stays false" \
|
||||||
"[false]" \
|
"[false]" \
|
||||||
-k none -s '@@b :f.bool : [*f*] @@ @b :f false @' -d
|
--klammersets none -s '@@b :f.bool : [*f*] @@ @b :f false @' -d
|
||||||
|
|
||||||
check_eq \
|
check_eq \
|
||||||
"10. bool absent with no default is empty" \
|
"10. bool absent with no default is empty" \
|
||||||
"[]" \
|
"[]" \
|
||||||
-k none -s '@@b :f.bool : [*f*] @@ @b@' -d
|
--klammersets none -s '@@b :f.bool : [*f*] @@ @b@' -d
|
||||||
|
|
||||||
check_eq \
|
check_eq \
|
||||||
"11. bool absent with a true parameter default" \
|
"11. bool absent with a true parameter default" \
|
||||||
"[true]" \
|
"[true]" \
|
||||||
-k none -s '@@b :f.bool true : [*f*] @@ @b@' -d
|
--klammersets none -s '@@b :f.bool true : [*f*] @@ @b@' -d
|
||||||
|
|
||||||
check_eq \
|
check_eq \
|
||||||
"12. bool false explicitly against a true default" \
|
"12. bool false explicitly against a true default" \
|
||||||
"[false]" \
|
"[false]" \
|
||||||
-k none -s '@@b :f.bool true : [*f*] @@ @b :f false @' -d
|
--klammersets none -s '@@b :f.bool true : [*f*] @@ @b :f false @' -d
|
||||||
|
|
||||||
# --- The value reaching @eval ---
|
# --- The value reaching @eval ---
|
||||||
|
|
||||||
check_eq \
|
check_eq \
|
||||||
"13. python value of a bool written alone" \
|
"13. python value of a bool written alone" \
|
||||||
"True" \
|
"True" \
|
||||||
-k none -s '@@b :f.bool : @eval repr(K.f) @ @@ @b :f @' -d
|
--klammersets none -s '@@b :f.bool : @eval repr(K.f) @ @@ @b :f @' -d
|
||||||
|
|
||||||
check_eq \
|
check_eq \
|
||||||
"14. python value of an absent bool" \
|
"14. python value of an absent bool" \
|
||||||
"None" \
|
"None" \
|
||||||
-k none -s '@@b :f.bool : @eval repr(K.f) @ @@ @b@' -d
|
--klammersets none -s '@@b :f.bool : @eval repr(K.f) @ @@ @b@' -d
|
||||||
|
|
||||||
check_eq \
|
check_eq \
|
||||||
"15. python value of a user type written alone" \
|
"15. python value of a user type written alone" \
|
||||||
"'3'" \
|
"'3'" \
|
||||||
-k none -s "$DEPTH @@d4 :n.depth : @eval repr(K.n) @ @@ @d4 :n @" -d
|
--klammersets none -s "$DEPTH @@d4 :n.depth : @eval repr(K.n) @ @@ @d4 :n @" -d
|
||||||
|
|
||||||
# --- Types that cannot delimit a bare option name ---
|
# --- Types that cannot delimit a bare option name ---
|
||||||
|
|
||||||
check_error \
|
check_error \
|
||||||
"16. :alone refused on a type matching running text" \
|
"16. :alone refused on a type matching running text" \
|
||||||
"cannot declare an :alone value" \
|
"cannot declare an :alone value" \
|
||||||
-k none -s '@@@argtype loose | anything at all :alone x @@@' -d
|
--klammersets none -s '@@@argtype loose | anything at all :alone x @@@' -d
|
||||||
|
|
||||||
check_error \
|
check_error \
|
||||||
"17. :alone refused on an explicit match-everything pattern" \
|
"17. :alone refused on an explicit match-everything pattern" \
|
||||||
"cannot declare an :alone value" \
|
"cannot declare an :alone value" \
|
||||||
-k none -s '@@@argtype loose | anything :pattern (?:.^|\n)* :alone x @@@' -d
|
--klammersets none -s '@@@argtype loose | anything :pattern (?:.^|\n)* :alone x @@@' -d
|
||||||
|
|
||||||
check_error \
|
check_error \
|
||||||
"18. an alone value must match its own type's pattern" \
|
"18. an alone value must match its own type's pattern" \
|
||||||
"does not match its own pattern" \
|
"does not match its own pattern" \
|
||||||
-k none -s '@@@argtype depth | a depth :pattern \d+ :alone many @@@' -d
|
--klammersets none -s '@@@argtype depth | a depth :pattern \d+ :alone many @@@' -d
|
||||||
|
|
||||||
# --- Delimitation: an option value still runs to the next bar or option
|
# --- Delimitation: an option value still runs to the next bar or option
|
||||||
# name, so text after a bare name is taken as the value and rejected
|
# name, so text after a bare name is taken as the value and rejected
|
||||||
@@ -219,19 +219,19 @@ check_error \
|
|||||||
check_error \
|
check_error \
|
||||||
"19. text after a bare option name is taken as its value" \
|
"19. text after a bare option name is taken as its value" \
|
||||||
"does not match" \
|
"does not match" \
|
||||||
-k none -s "$DEPTH $DK @d :n some words @" -d
|
--klammersets none -s "$DEPTH $DK @d :n some words @" -d
|
||||||
|
|
||||||
check_eq \
|
check_eq \
|
||||||
"20. a bar separates a bare option name from following text" \
|
"20. a bar separates a bare option name from following text" \
|
||||||
"[3]two" \
|
"[3]two" \
|
||||||
-k none -s "$DEPTH @@d5 :n.depth | t : [*n*]*t* @@ @d5 :n | two @" -d
|
--klammersets none -s "$DEPTH @@d5 :n.depth | t : [*n*]*t* @@ @d5 :n | two @" -d
|
||||||
|
|
||||||
# --- A type with no alone value is unchanged: a bare name is empty ---
|
# --- A type with no alone value is unchanged: a bare name is empty ---
|
||||||
|
|
||||||
check_eq \
|
check_eq \
|
||||||
"21. bare option of a type with no alone value is empty" \
|
"21. bare option of a type with no alone value is empty" \
|
||||||
"[]" \
|
"[]" \
|
||||||
-k none -s '@@s :t.word : [*t*] @@ @s :t @' -d
|
--klammersets none -s '@@s :t.word : [*t*] @@ @s :t @' -d
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "================================"
|
echo "================================"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
#
|
#
|
||||||
# check_test.sh — Static checking of klammer applications ("ktext --check").
|
# check_test.sh — Static checking of klammer applications ("kdiag --check").
|
||||||
#
|
#
|
||||||
# The engine applies klammers as it meets them, so it can only complain about
|
# The engine applies klammers as it meets them, so it can only complain about
|
||||||
# what it reaches. Two things it therefore never reaches:
|
# what it reaches. Two things it therefore never reaches:
|
||||||
@@ -24,14 +24,17 @@
|
|||||||
# literal parameters, which are code, filenames, and raw text -- not
|
# literal parameters, which are code, filenames, and raw text -- not
|
||||||
# applications.
|
# applications.
|
||||||
#
|
#
|
||||||
# Engine tier: no klammer set (-k none), every klammer defined inline.
|
# Engine tier: kdiag loads no klammerset, so every klammer is defined inline.
|
||||||
|
# The one case that needs the SKS reads it explicitly with "@read" -- which is
|
||||||
|
# how a klammerset reaches kdiag at all: the symbol mechanism is deliberately
|
||||||
|
# absent there, so that kdiag still works when symbol resolution is what broke.
|
||||||
#
|
#
|
||||||
# Usage: ./check_test.sh
|
# Usage: ./check_test.sh
|
||||||
# Exit code: 0 if all tests pass, 1 otherwise.
|
# Exit code: 0 if all tests pass, 1 otherwise.
|
||||||
|
|
||||||
PASS=0
|
PASS=0
|
||||||
FAIL=0
|
FAIL=0
|
||||||
KTEXT=ktext
|
KDIAG=kdiag
|
||||||
K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set}
|
K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set}
|
||||||
|
|
||||||
red=$'\033[31m'
|
red=$'\033[31m'
|
||||||
@@ -39,7 +42,7 @@ green=$'\033[32m'
|
|||||||
bold=$'\033[1m'
|
bold=$'\033[1m'
|
||||||
reset=$'\033[0m'
|
reset=$'\033[0m'
|
||||||
|
|
||||||
# check_finds TEST_NAME PATTERN KTEXT_ARGS...
|
# check_finds TEST_NAME PATTERN KDIAG_ARGS...
|
||||||
# --check must exit nonzero and report PATTERN.
|
# --check must exit nonzero and report PATTERN.
|
||||||
check_finds() {
|
check_finds() {
|
||||||
local test_name="$1"
|
local test_name="$1"
|
||||||
@@ -47,7 +50,7 @@ check_finds() {
|
|||||||
shift 2
|
shift 2
|
||||||
|
|
||||||
local output status
|
local output status
|
||||||
output=$("$KTEXT" --check "$@" 2>&1)
|
output=$("$KDIAG" --process --check "$@" 2>&1)
|
||||||
status=$?
|
status=$?
|
||||||
|
|
||||||
if [ $status -eq 0 ]; then
|
if [ $status -eq 0 ]; then
|
||||||
@@ -65,14 +68,14 @@ check_finds() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# check_clean TEST_NAME KTEXT_ARGS...
|
# check_clean TEST_NAME KDIAG_ARGS...
|
||||||
# --check must exit 0 and report no diagnostics.
|
# --check must exit 0 and report no diagnostics.
|
||||||
check_clean() {
|
check_clean() {
|
||||||
local test_name="$1"
|
local test_name="$1"
|
||||||
shift
|
shift
|
||||||
|
|
||||||
local output status
|
local output status
|
||||||
output=$("$KTEXT" --check "$@" 2>&1)
|
output=$("$KDIAG" --process --check "$@" 2>&1)
|
||||||
status=$?
|
status=$?
|
||||||
|
|
||||||
if [ $status -eq 0 ] && echo "$output" | grep -q "0 diagnostics"; then
|
if [ $status -eq 0 ] && echo "$output" | grep -q "0 diagnostics"; then
|
||||||
@@ -85,7 +88,7 @@ check_clean() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# check_count TEST_NAME N KTEXT_ARGS...
|
# check_count TEST_NAME N KDIAG_ARGS...
|
||||||
# --check must report exactly N diagnostics.
|
# --check must report exactly N diagnostics.
|
||||||
check_count() {
|
check_count() {
|
||||||
local test_name="$1"
|
local test_name="$1"
|
||||||
@@ -93,7 +96,7 @@ check_count() {
|
|||||||
shift 2
|
shift 2
|
||||||
|
|
||||||
local output got
|
local output got
|
||||||
output=$("$KTEXT" --check "$@" 2>&1)
|
output=$("$KDIAG" --process --check "$@" 2>&1)
|
||||||
got=$(echo "$output" | sed -nE 's/^([0-9]+) diagnostics?,.*/\1/p')
|
got=$(echo "$output" | sed -nE 's/^([0-9]+) diagnostics?,.*/\1/p')
|
||||||
|
|
||||||
if [ "$got" = "$expected" ]; then
|
if [ "$got" = "$expected" ]; then
|
||||||
@@ -116,87 +119,95 @@ echo
|
|||||||
|
|
||||||
check_finds " 1. undefined klammer in an unselected @cond branch (in a body)" \
|
check_finds " 1. undefined klammer in an unselected @cond branch (in a body)" \
|
||||||
"@nosuch is not defined" \
|
"@nosuch is not defined" \
|
||||||
-k none -s "@@pick p : @cond *p* | @nosuch x @ | ok @ @@ @pick false @"
|
"@@pick p : @cond *p* | @nosuch x @ | ok @ @@ @pick false @"
|
||||||
|
|
||||||
check_finds " 2. wrong arity in an unselected @cond branch (in a body)" \
|
check_finds " 2. wrong arity in an unselected @cond branch (in a body)" \
|
||||||
"is given 3" \
|
"is given 3" \
|
||||||
-k none -s "$GREET @@pick p : @cond *p* | @greet a | b | c @ | ok @ @@ @pick false @"
|
"$GREET @@pick p : @cond *p* | @greet a | b | c @ | ok @ @@ @pick false @"
|
||||||
|
|
||||||
# A @cond written at the top level of a DOCUMENT is resolved when the file is
|
# A top-level @cond branch is now checked like any other. It was the one
|
||||||
# read, so its unselected branch is gone before anything can be checked. This
|
# documented gap in the checker: a @cond written at the top level of a DOCUMENT
|
||||||
# test records that limitation rather than asserting the behavior is right; see
|
# was resolved when the file was READ, so its unselected branch was gone before
|
||||||
# notes/Klammertext_improvements.md, "When @cond is resolved".
|
# anything could look at it. Resolving @cond at APPLICATION time instead
|
||||||
check_clean " 2a. LIMITATION: a top-level @cond branch is resolved before checking" \
|
# (notes/Klammertext_improvements.md §4.2, decided 2026-08-15) closed it -- the
|
||||||
-k none -s "@cond false | @nosuch x @ | ok @"
|
# branches survive read, so both are visible to a pass that applies nothing.
|
||||||
|
check_finds " 2a. a top-level @cond branch is checked too" \
|
||||||
|
"@nosuch is not defined" \
|
||||||
|
"@cond false | @nosuch x @ | ok @"
|
||||||
|
check_finds " 2b. ... including the branch that WOULD be selected" \
|
||||||
|
"@nosuch is not defined" \
|
||||||
|
"@cond true | @nosuch x @ | ok @"
|
||||||
|
|
||||||
check_finds " 3. undefined klammer in a body that is never applied" \
|
check_finds " 3. undefined klammer in a body that is never applied" \
|
||||||
"@nosuch is not defined" \
|
"@nosuch is not defined" \
|
||||||
-k none -s "@@unused : @nosuch x @ @@ nothing applies it"
|
"@@unused : @nosuch x @ @@ nothing applies it"
|
||||||
|
|
||||||
check_finds " 4. the body it was found in is named" \
|
check_finds " 4. the body it was found in is named" \
|
||||||
"in body of @unused" \
|
"in body of @unused" \
|
||||||
-k none -s "@@unused : @nosuch x @ @@ nothing applies it"
|
"@@unused : @nosuch x @ @@ nothing applies it"
|
||||||
|
|
||||||
# --- Arity ---
|
# --- Arity ---
|
||||||
|
|
||||||
check_finds " 5. too few positional arguments" \
|
check_finds " 5. too few positional arguments" \
|
||||||
"needs 2 positional arguments but is given 1" \
|
"needs 2 positional arguments but is given 1" \
|
||||||
-k none -s '@@pair a | b : *a**b* @@ @pair x @'
|
'@@pair a | b : *a**b* @@ @pair x @'
|
||||||
|
|
||||||
check_finds " 6. too many positional arguments" \
|
check_finds " 6. too many positional arguments" \
|
||||||
"takes 1 positional argument but is given 2" \
|
"takes 1 positional argument but is given 2" \
|
||||||
-k none -s "$GREET @greet a | b @"
|
"$GREET @greet a | b @"
|
||||||
|
|
||||||
check_finds " 7. undefined optional argument" \
|
check_finds " 7. undefined optional argument" \
|
||||||
'has no optional argument ":nope"' \
|
'has no optional argument ":nope"' \
|
||||||
-k none -s "$GREET @greet a :nope 1 @"
|
"$GREET @greet a :nope 1 @"
|
||||||
|
|
||||||
check_finds " 8. the accepted optional arguments are listed" \
|
check_finds " 8. the accepted optional arguments are listed" \
|
||||||
"It accepts: :loud" \
|
"It accepts: :loud" \
|
||||||
-k none -s '@@greet name :loud : *name* @@ @greet a :nope 1 @'
|
'@@greet name :loud : *name* @@ @greet a :nope 1 @'
|
||||||
|
|
||||||
check_clean " 9. a rest argument accepts extra positional arguments" \
|
check_clean " 9. a rest argument accepts extra positional arguments" \
|
||||||
-k none -s '@@many a | rest.rest : *a* @@ @many x | y | z @'
|
'@@many a | rest.rest : *a* @@ @many x | y | z @'
|
||||||
|
|
||||||
# --- Nesting. A bar or an option name belonging to a nested klammer is not
|
# --- Nesting. A bar or an option name belonging to a nested klammer is not
|
||||||
# this klammer's; the checker counts at depth 0, as @cond does. ---
|
# this klammer's; the checker counts at depth 0, as @cond does. ---
|
||||||
|
|
||||||
check_clean "10. nested klammer's bars are not counted as the outer's" \
|
check_clean "10. nested klammer's bars are not counted as the outer's" \
|
||||||
-k none -s '@@frac a | b : *a*/*b* @@ @@one x : [*x*] @@ @one @frac 1 | 2 @ @'
|
'@@frac a | b : *a*/*b* @@ @@one x : [*x*] @@ @one @frac 1 | 2 @ @'
|
||||||
|
|
||||||
check_clean "11. nested klammer's option name is not counted as the outer's" \
|
check_clean "11. nested klammer's option name is not counted as the outer's" \
|
||||||
-k none -s '@@inner a :flag : *a* @@ @@outer x : [*x*] @@ @outer @inner q :flag y @ @'
|
'@@inner a :flag : *a* @@ @@outer x : [*x*] @@ @outer @inner q :flag y @ @'
|
||||||
|
|
||||||
# --- Target coverage ---
|
# --- Target coverage ---
|
||||||
|
|
||||||
check_finds "12. klammer not defined for a target" \
|
check_finds "12. klammer not defined for a target" \
|
||||||
'is not defined for the target "tex"' \
|
'is not defined for the target "tex"' \
|
||||||
-k none -s '@@@target html | HTML output @@@ @@@target tex | TeX output @@@ @@only.html : H @@ @only@'
|
'@@@target html | HTML output @@@ @@@target tex | TeX output @@@ @@only.html : H @@ @only@'
|
||||||
|
|
||||||
check_clean "13. defined for every target is clean" \
|
check_clean "13. defined for every target is clean" \
|
||||||
-k none -s '@@@target html | HTML output @@@ @@@target tex | TeX output @@@ @@both : B @@ @both@'
|
'@@@target html | HTML output @@@ @@@target tex | TeX output @@@ @@both : B @@ @both@'
|
||||||
|
|
||||||
# --- What the checker deliberately does not see ---
|
# --- What the checker deliberately does not see ---
|
||||||
|
|
||||||
# In a body, so the @eval is not evaluated at read time: what is being tested
|
# In a body, so the @eval is not evaluated at read time: what is being tested
|
||||||
# is that the checker does not read the eval's ARGUMENT as an application.
|
# is that the checker does not read the eval's ARGUMENT as an application.
|
||||||
check_clean "14. @eval argument content is code, not applications" \
|
check_clean "14. @eval argument content is code, not applications" \
|
||||||
-k none -s '@@w : @eval len("@nosuch") @ @@'
|
'@@w : @eval len("@nosuch") @ @@'
|
||||||
|
|
||||||
check_clean "15. a literal parameter's content is raw text" \
|
check_clean "15. a literal parameter's content is raw text" \
|
||||||
-k none -s '@@lit t.literal : *t* @@ @lit @nosuch x @ lit@'
|
'@@lit t.literal : *t* @@ @lit @nosuch x @ lit@'
|
||||||
|
|
||||||
# --- Reporting ---
|
# --- Reporting ---
|
||||||
|
|
||||||
check_count "16. a target-independent fault is reported once, not per target" \
|
check_count "16. a target-independent fault is reported once, not per target" \
|
||||||
1 \
|
1 \
|
||||||
-k none -s '@@@target html | HTML output @@@ @@@target tex | TeX output @@@ @@g : @nosuch@ @@'
|
'@@@target html | HTML output @@@ @@@target tex | TeX output @@@ @@g : @nosuch@ @@'
|
||||||
|
|
||||||
check_clean "17. a correct document checks clean" \
|
check_clean "17. a correct document checks clean" \
|
||||||
-k none -s "$GREET @greet World @"
|
"$GREET @greet World @"
|
||||||
|
|
||||||
|
# An absolute path: "make -C tst test" runs from tst/, and @read resolves
|
||||||
|
# against the current directory.
|
||||||
check_clean "18. the Standard Klammer Set checks clean" \
|
check_clean "18. the Standard Klammer Set checks clean" \
|
||||||
-s 'x'
|
"@read $K/sks/sks.k @"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "============================="
|
echo "============================="
|
||||||
|
|||||||
@@ -113,6 +113,76 @@ else
|
|||||||
echo "SKIP 13/14. two positionals (argv_test not built; run make -C tst)"
|
echo "SKIP 13/14. two positionals (argv_test not built; run make -C tst)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- the output policy: three categories, two streams --"
|
||||||
|
# The three commands display text in exactly three cases:
|
||||||
|
# 1. logging under "-v" > 0 -> STDERR
|
||||||
|
# 2. an error before termination -> STDERR
|
||||||
|
# 3. output the user asked for -> STDOUT
|
||||||
|
# For ktext, category 3 is a DOCUMENT that may be piped, so nothing else may
|
||||||
|
# share the stream. It did: K::log and msg() both wrote to stdout, so
|
||||||
|
# "ktext -d -v 1 > doc.txt" put the whole argument dump inside the document.
|
||||||
|
OUTF=$(mktemp /tmp/kout.XXXXXX); ERRF=$(mktemp /tmp/kerr.XXXXXX)
|
||||||
|
trap 'rm -f "$OUTF" "$ERRF"' EXIT
|
||||||
|
|
||||||
|
ktext --klammersets none -s 'hello' -d -v 1 > "$OUTF" 2> "$ERRF"
|
||||||
|
if [ "$(cat "$OUTF")" = "hello" ]; then
|
||||||
|
echo "${green}PASS${reset} 15. stdout under -d -v 1 is the document, nothing else"
|
||||||
|
PASS=$((PASS+1))
|
||||||
|
else
|
||||||
|
echo "${red}FAIL${reset} 15. stdout carried more than the document"
|
||||||
|
echo " got: $(head -3 "$OUTF")"; FAIL=$((FAIL+1))
|
||||||
|
fi
|
||||||
|
if [ -s "$ERRF" ]; then
|
||||||
|
echo "${green}PASS${reset} 16. ... and the -v 1 logging went to stderr"; PASS=$((PASS+1))
|
||||||
|
else
|
||||||
|
echo "${red}FAIL${reset} 16. -v 1 produced no stderr"; FAIL=$((FAIL+1))
|
||||||
|
fi
|
||||||
|
# -v 0 adds nothing anywhere: silence is the default, on both streams.
|
||||||
|
ktext --klammersets none -s 'hello' -d > "$OUTF" 2> "$ERRF"
|
||||||
|
if [ "$(cat "$OUTF")" = "hello" ] && [ ! -s "$ERRF" ]; then
|
||||||
|
echo "${green}PASS${reset} 17. -v 0 is silent on both streams"; PASS=$((PASS+1))
|
||||||
|
else
|
||||||
|
echo "${red}FAIL${reset} 17. -v 0 was not silent (stderr: $(head -2 "$ERRF"))"
|
||||||
|
FAIL=$((FAIL+1))
|
||||||
|
fi
|
||||||
|
# An error is category 2: stderr, and nothing on stdout to confuse a pipe.
|
||||||
|
ktext --klammersets none -s '@nosuch x @' -d > "$OUTF" 2> "$ERRF"
|
||||||
|
if [ ! -s "$OUTF" ] && [ -s "$ERRF" ]; then
|
||||||
|
echo "${green}PASS${reset} 18. an error goes to stderr, leaving stdout empty"; PASS=$((PASS+1))
|
||||||
|
else
|
||||||
|
echo "${red}FAIL${reset} 18. error stream discipline (stdout: $(head -2 "$OUTF"))"
|
||||||
|
FAIL=$((FAIL+1))
|
||||||
|
fi
|
||||||
|
# Colour is emitted only to a terminal, so a redirected stream never carries
|
||||||
|
# escape sequences -- into a document, a pipe, or a captured log.
|
||||||
|
for pair in "ktext:--klammersets none -s hello -d" "kdesc:-k table" "kdiag:@i-x"; do
|
||||||
|
cmd=${pair%%:*}; rest=${pair#*:}
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
$cmd $rest > "$OUTF" 2> "$ERRF"
|
||||||
|
if ! grep -q $'\033' "$OUTF" && ! grep -q $'\033' "$ERRF"; then
|
||||||
|
echo "${green}PASS${reset} 19. no escape sequences from $cmd when redirected"
|
||||||
|
PASS=$((PASS+1))
|
||||||
|
else
|
||||||
|
echo "${red}FAIL${reset} 19. $cmd emitted colour to a non-terminal"; FAIL=$((FAIL+1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- a bare command is a request, not a failure --"
|
||||||
|
# Usage is the result being asked for: stdout, exit 0. It exited 1, so
|
||||||
|
# "kdesc && echo ok" reported failure for a successful help request.
|
||||||
|
for cmd in ktext kdesc kdiag; do
|
||||||
|
$cmd > "$OUTF" 2> "$ERRF"; status=$?
|
||||||
|
if [ $status -eq 0 ] && [ -s "$OUTF" ] && ! [ -s "$ERRF" ]; then
|
||||||
|
echo "${green}PASS${reset} 20. $cmd with no arguments: usage on stdout, exit 0"
|
||||||
|
PASS=$((PASS+1))
|
||||||
|
else
|
||||||
|
echo "${red}FAIL${reset} 20. $cmd bare: exit $status, stdout $(wc -l < "$OUTF") lines, stderr $(wc -l < "$ERRF") lines"
|
||||||
|
FAIL=$((FAIL+1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "===================="
|
echo "===================="
|
||||||
echo "Results: ${PASS} passed, ${FAIL} failed"
|
echo "Results: ${PASS} passed, ${FAIL} failed"
|
||||||
|
|||||||
@@ -117,6 +117,11 @@ check_error() {
|
|||||||
|
|
||||||
FRAC='@@frac a | b : *a*/*b* @@'
|
FRAC='@@frac a | b : *a*/*b* @@'
|
||||||
|
|
||||||
|
# A file for the @read non-strictness cases (21-21b).
|
||||||
|
READ_FIXTURE=$(mktemp /tmp/cond_read.XXXXXX)
|
||||||
|
printf 'READ-FIXTURE\n' > "$READ_FIXTURE"
|
||||||
|
trap 'rm -f "$READ_FIXTURE"' EXIT
|
||||||
|
|
||||||
echo "${bold}@cond argument delimitation tests${reset}"
|
echo "${bold}@cond argument delimitation tests${reset}"
|
||||||
echo "================================="
|
echo "================================="
|
||||||
echo
|
echo
|
||||||
@@ -174,6 +179,99 @@ check_error "14. zero bars is still an error" \
|
|||||||
"one or two bar characters" \
|
"one or two bar characters" \
|
||||||
-s '@cond true @' -d
|
-s '@cond true @' -d
|
||||||
|
|
||||||
|
# --- The predicate relation: TOTAL AND STRICT (Andy, 2026-08-15) ---
|
||||||
|
#
|
||||||
|
# Deciding notes/Klammertext_improvements.md §4.1. There is a defined true
|
||||||
|
# set, a defined false set, and anything else is an error AT THE @cond. It was
|
||||||
|
# partial until then: is_true() recognized three strings and everything else
|
||||||
|
# took the false branch, so a misspelled variable, a "TRUE", a "yes", or a
|
||||||
|
# Python traceback all silently selected a branch. A warning had made that
|
||||||
|
# visible while the policy was open; it never fired on the SKS, which is the
|
||||||
|
# evidence that the blast radius is small.
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- the predicate relation --"
|
||||||
|
|
||||||
|
for p in true True 1; do
|
||||||
|
check_eq "15. \"$p\" is true" "T" -s "@cond $p | T | F @" -d
|
||||||
|
done
|
||||||
|
for p in false False 0; do
|
||||||
|
check_eq "16. \"$p\" is false" "F" -s "@cond $p | T | F @" -d
|
||||||
|
done
|
||||||
|
|
||||||
|
# Empty stays FALSE, and load-bearing: an optional argument that was not
|
||||||
|
# written substitutes as empty, which is what carries the "@cond *opt*" idiom.
|
||||||
|
# The entangled sub-question in §4.1 -- empty means false, or means "not
|
||||||
|
# supplied"? -- is answered "false" by that use.
|
||||||
|
check_eq "17. an absent optional argument is false" "F" \
|
||||||
|
-s '@@g :opt : @cond *opt* | T | F @ @@ @g@' -d
|
||||||
|
check_eq "17a. ... and the same argument written true is true" "T" \
|
||||||
|
-s '@@g :opt : @cond *opt* | T | F @ @@ @g :opt true @' -d
|
||||||
|
|
||||||
|
check_error "18. an unrecognized predicate is an error, not false" \
|
||||||
|
"is not a truth value" \
|
||||||
|
-s '@cond yes | T | F @' -d
|
||||||
|
check_error "18a. ... including a near miss of a true value" \
|
||||||
|
"is not a truth value" \
|
||||||
|
-s '@cond TRUE | T | F @' -d
|
||||||
|
# A state variable in a klammer BODY works: a body is processed at application
|
||||||
|
# time, after substitution, so @cond sees the value.
|
||||||
|
check_eq "19. a state variable in a body reaches the @cond" "T" \
|
||||||
|
-s '@@@state Flag :value true @@@ @@g : @cond *Flag* | T | F @ @@ @g@' -d
|
||||||
|
|
||||||
|
# A top-level state variable reaches the predicate too, since @cond is resolved
|
||||||
|
# at APPLICATION time (notes/Klammertext_improvements.md §4.2, decided
|
||||||
|
# 2026-08-15). It did not until then: a top-level @cond was resolved when the
|
||||||
|
# file was READ, which is before state substitution, so it saw the literal
|
||||||
|
# "*Flag*" and silently took the false branch -- the WRONG answer for a flag
|
||||||
|
# whose value was true. The document now behaves like a klammer body: its
|
||||||
|
# state variables are bound before its conditionals are decided.
|
||||||
|
check_eq "19a. a top-level state variable reaches the @cond" "T" \
|
||||||
|
-s '@@@state Flag :value true @@@ @cond *Flag* | T | F @' -d
|
||||||
|
check_eq "19b. ... and selects the false branch when it is false" "F" \
|
||||||
|
-s '@@@state Flag :value false @@@ @cond *Flag* | T | F @' -d
|
||||||
|
|
||||||
|
# --- Non-strictness: nothing in a discarded branch runs ---
|
||||||
|
#
|
||||||
|
# doc/cond_evaluation_order.md states this ("with side-effecting @read/@eval,
|
||||||
|
# wrong ... must not read the missing file"). @read honoured it; @eval did not,
|
||||||
|
# because the eval pass swept the list before the cond pass did. Both honour it
|
||||||
|
# now: mark_cond_content() makes a branch inert BEFORE either pass runs.
|
||||||
|
|
||||||
|
check_eq "21. a @read in a discarded branch is not performed" "ok" \
|
||||||
|
-s "@cond false | @read $READ_FIXTURE @ | ok @" -d
|
||||||
|
check_eq "21a. ... and IS performed when the branch is selected" "READ-FIXTURE" \
|
||||||
|
-s "@cond true | @read $READ_FIXTURE @ | ok @" -d
|
||||||
|
# The case the design document names: the file need not even exist.
|
||||||
|
check_eq "21b. ... so a missing file in a discarded branch is not an error" "ok" \
|
||||||
|
-s '@cond false | @read /nonexistent/no-such-file.txt @ | ok @' -d
|
||||||
|
|
||||||
|
# An @eval side effect is the observable test: the branch either touched the
|
||||||
|
# file or it did not.
|
||||||
|
SIDE=$(mktemp -u /tmp/cond_side.XXXXXX)
|
||||||
|
"$KTEXT" --klammersets none -s "@cond false | @eval :shell touch $SIDE @ | ok @" -d >/dev/null 2>&1
|
||||||
|
if [ -f "$SIDE" ]; then
|
||||||
|
echo "${red}FAIL${reset} 22. an @eval in a discarded branch ran"; FAIL=$((FAIL+1)); rm -f "$SIDE"
|
||||||
|
else
|
||||||
|
echo "${green}PASS${reset} 22. an @eval in a discarded branch does not run"; PASS=$((PASS+1))
|
||||||
|
fi
|
||||||
|
"$KTEXT" --klammersets none -s "@cond true | @eval :shell touch $SIDE @ | ok @" -d >/dev/null 2>&1
|
||||||
|
if [ -f "$SIDE" ]; then
|
||||||
|
echo "${green}PASS${reset} 22a. ... and does run when the branch is selected"; PASS=$((PASS+1)); rm -f "$SIDE"
|
||||||
|
else
|
||||||
|
echo "${red}FAIL${reset} 22a. an @eval in the selected branch did not run"; FAIL=$((FAIL+1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The PREDICATE is always evaluated -- a conditional that could not compute its
|
||||||
|
# own predicate would be useless. Only the branches are non-strict.
|
||||||
|
check_eq "23. the predicate is evaluated even though the branches are not" "yes" \
|
||||||
|
-s '@cond @eval 1==1 @ | yes | no @' -d
|
||||||
|
# The recognized sets are named in the message, since the whole point is that
|
||||||
|
# the writer has to know what they are.
|
||||||
|
check_error "20. the message names the recognized values" \
|
||||||
|
"true, True, 1" \
|
||||||
|
-s '@cond yes | T | F @' -d
|
||||||
|
|
||||||
rm -f /tmp/cond_test_err.$$
|
rm -f /tmp/cond_test_err.$$
|
||||||
|
|
||||||
echo
|
echo
|
||||||
|
|||||||
@@ -17,7 +17,10 @@
|
|||||||
# REPORTED, and a companion case checks that rendering is unaffected.
|
# REPORTED, and a companion case checks that rendering is unaffected.
|
||||||
#
|
#
|
||||||
# Engine tier: the fixtures in tst/coverage/ declare their own targets with
|
# Engine tier: the fixtures in tst/coverage/ declare their own targets with
|
||||||
# @@@target, so no klammer set is involved.
|
# @@@target, so no klammer set is involved -- hence "--klammersets none".
|
||||||
|
# Since the 2026-08-14 redesign "-i" ADDS a file to whatever klammersets are
|
||||||
|
# loaded (the SKS by default) rather than replacing them, so the exclusion has
|
||||||
|
# to be explicit or every count here would include the SKS.
|
||||||
#
|
#
|
||||||
# Usage: ./coverage_test.sh (needs KLAMMERTEXT_HOME set; kdesc on PATH)
|
# Usage: ./coverage_test.sh (needs KLAMMERTEXT_HOME set; kdesc on PATH)
|
||||||
# Exit code: 0 if all tests pass, 1 otherwise.
|
# Exit code: 0 if all tests pass, 1 otherwise.
|
||||||
@@ -34,23 +37,50 @@ green=$'\033[32m'
|
|||||||
bold=$'\033[1m'
|
bold=$'\033[1m'
|
||||||
reset=$'\033[0m'
|
reset=$'\033[0m'
|
||||||
|
|
||||||
# Each fixture is analysed once; the tests match against the saved report.
|
|
||||||
declare -A REPORT VREPORT
|
# GNU coreutils' "timeout" is NOT present on macOS, and Homebrew's is named
|
||||||
|
# "gtimeout", so a bare "timeout" made this suite fail WHOLESALE there -- every
|
||||||
|
# case, because the command never ran at all (found on Olion, 2026-08-16). The
|
||||||
|
# guard is a safety net against a hung command, not part of what is being
|
||||||
|
# tested, so it is optional: bound the command where the tool exists, run it
|
||||||
|
# directly where it does not.
|
||||||
|
if command -v timeout >/dev/null 2>&1; then
|
||||||
|
limited() { timeout 60 "$@"; }
|
||||||
|
elif command -v gtimeout >/dev/null 2>&1; then
|
||||||
|
limited() { gtimeout 60 "$@"; }
|
||||||
|
else
|
||||||
|
limited() { "$@"; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Each fixture is analysed once and its report SAVED TO A FILE, keyed by name.
|
||||||
|
#
|
||||||
|
# It used to be a pair of associative arrays. "declare -A" is bash 4, and
|
||||||
|
# macOS ships bash 3.2 as /bin/bash -- where the declaration fails and every
|
||||||
|
# string subscript then evaluates to 0, so all five fixtures overwrote one
|
||||||
|
# slot and the whole suite compared the wrong report against the wrong test.
|
||||||
|
# Files have no such floor and read the same on both systems.
|
||||||
|
REPORTS=$(mktemp -d /tmp/coverage_reports.XXXXXX)
|
||||||
|
trap 'rm -rf "$REPORTS"' EXIT
|
||||||
|
|
||||||
for f in basic undecidable cycle split clean; do
|
for f in basic undecidable cycle split clean; do
|
||||||
REPORT[$f]=$(timeout 30 "$KDESC" -i "$DIR/$f.k" --coverage 2>&1 |
|
limited "$KDESC" --klammersets none -i "$DIR/$f.k" --coverage 2>&1 |
|
||||||
sed 's/\x1b\[[0-9;]*m//g')
|
sed 's/\x1b\[[0-9;]*m//g' > "$REPORTS/$f"
|
||||||
# "all" adds the source-file column AND the empty problem categories.
|
# "all" adds the source-file column AND the empty problem categories.
|
||||||
VREPORT[$f]=$(timeout 30 "$KDESC" -i "$DIR/$f.k" --coverage all 2>&1 |
|
limited "$KDESC" --klammersets none -i "$DIR/$f.k" --coverage all 2>&1 |
|
||||||
sed 's/\x1b\[[0-9;]*m//g')
|
sed 's/\x1b\[[0-9;]*m//g' > "$REPORTS/$f.all"
|
||||||
if [ -z "${REPORT[$f]}" ] || [ -z "${VREPORT[$f]}" ]; then
|
if [ ! -s "$REPORTS/$f" ] || [ ! -s "$REPORTS/$f.all" ]; then
|
||||||
echo "${red}FAIL${reset} $f.k produced no report"; FAIL=$((FAIL+1))
|
echo "${red}FAIL${reset} $f.k produced no report"; FAIL=$((FAIL+1))
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# report FIXTURE [all] — the saved report, on stdout.
|
||||||
|
report() { cat "$REPORTS/$1"; }
|
||||||
|
vreport() { cat "$REPORTS/$1.all"; }
|
||||||
|
|
||||||
# vlacks NAME FIXTURE REGEX — the VERBOSE report does NOT match REGEX.
|
# vlacks NAME FIXTURE REGEX — the VERBOSE report does NOT match REGEX.
|
||||||
vlacks() {
|
vlacks() {
|
||||||
local name="$1" fixture="$2" rgx="$3"
|
local name="$1" fixture="$2" rgx="$3"
|
||||||
if printf '%s\n' "${VREPORT[$fixture]}" | grep -Eq "$rgx"; then
|
if vreport "$fixture" | grep -Eq "$rgx"; then
|
||||||
echo "${red}FAIL${reset} $name — unexpected match: $rgx"; FAIL=$((FAIL+1))
|
echo "${red}FAIL${reset} $name — unexpected match: $rgx"; FAIL=$((FAIL+1))
|
||||||
else
|
else
|
||||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||||
@@ -60,7 +90,7 @@ vlacks() {
|
|||||||
# vhas NAME FIXTURE REGEX — the VERBOSE report matches REGEX.
|
# vhas NAME FIXTURE REGEX — the VERBOSE report matches REGEX.
|
||||||
vhas() {
|
vhas() {
|
||||||
local name="$1" fixture="$2" rgx="$3"
|
local name="$1" fixture="$2" rgx="$3"
|
||||||
if printf '%s\n' "${VREPORT[$fixture]}" | grep -Eq "$rgx"; then
|
if vreport "$fixture" | grep -Eq "$rgx"; then
|
||||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||||
else
|
else
|
||||||
echo "${red}FAIL${reset} $name"
|
echo "${red}FAIL${reset} $name"
|
||||||
@@ -71,7 +101,7 @@ vhas() {
|
|||||||
# has NAME FIXTURE REGEX — the report matches REGEX.
|
# has NAME FIXTURE REGEX — the report matches REGEX.
|
||||||
has() {
|
has() {
|
||||||
local name="$1" fixture="$2" rgx="$3"
|
local name="$1" fixture="$2" rgx="$3"
|
||||||
if printf '%s\n' "${REPORT[$fixture]}" | grep -Eq "$rgx"; then
|
if report "$fixture" | grep -Eq "$rgx"; then
|
||||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||||
else
|
else
|
||||||
echo "${red}FAIL${reset} $name"
|
echo "${red}FAIL${reset} $name"
|
||||||
@@ -86,7 +116,7 @@ has() {
|
|||||||
section_lacks() {
|
section_lacks() {
|
||||||
local name="$1" fixture="$2" header="$3" rgx="$4"
|
local name="$1" fixture="$2" header="$3" rgx="$4"
|
||||||
local body
|
local body
|
||||||
body=$(printf '%s\n' "${REPORT[$fixture]}" |
|
body=$(report "$fixture" |
|
||||||
awk -v h="$header" 'index($0, h) == 1 { f = 1; next } f && /^$/ { exit } f')
|
awk -v h="$header" 'index($0, h) == 1 { f = 1; next } f && /^$/ { exit } f')
|
||||||
if [ -z "$body" ]; then
|
if [ -z "$body" ]; then
|
||||||
echo "${red}FAIL${reset} $name — section [$header] not found"; FAIL=$((FAIL+1)); return
|
echo "${red}FAIL${reset} $name — section [$header] not found"; FAIL=$((FAIL+1)); return
|
||||||
@@ -101,7 +131,7 @@ section_lacks() {
|
|||||||
# lacks NAME FIXTURE REGEX — the report does NOT match REGEX.
|
# lacks NAME FIXTURE REGEX — the report does NOT match REGEX.
|
||||||
lacks() {
|
lacks() {
|
||||||
local name="$1" fixture="$2" rgx="$3"
|
local name="$1" fixture="$2" rgx="$3"
|
||||||
if printf '%s\n' "${REPORT[$fixture]}" | grep -Eq "$rgx"; then
|
if report "$fixture" | grep -Eq "$rgx"; then
|
||||||
echo "${red}FAIL${reset} $name — unexpected match: $rgx"; FAIL=$((FAIL+1))
|
echo "${red}FAIL${reset} $name — unexpected match: $rgx"; FAIL=$((FAIL+1))
|
||||||
else
|
else
|
||||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||||
@@ -243,7 +273,7 @@ echo
|
|||||||
echo "-- the analysis changes nothing --"
|
echo "-- the analysis changes nothing --"
|
||||||
# Coverage is a report, not a policy: a klammer whose general body cannot be
|
# Coverage is a report, not a policy: a klammer whose general body cannot be
|
||||||
# interpreted is still offered to every target, exactly as before.
|
# interpreted is still offered to every target, exactly as before.
|
||||||
out=$("$KTEXT" -k none -s '@@@target ta | Target A @@@
|
out=$("$KTEXT" --klammersets none -s '@@@target ta | Target A @@@
|
||||||
@@e : @eval 6 * 7 @ @@ x @e@' -t ta -d 2>&1 | tr -d '\n ')
|
@@e : @eval 6 * 7 @ @@ x @e@' -t ta -d 2>&1 | tr -d '\n ')
|
||||||
if [ "$out" = "x42" ]; then
|
if [ "$out" = "x42" ]; then
|
||||||
echo "${green}PASS${reset} 22. an underivable klammer still renders"; PASS=$((PASS+1))
|
echo "${green}PASS${reset} 22. an underivable klammer still renders"; PASS=$((PASS+1))
|
||||||
|
|||||||
@@ -62,7 +62,16 @@ check_eq() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# check_warn NAME EXPECTED KTEXT_ARGS... — exit 0, stdout==EXPECTED, AND a warning on stderr.
|
# check_warn NAME EXPECTED KTEXT_ARGS... — exit 0, stdout==EXPECTED, the
|
||||||
|
# override REPORTED at "-v 1" and SILENT at the default verbosity.
|
||||||
|
#
|
||||||
|
# It was a warning until 2026-08-15. ":::" exists to override, and a klammer
|
||||||
|
# set the user did not write may be overridden by design (TODO #33), so the
|
||||||
|
# notice fired on the sanctioned use of a feature: a warning nobody can act on
|
||||||
|
# is not a warning. It is still worth reporting, because the definition being
|
||||||
|
# replaced usually lives in another file in another klammerset, which the user
|
||||||
|
# cannot see from what they wrote -- so it is "-v 1" (see the output policy in
|
||||||
|
# CLAUDE.md).
|
||||||
check_warn() {
|
check_warn() {
|
||||||
local name="$1" expected="$2"; shift 2
|
local name="$1" expected="$2"; shift 2
|
||||||
local out status err
|
local out status err
|
||||||
@@ -72,8 +81,14 @@ check_warn() {
|
|||||||
if [ $status -ne 0 ]; then
|
if [ $status -ne 0 ]; then
|
||||||
echo "${red}FAIL${reset} $name — ktext exited $status"; FAIL=$((FAIL+1)); return
|
echo "${red}FAIL${reset} $name — ktext exited $status"; FAIL=$((FAIL+1)); return
|
||||||
fi
|
fi
|
||||||
if ! printf '%s' "$err" | grep -qiF "warning"; then
|
if [ -s "$ERR" ]; then
|
||||||
echo "${red}FAIL${reset} $name — expected a warning, got none"; FAIL=$((FAIL+1)); return
|
echo "${red}FAIL${reset} $name — the default run was not silent: $(head -1 "$ERR")"
|
||||||
|
FAIL=$((FAIL+1)); return
|
||||||
|
fi
|
||||||
|
"$KTEXT" "$@" -v 1 >/dev/null 2>"$ERR"
|
||||||
|
err=$(cat "$ERR")
|
||||||
|
if ! printf '%s' "$err" | grep -qiF "overridden"; then
|
||||||
|
echo "${red}FAIL${reset} $name — no override reported at -v 1"; FAIL=$((FAIL+1)); return
|
||||||
fi
|
fi
|
||||||
if [ "$out" = "$expected" ]; then
|
if [ "$out" = "$expected" ]; then
|
||||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
# mac/machine.cpp), not the SKS. A target is a MACHINE construct — declared
|
# mac/machine.cpp), not the SKS. A target is a MACHINE construct — declared
|
||||||
# with the @@@target system command, not owned by any klammer set — so the
|
# with the @@@target system command, not owned by any klammer set — so the
|
||||||
# idiomatic engine-level test defines its own fixture target inline and loads
|
# idiomatic engine-level test defines its own fixture target inline and loads
|
||||||
# no klammer set (`-k none`); it does not "avoid" the SKS so much as have no
|
# no klammer set (`--klammersets none`); it does not "avoid" the SKS so much as have no
|
||||||
# need of it. Target `t` here escapes `& -> AMP`, `_ -> UND`, `\ -> BSL`
|
# need of it. Target `t` here escapes `& -> AMP`, `_ -> UND`, `\ -> BSL`
|
||||||
# (arbitrary tokens, easy to assert). The SKS's own targets (tex, html) and
|
# (arbitrary tokens, easy to assert). The SKS's own targets (tex, html) and
|
||||||
# the specific characters they declare are exercised by the SKS suite.
|
# the specific characters they declare are exercised by the SKS suite.
|
||||||
@@ -75,29 +75,29 @@ echo "======================================================================="
|
|||||||
echo
|
echo
|
||||||
|
|
||||||
# A self-contained fixture target, defined inline via the @@@target system
|
# A self-contained fixture target, defined inline via the @@@target system
|
||||||
# command, escaping & _ \ to distinct tokens. -k none loads no klammer set,
|
# command, escaping & _ \ to distinct tokens. --klammersets none loads no klammer set,
|
||||||
# so nothing below depends on the SKS.
|
# so nothing below depends on the SKS.
|
||||||
T='@@@target t | test target :escape & AMP _ UND \ BSL @@@'
|
T='@@@target t | test target :escape & AMP _ UND \ BSL @@@'
|
||||||
|
|
||||||
check_eq " 1. top-level text: & escaped" 'A AMP B' -k none -t t -s "$T A & B"
|
check_eq " 1. top-level text: & escaped" 'A AMP B' --klammersets none -t t -s "$T A & B"
|
||||||
check_eq " 2. general klammer body: & escaped" 'A AMP B' -k none -t t -s "$T @@g : A & B @@ @g@"
|
check_eq " 2. general klammer body: & escaped" 'A AMP B' --klammersets none -t t -s "$T @@g : A & B @@ @g@"
|
||||||
check_eq " 3. general klammer body: _ escaped" 'AUNDB' -k none -t t -s "$T @@g : A_B @@ @g@"
|
check_eq " 3. general klammer body: _ escaped" 'AUNDB' --klammersets none -t t -s "$T @@g : A_B @@ @g@"
|
||||||
check_eq " 4. general klammer body: backslash escaped" 'aBSLb' -k none -t t -s "$T @@g : a\\b @@ @g@"
|
check_eq " 4. general klammer body: backslash escaped" 'aBSLb' --klammersets none -t t -s "$T @@g : a\\b @@ @g@"
|
||||||
check_eq " 5. target-specific body: NOT escaped" 'A & B' -k none -t t -s "$T @@g.k : d @@ @@g.t :: A & B @@ @g@"
|
check_eq " 5. target-specific body: NOT escaped" 'A & B' --klammersets none -t t -s "$T @@g.k : d @@ @@g.t :: A & B @@ @g@"
|
||||||
check_eq " 6. general body ^'...'^ literal: NOT escaped" 'a&b' -k none -t t -s "$T @@g : ^'a&b'^ @@ @g@"
|
check_eq " 6. general body ^'...'^ literal: NOT escaped" 'a&b' --klammersets none -t t -s "$T @@g : ^'a&b'^ @@ @g@"
|
||||||
check_eq " 7. nested target-native klammer survives escape" 'X AMP Y \newline Z' -k none -t t -s "$T @@n.t : \\newline @@ @@g : X & Y @n@ Z @@ @g@"
|
check_eq " 7. nested target-native klammer survives escape" 'X AMP Y \newline Z' --klammersets none -t t -s "$T @@n.t : \\newline @@ @@g : X & Y @n@ Z @@ @g@"
|
||||||
# 8-9: @eval inside a general body. Code (with underscores) must not be
|
# 8-9: @eval inside a general body. Code (with underscores) must not be
|
||||||
# escaped or Python breaks; the Klammertext it returns is klammer output and
|
# escaped or Python breaks; the Klammertext it returns is klammer output and
|
||||||
# must not be escaped either. chr(64) builds a literal '@' so the returned
|
# must not be escaped either. chr(64) builds a literal '@' so the returned
|
||||||
# klammer call is not parsed as one in this source line.
|
# klammer call is not parsed as one in this source line.
|
||||||
check_eq " 8. general body @eval code NOT escaped" '3' -k none -t t -s "$T @@g : @eval (1).__add__(2) @ @@ @g@"
|
check_eq " 8. general body @eval code NOT escaped" '3' --klammersets none -t t -s "$T @@g : @eval (1).__add__(2) @ @@ @g@"
|
||||||
check_eq " 9. general body @eval klammer result NOT escaped" '\textbf{hi}' -k none -t t -s "$T @@b.k s : d @@ @@b.t :: \\textbf{*s*} @@ @@g : @eval chr(64)+'b hi '+chr(64) @ @@ @g@"
|
check_eq " 9. general body @eval klammer result NOT escaped" '\textbf{hi}' --klammersets none -t t -s "$T @@b.k s : d @@ @@b.t :: \\textbf{*s*} @@ @@g : @eval chr(64)+'b hi '+chr(64) @ @@ @g@"
|
||||||
# 10-11: an @eval result that still holds klammers is a GENERATOR (Klammertext
|
# 10-11: an @eval result that still holds klammers is a GENERATOR (Klammertext
|
||||||
# with data) -- its writer text is escaped for the target before the klammers
|
# with data) -- its writer text is escaped for the target before the klammers
|
||||||
# are applied; a result with no klammers is a RENDERER (final markup) -- left
|
# are applied; a result with no klammers is a RENDERER (final markup) -- left
|
||||||
# untouched. The signal is "does the read-back result contain a klammer".
|
# untouched. The signal is "does the read-back result contain a klammer".
|
||||||
check_eq "10. @eval generator: klammer result's data escaped" '[a AMP b]' -k none -t t -s "$T @@wrap z : [*z*] @@ @@g : @eval chr(64)+'wrap a & b '+chr(64) @ @@ @g@"
|
check_eq "10. @eval generator: klammer result's data escaped" '[a AMP b]' --klammersets none -t t -s "$T @@wrap z : [*z*] @@ @@g : @eval chr(64)+'wrap a & b '+chr(64) @ @@ @g@"
|
||||||
check_eq "11. @eval renderer: final markup NOT escaped" 'raw & markup' -k none -t t -s "$T @@g : @eval 'raw & markup' @ @@ @g@"
|
check_eq "11. @eval renderer: final markup NOT escaped" 'raw & markup' --klammersets none -t t -s "$T @@g : @eval 'raw & markup' @ @@ @g@"
|
||||||
|
|
||||||
# 12-18: quoted KLAMMERTEXT specials (^@ ^| ^# ^^ ^: ^*) and ^'...'^ literal
|
# 12-18: quoted KLAMMERTEXT specials (^@ ^| ^# ^^ ^: ^*) and ^'...'^ literal
|
||||||
# regions. The katomizer strips the "^"; hide_special_katoms() and
|
# regions. The katomizer strips the "^"; hide_special_katoms() and
|
||||||
@@ -108,21 +108,21 @@ check_eq "11. @eval renderer: final markup NOT escaped" 'raw & markup'
|
|||||||
# leaked as a bare apply-end katom into re-read text ("A klammer ends
|
# leaked as a bare apply-end katom into re-read text ("A klammer ends
|
||||||
# without a beginning"). Exact-match expectations also guard against KTESC
|
# without a beginning"). Exact-match expectations also guard against KTESC
|
||||||
# markers leaking into output.
|
# markers leaking into output.
|
||||||
check_eq "12. quoted @ | # resolve to the characters" 'x @ | # y' -k none -t t -s "$T x ^@ ^| ^# y"
|
check_eq "12. quoted @ | # resolve to the characters" 'x @ | # y' --klammersets none -t t -s "$T x ^@ ^| ^# y"
|
||||||
check_eq "13. quoted ^ : * resolve to the characters" 'x ^ : * y' -k none -t t -s "$T x ^^ ^: ^* y"
|
check_eq "13. quoted ^ : * resolve to the characters" 'x ^ : * y' --klammersets none -t t -s "$T x ^^ ^: ^* y"
|
||||||
check_eq "14. ^'...'^ region: specials stay literal" 'a @ | b' -k none -t t -s "$T a ^' @ | '^ b"
|
check_eq "14. ^'...'^ region: specials stay literal" 'a @ | b' --klammersets none -t t -s "$T a ^' @ | '^ b"
|
||||||
check_eq "15. general body: quoted @ resolves" 'x @ y' -k none -t t -s "$T @@g : x ^@ y @@ @g@"
|
check_eq "15. general body: quoted @ resolves" 'x @ y' --klammersets none -t t -s "$T @@g : x ^@ y @@ @g@"
|
||||||
# 16: inside an @eval argument span a quoted special reaches the CODE as the
|
# 16: inside an @eval argument span a quoted special reaches the CODE as the
|
||||||
# character (the span is skipped by hide_special_katoms, like the escape pass).
|
# character (the span is skipped by hide_special_katoms, like the escape pass).
|
||||||
check_eq "16. @eval code: quoted : reaches shell as ':'" 'x:y' -k none -t t -s "$T @@g : @eval :shell echo x^:y @ @@ @g@"
|
check_eq "16. @eval code: quoted : reaches shell as ':'" 'x:y' --klammersets none -t t -s "$T @@g : @eval :shell echo x^:y @ @@ @g@"
|
||||||
# 17: an @eval result emitting the two characters ^ @ is re-read as a quoted
|
# 17: an @eval result emitting the two characters ^ @ is re-read as a quoted
|
||||||
# special and survives to the output as a literal @ (the generator idiom for
|
# special and survives to the output as a literal @ (the generator idiom for
|
||||||
# a literal @; a bare @ in a result is a parse error by design).
|
# a literal @; a bare @ in a result is a parse error by design).
|
||||||
check_eq "17. @eval result ^@ survives read-back as @" '@' -k none -t t -s "$T @@g : @eval chr(94)+chr(64) @ @@ @g@"
|
check_eq "17. @eval result ^@ survives read-back as @" '@' --klammersets none -t t -s "$T @@g : @eval chr(94)+chr(64) @ @@ @g@"
|
||||||
# 18: a bare-Python :after_apply phase receives the RESOLVED result text
|
# 18: a bare-Python :after_apply phase receives the RESOLVED result text
|
||||||
# (K_result) and its return is taken as raw target text, not re-read as
|
# (K_result) and its return is taken as raw target text, not re-read as
|
||||||
# Klammertext -- a resolved @ in the result must not be re-parsed.
|
# Klammertext -- a resolved @ in the result must not be re-parsed.
|
||||||
check_eq "18. :after_apply phase: raw result, @ intact" 'A @ B' -k none -t u -s '@@@target u | up :after_apply string.capwords @@@ a ^@ b'
|
check_eq "18. :after_apply phase: raw result, @ intact" 'A @ B' --klammersets none -t u -s '@@@target u | up :after_apply string.capwords @@@ a ^@ b'
|
||||||
|
|
||||||
rm -f "$ERR"
|
rm -f "$ERR"
|
||||||
|
|
||||||
|
|||||||
226
tst/eval_test.sh
Executable file
226
tst/eval_test.sh
Executable file
@@ -0,0 +1,226 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# eval_test.sh — the @eval primitive's contract with the outside world.
|
||||||
|
#
|
||||||
|
# @eval is the one primitive that reaches OUT of Klammertext, and until
|
||||||
|
# 2026-08-15 nothing tested what it did with what came back. ":shell" is
|
||||||
|
# covered here; the other modes (Python, :cpp, :haskell) are exercised
|
||||||
|
# incidentally by other suites and can grow into this one.
|
||||||
|
#
|
||||||
|
# The two defects this suite exists to hold shut, both of them the same shape
|
||||||
|
# as a msg() on the wrong stream -- output nobody chose to see, and a failure
|
||||||
|
# nobody was told about:
|
||||||
|
#
|
||||||
|
# * The command's STDERR went straight to the user's terminal, unattributed
|
||||||
|
# and unsuppressable. It is not the command's output in any of the three
|
||||||
|
# policy categories (CLAUDE.md, "Command output policy"): it belongs to a
|
||||||
|
# subprocess a klammer invoked, at a location the Locator can name. It is
|
||||||
|
# now captured and reported at "-v 1".
|
||||||
|
# * The EXIT STATUS was discarded, so a command that failed contributed its
|
||||||
|
# partial output (or nothing) to the document and said nothing at all.
|
||||||
|
#
|
||||||
|
# NOT to be confused with "eval_test", the C++ diagnostic program built from
|
||||||
|
# eval_test.cpp in this directory: that one constructs engine objects and prints
|
||||||
|
# what it gets, for a person to read, and asserts nothing (see the `smoke`
|
||||||
|
# target in tst/Makefile). This is the regression suite. The ".sh" is the only
|
||||||
|
# thing distinguishing them, and it is the first such collision in tst/.
|
||||||
|
#
|
||||||
|
# Usage: ./eval_test.sh (needs KLAMMERTEXT_HOME set; ktext on PATH)
|
||||||
|
# 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'
|
||||||
|
|
||||||
|
OUTF=$(mktemp /tmp/eval_out.XXXXXX)
|
||||||
|
ERRF=$(mktemp /tmp/eval_err.XXXXXX)
|
||||||
|
trap 'rm -f "$OUTF" "$ERRF"' EXIT
|
||||||
|
|
||||||
|
plain() { sed 's/\x1b\[[0-9;]*m//g'; }
|
||||||
|
|
||||||
|
# run SOURCE [EXTRA...] — sets STATUS, and fills OUTF/ERRF.
|
||||||
|
STATUS=0
|
||||||
|
run() {
|
||||||
|
local src="$1"; shift
|
||||||
|
"$KTEXT" --klammersets none -s "$src" -d "$@" >"$OUTF" 2>"$ERRF"
|
||||||
|
STATUS=$?
|
||||||
|
}
|
||||||
|
|
||||||
|
pass() { echo "${green}PASS${reset} $1"; PASS=$((PASS + 1)); }
|
||||||
|
fail() { echo "${red}FAIL${reset} $1"; [ -n "$2" ] && echo " $2"; FAIL=$((FAIL + 1)); }
|
||||||
|
|
||||||
|
echo "${bold}@eval tests${reset}"
|
||||||
|
echo "==========="
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "-- :shell, the ordinary case --"
|
||||||
|
run '@eval :shell echo hello @'
|
||||||
|
if [ $STATUS -eq 0 ] && [ "$(cat "$OUTF")" = "hello" ]; then
|
||||||
|
pass " 1. the command's stdout becomes document text"
|
||||||
|
else
|
||||||
|
fail " 1. exit $STATUS, stdout [$(cat "$OUTF")]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- stderr belongs to the command, not to the terminal --"
|
||||||
|
# A signal death is not an error exit, and a crash must never read as a pass:
|
||||||
|
# every case here checks the status numerically.
|
||||||
|
run '@eval :shell echo OUT; echo NOISE >&2 @'
|
||||||
|
if [ $STATUS -eq 0 ] && [ "$(cat "$OUTF")" = "OUT" ]; then
|
||||||
|
pass " 2. stdout is the document; stderr is not in it"
|
||||||
|
else
|
||||||
|
fail " 2. exit $STATUS, stdout [$(cat "$OUTF")]"
|
||||||
|
fi
|
||||||
|
if [ ! -s "$ERRF" ]; then
|
||||||
|
pass " 3. ... and nothing leaks to the terminal at the default verbosity"
|
||||||
|
else
|
||||||
|
fail " 3. stderr leaked: $(head -1 "$ERRF")"
|
||||||
|
fi
|
||||||
|
run '@eval :shell echo OUT; echo NOISE >&2 @' -v 1
|
||||||
|
if grep -q "NOISE" "$ERRF"; then
|
||||||
|
pass " 4. ... while -v 1 reports what the command said"
|
||||||
|
else
|
||||||
|
fail " 4. -v 1 did not report the command's stderr" "$(head -2 "$ERRF")"
|
||||||
|
fi
|
||||||
|
if grep -qi "stderr" "$ERRF"; then
|
||||||
|
pass " 5. ... and says that is what it is"
|
||||||
|
else
|
||||||
|
fail " 5. the -v 1 report does not identify the stream"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- a failing command is an error, not silence --"
|
||||||
|
run '@eval :shell exit 3 @'
|
||||||
|
if [ $STATUS -ne 0 ] && [ $STATUS -lt 128 ]; then
|
||||||
|
pass " 6. a nonzero exit status fails the run"
|
||||||
|
else
|
||||||
|
fail " 6. exit $STATUS (128+ would be a signal death, 0 a silent pass)"
|
||||||
|
fi
|
||||||
|
for want in "exit status 3" "The shell command failed"; do
|
||||||
|
if grep -qF "$want" "$ERRF"; then
|
||||||
|
pass " 7. the error says [$want]"
|
||||||
|
else
|
||||||
|
fail " 7. the error does not say [$want]" "$(plain < "$ERRF" | head -2)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
# What the command itself reported is the useful half of the diagnosis.
|
||||||
|
run '@eval :shell echo WHY-IT-FAILED >&2; exit 1 @'
|
||||||
|
if grep -qF "WHY-IT-FAILED" "$ERRF"; then
|
||||||
|
pass " 8. ... and includes what the command wrote to stderr"
|
||||||
|
else
|
||||||
|
fail " 8. the command's own message was dropped" "$(plain < "$ERRF" | head -3)"
|
||||||
|
fi
|
||||||
|
# An error is category 2: stderr, and nothing on stdout to confuse a pipe.
|
||||||
|
if [ ! -s "$OUTF" ]; then
|
||||||
|
pass " 9. ... and leaves stdout empty"
|
||||||
|
else
|
||||||
|
fail " 9. stdout carried [$(head -c 60 "$OUTF")]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- the escape hatch, because some commands exit nonzero on purpose --"
|
||||||
|
# "grep" finding no match is the usual one. Strictness with an explicit way to
|
||||||
|
# say "I meant that" is the same shape as the @cond predicate rule.
|
||||||
|
run '@eval :shell exit 3 @'
|
||||||
|
if grep -qF "|| true" "$ERRF"; then
|
||||||
|
pass "10. the error names the way to say a nonzero status was intended"
|
||||||
|
else
|
||||||
|
fail "10. the error does not offer the remedy" "$(plain < "$ERRF" | head -3)"
|
||||||
|
fi
|
||||||
|
# NOT "exit N || true": exit terminates the shell before "||" is reached, so
|
||||||
|
# that spelling cannot work and is not what the message suggests. A command
|
||||||
|
# that merely RETURNS nonzero is the case the remedy is for.
|
||||||
|
run '@eval :shell echo kept; grep -q zzz /dev/null || true @'
|
||||||
|
if [ $STATUS -eq 0 ] && [ "$(cat "$OUTF")" = "kept" ]; then
|
||||||
|
pass "11. ... and it works"
|
||||||
|
else
|
||||||
|
fail "11. exit $STATUS, stdout [$(cat "$OUTF")]"
|
||||||
|
fi
|
||||||
|
# The same command without the remedy is an error, or case 11 proves nothing.
|
||||||
|
run '@eval :shell echo kept; grep -q zzz /dev/null @'
|
||||||
|
if [ $STATUS -ne 0 ] && [ $STATUS -lt 128 ]; then
|
||||||
|
pass "11a. ... and without it the same command fails"
|
||||||
|
else
|
||||||
|
fail "11a. exit $STATUS — expected a nonzero, non-signal exit"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- the command may contain its own pipeline --"
|
||||||
|
# The redirection that captures stderr must not disturb the writer's command.
|
||||||
|
run '@eval :shell echo one two three | tr " " "-" @'
|
||||||
|
if [ $STATUS -eq 0 ] && [ "$(cat "$OUTF")" = "one-two-three" ]; then
|
||||||
|
pass "12. a pipeline inside the command still works"
|
||||||
|
else
|
||||||
|
fail "12. exit $STATUS, stdout [$(cat "$OUTF")]"
|
||||||
|
fi
|
||||||
|
run '@eval :shell echo a > /dev/null; echo b @'
|
||||||
|
if [ $STATUS -eq 0 ] && [ "$(cat "$OUTF")" = "b" ]; then
|
||||||
|
pass "13. ... and so does a redirection of its own"
|
||||||
|
else
|
||||||
|
fail "13. exit $STATUS, stdout [$(cat "$OUTF")]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- the other modes still work --"
|
||||||
|
run '@eval 6*7 @'
|
||||||
|
if [ $STATUS -eq 0 ] && [ "$(cat "$OUTF")" = "42" ]; then
|
||||||
|
pass "14. a Python expression"
|
||||||
|
else
|
||||||
|
fail "14. exit $STATUS, stdout [$(cat "$OUTF")]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- :haskell, the same contract --"
|
||||||
|
# Skipped where GHC is absent: runghc is an optional dependency (the
|
||||||
|
# akopra/klammertext:haskell image, or a local GHCup install), and a suite that
|
||||||
|
# fails for its absence would be reporting the machine, not the code.
|
||||||
|
if ! command -v runghc >/dev/null 2>&1; then
|
||||||
|
echo "SKIP 15-18. :haskell (runghc not installed)"
|
||||||
|
else
|
||||||
|
# The defect: runghc ran with "2>&1", so on a SUCCESSFUL run everything the
|
||||||
|
# program or GHC wrote to stderr was merged into the result and became part
|
||||||
|
# of the document.
|
||||||
|
HS='@eval :haskell import System.IO
|
||||||
|
main = hPutStrLn stderr "HS-NOISE" >> putStrLn "HS-OUT" @'
|
||||||
|
run "$HS"
|
||||||
|
if [ $STATUS -eq 0 ] && [ "$(cat "$OUTF")" = "HS-OUT" ]; then
|
||||||
|
pass "15. the program's stdout is the document; its stderr is not"
|
||||||
|
else
|
||||||
|
fail "15. exit $STATUS, stdout [$(cat "$OUTF")]"
|
||||||
|
fi
|
||||||
|
run "$HS" -v 1
|
||||||
|
if grep -q "HS-NOISE" "$ERRF"; then
|
||||||
|
pass "16. ... and -v 1 reports what it wrote to stderr"
|
||||||
|
else
|
||||||
|
fail "16. -v 1 did not report it" "$(plain < "$ERRF" | head -2)"
|
||||||
|
fi
|
||||||
|
# A compile error was ALREADY reported rather than swallowed -- the exit
|
||||||
|
# status was checked -- so this pins behaviour that was right, and that the
|
||||||
|
# detail now comes from the captured stderr rather than a merged stream.
|
||||||
|
run '@eval :haskell main = putStrLn (1 + "x") @'
|
||||||
|
if [ $STATUS -ne 0 ] && [ $STATUS -lt 128 ] && grep -qi "error" "$ERRF"; then
|
||||||
|
pass "17. a compile error fails the run and shows what runghc said"
|
||||||
|
else
|
||||||
|
fail "17. exit $STATUS" "$(plain < "$ERRF" | head -3)"
|
||||||
|
fi
|
||||||
|
# A program that compiles, runs, and then exits nonzero is the other half.
|
||||||
|
run '@eval :haskell import System.Exit
|
||||||
|
main = putStrLn "partial" >> exitWith (ExitFailure 3) @'
|
||||||
|
if [ $STATUS -ne 0 ] && grep -qF "exit status 3" "$ERRF"; then
|
||||||
|
pass "18. a nonzero exit from the program itself is reported too"
|
||||||
|
else
|
||||||
|
fail "18. exit $STATUS" "$(plain < "$ERRF" | head -3)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "==========="
|
||||||
|
echo "Results: ${PASS} passed, ${FAIL} failed"
|
||||||
|
[ "$FAIL" -eq 0 ] || exit 1
|
||||||
|
exit 0
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
# regrouping is announced on stderr.
|
# regrouping is announced on stderr.
|
||||||
# - A leading "~/" expands to $HOME (shells do not expand a quoted tilde).
|
# - A leading "~/" expands to $HOME (shells do not expand a quoted tilde).
|
||||||
#
|
#
|
||||||
# Engine tier: uses -k none; no SKS.
|
# Engine tier: uses --klammersets none; no SKS.
|
||||||
#
|
#
|
||||||
# Usage: ./filename_test.sh
|
# Usage: ./filename_test.sh
|
||||||
# Exit code: 0 if all tests pass, 1 otherwise.
|
# Exit code: 0 if all tests pass, 1 otherwise.
|
||||||
@@ -98,36 +98,49 @@ echo "======================="
|
|||||||
|
|
||||||
check "1. quoted filename with a space is one file" \
|
check "1. quoted filename with a space is one file" \
|
||||||
"ALPHA" "" \
|
"ALPHA" "" \
|
||||||
"my file.kt" -d -k none
|
"my file.kt" -d --klammersets none
|
||||||
|
|
||||||
check "2. unquoted spaces rescued into an existing file (announced)" \
|
# The regrouping is a DERIVED value -- the command completed a name the input
|
||||||
|
# left ambiguous -- so since 2026-08-15 it is reported at "-v 1" and the
|
||||||
|
# default run is silent. Silence is safe because a rescue only succeeds when
|
||||||
|
# the joined name names an existing file: a mistyped name fails instead of
|
||||||
|
# resolving to something else.
|
||||||
|
check "2. unquoted spaces are rescued into an existing file" \
|
||||||
|
"ALPHA" "" \
|
||||||
|
my file.kt -d --klammersets none
|
||||||
|
|
||||||
|
check "2a. ... silently at the default verbosity" \
|
||||||
|
"ALPHA" "" \
|
||||||
|
my file.kt -d --klammersets none
|
||||||
|
|
||||||
|
check "2b. ... and reported at -v 1" \
|
||||||
"ALPHA" "interpreting \"my file.kt\" as one filename" \
|
"ALPHA" "interpreting \"my file.kt\" as one filename" \
|
||||||
my file.kt -d -k none
|
my file.kt -d --klammersets none -v 1
|
||||||
|
|
||||||
check "3. standalone / separates a filename list" \
|
check "3. standalone / separates a filename list" \
|
||||||
"ALPHA
|
"ALPHA
|
||||||
BETA" "" \
|
BETA" "" \
|
||||||
my file.kt / b.kt -d -k none
|
my file.kt / b.kt -d --klammersets none
|
||||||
|
|
||||||
check "4. space in a directory component" \
|
check "4. space in a directory component" \
|
||||||
"GAMMA" "" \
|
"GAMMA" "" \
|
||||||
"my dir/c.kt" -d -k none
|
"my dir/c.kt" -d --klammersets none
|
||||||
|
|
||||||
check "5. quoted name that exists is never split (b.kt also exists)" \
|
check "5. quoted name that exists is never split (b.kt also exists)" \
|
||||||
"ALPHA" "" \
|
"ALPHA" "" \
|
||||||
"my file.kt" -d -k none
|
"my file.kt" -d --klammersets none
|
||||||
|
|
||||||
HOME="$DIR" check "6. quoted ~/ expands to \$HOME inside ktext" \
|
HOME="$DIR" check "6. quoted ~/ expands to \$HOME inside ktext" \
|
||||||
"BETA" "" \
|
"BETA" "" \
|
||||||
"~/b.kt" -d -k none
|
"~/b.kt" -d --klammersets none
|
||||||
|
|
||||||
check "7. @read argument keeps its internal space" \
|
check "7. @read argument keeps its internal space" \
|
||||||
"ALPHA" "" \
|
"ALPHA" "" \
|
||||||
-s '@read my file.kt @' -d -k none
|
-s '@read my file.kt @' -d --klammersets none
|
||||||
|
|
||||||
check_error "8. unrescuable name is reported as written" \
|
check_error "8. unrescuable name is reported as written" \
|
||||||
"no such.kt" \
|
"no such.kt" \
|
||||||
"no such.kt" -d -k none
|
"no such.kt" -d --klammersets none
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "======================="
|
echo "======================="
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
# * a flag a user reaches for often gets a single letter (-k klammers,
|
# * a flag a user reaches for often gets a single letter (-k klammers,
|
||||||
# -t targets, -c character codes, -i input); a more specialised topic gets
|
# -t targets, -c character codes, -i input); a more specialised topic gets
|
||||||
# a multi-letter name (--argtypes, --katoms, --rewrite, --optionsets,
|
# a multi-letter name (--argtypes, --katoms, --rewrite, --optionsets,
|
||||||
# --coverage, --klammerset, --font);
|
# --coverage, --klammersets, --font);
|
||||||
# * -v says how much to show about the command's PROCESSING and never what
|
# * -v says how much to show about the command's PROCESSING and never what
|
||||||
# its RESULT contains. So the katom regex column is "--katoms full" and
|
# its RESULT contains. So the katom regex column is "--katoms full" and
|
||||||
# the coverage file column is "--coverage all", not verbosity levels.
|
# the coverage file column is "--coverage all", not verbosity levels.
|
||||||
@@ -34,11 +34,25 @@ reset=$'\033[0m'
|
|||||||
|
|
||||||
plain() { sed 's/\x1b\[[0-9;]*m//g'; }
|
plain() { sed 's/\x1b\[[0-9;]*m//g'; }
|
||||||
|
|
||||||
|
# GNU coreutils' "timeout" is NOT present on macOS, and Homebrew's is named
|
||||||
|
# "gtimeout", so a bare "timeout" made this suite fail WHOLESALE there -- every
|
||||||
|
# case, because the command never ran at all (found on Olion, 2026-08-16). The
|
||||||
|
# guard is a safety net against a hung command, not part of what is being
|
||||||
|
# tested, so it is optional: bound the command where the tool exists, run it
|
||||||
|
# directly where it does not.
|
||||||
|
if command -v timeout >/dev/null 2>&1; then
|
||||||
|
limited() { timeout 60 "$@"; }
|
||||||
|
elif command -v gtimeout >/dev/null 2>&1; then
|
||||||
|
limited() { gtimeout 60 "$@"; }
|
||||||
|
else
|
||||||
|
limited() { "$@"; }
|
||||||
|
fi
|
||||||
|
|
||||||
# shows NAME PATTERN CMD... — exits 0 and the output matches PATTERN.
|
# shows NAME PATTERN CMD... — exits 0 and the output matches PATTERN.
|
||||||
shows() {
|
shows() {
|
||||||
local name="$1" pattern="$2"; shift 2
|
local name="$1" pattern="$2"; shift 2
|
||||||
local out status
|
local out status
|
||||||
out=$(timeout 60 "$@" 2>&1 | plain); status=$?
|
out=$(limited "$@" 2>&1 | plain); status=$?
|
||||||
if [ $status -gt 128 ]; then
|
if [ $status -gt 128 ]; then
|
||||||
echo "${red}FAIL${reset} $name — died by signal $((status-128))"; FAIL=$((FAIL+1)); return
|
echo "${red}FAIL${reset} $name — died by signal $((status-128))"; FAIL=$((FAIL+1)); return
|
||||||
fi
|
fi
|
||||||
@@ -53,7 +67,7 @@ shows() {
|
|||||||
# absent NAME PATTERN CMD... — the output does NOT match PATTERN.
|
# absent NAME PATTERN CMD... — the output does NOT match PATTERN.
|
||||||
absent() {
|
absent() {
|
||||||
local name="$1" pattern="$2"; shift 2
|
local name="$1" pattern="$2"; shift 2
|
||||||
if timeout 60 "$@" 2>&1 | plain | grep -Eq -- "$pattern"; then
|
if limited "$@" 2>&1 | plain | grep -Eq -- "$pattern"; then
|
||||||
echo "${red}FAIL${reset} $name — unexpected match [$pattern]"; FAIL=$((FAIL+1))
|
echo "${red}FAIL${reset} $name — unexpected match [$pattern]"; FAIL=$((FAIL+1))
|
||||||
else
|
else
|
||||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||||
@@ -93,17 +107,17 @@ absent "12. --coverage alone omits it" 'sks/table/table\.k' "$KDESC"
|
|||||||
# reason that has nothing to do with what it is testing. (It did: adopting
|
# reason that has nothing to do with what it is testing. (It did: adopting
|
||||||
# ".*" in the SKS made "All targets, declared" non-empty.)
|
# ".*" in the SKS made "All targets, declared" non-empty.)
|
||||||
shows "12a. --coverage all shows an empty reporting category" \
|
shows "12a. --coverage all shows an empty reporting category" \
|
||||||
'All targets, declared \(0\)' "$KDESC" -i "$DIR/cycle.k" --coverage all
|
'All targets, declared \(0\)' "$KDESC" --klammersets none -i "$DIR/cycle.k" --coverage all
|
||||||
absent "12b. ... hidden without it" \
|
absent "12b. ... hidden without it" \
|
||||||
'All targets, declared \(0\)' "$KDESC" -i "$DIR/cycle.k" --coverage
|
'All targets, declared \(0\)' "$KDESC" --klammersets none -i "$DIR/cycle.k" --coverage
|
||||||
absent "12c. an empty problem category stays hidden" \
|
absent "12c. an empty problem category stays hidden" \
|
||||||
'Declared but never defined \(0\)' "$KDESC" -i "$DIR/cycle.k" --coverage all
|
'Declared but never defined \(0\)' "$KDESC" --klammersets none -i "$DIR/cycle.k" --coverage all
|
||||||
absent "13. -v adds no result detail" 'Regex' "$KDESC" --katoms -v 3
|
absent "13. -v adds no result detail" 'Regex' "$KDESC" --katoms -v 3
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "-- -v is about processing only --"
|
echo "-- -v is about processing only --"
|
||||||
shows "14. -v names the input it read" 'input_filenames' "$KDESC" -t -v 1
|
shows "14. -v names the input it read" 'input_filenames' "$KDESC" -i "$DIR/clean.k" -t -v 1
|
||||||
absent "15. ... and is silent without it" 'input_filenames' "$KDESC" -t
|
absent "15. ... and is silent without it" 'input_filenames' "$KDESC" -i "$DIR/clean.k" -t
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "-- the klammer search --"
|
echo "-- the klammer search --"
|
||||||
@@ -126,13 +140,46 @@ shows "25. --katoms help explains" 'Katom commands' "$KDESC" --katom
|
|||||||
shows "26. an unknown coverage word" 'Unrecognized coverage command' "$KDESC" --coverage nonsense
|
shows "26. an unknown coverage word" 'Unrecognized coverage command' "$KDESC" --coverage nonsense
|
||||||
shows "27. an unknown katom word" 'Unrecognized katom command' "$KDESC" --katoms nonsense
|
shows "27. an unknown katom word" 'Unrecognized katom command' "$KDESC" --katoms nonsense
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- provenance: -i shows what the INPUT defines --"
|
||||||
|
# A klammerset is loaded so the input can be ANALYSED against it, but it is
|
||||||
|
# not what the user asked about. A designer wants the klammers this file
|
||||||
|
# defines; a reader of an unfamiliar document wants the custom ones it
|
||||||
|
# carries. So "-i" restricts every listing to definitions from outside the
|
||||||
|
# loaded klammersets -- while the klammersets stay loaded.
|
||||||
|
PROV=$(mktemp /tmp/kdesc_prov.XXXXXX.k)
|
||||||
|
printf '@@shout.k s : Say it loudly @@\n@@shout.html :: <b>*s*</b> @@\n' > "$PROV"
|
||||||
|
shows "32. -i lists the input's own klammer" '@shout' "$KDESC" -i "$PROV" -k
|
||||||
|
absent "33. ... and not the klammerset's" '@table' "$KDESC" -i "$PROV" -k
|
||||||
|
shows "34. without -i the klammerset is shown" '@table' "$KDESC" -k
|
||||||
|
# The klammerset is still LOADED: a coverage derived from a klammer it
|
||||||
|
# supplies proves the analysis saw it even though the report does not.
|
||||||
|
DERIVE=$(mktemp /tmp/kdesc_derive.XXXXXX.k)
|
||||||
|
printf '@@wave.k s : Wave @@\n@@wave :: @i *s* @ @@\n' > "$DERIVE"
|
||||||
|
shows "35. coverage is derived through the unlisted klammerset" \
|
||||||
|
'@wave +html pdf tex txt +from @i' "$KDESC" -i "$DERIVE" --coverage
|
||||||
|
absent "36. ... and the klammerset is not reported" '@table' "$KDESC" -i "$DERIVE" --coverage
|
||||||
|
# A klammer the input REDEFINES came from the klammerset, but the input
|
||||||
|
# changed it -- which is exactly what a reader needs to know.
|
||||||
|
REDEF=$(mktemp /tmp/kdesc_redef.XXXXXX.k)
|
||||||
|
printf '@@i.html ::: <em class="mine">*text*</em> @@\n' > "$REDEF"
|
||||||
|
shows "37. a redefined klammer is the input's too" '@i' "$KDESC" -i "$REDEF" -k
|
||||||
|
# An input that declares its own klammerset is still the input.
|
||||||
|
DECL=$(mktemp -d /tmp/kdesc_decl.XXXXXX)
|
||||||
|
mkdir -p "$DECL/own"
|
||||||
|
printf '@@@klammerset own | A designer set @@@\n@@yell.k s : Yell @@\n@@yell.html :: <b>*s*</b> @@\n' \
|
||||||
|
> "$DECL/own/own.k"
|
||||||
|
shows "38. an input declaring a klammerset is not filtered out" \
|
||||||
|
'@yell' "$KDESC" -i "$DECL/own/own.k" -k
|
||||||
|
rm -rf "$PROV" "$DERIVE" "$REDEF" "$DECL"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "-- the usage text --"
|
echo "-- the usage text --"
|
||||||
shows "28. -k shows its optional argument" '\-k \[<text>\]' "$KDESC"
|
shows "28. -k shows its optional argument" '\-k \[<text>\]' "$KDESC"
|
||||||
shows "29. -i shows its filename" '\-i <filename>' "$KDESC"
|
shows "29. -i shows its filename" '\-i <filename>' "$KDESC"
|
||||||
# Ordered by likely use: the single letters come before the long names.
|
# Ordered by likely use: the single letters come before the long names.
|
||||||
# Line numbers, not a multi-line pattern -- grep is line-oriented.
|
# Line numbers, not a multi-line pattern -- grep is line-oriented.
|
||||||
usage=$(timeout 60 "$KDESC" 2>&1 | plain)
|
usage=$(limited "$KDESC" 2>&1 | plain)
|
||||||
k_line=$(printf '%s\n' "$usage" | grep -n -- '-k \[<text>\]' | head -1 | cut -d: -f1)
|
k_line=$(printf '%s\n' "$usage" | grep -n -- '-k \[<text>\]' | head -1 | cut -d: -f1)
|
||||||
katoms_line=$(printf '%s\n' "$usage" | grep -n -- '--katoms' | head -1 | cut -d: -f1)
|
katoms_line=$(printf '%s\n' "$usage" | grep -n -- '--katoms' | head -1 | cut -d: -f1)
|
||||||
if [ -n "$k_line" ] && [ -n "$katoms_line" ] && [ "$k_line" -lt "$katoms_line" ]; then
|
if [ -n "$k_line" ] && [ -n "$katoms_line" ] && [ "$k_line" -lt "$katoms_line" ]; then
|
||||||
@@ -144,7 +191,7 @@ fi
|
|||||||
|
|
||||||
echo
|
echo
|
||||||
echo "-- a no-result search is not an error --"
|
echo "-- a no-result search is not an error --"
|
||||||
timeout 60 "$KDESC" -k zzqq >/dev/null 2>&1
|
limited "$KDESC" -k zzqq >/dev/null 2>&1
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
echo "${green}PASS${reset} 31. finding nothing exits 0"; PASS=$((PASS+1))
|
echo "${green}PASS${reset} 31. finding nothing exits 0"; PASS=$((PASS+1))
|
||||||
else
|
else
|
||||||
|
|||||||
311
tst/kdiag_test.sh
Executable file
311
tst/kdiag_test.sh
Executable file
@@ -0,0 +1,311 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# kdiag_test.sh — the kdiag command's interface.
|
||||||
|
#
|
||||||
|
# kdiag is the PROGRAMMER's command in the 2026-08-14 three-command redesign:
|
||||||
|
# ktext renders a document for an author, kdesc describes a klammer set for a
|
||||||
|
# designer, kdiag dissects katoms and the Machine. It had no suite of its own,
|
||||||
|
# which is how four defects survived in it -- including a "--check" that
|
||||||
|
# reported "0 diagnostics, 0 errors" for every input there is, while every
|
||||||
|
# other suite stayed green. An absent test is what let that read as a pass, so
|
||||||
|
# each of the four is pinned here by OUTCOME.
|
||||||
|
#
|
||||||
|
# Its peculiarities, and why each is what it is:
|
||||||
|
#
|
||||||
|
# * Its input positional is OPTIONAL, so "kdiag --machine" answers a question
|
||||||
|
# about the Machine rather than about a document.
|
||||||
|
# * --klammer registers the @@ tier and --system the @@@ tier; --process does
|
||||||
|
# both. A registered definition's katoms are CONSUMED, so they show up
|
||||||
|
# only under --replaced -- their disappearance is the evidence.
|
||||||
|
# * Extraction is TOLERANT here and nowhere else. The tiers are ordered (a
|
||||||
|
# klammer names a target), so running one without the other leaves
|
||||||
|
# definitions that cannot be registered. In kdiag that is not an error:
|
||||||
|
# the definition is skipped and its katoms stay visible, which IS the
|
||||||
|
# report. ktext must keep throwing on the same input, and the contrast is
|
||||||
|
# asserted, not assumed.
|
||||||
|
# * It deliberately has NO --klammersets. A klammerset arrives the way
|
||||||
|
# anything else does, "@read sks/sks.k @": a debugger must not depend on
|
||||||
|
# the machinery it debugs, so kdiag still runs when symbol resolution is
|
||||||
|
# what broke.
|
||||||
|
#
|
||||||
|
# Engine tier: no klammerset is loaded, so every fixture defines what it needs
|
||||||
|
# inline. The one case that wants the SKS reads it by path.
|
||||||
|
#
|
||||||
|
# Usage: ./kdiag_test.sh (needs KLAMMERTEXT_HOME set; kdiag and ktext on PATH)
|
||||||
|
# Exit code: 0 if all tests pass, 1 otherwise.
|
||||||
|
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
KDIAG=kdiag
|
||||||
|
KTEXT=ktext
|
||||||
|
K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set}
|
||||||
|
|
||||||
|
red=$'\033[31m'
|
||||||
|
green=$'\033[32m'
|
||||||
|
bold=$'\033[1m'
|
||||||
|
reset=$'\033[0m'
|
||||||
|
|
||||||
|
plain() { printf '%s' "$1" | sed 's/\x1b\[[0-9;]*m//g'; }
|
||||||
|
|
||||||
|
# GNU coreutils' "timeout" is NOT present on macOS, and Homebrew's is named
|
||||||
|
# "gtimeout", so a bare "timeout" made this suite fail WHOLESALE there -- every
|
||||||
|
# case, because the command never ran at all (found on Olion, 2026-08-16). The
|
||||||
|
# guard is a safety net against a hung command, not part of what is being
|
||||||
|
# tested, so it is optional: bound the command where the tool exists, run it
|
||||||
|
# directly where it does not.
|
||||||
|
if command -v timeout >/dev/null 2>&1; then
|
||||||
|
limited() { timeout 60 "$@"; }
|
||||||
|
elif command -v gtimeout >/dev/null 2>&1; then
|
||||||
|
limited() { gtimeout 60 "$@"; }
|
||||||
|
else
|
||||||
|
limited() { "$@"; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
# run KDIAG_ARGS... — sets OUT (colour codes stripped) and STATUS. The status
|
||||||
|
# must be taken from the command itself: a pipeline inside a command
|
||||||
|
# substitution reports the SED's status, so "kdiag | plain" would call every
|
||||||
|
# failure a success. Strip afterwards instead.
|
||||||
|
OUT=''
|
||||||
|
STATUS=0
|
||||||
|
run() {
|
||||||
|
OUT=$(limited "$@" 2>&1); STATUS=$?
|
||||||
|
OUT=$(plain "$OUT")
|
||||||
|
}
|
||||||
|
|
||||||
|
# A signal death is not an error exit: 128+n. Report it distinctly, so a
|
||||||
|
# crash can never be read as a reported error.
|
||||||
|
died_by_signal() { [ "$1" -gt 128 ]; }
|
||||||
|
|
||||||
|
pass() { echo "${green}PASS${reset} $1"; PASS=$((PASS + 1)); }
|
||||||
|
fail() { echo "${red}FAIL${reset} $1"; [ -n "$2" ] && echo " $2"; FAIL=$((FAIL + 1)); }
|
||||||
|
|
||||||
|
# shows NAME PATTERN KDIAG_ARGS... — exits 0 and the output matches PATTERN.
|
||||||
|
shows() {
|
||||||
|
local name="$1" pattern="$2"; shift 2
|
||||||
|
run "$KDIAG" "$@"
|
||||||
|
if died_by_signal "$STATUS"; then
|
||||||
|
fail "$name" "died by signal $((STATUS - 128))"; return
|
||||||
|
fi
|
||||||
|
if [ "$STATUS" -ne 0 ]; then
|
||||||
|
fail "$name" "exit $STATUS: $(printf '%s' "$OUT" | head -2)"; return
|
||||||
|
fi
|
||||||
|
if printf '%s' "$OUT" | grep -Eq -- "$pattern"; then
|
||||||
|
pass "$name"
|
||||||
|
else
|
||||||
|
fail "$name" "no match for [$pattern]; got: $(printf '%s' "$OUT" | head -2)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# absent NAME PATTERN KDIAG_ARGS... — exits 0 and the output does NOT match.
|
||||||
|
absent() {
|
||||||
|
local name="$1" pattern="$2"; shift 2
|
||||||
|
run "$KDIAG" "$@"
|
||||||
|
if died_by_signal "$STATUS"; then
|
||||||
|
fail "$name" "died by signal $((STATUS - 128))"; return
|
||||||
|
fi
|
||||||
|
if [ "$STATUS" -ne 0 ]; then
|
||||||
|
fail "$name" "exit $STATUS: $(printf '%s' "$OUT" | head -2)"; return
|
||||||
|
fi
|
||||||
|
if printf '%s' "$OUT" | grep -Eq -- "$pattern"; then
|
||||||
|
fail "$name" "unexpected match [$pattern]"
|
||||||
|
else
|
||||||
|
pass "$name"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# reports NAME PATTERN KDIAG_ARGS... — NONZERO exit, no signal, PATTERN shown.
|
||||||
|
reports() {
|
||||||
|
local name="$1" pattern="$2"; shift 2
|
||||||
|
run "$KDIAG" "$@"
|
||||||
|
if died_by_signal "$STATUS"; then
|
||||||
|
fail "$name" "died by signal $((STATUS - 128))"; return
|
||||||
|
fi
|
||||||
|
if [ "$STATUS" -eq 0 ]; then
|
||||||
|
fail "$name" "expected a nonzero exit, got 0"; return
|
||||||
|
fi
|
||||||
|
if printf '%s' "$OUT" | grep -Eq -- "$pattern"; then
|
||||||
|
pass "$name"
|
||||||
|
else
|
||||||
|
fail "$name" "no match for [$pattern]; got: $(printf '%s' "$OUT" | head -2)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# A klammer definition and an application of it, with no target named: the
|
||||||
|
# general target needs no @@@target, so the @@ tier can be exercised alone.
|
||||||
|
GREET='@@greet name : Hello, *name*. @@ @greet World @'
|
||||||
|
|
||||||
|
# Input that declares its own target AND a klammer for it. The two tiers are
|
||||||
|
# ordered, so this is what tells --klammer and --system apart.
|
||||||
|
OWNTARGET='@@@target foo | Foo output @@@ @@bar.foo : B @@ text'
|
||||||
|
|
||||||
|
# A file for --read to pull in.
|
||||||
|
READFIXTURE=$(mktemp /tmp/kdiag_read.XXXXXX)
|
||||||
|
printf 'READFILE\n' > "$READFIXTURE"
|
||||||
|
trap 'rm -f "$READFIXTURE"' EXIT
|
||||||
|
|
||||||
|
echo "${bold}kdiag interface tests${reset}"
|
||||||
|
echo "====================="
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "-- the input positional is optional --"
|
||||||
|
# The Machine's initial state is a question about the Machine, and the one
|
||||||
|
# thing a programmer wants before feeding it anything.
|
||||||
|
shows " 1. --machine with no input at all" 'Machine' --machine
|
||||||
|
shows " 2. ... and shows the built-in argtypes" 'Argtypes \([0-9]+\)' --machine
|
||||||
|
shows " 3. ... and the pseudo-targets" 'Targets \([0-9]+\)' --machine
|
||||||
|
shows " 4. an input is still accepted" '⟨@i' --type '@i x @'
|
||||||
|
# Listed under "Arguments:", so it must not be repeated under "Options:".
|
||||||
|
usage=$(plain "$(limited "$KDIAG" 2>&1)")
|
||||||
|
n_input=$(printf '%s\n' "$usage" | grep -c '\[<input>\]')
|
||||||
|
if [ "$n_input" = "2" ]; then
|
||||||
|
pass " 5. usage lists [<input>] once as a positional (plus the Usage: line)"
|
||||||
|
else
|
||||||
|
fail " 5. usage lists [<input>] $n_input times, expected 2" \
|
||||||
|
"$(printf '%s\n' "$usage" | grep -n '\[<input>\]')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- registration: one flag per definition tier --"
|
||||||
|
# A registered definition is CONSUMED. Its disappearance from the katom
|
||||||
|
# display is the evidence that it registered, and --replaced brings it back.
|
||||||
|
absent " 6. --klammer consumes the @@ definition" '⟨@@greet⟩' --klammer "$GREET"
|
||||||
|
shows " 7. ... and --replaced shows it again" '⟨@@greet⟩' --klammer --replaced "$GREET"
|
||||||
|
shows " 8. ... and the klammer is in the registry" 'Klammers \(1\)' --klammer --machine "$GREET"
|
||||||
|
absent " 9. --system consumes the @@@ definition" '⟨@@@target⟩' --system "$OWNTARGET"
|
||||||
|
shows "10. ... and the target is in the registry" 'foo → ' --system --machine "$OWNTARGET"
|
||||||
|
shows "11. --process registers both tiers" 'Klammers \(1\)' --process --machine "$OWNTARGET"
|
||||||
|
absent "12. ... and consumes both" '⟨@@' --process "$OWNTARGET"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- tolerant extraction: kdiag only --"
|
||||||
|
# --klammer alone cannot register @@bar.foo, because the target "foo" it names
|
||||||
|
# is declared by the @@@ tier this run did not process. Skipping it is the
|
||||||
|
# report, not a failure -- the definition's katoms stay on screen.
|
||||||
|
shows "13. --klammer alone does not fail on an unregisterable klammer" \
|
||||||
|
'⟨@@bar.foo⟩' --klammer "$OWNTARGET"
|
||||||
|
absent "14. ... and does not register it either" \
|
||||||
|
'Klammers \(1\)' --klammer --machine "$OWNTARGET"
|
||||||
|
shows "15. --system alone leaves the @@ katoms visible" \
|
||||||
|
'⟨@@bar.foo⟩' --system "$OWNTARGET"
|
||||||
|
# The contrast. In ktext the same input is a genuine error: a document naming
|
||||||
|
# an undefined target would otherwise render wrongly and silently.
|
||||||
|
# The pattern must sit on ONE line of the message: an error is wrapped for the
|
||||||
|
# terminal, so "is not defined" can arrive with a newline inside it and a
|
||||||
|
# line-oriented grep will never see it.
|
||||||
|
run "$KTEXT" --klammersets none -s '@@bar.nosuchtarget : B @@' -d
|
||||||
|
if [ "$STATUS" -ne 0 ] && printf '%s' "$OUT" | grep -q 'target "nosuchtarget"'; then
|
||||||
|
pass "16. ktext still THROWS on a klammer naming an undefined target"
|
||||||
|
else
|
||||||
|
fail "16. ktext accepted an undefined target (exit $STATUS)" \
|
||||||
|
"$(printf '%s' "$OUT" | head -2)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- --check --"
|
||||||
|
# The regression: check_machine() read machine.m_katoms, which read() fills and
|
||||||
|
# process() -- how kdiag builds its katoms -- does not. So it had nothing to
|
||||||
|
# check and said so, for every input there is. These two are the guard: a
|
||||||
|
# clean check must be able to fail.
|
||||||
|
shows "17. a correct input checks clean" '0 diagnostics' --process --check "$GREET"
|
||||||
|
reports "18. an undefined klammer is found" '@nosuch is not defined' \
|
||||||
|
--process --check '@@g : @nosuch x @ @@'
|
||||||
|
reports "19. ... and the body it sits in is named" 'in body of @g' \
|
||||||
|
--process --check '@@g : @nosuch x @ @@'
|
||||||
|
reports "20. a wrong argument count is found" 'is given 2' \
|
||||||
|
--process --check "@@greet name : Hello, *name*. @@ @greet a | b @"
|
||||||
|
# Without --klammer/--system/--process nothing is registered, so every
|
||||||
|
# application is undefined. Reporting 80 spurious errors without saying why
|
||||||
|
# would be worse than the old silence; the hint is part of the report.
|
||||||
|
reports "21. --check alone explains why nothing is registered" \
|
||||||
|
'No klammers are registered' --check "$GREET"
|
||||||
|
reports "22. ... and names the way to fix it" '--process' --check "$GREET"
|
||||||
|
# --machine must print even when the check failed: the state shown last has to
|
||||||
|
# reflect everything that happened, so the nonzero exit waits for it.
|
||||||
|
reports "23. --machine still prints after a failed check" \
|
||||||
|
'Machine' --process --check --machine '@nosuch x @'
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- a klammerset reaches kdiag only as input --"
|
||||||
|
# No --klammersets, deliberately: a debugger must not depend on the machinery
|
||||||
|
# it debugs. The flag must be REJECTED, not silently ignored.
|
||||||
|
reports "24. --klammersets is not a kdiag argument" 'argument error' \
|
||||||
|
--klammersets sks '@i x @'
|
||||||
|
# ...so the advice on an undefined-klammer error must not offer it. It did
|
||||||
|
# until 2026-08-15: the one remedy the error suggested was a flag this command
|
||||||
|
# rejects. A wrong hint is worse than none, because it is followed.
|
||||||
|
#
|
||||||
|
# The trigger is a GENERATOR: an @eval whose result holds a klammer. A klammer
|
||||||
|
# written literally in the input is only displayed, never applied, so nothing
|
||||||
|
# reaches the catch block -- which is also why this advice went years unread.
|
||||||
|
UNDEF='@eval chr(64)+"nosuch 7 "+chr(64) @'
|
||||||
|
reports "24a. the error advice names a way kdiag has" '@read sks/sks\.k @' \
|
||||||
|
--process "$UNDEF"
|
||||||
|
# absent_in_error NAME PATTERN ARGS... -- the pattern is missing whatever the
|
||||||
|
# exit status; these run on a path that exits 1 by design.
|
||||||
|
absent_in_error() {
|
||||||
|
local name="$1" pattern="$2"; shift 2
|
||||||
|
run "$KDIAG" "$@"
|
||||||
|
if printf '%s' "$OUT" | grep -Eq -- "$pattern"; then
|
||||||
|
fail "$name" "unexpected match [$pattern]"
|
||||||
|
else
|
||||||
|
pass "$name"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
absent_in_error "24b. ... and not the flag it rejects" '\-\-klammersets' \
|
||||||
|
--process "$UNDEF"
|
||||||
|
# The advice is appended to the description, which is then justified to 80
|
||||||
|
# columns -- without a blank line between them the two ran together into one
|
||||||
|
# word ("...unspecified targetkdiag loads no klammerset...").
|
||||||
|
absent_in_error "24c. ... and does not run onto the message" 'target[a-z]' \
|
||||||
|
--process "$UNDEF"
|
||||||
|
shows "25. @read loads the SKS instead" 'Klammers \([0-9][0-9]+\)' \
|
||||||
|
"@read $K/sks/sks.k @" --process --machine
|
||||||
|
shows "26. ... and the SKS then checks clean" '0 diagnostics' \
|
||||||
|
"@read $K/sks/sks.k @" --process --check
|
||||||
|
# ...and the clean result above is a real one: the same input with a fault
|
||||||
|
# added must still be caught, or case 26 says nothing.
|
||||||
|
reports "27. ... while a fault added to it is still caught" '@nosuchklammer is not defined' \
|
||||||
|
"@read $K/sks/sks.k @ @@bad : @nosuchklammer x @ @@" --process --check
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- the katom display flags --"
|
||||||
|
shows "28. --type subscripts the katom type" '⟨@i.⟩' --type '@i x @'
|
||||||
|
shows "29. --index subscripts the list index" '⟨@i.' --index '@i x @'
|
||||||
|
shows "30. --spans shows span endpoints" '⟨@i.⟩.*⟨@.⟩' --spans '@i x @'
|
||||||
|
shows "31. --args parses positional arguments" 'required' --args --pos 2 'a | b | c'
|
||||||
|
shows "32. --pos sets how many are positional" 'rest: c' --args --pos 2 'a | b | c'
|
||||||
|
shows "33. --ignore removes removed text" '^a b' --ignore 'a #[gone]# b'
|
||||||
|
shows "34. --all shows what was removed" '⟨gone⟩' --ignore --all 'a #[gone]# b'
|
||||||
|
shows "35. --ws applies whitespace operators" 'ab' --ws 'a#- b'
|
||||||
|
# A DECOMPOSED sequence: the base letter followed by U+0308 COMBINING
|
||||||
|
# DIAERESIS, not the precomposed U+00FC. The diacritic FOLLOWS the base
|
||||||
|
# letter -- that is the design of the ^ codes -- so the two spellings look
|
||||||
|
# identical in an editor and only one of them matches.
|
||||||
|
shows "36. --nonascii decodes to a combining sequence" 'ü' --nonascii '^u"'
|
||||||
|
shows "37. --literal marks a literal span" 'x' --literal "^'x'^"
|
||||||
|
# Evaluating needs a target: Eval::eval re-reads its result under K_target, and
|
||||||
|
# kdiag has no target argument, so with the variable unset EVERY @eval died
|
||||||
|
# with an argument error naming something the user never wrote. A command that
|
||||||
|
# specifies no target evaluates under the GENERAL target (Andy, 2026-08-15),
|
||||||
|
# which is also what kdiag means -- it loads no klammerset, so nothing
|
||||||
|
# target-specific is in scope. All three eval modes are checked, because the
|
||||||
|
# failure was in the shared read-back and not in any one of them.
|
||||||
|
shows "38. --eval evaluates a Python expression" '^4$' --eval '@eval 2 + 2 @'
|
||||||
|
shows "38a. ... and --process does too" '^4$' --process '@eval 2 + 2 @'
|
||||||
|
shows "38b. ... in :shell mode" '^hi$' --process '@eval :shell echo hi @'
|
||||||
|
shows "38c. ... and a module.function auto-imports" '^3$' --process '@eval len("abc") @'
|
||||||
|
shows "38d. --read reads a file" 'READFILE' --read "@read $READFIXTURE @"
|
||||||
|
shows "39. --cond selects a branch" 'yes' --cond '@cond true | yes | no @'
|
||||||
|
shows "40. --rewrite reports a rewrite" '.' --rewrite '@i x @'
|
||||||
|
shows "41. --text shows text katoms" 'x' --text '@i x @'
|
||||||
|
shows "42. --ignored shows ignored katoms" '.' --ignore --ignored 'a #[gone]# b'
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- verbosity is about processing --"
|
||||||
|
shows "43. -v takes a value" '.' -v 1 '@i x @'
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "====================="
|
||||||
|
echo "Results: ${PASS} passed, ${FAIL} failed"
|
||||||
|
[ "$FAIL" -eq 0 ] || exit 1
|
||||||
|
exit 0
|
||||||
12
tst/klammerset/base/base.k
Normal file
12
tst/klammerset/base/base.k
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Engine-tier fixture: the base of the klammerset COMBINING tests. It owns the
|
||||||
|
# target and the ".k" declaration that the two "house" sets override, so each
|
||||||
|
# of them can require it and neither has to be loaded first on the command
|
||||||
|
# line -- which is what makes command-line ORDER the only thing under test.
|
||||||
|
#
|
||||||
|
# The definitions sit in a :files entry rather than in this declaring file,
|
||||||
|
# which is NOT a style choice: the already-loaded guard skips a repeated
|
||||||
|
# declaration's :files, but the declaring file itself is read again every time
|
||||||
|
# a :requires names it. With the "@@@target fx" written here, requiring
|
||||||
|
# "base" from two sets reads it twice and the second read is a fatal "Target
|
||||||
|
# fx is already defined". See case 21c, which pins that.
|
||||||
|
@@@klammerset base | Base set for combining tests :date 2026-08-15 :files defs.k @@@
|
||||||
4
tst/klammerset/base/defs.k
Normal file
4
tst/klammerset/base/defs.k
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# The base set's definitions. See base.k for why they are not in it.
|
||||||
|
@@@target fx | Combining fixture target @@@
|
||||||
|
@@who.k : Which klammerset supplied this definition @@
|
||||||
|
@@who.fx :: BASE @@
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# Engine-tier fixture: a klammerset with metadata, a dependency, an ordered
|
|
||||||
# file list (klammers.k uses the target defined in base.k), and a trailing
|
|
||||||
# definition that must load after the files (program order).
|
|
||||||
@@@klammerset kit | Engine-test klammerset
|
|
||||||
:name Klammerset Integration Test
|
|
||||||
:author Klammertext tests
|
|
||||||
:date 2026-07-30
|
|
||||||
:requires util.k
|
|
||||||
:files base.k / klammers.k
|
|
||||||
@@@
|
|
||||||
@@after : AFTER @@
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
@@@klammerset kit | Duplicate declaration of kit :files dup_only.k @@@
|
|
||||||
9
tst/klammerset/housea/housea.k
Normal file
9
tst/klammerset/housea/housea.k
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# One of two independent sets of house overrides. Both require "base" (loaded
|
||||||
|
# once, whichever asks first) and both override the same klammer with ":::", so
|
||||||
|
# the one written LAST on the command line is the one that wins. Its own
|
||||||
|
# klammer @a_only shows that the sets share one flat namespace rather than
|
||||||
|
# each keeping its klammers to itself.
|
||||||
|
@@@klammerset housea | House overrides A :requires base :date 2026-08-15 @@@
|
||||||
|
@@who.fx ::: HOUSE-A @@
|
||||||
|
@@a_only.k : A klammer only set A defines @@
|
||||||
|
@@a_only.fx :: ONLY-A @@
|
||||||
5
tst/klammerset/houseb/houseb.k
Normal file
5
tst/klammerset/houseb/houseb.k
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# The mirror image of housea; see the comment there.
|
||||||
|
@@@klammerset houseb | House overrides B :requires base :date 2026-08-15 @@@
|
||||||
|
@@who.fx ::: HOUSE-B @@
|
||||||
|
@@b_only.k : A klammer only set B defines @@
|
||||||
|
@@b_only.fx :: ONLY-B @@
|
||||||
18
tst/klammerset/kit/kit.k
Normal file
18
tst/klammerset/kit/kit.k
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Engine-tier fixture: a klammerset with metadata, a dependency, an ordered
|
||||||
|
# file list (klammers.k uses the target defined in base.k), and a trailing
|
||||||
|
# definition that must load after the files (program order).
|
||||||
|
#
|
||||||
|
# Laid out as <symbol>/<symbol>.k, which is what a symbol names: since
|
||||||
|
# 2026-08-14 the command line takes symbols only, so a klammerset that
|
||||||
|
# cannot be named by one cannot be loaded at all. ":requires util" is a
|
||||||
|
# SYMBOL, resolved on the search path (this directory is not util's parent,
|
||||||
|
# so it is found at stage 2, KLAMMERTEXT_KLAMMERSETS); ":files" entries are
|
||||||
|
# filenames, resolved against this file's directory.
|
||||||
|
@@@klammerset kit | Engine-test klammerset
|
||||||
|
:name Klammerset Integration Test
|
||||||
|
:author Klammertext tests
|
||||||
|
:date 2026-07-30
|
||||||
|
:requires util
|
||||||
|
:files base.k / klammers.k
|
||||||
|
@@@
|
||||||
|
@@after : AFTER @@
|
||||||
2
tst/klammerset/needs_a/needs_a.k
Normal file
2
tst/klammerset/needs_a/needs_a.k
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Requires selfdef; paired with needs_b to make the diamond of case 21c.
|
||||||
|
@@@klammerset needs_a | Requires selfdef :requires selfdef :date 2026-08-15 @@@
|
||||||
2
tst/klammerset/needs_b/needs_b.k
Normal file
2
tst/klammerset/needs_b/needs_b.k
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Requires selfdef; paired with needs_a to make the diamond of case 21c.
|
||||||
|
@@@klammerset needs_b | Requires selfdef :requires selfdef :date 2026-08-15 @@@
|
||||||
8
tst/klammerset/selfdef/selfdef.k
Normal file
8
tst/klammerset/selfdef/selfdef.k
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# A klammerset whose declaring file carries its own definitions (the
|
||||||
|
# documented alternative to a :text option: program order plays that role).
|
||||||
|
# Requiring it from two different sets reads this file twice, and the second
|
||||||
|
# read of the @@@target is fatal. Case 21c pins that defect; when it is
|
||||||
|
# fixed, that case fails and must be rewritten.
|
||||||
|
@@@klammerset selfdef | Definitions in the declaring file :date 2026-08-15 @@@
|
||||||
|
@@@target fy | Self-defining fixture target @@@
|
||||||
|
@@sd.fy : SELFDEF @@
|
||||||
6
tst/klammerset/sp/sp.k
Normal file
6
tst/klammerset/sp/sp.k
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# The target and @greet come from kit, required by symbol; this set adds
|
||||||
|
# only the file whose name contains a space (standalone "/" separator).
|
||||||
|
@@@klammerset sp | Spacey filename in the list
|
||||||
|
:requires kit
|
||||||
|
:files space name.k
|
||||||
|
@@@
|
||||||
@@ -1 +0,0 @@
|
|||||||
@@@klammerset sp | Spacey filename in the list :files base.k / space name.k @@@
|
|
||||||
6
tst/klammerset/wrap/dup.k
Normal file
6
tst/klammerset/wrap/dup.k
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# A declaration of the symbol "kit" from a file that is NOT kit/kit.k.
|
||||||
|
# Since 2026-08-15 that is an ERROR: a klammerset that can be named is
|
||||||
|
# identified by where it is, so a symbol names one file and one file declares
|
||||||
|
# one symbol. This fixture exists to prove the error; it formerly proved that
|
||||||
|
# such a duplicate was tolerated and skipped.
|
||||||
|
@@@klammerset kit | Duplicate declaration of kit :files dup_only.k @@@
|
||||||
3
tst/klammerset/wrap/wrap.k
Normal file
3
tst/klammerset/wrap/wrap.k
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Requires "kit" twice over: once by symbol, once through dup.k, which
|
||||||
|
# re-declares it. Loading "wrap" is therefore an error now -- see dup.k.
|
||||||
|
@@@klammerset wrap | Wrapper requiring kit twice :requires kit / dup.k @@@
|
||||||
@@ -1 +0,0 @@
|
|||||||
@@@klammerset wrap | Wrapper requiring kit twice :requires decl.k / decl2.k @@@
|
|
||||||
@@ -23,6 +23,14 @@ KTEXT=ktext
|
|||||||
K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set}
|
K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set}
|
||||||
FIX=$K/tst/klammerset
|
FIX=$K/tst/klammerset
|
||||||
|
|
||||||
|
# The fixtures are laid out as <symbol>/<symbol>.k under $FIX, because since
|
||||||
|
# 2026-08-14 the command line takes klammerset SYMBOLS only -- a set that
|
||||||
|
# cannot be named by one cannot be loaded. Putting $FIX on the search path is
|
||||||
|
# how the suite makes its own fixtures nameable, and it also exercises stage 2
|
||||||
|
# (KLAMMERTEXT_KLAMMERSETS) for both the command line and ":requires".
|
||||||
|
# Tests that need a different path set it themselves on the command.
|
||||||
|
export KLAMMERTEXT_KLAMMERSETS=$FIX
|
||||||
|
|
||||||
red=$'\033[31m'
|
red=$'\033[31m'
|
||||||
green=$'\033[32m'
|
green=$'\033[32m'
|
||||||
bold=$'\033[1m'
|
bold=$'\033[1m'
|
||||||
@@ -127,23 +135,23 @@ echo
|
|||||||
# that base.k declares, so base.k must have been read first.
|
# that base.k declares, so base.k must have been read first.
|
||||||
check_eq "1. :files load in order (target before klammer)" \
|
check_eq "1. :files load in order (target before klammer)" \
|
||||||
"Hello World" \
|
"Hello World" \
|
||||||
-k "$FIX/decl.k" -s '@greet World @' -t fix
|
--klammersets kit -s '@greet World @' -t fix
|
||||||
|
|
||||||
# 2. :requires loads the dependency before the set's own files.
|
# 2. :requires loads the dependency before the set's own files.
|
||||||
check_eq "2. :requires loads the dependency" \
|
check_eq "2. :requires loads the dependency" \
|
||||||
"--" \
|
"--" \
|
||||||
-k "$FIX/decl.k" -s '@dash@' -t fix
|
--klammersets kit -s '@dash@' -t fix
|
||||||
|
|
||||||
# 3. Program order: a definition AFTER the declaration in the declaring file
|
# 3. Program order: a definition AFTER the declaration in the declaring file
|
||||||
# is available (there is no :text argument; the declaring file's own
|
# is available (there is no :text argument; the declaring file's own
|
||||||
# content plays that role).
|
# content plays that role).
|
||||||
check_eq "3. trailing definition in the declaring file" \
|
check_eq "3. trailing definition in the declaring file" \
|
||||||
"AFTER" \
|
"AFTER" \
|
||||||
-k "$FIX/decl.k" -s '@after@' -t fix
|
--klammersets kit -s '@after@' -t fix
|
||||||
|
|
||||||
# 4. Relative :files names resolve against the DECLARING file's directory,
|
# 4. Relative :files names resolve against the DECLARING file's directory,
|
||||||
# not the cwd (run from an unrelated directory).
|
# not the cwd (run from an unrelated directory).
|
||||||
output=$( (cd /tmp && "$KTEXT" -k "$FIX/decl.k" -s '@greet Elsewhere @' -t fix) 2>/dev/null | trim )
|
output=$( (cd /tmp && "$KTEXT" --klammersets kit -s '@greet Elsewhere @' -t fix) 2>/dev/null | trim )
|
||||||
if [ "$output" = "Hello Elsewhere" ]; then
|
if [ "$output" = "Hello Elsewhere" ]; then
|
||||||
echo "${green}PASS${reset} 4. :files resolve against the declaring file's directory"
|
echo "${green}PASS${reset} 4. :files resolve against the declaring file's directory"
|
||||||
PASS=$((PASS + 1))
|
PASS=$((PASS + 1))
|
||||||
@@ -157,45 +165,83 @@ fi
|
|||||||
# 5. A filename with a space in the :files list (standalone "/" separator).
|
# 5. A filename with a space in the :files list (standalone "/" separator).
|
||||||
check_eq "5. spacey filename in :files" \
|
check_eq "5. spacey filename in :files" \
|
||||||
"SPACEY" \
|
"SPACEY" \
|
||||||
-k "$FIX/spacey.k" -s '@spacey@' -t fix
|
--klammersets sp -s '@spacey@' -t fix
|
||||||
|
|
||||||
# --- The already-loaded guard ------------------------------------------------
|
# --- A klammerset is identified by where it is -------------------------------
|
||||||
|
#
|
||||||
|
# Symbol X must be declared in X/X.k (Andy, 2026-08-15). The convention already
|
||||||
|
# governed symbol RESOLUTION; requiring it of the DECLARATION makes the symbol a
|
||||||
|
# function of the path, which is what lets the already-loaded guard run before a
|
||||||
|
# file is opened (cases 23-23b below) instead of after it has been re-executed.
|
||||||
|
#
|
||||||
|
# It also continues the reasoning behind symbols-only on the command line: if
|
||||||
|
# every klammerset is reachable by symbol the set of all of them is ENUMERABLE;
|
||||||
|
# if every declaration is in X/X.k each one is also IDENTIFIABLE from where it
|
||||||
|
# sits, so a symbol names one file and one file declares one symbol.
|
||||||
|
#
|
||||||
|
# "wrap" is the fixture that violates it: wrap/dup.k declares the symbol "kit".
|
||||||
|
# It formerly tested that such a duplicate was tolerated and SKIPPED; the
|
||||||
|
# duplicate is now impossible instead, which is the stronger guarantee.
|
||||||
|
check_fails "6. a declaration outside X/X.k is an error" \
|
||||||
|
'must be declared in a file named "kit/kit.k"' \
|
||||||
|
--klammersets wrap -s '@greet Again @' -t fix
|
||||||
|
check_fails "6a. ... and the error names the file it is actually in" \
|
||||||
|
'dup.k' \
|
||||||
|
--klammersets wrap -s '@greet Again @' -t fix
|
||||||
|
|
||||||
# 6. A second declaration of an already-registered symbol is skipped, not an
|
# 7. EXEMPT: a document may declare a klammerset -- that is how a designer
|
||||||
# error: wrapper.k requires decl.k (registers "kit") and then decl2.k
|
# writes one, and kdesc's provenance filter depends on it. Such a set is
|
||||||
# (re-declares "kit"); the wrapper still loads and kit's klammers work.
|
# local to the document: nothing can ":requires" it, so it has no identity to
|
||||||
check_eq "6. duplicate declaration is skipped, not an error" \
|
# protect and needs no file named after it.
|
||||||
"Hello Again" \
|
check_eq "7. a klammerset declared in a document is exempt" \
|
||||||
-k "$FIX/wrapper.k" -s '@greet Again @' -t fix
|
"ok" \
|
||||||
|
--klammersets none -s '@@@klammerset mine | Local to this document @@@ ok'
|
||||||
# 7. ...and the skipped declaration's files are NOT loaded.
|
|
||||||
check_fails "7. skipped declaration loads none of its files" \
|
|
||||||
"only_dup" \
|
|
||||||
-k "$FIX/wrapper.k" -s '@only_dup@' -t fix
|
|
||||||
|
|
||||||
# --- Introspection -----------------------------------------------------------
|
# --- Introspection -----------------------------------------------------------
|
||||||
|
|
||||||
# 8. -m lists the registered klammersets.
|
# The machine state moved from "ktext -m" to "kdiag --machine" in the
|
||||||
check_contains "8. -m shows the klammerset symbol" \
|
# 2026-08-14 argument redesign: the Machine's internals are the programmer's
|
||||||
"kit" \
|
# question, and kdiag is the programmer's command. kdiag's input is optional,
|
||||||
-k "$FIX/decl.k" -s 'x' -t fix -m
|
# so the state can be shown without a document at all.
|
||||||
|
kdiag_contains() {
|
||||||
|
local test_name="$1" expected="$2"; shift 2
|
||||||
|
local output
|
||||||
|
output=$(kdiag "$@" 2>/dev/null)
|
||||||
|
if printf '%s' "$output" | grep -qF -- "$expected"; then
|
||||||
|
echo "${green}PASS${reset} $test_name"; PASS=$((PASS + 1))
|
||||||
|
else
|
||||||
|
echo "${red}FAIL${reset} $test_name"
|
||||||
|
echo " expected to contain: [$expected]"
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
check_contains "9. -m shows the required klammerset too" \
|
# 8. --machine lists the registered klammersets. kdiag has no --klammersets:
|
||||||
|
# a klammerset reaches it the way anything else does, by being read in the
|
||||||
|
# input. That is deliberate -- the symbol mechanism is a higher-level
|
||||||
|
# convenience, and kdiag must still work when symbol resolution is what
|
||||||
|
# broke. (The :requires inside kit.k still uses a symbol, resolved by the
|
||||||
|
# Machine, so this exercises both paths at once.)
|
||||||
|
kdiag_contains "8. --machine shows the klammerset symbol" \
|
||||||
|
"kit" \
|
||||||
|
"@read $FIX/kit/kit.k @" --process --machine
|
||||||
|
|
||||||
|
kdiag_contains "9. --machine shows the required klammerset too" \
|
||||||
"Utility klammers for engine tests" \
|
"Utility klammers for engine tests" \
|
||||||
-k "$FIX/decl.k" -s 'x' -t fix -m
|
"@read $FIX/kit/kit.k @" --process --machine
|
||||||
|
|
||||||
# --- Errors --------------------------------------------------------------------
|
# --- Errors --------------------------------------------------------------------
|
||||||
|
|
||||||
# 10. A listed file that does not exist is a clean klammerset error.
|
# 10. A listed file that does not exist is a clean klammerset error.
|
||||||
check_fails "10. missing file in :files" \
|
check_fails "10. missing file in :files" \
|
||||||
"missing.k" \
|
"missing.k" \
|
||||||
-k "$FIX/bad_file.k" -s 'x' -t fix
|
--klammersets broken -s 'x' -t fix
|
||||||
|
|
||||||
# 11. A symbol must be an identifier (starts with a letter; letters, digits,
|
# 11. A symbol must be an identifier (starts with a letter; letters, digits,
|
||||||
# underscores).
|
# underscores).
|
||||||
check_fails "11. invalid symbol rejected" \
|
check_fails "11. invalid symbol rejected" \
|
||||||
"not valid" \
|
"not valid" \
|
||||||
-k none -s '@@@klammerset 9bad | Bad symbol @@@' -d
|
--klammersets none -s '@@@klammerset 9bad | Bad symbol @@@' -d
|
||||||
|
|
||||||
# --- The search path (symbol -> <dir>/<symbol>/<symbol>.k) -------------------
|
# --- The search path (symbol -> <dir>/<symbol>/<symbol>.k) -------------------
|
||||||
|
|
||||||
@@ -217,10 +263,10 @@ printf '@@@klammerset locset | Shadow candidate @@@\n@@@target fixL | Fixture @@
|
|||||||
# 12. Stage 1 for a file input: the document's directory.
|
# 12. Stage 1 for a file input: the document's directory.
|
||||||
check_eq "12. symbol resolves in the document's directory" \
|
check_eq "12. symbol resolves in the document's directory" \
|
||||||
"LOCAL" \
|
"LOCAL" \
|
||||||
"$DOCDIR/doc.kt" -k locset -t fixL -d
|
"$DOCDIR/doc.kt" --klammersets locset -t fixL -d
|
||||||
|
|
||||||
# 13. Stage 1 for string input: the cwd stands in for the document.
|
# 13. Stage 1 for string input: the cwd stands in for the document.
|
||||||
output=$( (cd "$DOCDIR" && "$KTEXT" -s '@local_k@' -k locset -t fixL) 2>/dev/null | trim )
|
output=$( (cd "$DOCDIR" && "$KTEXT" -s '@local_k@' --klammersets locset -t fixL) 2>/dev/null | trim )
|
||||||
if [ "$output" = "LOCAL" ]; then
|
if [ "$output" = "LOCAL" ]; then
|
||||||
echo "${green}PASS${reset} 13. cwd stands in for the document (string input)"
|
echo "${green}PASS${reset} 13. cwd stands in for the document (string input)"
|
||||||
PASS=$((PASS + 1))
|
PASS=$((PASS + 1))
|
||||||
@@ -231,7 +277,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# 14. Stage 2: the KLAMMERTEXT_KLAMMERSETS directories.
|
# 14. Stage 2: the KLAMMERTEXT_KLAMMERSETS directories.
|
||||||
output=$(KLAMMERTEXT_KLAMMERSETS=$ENVDIR "$KTEXT" -s '@env_k@' -k envset -t fixE 2>/dev/null | trim)
|
output=$(KLAMMERTEXT_KLAMMERSETS=$ENVDIR "$KTEXT" -s '@env_k@' --klammersets envset -t fixE 2>/dev/null | trim)
|
||||||
if [ "$output" = "ENV" ]; then
|
if [ "$output" = "ENV" ]; then
|
||||||
echo "${green}PASS${reset} 14. symbol resolves in KLAMMERTEXT_KLAMMERSETS"
|
echo "${green}PASS${reset} 14. symbol resolves in KLAMMERTEXT_KLAMMERSETS"
|
||||||
PASS=$((PASS + 1))
|
PASS=$((PASS + 1))
|
||||||
@@ -242,7 +288,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# 15. Shadowing: the document-local set wins over the installed one.
|
# 15. Shadowing: the document-local set wins over the installed one.
|
||||||
output=$(KLAMMERTEXT_KLAMMERSETS=$ENVDIR "$KTEXT" "$DOCDIR/doc.kt" -k locset -t fixL -d 2>/dev/null | trim)
|
output=$(KLAMMERTEXT_KLAMMERSETS=$ENVDIR "$KTEXT" "$DOCDIR/doc.kt" --klammersets locset -t fixL -d 2>/dev/null | trim)
|
||||||
if [ "$output" = "LOCAL" ]; then
|
if [ "$output" = "LOCAL" ]; then
|
||||||
echo "${green}PASS${reset} 15. document-local set shadows the installed one"
|
echo "${green}PASS${reset} 15. document-local set shadows the installed one"
|
||||||
PASS=$((PASS + 1))
|
PASS=$((PASS + 1))
|
||||||
@@ -257,26 +303,170 @@ fi
|
|||||||
mkdir -p "$DOCDIR/kit2/locset"
|
mkdir -p "$DOCDIR/kit2/locset"
|
||||||
cp "$DOCDIR/locset/locset.k" "$DOCDIR/kit2/locset/locset.k"
|
cp "$DOCDIR/locset/locset.k" "$DOCDIR/kit2/locset/locset.k"
|
||||||
printf '@@@klammerset kit2 | Requires by symbol :requires locset @@@\n' > "$DOCDIR/kit2/kit2.k"
|
printf '@@@klammerset kit2 | Requires by symbol :requires locset @@@\n' > "$DOCDIR/kit2/kit2.k"
|
||||||
check_eq "16. :requires accepts a symbol (declaring-dir stage)" \
|
output=$(KLAMMERTEXT_KLAMMERSETS=$DOCDIR "$KTEXT" --klammersets kit2 \
|
||||||
"LOCAL" \
|
-s '@local_k@' -t fixL 2>/dev/null | trim)
|
||||||
-k "$DOCDIR/kit2/kit2.k" -s '@local_k@' -t fixL
|
if [ "$output" = "LOCAL" ]; then
|
||||||
|
echo "${green}PASS${reset} 16. :requires accepts a symbol (declaring-dir stage)"
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
else
|
||||||
|
echo "${red}FAIL${reset} 16. :requires accepts a symbol (declaring-dir stage)"
|
||||||
|
echo " expected: [LOCAL] got: [$output]"
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
# 17. Stage 3: $KLAMMERTEXT_HOME — the kdesc listing enumerates sks
|
# 17. Stage 3: $KLAMMERTEXT_HOME — the kdesc listing enumerates sks
|
||||||
# (sks/sks.k already satisfies the <symbol>/<symbol>.k convention).
|
# (sks/sks.k already satisfies the <symbol>/<symbol>.k convention).
|
||||||
output=$(kdesc --klammerset 2>/dev/null)
|
output=$(kdesc --klammersets 2>/dev/null)
|
||||||
if printf '%s' "$output" | grep -q 'sks/sks\.k'; then
|
if printf '%s' "$output" | grep -q 'sks/sks\.k'; then
|
||||||
echo "${green}PASS${reset} 17. kdesc --klammerset lists sks from KLAMMERTEXT_HOME"
|
echo "${green}PASS${reset} 17. kdesc --klammersets lists sks from KLAMMERTEXT_HOME"
|
||||||
PASS=$((PASS + 1))
|
PASS=$((PASS + 1))
|
||||||
else
|
else
|
||||||
echo "${red}FAIL${reset} 17. kdesc --klammerset lists sks from KLAMMERTEXT_HOME"
|
echo "${red}FAIL${reset} 17. kdesc --klammersets lists sks from KLAMMERTEXT_HOME"
|
||||||
echo " got: $(echo "$output" | head -3)"
|
echo " got: $(echo "$output" | head -3)"
|
||||||
FAIL=$((FAIL + 1))
|
FAIL=$((FAIL + 1))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 18. An unknown symbol is a clean error naming the search directories.
|
# 18. An unknown symbol is a clean error, and it names both the convention and
|
||||||
check_fails "18. unknown symbol names the search path" \
|
# the directories searched -- without those a user cannot tell whether the
|
||||||
|
# name or the location is wrong. This is the failure mode the symbol-only
|
||||||
|
# rule makes common: there is no longer a pathname to fall back on.
|
||||||
|
check_fails "18. unknown symbol is a clean error" \
|
||||||
"was not found" \
|
"was not found" \
|
||||||
-s 'x' -k nosuchset -d
|
-s 'x' --klammersets nosuchset -d
|
||||||
|
check_fails "18a. ... naming the <symbol>/<symbol>.k convention" \
|
||||||
|
"x/x.k" \
|
||||||
|
-s 'x' --klammersets nosuchset -d
|
||||||
|
check_fails "18b. ... and the directories searched" \
|
||||||
|
"$FIX" \
|
||||||
|
-s 'x' --klammersets nosuchset -d
|
||||||
|
|
||||||
|
# 19. A pathname is no longer accepted: only symbols resolve. The error must
|
||||||
|
# say so rather than reporting a missing file.
|
||||||
|
check_fails "19. a pathname is rejected as a symbol" \
|
||||||
|
"not found" \
|
||||||
|
-s 'x' --klammersets "$FIX/kit/kit.k" -d
|
||||||
|
|
||||||
|
# 20. One bad symbol among good ones fails the whole load rather than
|
||||||
|
# silently loading the rest.
|
||||||
|
check_fails "20. a bad symbol among good ones still fails" \
|
||||||
|
"was not found" \
|
||||||
|
-s 'x' --klammersets kit nosuchset -t fix -d
|
||||||
|
|
||||||
|
# --- Several klammersets combine ---------------------------------------------
|
||||||
|
#
|
||||||
|
# The headline claim of "--klammersets" (plural), stated in the ktext usage and
|
||||||
|
# in "kdesc --klammersets help": the symbols are a LIST, loaded in the order
|
||||||
|
# given, and the klammers of all of them share ONE flat namespace -- membership
|
||||||
|
# is provenance, not containment. Where two sets define the same klammer the
|
||||||
|
# definition modes decide, so a set of house overrides is loaded after the set
|
||||||
|
# it adjusts and a set of defaults before it.
|
||||||
|
#
|
||||||
|
# The fixtures: "base" declares the target and the ".k", "housea" and "houseb"
|
||||||
|
# each require base and each override @who with ":::". Both requiring base is
|
||||||
|
# what lets either be written first, so command-line order is the only variable.
|
||||||
|
|
||||||
|
# 21. One namespace: each set's own klammers are available whatever the order.
|
||||||
|
check_eq "21. combined sets share one namespace" \
|
||||||
|
"ONLY-A ONLY-B" \
|
||||||
|
--klammersets housea houseb -s '@a_only@ @b_only@' -t fx
|
||||||
|
check_eq "21a. ... in either order" \
|
||||||
|
"ONLY-A ONLY-B" \
|
||||||
|
--klammersets houseb housea -s '@a_only@ @b_only@' -t fx
|
||||||
|
|
||||||
|
# 22. Order decides: two ":::" overrides of the same klammer, so the set
|
||||||
|
# written LAST is the one whose definition survives.
|
||||||
|
check_eq "22. the last set written wins an override" \
|
||||||
|
"HOUSE-B" \
|
||||||
|
--klammersets housea houseb -s '@who@' -t fx
|
||||||
|
check_eq "22a. ... and reversing the order reverses the outcome" \
|
||||||
|
"HOUSE-A" \
|
||||||
|
--klammersets houseb housea -s '@who@' -t fx
|
||||||
|
|
||||||
|
# 23. :requires is loaded once even though both sets ask for it -- the
|
||||||
|
# already-loaded guard -- so the base definition is there exactly once.
|
||||||
|
check_eq "23. a set required by both is loaded once" \
|
||||||
|
"BASE" \
|
||||||
|
--klammersets base -s '@who@' -t fx
|
||||||
|
|
||||||
|
# 23a. A DIAMOND: two sets requiring a third. "needs_a" and "needs_b" both
|
||||||
|
# require "selfdef", whose declaring file holds its own @@@target -- which
|
||||||
|
# is legal and normal, since there is deliberately no :text option and the
|
||||||
|
# declaring file's content plays that role.
|
||||||
|
#
|
||||||
|
# This failed until 2026-08-15 with "Target fy is already defined",
|
||||||
|
# pointing at a line the author wrote once. The already-loaded guard sat
|
||||||
|
# in Klammerset_registry::add, which runs only after a file has been read
|
||||||
|
# and its declaration reached -- by which time the second read had
|
||||||
|
# re-executed the declaring file's own definitions. The guard now runs
|
||||||
|
# BEFORE the read, which is possible only because X is declared in X/X.k
|
||||||
|
# and the symbol is therefore known from the path.
|
||||||
|
check_eq "23a. a diamond :requires loads the shared set once" \
|
||||||
|
"SELFDEF" \
|
||||||
|
--klammersets needs_a needs_b -s '@sd@' -t fy
|
||||||
|
check_eq "23b. ... and requiring it once is unchanged" \
|
||||||
|
"SELFDEF" \
|
||||||
|
--klammersets needs_a -s '@sd@' -t fy
|
||||||
|
check_eq "23c. ... as is naming the same set twice on the command line" \
|
||||||
|
"SELFDEF" \
|
||||||
|
--klammersets selfdef selfdef -s '@sd@' -t fy
|
||||||
|
|
||||||
|
# 23d. The search path is FROZEN for the run. A path decides WHICH FILE a
|
||||||
|
# symbol means -- identity, not value -- so if a document could extend it
|
||||||
|
# mid-run, "which klammerset is X" would depend on evaluation order and
|
||||||
|
# "kdesc --klammersets" could not be a complete answer. It could:
|
||||||
|
# the embedded Python shares the process, so an @eval doing os.environ
|
||||||
|
# changed what a later getenv returned. The environment is now read once.
|
||||||
|
LATE=$(mktemp -d /tmp/klammerset_late.XXXXXX)
|
||||||
|
mkdir -p "$LATE/late"
|
||||||
|
printf '@@@klammerset late | Appeared mid-run @@@\n@@@target lt | T @@@\n@@l.lt : LATE @@\n' \
|
||||||
|
> "$LATE/late/late.k"
|
||||||
|
# The @eval still sets the variable -- what changed is that resolution no
|
||||||
|
# longer consults it -- so the symbol must remain unfindable.
|
||||||
|
out=$(KLAMMERTEXT_KLAMMERSETS=$FIX "$KTEXT" --klammersets none -d \
|
||||||
|
-s "@eval os.environ.__setitem__('KLAMMERTEXT_KLAMMERSETS','$LATE') or '' @" 2>&1)
|
||||||
|
if printf '%s' "$out" | grep -q "was not found" || \
|
||||||
|
! KLAMMERTEXT_KLAMMERSETS=$FIX "$KTEXT" --klammersets late -s 'x' -d >/dev/null 2>&1; then
|
||||||
|
echo "${green}PASS${reset} 23d. an @eval cannot extend the klammerset search path"
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
else
|
||||||
|
echo "${red}FAIL${reset} 23d. the search path moved during the run"
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
fi
|
||||||
|
rm -rf "$LATE"
|
||||||
|
|
||||||
|
# --- "none" is the absence of a klammerset, not the name of one ---------------
|
||||||
|
#
|
||||||
|
# It therefore says nothing about what should be loaded alongside it. Written
|
||||||
|
# first it used to discard the rest of the list silently -- the user asked for
|
||||||
|
# a set, got none, and was told only that some target was undefined; written
|
||||||
|
# last it was looked up as a symbol and reported missing. Neither reading is
|
||||||
|
# guessed at now: "none" is accepted only alone.
|
||||||
|
|
||||||
|
# 24. "none" alone still loads nothing, which is what it is for.
|
||||||
|
check_eq "24. none alone loads no klammerset" \
|
||||||
|
"x" \
|
||||||
|
--klammersets none -s 'x' -d
|
||||||
|
|
||||||
|
check_fails "25. none before another symbol is an error" \
|
||||||
|
"cannot be combined" \
|
||||||
|
--klammersets none kit -s 'x' -t fix -d
|
||||||
|
check_fails "25a. none after another symbol is an error too" \
|
||||||
|
"cannot be combined" \
|
||||||
|
--klammersets kit none -s 'x' -t fix -d
|
||||||
|
|
||||||
|
# 26. The rule belongs to load_klammersets(), the single loader all three
|
||||||
|
# commands share -- so kdesc obeys it as well. It did not: kdesc decided
|
||||||
|
# what "none" meant a second time, and so accepted "none sks" and loaded
|
||||||
|
# neither. Deciding it twice is how the two commands came to disagree.
|
||||||
|
out=$(kdesc --klammersets none sks -k table 2>&1); status=$?
|
||||||
|
if [ $status -ne 0 ] && printf '%s' "$out" | grep -qF "cannot be combined"; then
|
||||||
|
echo "${green}PASS${reset} 26. kdesc applies the same none rule"
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
else
|
||||||
|
echo "${red}FAIL${reset} 26. kdesc applies the same none rule — exit $status"
|
||||||
|
echo " got: $(printf '%s' "$out" | head -2)"
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "======================="
|
echo "======================="
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
# files read earlier. A klammer DEFINED in a read file is applied later and
|
# files read earlier. A klammer DEFINED in a read file is applied later and
|
||||||
# always sees its own directory — the realistic case, tested below.
|
# always sees its own directory — the realistic case, tested below.
|
||||||
#
|
#
|
||||||
# Engine tier: -k none, fixtures created inline in a temp tree, no SKS.
|
# Engine tier: --klammersets none, fixtures created inline in a temp tree, no SKS.
|
||||||
# Usage: ./modulepath_test.sh (ktext on PATH)
|
# Usage: ./modulepath_test.sh (ktext on PATH)
|
||||||
|
|
||||||
PASS=0
|
PASS=0
|
||||||
@@ -50,7 +50,7 @@ printf '@eval nosuch_module_xyz.f() @\n' > "$T/setb/missing.kt"
|
|||||||
check() { # check NAME EXPECTED ARGS...
|
check() { # check NAME EXPECTED ARGS...
|
||||||
local name="$1" expected="$2"; shift 2
|
local name="$1" expected="$2"; shift 2
|
||||||
local out
|
local out
|
||||||
out=$(ktext "$@" -k none -d 2>/dev/null)
|
out=$(ktext "$@" --klammersets none -d 2>/dev/null)
|
||||||
if [ "$out" = "$expected" ]; then
|
if [ "$out" = "$expected" ]; then
|
||||||
echo "${green}PASS${reset} $name"; PASS=$((PASS + 1))
|
echo "${green}PASS${reset} $name"; PASS=$((PASS + 1))
|
||||||
else
|
else
|
||||||
@@ -75,7 +75,7 @@ check " 2. the same, input given as a relative path" \
|
|||||||
"../setb/doc.kt"
|
"../setb/doc.kt"
|
||||||
|
|
||||||
name=" 3. an unknown module still reports cleanly"
|
name=" 3. an unknown module still reports cleanly"
|
||||||
err=$(ktext "$T/setb/missing.kt" -k none -d 2>&1 >/dev/null)
|
err=$(ktext "$T/setb/missing.kt" --klammersets none -d 2>&1 >/dev/null)
|
||||||
case "$err" in
|
case "$err" in
|
||||||
*'Cannot import module "nosuch_module_xyz"'*)
|
*'Cannot import module "nosuch_module_xyz"'*)
|
||||||
echo "${green}PASS${reset} $name"; PASS=$((PASS + 1)) ;;
|
echo "${green}PASS${reset} $name"; PASS=$((PASS + 1)) ;;
|
||||||
@@ -111,7 +111,7 @@ check " 7. a :cwd directory containing spaces" \
|
|||||||
|
|
||||||
name=" 8. a nonexistent :cwd is a clean argument error"
|
name=" 8. a nonexistent :cwd is a clean argument error"
|
||||||
printf '@eval :cwd /nonexistent_kt_dir :shell true @\n' > "$T/setb/bad.kt"
|
printf '@eval :cwd /nonexistent_kt_dir :shell true @\n' > "$T/setb/bad.kt"
|
||||||
err=$(ktext "$T/setb/bad.kt" -k none -d 2>&1 >/dev/null)
|
err=$(ktext "$T/setb/bad.kt" --klammersets none -d 2>&1 >/dev/null)
|
||||||
case "$err" in
|
case "$err" in
|
||||||
*'The :cwd directory does not exist'*)
|
*'The :cwd directory does not exist'*)
|
||||||
echo "${green}PASS${reset} $name"; PASS=$((PASS + 1)) ;;
|
echo "${green}PASS${reset} $name"; PASS=$((PASS + 1)) ;;
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ PRELUDE='@@@target fix | a fixture target @@@
|
|||||||
check_eq() {
|
check_eq() {
|
||||||
local name="$1" expected="$2" source="$3" target="${4:-fix}"
|
local name="$1" expected="$2" source="$3" target="${4:-fix}"
|
||||||
local output status
|
local output status
|
||||||
output=$("$KTEXT" -k none -s "$PRELUDE
|
output=$("$KTEXT" --klammersets none -s "$PRELUDE
|
||||||
$source" -t "$target" -d 2>&1)
|
$source" -t "$target" -d 2>&1)
|
||||||
status=$?
|
status=$?
|
||||||
output=$(printf '%s' "$output" | tr -d '\n' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
|
output=$(printf '%s' "$output" | tr -d '\n' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
|
||||||
@@ -76,7 +76,7 @@ $source" -t "$target" -d 2>&1)
|
|||||||
check_fails() {
|
check_fails() {
|
||||||
local name="$1" needle="$2" source="$3"
|
local name="$1" needle="$2" source="$3"
|
||||||
local output status
|
local output status
|
||||||
output=$("$KTEXT" -k none -s "$PRELUDE
|
output=$("$KTEXT" --klammersets none -s "$PRELUDE
|
||||||
$source" -t fix -d 2>&1)
|
$source" -t fix -d 2>&1)
|
||||||
status=$?
|
status=$?
|
||||||
if [ $status -eq 0 ]; then
|
if [ $status -eq 0 ]; then
|
||||||
@@ -98,11 +98,23 @@ $source" -t fix -d 2>&1)
|
|||||||
# check_warns NAME SUBSTRING SOURCE — SOURCE must be accepted, and its
|
# check_warns NAME SUBSTRING SOURCE — SOURCE must be accepted, and its
|
||||||
# combined output must contain SUBSTRING (used where a warning is expected
|
# combined output must contain SUBSTRING (used where a warning is expected
|
||||||
# beside the result, so an exact comparison would test the warning's wording).
|
# beside the result, so an exact comparison would test the warning's wording).
|
||||||
|
# Since 2026-08-15 the override notice is reported at "-v 1" rather than
|
||||||
|
# warned about: ":::" exists to override, so the notice fired on the sanctioned
|
||||||
|
# use of a feature (see tst/deftype_test.sh's check_warn for the reasoning).
|
||||||
|
# The default run must be SILENT and the notice must appear at "-v 1".
|
||||||
check_warns() {
|
check_warns() {
|
||||||
local name="$1" needle="$2" source="$3"
|
local name="$1" needle="$2" source="$3"
|
||||||
local output status
|
local output status quiet
|
||||||
output=$("$KTEXT" -k none -s "$PRELUDE
|
quiet=$("$KTEXT" --klammersets none -s "$PRELUDE
|
||||||
$source" -t fix -d 2>&1)
|
$source" -t fix -d 2>&1 >/dev/null)
|
||||||
|
if [ -n "$quiet" ]; then
|
||||||
|
echo "${red}FAIL${reset} $name — the default run was not silent"
|
||||||
|
echo " got: $(printf '%s' "$quiet" | head -1)"
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
output=$("$KTEXT" --klammersets none -s "$PRELUDE
|
||||||
|
$source" -t fix -d -v 1 2>&1)
|
||||||
status=$?
|
status=$?
|
||||||
if [ $status -ne 0 ]; then
|
if [ $status -ne 0 ]; then
|
||||||
echo "${red}FAIL${reset} $name — ktext exited $status"
|
echo "${red}FAIL${reset} $name — ktext exited $status"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
# growing -- and exceeding its round limit is an error rather than a message
|
# growing -- and exceeding its round limit is an error rather than a message
|
||||||
# followed by rendering a document with live klammers still in it.
|
# 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
|
# These are engine tests: no klammer set is loaded (--klammersets none) and every klammer
|
||||||
# used is defined inline as a fixture.
|
# used is defined inline as a fixture.
|
||||||
#
|
#
|
||||||
# Usage: ./recursion_test.sh
|
# Usage: ./recursion_test.sh
|
||||||
@@ -100,29 +100,29 @@ echo
|
|||||||
|
|
||||||
check_error " 1. direct self-recursion is caught" \
|
check_error " 1. direct self-recursion is caught" \
|
||||||
"does not terminate" \
|
"does not terminate" \
|
||||||
-k none -s '@@f : x @f@ @@ @f@' -d
|
--klammersets none -s '@@f : x @f@ @@ @f@' -d
|
||||||
|
|
||||||
check_error " 2. the offending klammer is named" \
|
check_error " 2. the offending klammer is named" \
|
||||||
'applying "f"' \
|
'applying "f"' \
|
||||||
-k none -s '@@f : x @f@ @@ @f@' -d
|
--klammersets none -s '@@f : x @f@ @@ @f@' -d
|
||||||
|
|
||||||
check_error " 3. mutual recursion is caught" \
|
check_error " 3. mutual recursion is caught" \
|
||||||
"does not terminate" \
|
"does not terminate" \
|
||||||
-k none -s '@@a : ( @b@ ) @@ @@b : [ @a@ ] @@ @a@' -d
|
--klammersets none -s '@@a : ( @b@ ) @@ @@b : [ @a@ ] @@ @a@' -d
|
||||||
|
|
||||||
check_error " 4. self-recursion through an argument is caught" \
|
check_error " 4. self-recursion through an argument is caught" \
|
||||||
"does not terminate" \
|
"does not terminate" \
|
||||||
-k none -s '@@w t : < *t* > @@ @@r : @w @r@ @ @@ @r@' -d
|
--klammersets none -s '@@w t : < *t* > @@ @@r : @w @r@ @ @@ @r@' -d
|
||||||
|
|
||||||
# --- Terminating nesting is untouched ---
|
# --- Terminating nesting is untouched ---
|
||||||
|
|
||||||
check_eq " 5. deep but finite nesting still reduces" \
|
check_eq " 5. deep but finite nesting still reduces" \
|
||||||
"<<<<<x>>>>>" \
|
"<<<<<x>>>>>" \
|
||||||
-k none -s '@@w t : <*t*> @@ @w @w @w @w @w x @ @ @ @ @' -d
|
--klammersets none -s '@@w t : <*t*> @@ @w @w @w @w @w x @ @ @ @ @' -d
|
||||||
|
|
||||||
check_eq " 6. a chain of klammers generating klammers reduces" \
|
check_eq " 6. a chain of klammers generating klammers reduces" \
|
||||||
"END" \
|
"END" \
|
||||||
-k none -s '@@k1 : @k2@ @@ @@k2 : @k3@ @@ @@k3 : @k4@ @@ @@k4 : @k5@ @@ @@k5 : @k6@ @@ @@k6 : @k7@ @@ @@k7 : @k8@ @@ @@k8 : END @@ @k1@' -d
|
--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" ---
|
# --- The fixed point ends on "nothing was applied", not "nothing was added" ---
|
||||||
#
|
#
|
||||||
@@ -131,7 +131,7 @@ check_eq " 6. a chain of klammers generating klammers reduces" \
|
|||||||
|
|
||||||
check_eq " 7. a klammer with an empty body reduces" \
|
check_eq " 7. a klammer with an empty body reduces" \
|
||||||
"a b" \
|
"a b" \
|
||||||
-k none -s '@@nothing : @@ a @nothing@ b' -d
|
--klammersets none -s '@@nothing : @@ a @nothing@ b' -d
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "============================="
|
echo "============================="
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
# target a "::" instance with no parameter list of its own. That is the
|
# target a "::" instance with no parameter list of its own. That is the
|
||||||
# migration from repeating an argument list per target to declaring it once.
|
# migration from repeating an argument list per target to declaring it once.
|
||||||
#
|
#
|
||||||
# These are engine tests, so they use -k none and define their own targets
|
# These are engine tests, so they use --klammersets none and define their own targets
|
||||||
# inline: a target is a Machine construct (@@@target), not owned by any
|
# inline: a target is a Machine construct (@@@target), not owned by any
|
||||||
# klammer set.
|
# klammer set.
|
||||||
#
|
#
|
||||||
@@ -46,7 +46,7 @@ TARGETS='@@@target ta | Target A @@@
|
|||||||
# accepted NAME SRC — the definitions are consistent and the klammer applies.
|
# accepted NAME SRC — the definitions are consistent and the klammer applies.
|
||||||
accepted() {
|
accepted() {
|
||||||
local name="$1" src="$2" out
|
local name="$1" src="$2" out
|
||||||
out=$("$KTEXT" -k none -s "$TARGETS$src" -t ta -d 2>&1)
|
out=$("$KTEXT" --klammersets none -s "$TARGETS$src" -t ta -d 2>&1)
|
||||||
if echo "$out" | grep -qi "error"; then
|
if echo "$out" | grep -qi "error"; then
|
||||||
fail "$name" "no error" "$(echo "$out" | grep -i -A1 error | tail -1)"
|
fail "$name" "no error" "$(echo "$out" | grep -i -A1 error | tail -1)"
|
||||||
else
|
else
|
||||||
@@ -57,7 +57,7 @@ accepted() {
|
|||||||
# rejected NAME SRC PATTERN — the drift is caught, and the message says how.
|
# rejected NAME SRC PATTERN — the drift is caught, and the message says how.
|
||||||
rejected() {
|
rejected() {
|
||||||
local name="$1" src="$2" want="$3" out
|
local name="$1" src="$2" want="$3" out
|
||||||
out=$("$KTEXT" -k none -s "$TARGETS$src" -t ta -d 2>&1)
|
out=$("$KTEXT" --klammersets none -s "$TARGETS$src" -t ta -d 2>&1)
|
||||||
if ! echo "$out" | grep -qi "error"; then
|
if ! echo "$out" | grep -qi "error"; then
|
||||||
fail "$name" "a definition error" "accepted"
|
fail "$name" "a definition error" "accepted"
|
||||||
elif ! echo "$out" | grep -qF "$want"; then
|
elif ! echo "$out" | grep -qF "$want"; then
|
||||||
@@ -102,7 +102,7 @@ echo "-- the message must show HOW they differ, not just where"
|
|||||||
# With more than two targets the designer would otherwise have to diff the
|
# With more than two targets the designer would otherwise have to diff the
|
||||||
# definitions by hand; each target's own signature is listed beside its name.
|
# definitions by hand; each target's own signature is listed beside its name.
|
||||||
name=" 6. each target's signature is shown"
|
name=" 6. each target's signature is shown"
|
||||||
out=$("$KTEXT" -k none -s "$TARGETS"'@@k6.ta s :one : [*s*] @@
|
out=$("$KTEXT" --klammersets none -s "$TARGETS"'@@k6.ta s :one : [*s*] @@
|
||||||
@@k6.tb s :two : [*s*] @@
|
@@k6.tb s :two : [*s*] @@
|
||||||
@k6 x @' -t ta -d 2>&1)
|
@k6 x @' -t ta -d 2>&1)
|
||||||
if echo "$out" | grep -q "ta .*s :one" && echo "$out" | grep -q "tb .*s :two"; then
|
if echo "$out" | grep -q "ta .*s :one" && echo "$out" | grep -q "tb .*s :two"; then
|
||||||
@@ -114,7 +114,7 @@ fi
|
|||||||
# The error path once printed internal katom detail through msg(), which is
|
# The error path once printed internal katom detail through msg(), which is
|
||||||
# debugging scaffolding and must never reach a user-facing message.
|
# debugging scaffolding and must never reach a user-facing message.
|
||||||
name=" 7. no internal debug output on the error path"
|
name=" 7. no internal debug output on the error path"
|
||||||
out=$("$KTEXT" -k none -s "$TARGETS"'@@k7.k s : a declaration @@
|
out=$("$KTEXT" --klammersets none -s "$TARGETS"'@@k7.k s : a declaration @@
|
||||||
@@k7.ta s2 :other : [*s2*] @@
|
@@k7.ta s2 :other : [*s2*] @@
|
||||||
@k7 x @' -t ta -d 2>&1)
|
@k7 x @' -t ta -d 2>&1)
|
||||||
if echo "$out" | grep -q "klammer-definition"; then
|
if echo "$out" | grep -q "klammer-definition"; then
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
# "*arg*" variables, and the closing delimiters
|
# "*arg*" variables, and the closing delimiters
|
||||||
# Case 14 guards the second: a comma next to an application is writer text.
|
# Case 14 guards the second: a comma next to an application is writer text.
|
||||||
#
|
#
|
||||||
# These are engine tests, so they use -k none and define their own targets
|
# These are engine tests, so they use --klammersets none and define their own targets
|
||||||
# inline: a target is a Machine construct (@@@target), not owned by any
|
# inline: a target is a Machine construct (@@@target), not owned by any
|
||||||
# klammer set.
|
# klammer set.
|
||||||
#
|
#
|
||||||
@@ -54,7 +54,7 @@ trim() { awk '{ sub(/[ \t\r]+$/, "") } { line[NR]=$0 } END { f=1; while (f<=NR &
|
|||||||
check_eq() {
|
check_eq() {
|
||||||
local name="$1" target="$2" expected="$3" src="$4"
|
local name="$1" target="$2" expected="$3" src="$4"
|
||||||
local out status err
|
local out status err
|
||||||
out=$("$KTEXT" -k none -s "$TARGETS$src" -t "$target" -d 2>"$ERR"); status=$?
|
out=$("$KTEXT" --klammersets none -s "$TARGETS$src" -t "$target" -d 2>"$ERR"); status=$?
|
||||||
err=$(cat "$ERR")
|
err=$(cat "$ERR")
|
||||||
out=$(printf '%s' "$out" | trim)
|
out=$(printf '%s' "$out" | trim)
|
||||||
if [ $status -ne 0 ]; then
|
if [ $status -ne 0 ]; then
|
||||||
@@ -73,18 +73,25 @@ check_eq() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# check_warn NAME TARGET EXPECTED SRC — exit 0, stdout==EXPECTED, AND a warning.
|
# check_warn NAME TARGET EXPECTED SRC — exit 0, stdout==EXPECTED, the override
|
||||||
|
# SILENT at the default verbosity and REPORTED at "-v 1" (2026-08-15; see
|
||||||
|
# tst/deftype_test.sh's check_warn for why it is no longer a warning).
|
||||||
check_warn() {
|
check_warn() {
|
||||||
local name="$1" target="$2" expected="$3" src="$4"
|
local name="$1" target="$2" expected="$3" src="$4"
|
||||||
local out status err
|
local out status err
|
||||||
out=$("$KTEXT" -k none -s "$TARGETS$src" -t "$target" -d 2>"$ERR"); status=$?
|
out=$("$KTEXT" --klammersets none -s "$TARGETS$src" -t "$target" -d 2>"$ERR"); status=$?
|
||||||
err=$(cat "$ERR")
|
|
||||||
out=$(printf '%s' "$out" | trim)
|
out=$(printf '%s' "$out" | trim)
|
||||||
if [ $status -ne 0 ]; then
|
if [ $status -ne 0 ]; then
|
||||||
echo "${red}FAIL${reset} $name — ktext exited $status"; FAIL=$((FAIL+1)); return
|
echo "${red}FAIL${reset} $name — ktext exited $status"; FAIL=$((FAIL+1)); return
|
||||||
fi
|
fi
|
||||||
if ! printf '%s' "$err" | grep -qiF "warning"; then
|
if [ -s "$ERR" ]; then
|
||||||
echo "${red}FAIL${reset} $name — expected a warning, got none"; FAIL=$((FAIL+1)); return
|
echo "${red}FAIL${reset} $name — the default run was not silent: $(head -1 "$ERR")"
|
||||||
|
FAIL=$((FAIL+1)); return
|
||||||
|
fi
|
||||||
|
"$KTEXT" --klammersets none -s "$TARGETS$src" -t "$target" -d -v 1 >/dev/null 2>"$ERR"
|
||||||
|
err=$(cat "$ERR")
|
||||||
|
if ! printf '%s' "$err" | grep -qiF "overridden"; then
|
||||||
|
echo "${red}FAIL${reset} $name — no override reported at -v 1"; FAIL=$((FAIL+1)); return
|
||||||
fi
|
fi
|
||||||
if [ "$out" = "$expected" ]; then
|
if [ "$out" = "$expected" ]; then
|
||||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||||
@@ -98,7 +105,7 @@ check_warn() {
|
|||||||
check_error() {
|
check_error() {
|
||||||
local name="$1" target="$2" pattern="$3" src="$4"
|
local name="$1" target="$2" pattern="$3" src="$4"
|
||||||
local out status
|
local out status
|
||||||
out=$("$KTEXT" -k none -s "$TARGETS$src" -t "$target" -d 2>&1); status=$?
|
out=$("$KTEXT" --klammersets none -s "$TARGETS$src" -t "$target" -d 2>&1); status=$?
|
||||||
if [ $status -eq 0 ]; then
|
if [ $status -eq 0 ]; then
|
||||||
echo "${red}FAIL${reset} $name — expected an error but ktext succeeded"; FAIL=$((FAIL+1)); return
|
echo "${red}FAIL${reset} $name — expected an error but ktext succeeded"; FAIL=$((FAIL+1)); return
|
||||||
fi
|
fi
|
||||||
|
|||||||
155
tst/verbosity_test.sh
Executable file
155
tst/verbosity_test.sh
Executable file
@@ -0,0 +1,155 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# verbosity_test.sh — what "-v" says, and what it must not say.
|
||||||
|
#
|
||||||
|
# The output policy (CLAUDE.md, "Command output policy") fixes what each
|
||||||
|
# verbosity level is FOR, and a policy with no test drifts back — which is how
|
||||||
|
# the suites this session repaired got where they were. The rule:
|
||||||
|
#
|
||||||
|
# -v 0 silent. Nothing but the result.
|
||||||
|
# -v 1 every decision whose outcome the user COULD NOT HAVE READ OFF THEIR
|
||||||
|
# OWN INPUT: a klammerset symbol resolved to a file, a default applied,
|
||||||
|
# "-o" expanded into a directory and a basename, a filename regrouped,
|
||||||
|
# a klammer overridden by a definition in another klammer set.
|
||||||
|
# -v 2+ the trace: what the command DID, for someone reading the code.
|
||||||
|
#
|
||||||
|
# The boundary is the part worth testing. Andy's first formulation was "-v 1
|
||||||
|
# describes a change of state", and taken literally that swallows -v 2 — every
|
||||||
|
# definition is a change of state. So the test for -v 1 is not "did something
|
||||||
|
# happen" but "could the user have predicted the outcome from what they wrote".
|
||||||
|
# Registering a klammer the user wrote, in the file they wrote, is NOT -v 1
|
||||||
|
# material; resolving a symbol through a three-stage search path with shadowing
|
||||||
|
# is.
|
||||||
|
#
|
||||||
|
# Engine tier: no klammerset beyond what a case loads deliberately. The SKS
|
||||||
|
# half of this policy — font resolution and ":files" resolution — is in
|
||||||
|
# sks/tst/verbosity_test.sh, because those are @document's decisions.
|
||||||
|
#
|
||||||
|
# Usage: ./verbosity_test.sh (needs KLAMMERTEXT_HOME set; commands on PATH)
|
||||||
|
# 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'
|
||||||
|
|
||||||
|
OUTF=$(mktemp /tmp/verb_out.XXXXXX)
|
||||||
|
ERRF=$(mktemp /tmp/verb_err.XXXXXX)
|
||||||
|
TMPD=$(mktemp -d /tmp/verb_dir.XXXXXX)
|
||||||
|
trap 'rm -f "$OUTF" "$ERRF"; rm -rf "$TMPD"' EXIT
|
||||||
|
|
||||||
|
plain() { sed 's/\x1b\[[0-9;]*m//g'; }
|
||||||
|
|
||||||
|
pass() { echo "${green}PASS${reset} $1"; PASS=$((PASS + 1)); }
|
||||||
|
fail() { echo "${red}FAIL${reset} $1"; [ -n "$2" ] && echo " $2"; FAIL=$((FAIL + 1)); }
|
||||||
|
|
||||||
|
# run CMD ARGS... — fills OUTF/ERRF, sets STATUS.
|
||||||
|
STATUS=0
|
||||||
|
run() { "$@" >"$OUTF" 2>"$ERRF"; STATUS=$?; }
|
||||||
|
|
||||||
|
# reports NAME PATTERN — ERRF matches PATTERN (the log is on stderr, always).
|
||||||
|
reports() {
|
||||||
|
if plain < "$ERRF" | grep -Eq -- "$2"; then pass "$1"
|
||||||
|
else fail "$1" "no [$2] in: $(plain < "$ERRF" | head -3 | tr '\n' ' ')"; fi
|
||||||
|
}
|
||||||
|
absent() {
|
||||||
|
if plain < "$ERRF" | grep -Eq -- "$2"; then fail "$1" "unexpected [$2]"
|
||||||
|
else pass "$1"; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "${bold}Verbosity tests${reset}"
|
||||||
|
echo "==============="
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "-- -v 0 is silent --"
|
||||||
|
run "$KTEXT" --klammersets none -s 'hello' -d
|
||||||
|
if [ $STATUS -eq 0 ] && [ "$(cat "$OUTF")" = "hello" ] && [ ! -s "$ERRF" ]; then
|
||||||
|
pass " 1. a successful run says nothing but its result"
|
||||||
|
else
|
||||||
|
fail " 1. exit $STATUS, stdout [$(cat "$OUTF")], stderr [$(head -1 "$ERRF")]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- -v 1 answers \"what did the command think I asked for?\" --"
|
||||||
|
run "$KTEXT" --klammersets none -s 'hello' -d -v 1
|
||||||
|
reports " 2. the resolved argument list is shown" '\-s +: hello'
|
||||||
|
reports " 3. ... including a default the user did not write" '\-v +: 1'
|
||||||
|
# The document is on stdout and nothing shares it, whatever -v says.
|
||||||
|
if [ "$(cat "$OUTF")" = "hello" ]; then
|
||||||
|
pass " 4. and the result is still the only thing on stdout"
|
||||||
|
else
|
||||||
|
fail " 4. stdout carried [$(head -c 60 "$OUTF")]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- -v 1 reports DERIVED values --"
|
||||||
|
# The output path: the user gave a target and an input name; the directory, the
|
||||||
|
# basename and the final filename were all constructed.
|
||||||
|
printf 'hello\n' > "$TMPD/doc.kt"
|
||||||
|
run "$KTEXT" "$TMPD/doc.kt" -t txt --klammersets none -v 1
|
||||||
|
reports " 5. the output directory it constructed" 'Output directory: '
|
||||||
|
reports " 6. the output basename it constructed" 'Output basename: doc'
|
||||||
|
reports " 7. the output filename it constructed" 'Output filename: .*doc\.txt'
|
||||||
|
|
||||||
|
# A klammerset symbol resolves through a three-stage search path with
|
||||||
|
# shadowing, so the FILE it landed on is the derived value. Both branches are
|
||||||
|
# checked: the default (no --klammersets at all) reported nothing until
|
||||||
|
# 2026-08-15, which was the commonest case of all.
|
||||||
|
run "$KTEXT" -s 'x' -d -v 1
|
||||||
|
reports " 8. the DEFAULT klammerset names the file it loaded" 'Klammerset \(default\): .*sks/sks\.k'
|
||||||
|
mkdir -p "$TMPD/kset"
|
||||||
|
printf '@@@klammerset kset | A set @@@\n' > "$TMPD/kset/kset.k"
|
||||||
|
run env KLAMMERTEXT_KLAMMERSETS="$TMPD" "$KTEXT" -s 'x' -d --klammersets kset -v 1
|
||||||
|
reports " 9. a symbol names the file it resolved to" 'Klammerset "kset": .*kset/kset\.k'
|
||||||
|
run "$KTEXT" -s 'x' -d --klammersets none -v 1
|
||||||
|
reports "10. and \"none\" says that none was loaded" 'Klammersets: none'
|
||||||
|
|
||||||
|
# An override: the definition being replaced usually lives somewhere the user
|
||||||
|
# cannot see, which is what earns it a place here rather than a warning.
|
||||||
|
run "$KTEXT" --klammersets none -t fx -d -v 1 \
|
||||||
|
-s '@@@target fx | F @@@ @@w.k : W @@ @@w.fx :: A @@ @@w.fx ::: B @@ @w@'
|
||||||
|
reports "11. a klammer overridden by a later definition" 'overridden'
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- the boundary: -v 2 is the trace, -v 1 is not --"
|
||||||
|
run "$KTEXT" --klammersets none -s 'hello' -d -v 1
|
||||||
|
v1=$(wc -l < "$ERRF")
|
||||||
|
run "$KTEXT" --klammersets none -s 'hello' -d -v 2
|
||||||
|
v2=$(wc -l < "$ERRF")
|
||||||
|
if [ "$v2" -gt "$v1" ]; then
|
||||||
|
pass "12. -v 2 says more than -v 1 ($v1 lines -> $v2)"
|
||||||
|
else
|
||||||
|
fail "12. -v 2 added nothing ($v1 -> $v2)"
|
||||||
|
fi
|
||||||
|
# The trace names the code; the -v 1 report never should. A source location in
|
||||||
|
# a level-1 line means a msg() or a stray log level, not a decision.
|
||||||
|
run "$KTEXT" --klammersets none -s 'hello' -d -v 1
|
||||||
|
absent "13. -v 1 does not name source files" '\[[a-z_]+\.cpp:[0-9]+\]'
|
||||||
|
run "$KTEXT" --klammersets none -s 'hello' -d -v 2
|
||||||
|
reports "14. ... while -v 2 does" '\[[a-z_]+\.(cpp|h):[0-9]+\]'
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- the other two commands honour -v as well --"
|
||||||
|
run kdesc --katoms -v 1
|
||||||
|
if [ $STATUS -eq 0 ] && [ -s "$OUTF" ]; then
|
||||||
|
pass "15. kdesc -v 1 still produces its result on stdout"
|
||||||
|
else
|
||||||
|
fail "15. exit $STATUS, stdout $(wc -c < "$OUTF") bytes"
|
||||||
|
fi
|
||||||
|
run kdiag -v 1 '@i-x'
|
||||||
|
if [ $STATUS -eq 0 ] && [ -s "$OUTF" ]; then
|
||||||
|
pass "16. kdiag -v 1 still produces its result on stdout"
|
||||||
|
else
|
||||||
|
fail "16. exit $STATUS, stdout $(wc -c < "$OUTF") bytes"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "==============="
|
||||||
|
echo "Results: ${PASS} passed, ${FAIL} failed"
|
||||||
|
[ "$FAIL" -eq 0 ] || exit 1
|
||||||
|
exit 0
|
||||||
Reference in New Issue
Block a user