An optional argument has three values: the default (the name is absent), the argument type's :alone value (the name is written alone), and a written value. :alone is declared by the argument type only, never by a klammer's parameter declaration -- a default is what one klammer means by silence, but a bare option name must read the same way in every klammer. The bool type declares :alone true, which is the whole of the convention that a bare boolean option means true; there is no boolean special case in the engine. A type whose pattern matches running text cannot declare :alone, since an option's value runs to the next bar or option name and would swallow the following text. kdesc and `ktext -m` now show [default: X] and [alone: Y] per argument type. In the Standard Klammer Set: @code :number becomes a bool (it was an untyped string tested only for truthiness, so a bare :number was a no-op); decimal_mark declares :alone comma; table_hline and table_vline declare :alone all. The 35 bools that default false gained the bare form for free. Tests: tst/alone_test.sh (21 cases) joins the shipped suite. (from dev 6024f49c2859)
527 lines
18 KiB
C++
527 lines
18 KiB
C++
#include <ranges>
|
|
#include <algorithm>
|
|
#include <utility>
|
|
|
|
#include "show.h"
|
|
#include "log.h"
|
|
#include "katom.h"
|
|
#include "argtype_set.h"
|
|
#include "argument_set.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_set& 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_set argtypes {};
|
|
parse_parameters(
|
|
katomize(line_split(parameter_string), Locator().str()),
|
|
argtypes);
|
|
}
|
|
|
|
|
|
Parameter_set::Parameter_set(const std::vector<Katom>& katoms)
|
|
: m_katoms(katoms)
|
|
{
|
|
(void)K::log(3);
|
|
Argtype_set argtypes {};
|
|
parse_parameters(m_katoms, argtypes);
|
|
}
|
|
|
|
Parameter_set::Parameter_set(const std::vector<Katom>& katoms, const Argtype_set& 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_set& argtypes)
|
|
{
|
|
// (void)K::log(3, katoms);
|
|
if (katoms.size() > 1) {
|
|
throw Argument_error("Multiple katoms for positional argument: " +
|
|
as_string(katoms.begin(), katoms.end(), true) +
|
|
"\nPositional arguments are separated by the bar (|) character.",
|
|
katoms[0].m_loc, false);
|
|
}
|
|
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_set& 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_set& 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(
|
|
std::string label,
|
|
std::vector<std::vector<Katom>> positional,
|
|
std::vector<std::vector<Katom>> optional,
|
|
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, false);
|
|
} 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, false);
|
|
}
|
|
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\n"
|
|
<< trim(argtype.m_desc) << "\n";
|
|
throw Argument_error(ss.str(), loc, false);
|
|
}
|
|
}
|
|
|
|
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, false);
|
|
}
|
|
return result;
|
|
}
|