A character written in the ^...^ mnemonic form -- ^oe^, ^zz^, ^hello^ --
used to kill ktext outright: no message, no output, and an exit status
that looked like a crash because it was one. Only the forms that happen
to spell hexadecimal digits escaped, and they were right by luck rather
than by reading. Any caret form that names no character is now a located
error that quotes what was written.
A klammer may now sit inside square brackets and braces as it already
could inside parentheses, and may be followed by the punctuation that
ends a sentence: [@i word@], {@b word@}, @i word@'s. The rule is a fixed
list of characters, because none of them can occur inside a klammer name
and so none of them is ambiguous next to a delimiter. A letter still
cannot: "x@" is the named closing delimiter, so a letter before "@" has
two readings and no rule can separate them.
The abbreviations an author writes -- the hyphen form @i-word, a klammer
glued to its neighbouring punctuation -- are now recognised as author
conveniences only. They are not applied to @@ definitions or @@@ system
commands, which are written by someone who needs no such shorthand and
whose forms are already whole.
Also fixed, each of them silent: an exported shell function in the
environment broke every @eval in the document; a line break written with
@nl was destroyed when it passed through a klammer that generates a
table, printing as \textbackslash{}newline in the finished PDF; and
"kdesc --coverage" shortened its file column by comparing the spelling of
a path rather than the path, so a klammer set reached by a symbolic link
showed a full path where its neighbours showed a short one.
New in the distribution: ref/, a small klammerset that describes the
Klammermachine's own tables in Klammertext. It currently provides the
character tables -- the diacritics and the extended Latin letters -- as
klammers that render to any target, so the documentation of what can be
written cannot drift from what the machine accepts. It is early, and
will grow.
The Klammertext taxonomy now has a section on targets, describes the
@@@klammerset system command alongside the other three, and states the
precedence of "^" -- what a caret means before a letter, a digit, a
punctuation character, and a quote, in one ordered list.
(from dev 6594c7ff8c1f)
532 lines
19 KiB
C++
532 lines
19 KiB
C++
#include <ranges>
|
|
#include <algorithm>
|
|
#include <utility>
|
|
|
|
#include "show.h"
|
|
#include "log.h"
|
|
#include "katom.h"
|
|
#include "argtype_registry.h"
|
|
#include "argument_set.h"
|
|
#include "katomize.h"
|
|
#include "file.h"
|
|
#include "util.h"
|
|
|
|
bool operator==(Parameter_set lhs, Parameter_set rhs)
|
|
{
|
|
return as_string(lhs.m_katoms.begin(), lhs.m_katoms.end(), true) ==
|
|
as_string(rhs.m_katoms.begin(), rhs.m_katoms.end(), true);
|
|
}
|
|
|
|
std::regex parameter_regex(bool optional=false)
|
|
{
|
|
// The type component may carry a numeric type parameter, e.g.
|
|
// rows.rest(2) (see resolve_argtype below). In the optional
|
|
// three-part form the type may be empty (:paper_size..tex).
|
|
std::string type = R"(\w+(?:\(\d+\))?)";
|
|
std::string opt_type = R"(\w*(?:\(\d+\))?)";
|
|
std::string pattern = R"((?:([A-Za-z]\w*))|(?:([A-Za-z]\w*)\.()" + type +
|
|
R"())|(?:([A-Za-z]\w*)\.()" + type + R"()\.(\w+)))";
|
|
if (optional) {
|
|
pattern = R"((?::([A-Za-z]\w*))|(?::([A-Za-z]\w*)\.()" + type +
|
|
R"())|(?::([A-Za-z]\w*)\.()" + opt_type + R"()\.(\w+)))";
|
|
}
|
|
// (void)K::log(3, pattern);
|
|
return std::regex(pattern);
|
|
}
|
|
|
|
// Look up an argument type, specializing a parameterized use such as
|
|
// rest(2): the base type is copied, its type parameter set, and its
|
|
// display name extended, so kdesc signatures show rows.rest(2).
|
|
static Argtype resolve_argtype(
|
|
const std::string& type_text, const Argtype_registry& argtypes, const Locator& loc)
|
|
{
|
|
static const std::regex parameterized(R"((\w+)\((\d+)\))");
|
|
std::smatch match {};
|
|
if (std::regex_match(type_text, match, parameterized)) {
|
|
Argtype argtype = argtypes.get(match[1], loc);
|
|
argtype.m_parameter = match[2];
|
|
argtype.m_name += "(" + std::string(match[2]) + ")";
|
|
return argtype;
|
|
}
|
|
return argtypes.get(type_text, loc);
|
|
}
|
|
|
|
// True for the rest type in any parameterization (rest, rest(2), ...).
|
|
static bool is_rest(const Argtype& argtype)
|
|
{
|
|
return argtype.m_name == "rest" || argtype.m_name.rfind("rest(", 0) == 0;
|
|
}
|
|
|
|
Parameter_set::Parameter_set(const std::string parameter_string)
|
|
{
|
|
(void)K::log(3);
|
|
Argtype_registry argtypes {};
|
|
parse_parameters(
|
|
katomize_lines(line_split(parameter_string), Locator().str()),
|
|
argtypes);
|
|
}
|
|
|
|
|
|
Parameter_set::Parameter_set(const std::vector<Katom>& katoms)
|
|
: m_katoms(katoms)
|
|
{
|
|
(void)K::log(3);
|
|
Argtype_registry argtypes {};
|
|
parse_parameters(m_katoms, argtypes);
|
|
}
|
|
|
|
Parameter_set::Parameter_set(const std::vector<Katom>& katoms, const Argtype_registry& argtypes)
|
|
: m_katoms(katoms)
|
|
{
|
|
(void)K::log(3);
|
|
parse_parameters(m_katoms, argtypes);
|
|
}
|
|
|
|
// Parameter parsing
|
|
|
|
Parameter parse_positional_parameter(const katom_list& katoms, const Argtype_registry& argtypes)
|
|
{
|
|
// (void)K::log(3, katoms);
|
|
if (katoms.size() > 1) {
|
|
throw Argument_error("Multiple katoms for positional argument:\n " +
|
|
as_string(katoms.begin(), katoms.end(), true) +
|
|
"\nPositional arguments are separated by the bar (|) character.",
|
|
katoms[0].m_loc);
|
|
}
|
|
Katom k = katoms[0];
|
|
std::string name = k.m_text;
|
|
std::smatch match {};
|
|
if (!std::regex_match(name, match, parameter_regex())) {
|
|
throw Argument_error(
|
|
"The structure of the word \"" + name + "\" is not correct for a positional parameter",
|
|
k.m_loc);
|
|
} else {
|
|
std::string match_name = std::string(match[1]) + std::string(match[2]) + std::string(match[4]);
|
|
std::string match_type = std::string(match[3]) + std::string(match[5]);
|
|
std::string match_target = match[6];
|
|
if (match_type.empty()) {
|
|
match_type = "string";
|
|
}
|
|
return Parameter(match_name, resolve_argtype(match_type, argtypes, k.m_loc), k.m_loc);
|
|
}
|
|
}
|
|
|
|
|
|
Parameter parse_optional_parameter(const katom_list& katoms, const Argtype_registry& argtypes)
|
|
{
|
|
//(void)K::log(3);
|
|
Katom k = katoms[0];
|
|
std::string default_value {};
|
|
if (katoms.size() > 1) {
|
|
default_value = to_string(katoms.cbegin() + 1, katoms.cend(), true);
|
|
}
|
|
|
|
std::string name = k.m_text;
|
|
std::smatch match {};
|
|
if (!std::regex_match(name, match, parameter_regex(true))) {
|
|
throw Argument_error(
|
|
"The structure of the word \"" + name +
|
|
"\" is not correct for an optional parameter",
|
|
k.m_loc);
|
|
} else {
|
|
std::string match_name = std::string(match[1]) + std::string(match[2]) + std::string(match[4]);
|
|
std::string match_type = std::string(match[3]) + std::string(match[5]);
|
|
std::string match_target = match[6];
|
|
if (match_type.empty()) {
|
|
match_type = "string";
|
|
}
|
|
Parameter parameter(match_name, resolve_argtype(match_type, argtypes, k.m_loc),
|
|
k.m_loc, true, default_value);
|
|
// Two-level default resolution: a default declared in the
|
|
// parameter list wins; otherwise the argument type's :default
|
|
// fills in. Both are validated here, at definition time, so an
|
|
// invalid default cannot reach an application.
|
|
if (default_value.empty() && !parameter.m_argtype.m_default.empty()) {
|
|
parameter.m_default = parameter.m_argtype.m_default;
|
|
parameter.m_default_from_type = true;
|
|
} else if (!default_value.empty()) {
|
|
Parameter_set::validate(parameter, default_value, k.m_loc);
|
|
}
|
|
return parameter;
|
|
}
|
|
}
|
|
|
|
void check_for_missing_parameter(const katom_list& katoms)
|
|
{
|
|
(void)K::log(3, katoms.size());
|
|
// Yeah, yeah, "algorithms."
|
|
auto ki = katoms.begin();
|
|
while (ki < katoms.end() - 1) {
|
|
ki = std::find_if(ki, katoms.end(), [](const Katom& k) {
|
|
return k.m_type == katom_t::bar; });
|
|
if (ki == katoms.end()) {
|
|
break;
|
|
}
|
|
auto kstart = ki;
|
|
ki = std::find_if(ki + 1, katoms.end(), [](const Katom& k) {
|
|
return !k.is_whitespace(); });
|
|
if (ki == katoms.end()) {
|
|
throw Argument_error(
|
|
"A parameter list ends with a bar character", kstart->m_loc);
|
|
}
|
|
auto type_after_bar = ki->m_type;
|
|
if (type_after_bar == katom_t::bar) {
|
|
throw Argument_error(
|
|
"A parameter name was missing between two bar characters", kstart->m_loc);
|
|
} else if (type_after_bar == katom_t::option_name) {
|
|
throw Argument_error(
|
|
"A parameter name was missing between a bar character and an option name",
|
|
kstart->m_loc);
|
|
}
|
|
++ki;
|
|
}
|
|
}
|
|
|
|
bool is_boundary(katom_list::const_iterator ki)
|
|
{
|
|
return ki->m_type == katom_t::option_name || ki->m_type == katom_t::bar;
|
|
}
|
|
|
|
std::vector<std::vector<Katom>>
|
|
function_symbol_parts(katom_list::const_iterator kbegin, katom_list::const_iterator kend)
|
|
{
|
|
std::vector<std::vector<Katom>> parts;
|
|
katom_list part {};
|
|
auto ki = kbegin;
|
|
while (ki != kend && ki->is_whitespace()) {
|
|
ki++;
|
|
}
|
|
if (ki == kend) {
|
|
return {};
|
|
}
|
|
if (ki->m_type != katom_t::option_name) {
|
|
part.push_back(Katom("|", katom_t::bar, kbegin->m_loc));
|
|
}
|
|
while (ki < kend) {
|
|
if (!part.empty() && is_boundary(ki)) {
|
|
parts.push_back(trim(part));
|
|
part = {};
|
|
}
|
|
part.push_back(*ki);
|
|
ki++;
|
|
}
|
|
if (!part.empty()) {
|
|
parts.push_back(trim(part));
|
|
}
|
|
// std::cout << "PARTS:\n";
|
|
// for (size_t i = 0; i < parts.size(); i++) {
|
|
// std::cout << i << sp_arrow << parts[i] << "\n";
|
|
// }
|
|
return parts;
|
|
}
|
|
|
|
katom_list trim_part(katom_list part)
|
|
{
|
|
return trim(part, {katom_t::space, katom_t::newline, katom_t::bar});
|
|
}
|
|
|
|
std::tuple<katom_lists,katom_lists>
|
|
parameter_split(katom_list::const_iterator kbegin, katom_list::const_iterator kend)
|
|
{
|
|
(void)K::log(3); //, "begin:", *kbegin, "end:", *(kend - 1));
|
|
// "distance:", std::distance(kbegin, kend));
|
|
auto parts = function_symbol_parts(kbegin, kend);
|
|
// std::cout << "parts: " << parts << "\n";
|
|
katom_lists positional {};
|
|
katom_lists optional {};
|
|
for (auto p : parts) {
|
|
if (p[0].m_type == katom_t::option_name) {
|
|
optional.push_back(p);
|
|
} else {
|
|
positional.push_back(trim_part(p));
|
|
}
|
|
}
|
|
return {positional, optional};
|
|
}
|
|
|
|
void Parameter_set::parse_parameters(const katom_list& katoms, const Argtype_registry& argtypes)
|
|
{
|
|
(void)K::log(3, trim(katoms));
|
|
if (katoms.empty()) {
|
|
return;
|
|
}
|
|
check_for_missing_parameter(katoms);
|
|
auto [positional, optional] = parameter_split(katoms.cbegin(), katoms.cend());
|
|
for (auto req : positional) {
|
|
auto pos = parse_positional_parameter(req, argtypes);
|
|
if (is_rest(pos.m_argtype)) {
|
|
m_rest.push_back(pos);
|
|
} else {
|
|
m_positional.push_back(pos);
|
|
}
|
|
}
|
|
for (auto opt : optional) {
|
|
auto param = parse_optional_parameter(opt, argtypes);
|
|
if (std::ranges::count(m_optional_names, param.m_name) > 0) {
|
|
throw Argument_error(
|
|
"Optional parameter \":" + param.m_name + "\" already defined",
|
|
katoms[0].m_loc);
|
|
}
|
|
m_optional.push_back(param);
|
|
m_optional_names.push_back(param.m_name);
|
|
}
|
|
if (!m_rest.empty()) {
|
|
m_positional_count = m_positional.size();
|
|
}
|
|
}
|
|
|
|
void describe_arguments(
|
|
const std::string& label,
|
|
const std::vector<std::vector<Katom>>& positional,
|
|
const std::vector<std::vector<Katom>>& optional,
|
|
const std::vector<Katom>& rest)
|
|
{
|
|
std::cout << label << ":\n"
|
|
<< " positional: " << positional << "\n"
|
|
<< " optional: " << optional << "\n"
|
|
<< " rest: " << rest << "\n";
|
|
}
|
|
|
|
|
|
void Parameter_set::describe_parameters()
|
|
{
|
|
(void)K::log(3);
|
|
std::cout << " positional: ";
|
|
if (!m_positional.empty()) {
|
|
for (auto p : m_positional) {
|
|
std::cout << p << " ";
|
|
}
|
|
} else {
|
|
std::cout << "[none]";
|
|
}
|
|
std::cout << "\n optional: ";
|
|
if (!m_optional.empty()) {
|
|
for (auto p : m_optional) {
|
|
std::cout << p << " ";
|
|
}
|
|
} else {
|
|
std::cout << "[none]";
|
|
}
|
|
std::cout << "\n rest: ";
|
|
if (!m_rest.empty()) {
|
|
std::cout << kall << m_rest << kreset << "\n";
|
|
} else {
|
|
std::cout << "[none]";
|
|
}
|
|
std::cout << "\n";
|
|
}
|
|
|
|
std::tuple<katom_lists,katom_lists,katom_list>
|
|
argument_split(katom_list::const_iterator kbegin, katom_list::const_iterator kend,
|
|
long unsigned int positional_limit)
|
|
{
|
|
// (void)K::log(3, "begin:", *(kbegin+1), "end:", *(kend - 1),
|
|
// "distance:", std::distance(kbegin, kend), "limit:", positional_limit);
|
|
(void)K::log(3);
|
|
// msg() << std::pair(kbegin, kend) << "\n";
|
|
auto parts = function_symbol_parts(kbegin, kend);
|
|
|
|
katom_lists positional {};
|
|
katom_lists optional {};
|
|
katom_list rest {};
|
|
|
|
for (auto p : parts) {
|
|
if (p[0].m_type == katom_t::option_name) {
|
|
optional.push_back(p);
|
|
} else if (positional.size() < positional_limit) {
|
|
positional.push_back(trim_part(p));
|
|
} else {
|
|
// Parts were trimmed, so adjacent parts would abut their bar
|
|
// katoms (a row separator "||" next to an empty cell's "|"
|
|
// would serialize as "|||"). A space keeps the writer's
|
|
// bar/double-bar distinction parseable.
|
|
if (!rest.empty()) {
|
|
rest.push_back(Katom(" ", katom_t::space, p[0].m_loc));
|
|
}
|
|
rest.insert(rest.end(), p.begin(), p.end());
|
|
}
|
|
}
|
|
return {positional, optional, trim_part(rest)};
|
|
}
|
|
|
|
// Parameter/argument mapping
|
|
|
|
void Parameter_set::check_positional(const katom_lists& positional_arguments, const Locator& loc)
|
|
{
|
|
(void)K::log(3, "required:", m_positional.size(), positional_arguments.size()); //, positional_arguments);
|
|
auto positional_count = m_positional.size();
|
|
auto given_count = positional_arguments.size();
|
|
if (positional_count > given_count) {
|
|
// std::cout << "Given less than required\n";
|
|
std::vector<Parameter> missing(m_positional.begin() + given_count, m_positional.end());
|
|
//std::cout << "missing: " << missing << "\n";
|
|
auto missing_count = missing.size();
|
|
std::stringstream ss {};
|
|
ss << "Positional " << plural("argument", missing_count) << " " << to_be(missing_count)
|
|
<< " missing:\n";
|
|
std::cout << ss.str();
|
|
for (auto arg : missing) {
|
|
ss << " " << arg.m_name << "\n";
|
|
}
|
|
//std::cout << ss.str();
|
|
throw Argument_error(ss.str(), loc);
|
|
} else if (positional_count < given_count) {
|
|
//std::cout << "DESCRIBE\n";
|
|
//describe_parameters();
|
|
std::stringstream ss {};
|
|
ss << "Too many positional arguments were given; "
|
|
<< positional_count << " needed but " << given_count << " given";
|
|
throw Argument_error(ss.str(), loc);
|
|
}
|
|
}
|
|
|
|
std::map<std::string, std::string>
|
|
Parameter_set::check_optional(const katom_lists& optional_arguments, const Locator& loc)
|
|
{
|
|
(void)K::log(3, optional_arguments.size());
|
|
std::vector<std::string> optional_names_used {};
|
|
std::map<std::string, std::string> values {};
|
|
for (const auto& opt : optional_arguments) {
|
|
std::string name(opt[0].m_text, 1);
|
|
if (std::ranges::count(m_optional_names, name) == 0) {
|
|
throw Argument_error("Optional argument \":" + name + "\" not defined", loc);
|
|
}
|
|
if (std::ranges::count(optional_names_used, name) > 0) {
|
|
throw Argument_error("Optional argument \":" + name + "\" already provided "
|
|
+ "with a value of:\n" + values[name], loc);
|
|
}
|
|
katom_list value_katoms(opt.begin()+1, opt.end());
|
|
std::string value = trim(to_string(value_katoms));
|
|
values[name] = value;
|
|
optional_names_used.push_back(name);
|
|
}
|
|
return values;
|
|
}
|
|
|
|
|
|
const std::map<std::string, std::string>
|
|
Parameter_set::value_map(
|
|
const katom_lists& positional, const katom_lists& optional, const katom_list& rest,
|
|
const Locator& loc)
|
|
{
|
|
(void)K::log(3, "positional:", positional.size(), "optional:", optional.size(), "rest:", rest.size());
|
|
std::map<std::string, std::string> values {};
|
|
check_positional(positional, loc);
|
|
for (size_t i = 0; i < m_positional.size(); ++i) {
|
|
auto param = m_positional[i];
|
|
std::string arg = as_string(positional[i].begin(), positional[i].end(), true);
|
|
values[param.m_name] = arg;
|
|
}
|
|
auto optional_values = check_optional(optional, loc);
|
|
for (auto [key, value] : optional_values) {
|
|
// check_optional returns only the options that were actually
|
|
// written, so an empty value here means the name was written alone
|
|
// (":number" rather than ":number 10") — distinct from the option
|
|
// being absent, which is filled from the default below. The
|
|
// argument type supplies the alone value; bool declares "true",
|
|
// which is what makes a bare boolean option mean true.
|
|
const Parameter* parameter = find(key);
|
|
if (value.empty() && parameter && !parameter->m_argtype.m_alone.empty()) {
|
|
value = parameter->m_argtype.m_alone;
|
|
}
|
|
values[key] = value;
|
|
}
|
|
for (auto opt : m_optional) {
|
|
values.try_emplace(opt.m_name, opt.m_default);
|
|
}
|
|
if (active(rest)) {
|
|
if (!m_rest.empty()) {
|
|
values[m_rest[0].m_name] = as_string(rest.begin(), rest.end(), true);
|
|
} else {
|
|
std::stringstream ss {};
|
|
ss << "More positional arguments were given (" << positional.size() + rest.size()
|
|
<< ") than defined (" << m_positional.size() << ")";
|
|
throw Argument_error(ss.str(), loc);
|
|
}
|
|
}
|
|
for (const auto& [name, value] : values) {
|
|
const Parameter* parameter = find(name);
|
|
if (parameter) {
|
|
validate(*parameter, value, loc);
|
|
}
|
|
}
|
|
return values;
|
|
}
|
|
|
|
// Check an argument value against its argument type's pattern. An empty
|
|
// value (an unsupplied optional argument without a default) is not checked.
|
|
// The error message includes the argument type's description from its
|
|
// @@@argtype definition, so the .k description text is what the writer
|
|
// sees when a complicated value (e.g. a table line specification) is wrong.
|
|
// The value-size limit guards against std::regex stack overflow: the
|
|
// libstdc++ executor recurses per character, so a pattern applied to a
|
|
// very large value crashes. Typed argument values are short; large
|
|
// values are content (rest, :text) whose types match everything and are
|
|
// excluded by matches_all() anyway.
|
|
const size_t validation_size_limit = 4096;
|
|
|
|
void Parameter_set::validate(
|
|
const Parameter& parameter, const std::string& value, const Locator& loc)
|
|
{
|
|
if (value.empty() || value.size() > validation_size_limit) {
|
|
return;
|
|
}
|
|
const Argtype& argtype = parameter.m_argtype;
|
|
if (argtype.matches_all()) {
|
|
return;
|
|
}
|
|
if (!std::regex_match(value, argtype.m_regex)) {
|
|
std::stringstream ss {};
|
|
ss << "The value \"" << value << "\" given for the argument \""
|
|
<< parameter.m_name << "\" does not match the \"" << argtype.m_name
|
|
<< "\" argument type.\n\nThe \"" << argtype.m_name << "\" argument type describes "
|
|
// A .k description is prose: collapse its source layout (line
|
|
// breaks, continuation indentation) so it re-flows with the
|
|
// sentence -- indented .k lines would otherwise read as verbatim.
|
|
<< collapse_whitespace(argtype.m_desc) << "\n";
|
|
throw Argument_error(ss.str(), loc);
|
|
}
|
|
}
|
|
|
|
const Parameter* Parameter_set::find(const std::string& name) const
|
|
{
|
|
for (const auto& params : {&m_positional, &m_optional, &m_rest}) {
|
|
for (const Parameter& p : *params) {
|
|
if (p.m_name == name) {
|
|
return &p;
|
|
}
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// Parameter/argument substitution
|
|
|
|
std::string replace_arguments(
|
|
const std::map<std::string, std::string>& values,
|
|
const std::string& parameterized_text, const Locator& loc)
|
|
{
|
|
(void)K::log(3);
|
|
std::string result = parameterized_text;
|
|
for (auto [name, value] : values) {
|
|
result = string_replace(result, '*' + name + '*', value);
|
|
}
|
|
auto matches = find_all(result, std::regex(R"((\*.*?\*))"));
|
|
std::vector<std::string> unmatched;
|
|
unmatched.reserve(matches.size());
|
|
std::copy(matches.begin(), matches.end(), std::back_inserter(unmatched));
|
|
|
|
auto unmatched_count = unmatched.size();
|
|
if (unmatched_count > 0) {
|
|
std::stringstream ss {};
|
|
ss << "Undefined " << plural("argument", unmatched_count) << " in klammer:\n";
|
|
for (const auto& arg : unmatched) {
|
|
ss << " " << arg << "\n";
|
|
}
|
|
ss << "To prevent the \"*\" character from specifying an argument, "
|
|
<< "precede it with the \"^\" character.";
|
|
throw Argument_error(ss.str(), loc);
|
|
}
|
|
return result;
|
|
}
|