Files
klammertext/mac/option_set_registry.cpp
T
ack e4f1307aa9 Characters that killed a render, what may sit next to a klammer, and ref/
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)
2026-09-07 22:37:05 +02:00

365 lines
16 KiB
C++

#include <sstream>
#include "option_set_registry.h"
#include "argument_set.h"
#include "error.h"
#include "katom_process.h"
#include "klammer.h"
#include "log.h"
#include "show.h"
#include "target_registry.h"
#include "util.h"
// --- Registration ---------------------------------------------------------
void Option_set_registry::add(
const Argtype_registry& argtypes, const std::string& name,
katom_iter begin, katom_iter end, katom_list& katoms)
{
(void)K::log(3, *begin, *(end - 1));
restore_initial_type(begin, end);
katom_iter definition_begin = begin + 1;
katom_iter definition_end = end - 1;
auto [deftype, parameters, body, loc] = parse_definition_katoms(
name, Target_registry::optionset_name, argtypes, *this,
definition_begin, definition_end);
if (deftype.m_initial_type == katom_t::klammer_instance) {
throw Definition_error(
"The option set " + q_(name) + " is declared with \"::\", which takes its "
"parameters from a \".k\" declaration. An option set is a declaration; "
"it declares its parameters itself, after \":\".",
begin->m_loc);
}
defmode_t incoming_mode = defmode_from_katom(deftype.m_initial_type);
if (has(name)) {
const Option_set& current = m_option_sets.at(name);
const auto& result = defmode_transition(current.m_defmode, incoming_mode);
// The transition table is the single statement of the redefinition
// policy, for klammers and option sets alike; only the noun in its
// messages is specific to what is being redefined.
std::string message = string_replace(result.message, "Klammer NAME", "Option set NAME");
message = string_replace(message, "klammer NAME", "option set NAME");
message = string_replace(message, "NAME", q_(name + ".o"));
message = string_replace(message, "AT", current.m_loc.desc());
if (!result.replace) {
if (message.empty()) { // a default silently superseded
modify_type(katom_t::replaced, begin, end);
auto next_iter = end;
ignore_whitespace(next_iter, katoms);
return;
}
throw Definition_error(message, begin->m_loc);
}
if (result.warn) {
// Reported, not warned: see the same case in klammer_registry.cpp.
(void)K::log(1, message + " at " + begin->m_loc.desc());
}
}
// Optional parameters only. A positional parameter is not writer-facing
// -- the author never types its name -- so there is nothing for an option
// set to standardize, and a set of them would be a klammer signature
// rather than a shared vocabulary.
if (!parameters.m_positional.empty() || !parameters.m_rest.empty()) {
std::vector<std::string> names {};
for (const auto& p : parameters.m_positional) names.push_back(p.m_name);
for (const auto& p : parameters.m_rest) names.push_back(p.m_name);
throw Definition_error(
"The option set " + q_(name) + " declares the positional "
+ plural("parameter", static_cast<int>(names.size())) + " "
+ join(names, ", ") + ".\n\n"
"An option set declares only optional parameters -- names written with "
"a leading \":\".",
begin->m_loc);
}
if (parameters.m_optional.empty()) {
throw Definition_error(
"The option set " + q_(name) + " declares no parameters.\n\n"
"The form is: @@" + name + ".o :name.argtype default ... : <description> @@",
begin->m_loc);
}
// The members as written: these katoms are what is spliced into the
// parameter list of a declaration that uses the set.
auto [positional_katoms, member_katoms] =
parameter_split(parameters.m_katoms.cbegin(), parameters.m_katoms.cend());
(void)positional_katoms; // already rejected above
// The two views of the members -- as katoms and as parsed parameters --
// are used together when the set is spliced into a parameter list, and
// are paired by position.
if (member_katoms.size() != parameters.m_optional.size()) {
throw Internal_error(
"The option set " + q_(name) + " parsed " + std::to_string(parameters.m_optional.size())
+ " parameters from " + std::to_string(member_katoms.size()) + " declarations",
begin->m_loc);
}
Option_set option_set(name, to_string(body, true), parameters, member_katoms, begin->m_loc);
option_set.m_defmode = incoming_mode;
if (has(name)) {
// A redefinition keeps the users recorded so far: they used the name,
// and the name is what they are bound to.
option_set.m_users = m_option_sets.at(name).m_users;
} else {
m_names.push_back(name);
}
m_option_sets[name] = option_set;
modify_type(katom_t::replaced, begin, end);
auto next_iter = end;
ignore_whitespace(next_iter, katoms);
}
bool Option_set_registry::has(const std::string& name) const
{
return m_option_sets.count(name) > 0;
}
const Option_set& Option_set_registry::get(const std::string& name, const Locator& loc) const
{
auto it = m_option_sets.find(name);
if (it == m_option_sets.end()) {
throw Definition_error("The option set " + q_(name) + " is not declared", loc);
}
return it->second;
}
void Option_set_registry::add_user(const std::string& set_name, const std::string& klammer_name)
{
auto it = m_option_sets.find(set_name);
if (it != m_option_sets.end() && !is_in(klammer_name, it->second.m_users)) {
it->second.m_users.push_back(klammer_name);
}
}
std::string Option_set_registry::available() const
{
return m_names.empty() ? "(none are declared)" : join(m_names, ", ");
}
std::string Option_set_registry::describe(int margin, const strings_t& defined_outside) const
{
std::string result {};
for (const auto& name : m_names) {
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;
}
// --- Use in a parameter list ----------------------------------------------
namespace {
// The end of the application opening at `begin`: one past its closing
// delimiter. Depth is counted over klammer applications only, which is all
// that can nest inside an option-set use.
katom_iter application_end(katom_iter begin, katom_iter end)
{
int depth = 0;
for (auto k = begin; k != end; ++k) {
if (begin_klammer_apply(*k)) {
++depth;
} else if (end_klammer_apply(*k)) {
if (--depth == 0) return k + 1;
}
}
throw Definition_error(
"The klammer " + q_(trim_char(begin->m_text, '@')) +
" in a parameter list is not closed", begin->m_loc);
}
// The parameter name in an option-name katom, which carries the type and any
// target as written: ":number.bool" declares the parameter named "number".
std::string option_name(const Katom& katom)
{
std::string name(katom.m_text, 1);
auto type = name.find('.');
return type == std::string::npos ? name : name.substr(0, type);
}
// How a definition is named in a diagnostic: a general definition has no
// target to name, and the pseudo-targets read better with their suffix.
std::string definition_name(const std::string& klammer_name, const std::string& target_name)
{
return target_name == Target_registry::general_name
? q_(klammer_name) : q_(klammer_name + "." + target_name);
}
// A klammer application in a parameter list is never legal. Which of the two
// ways it is wrong decides what the writer has to do about it, so the message
// says which.
[[noreturn]] void reject_application(
const std::string& name, const std::string& klammer_name,
const std::string& target_name, const Option_set_registry& option_sets,
const Locator& loc)
{
std::stringstream ss {};
if (option_sets.has(name) && target_name == Target_registry::optionset_name) {
ss << "The option set " << q_(name) << " is used in the declaration of the option "
<< "set " << q_(klammer_name) << ".\n\n"
<< "An option set is used only in the parameter list of a \".k\" declaration, "
<< "so a set does not include another set: a klammer that needs two "
<< "vocabularies names two sets, and each set stays a vocabulary that can "
<< "be learned whole.";
} else if (option_sets.has(name)) {
ss << "The option set " << q_(name) << " is used in the parameter list of "
<< definition_name(klammer_name, target_name) << ".\n\n"
<< "An option set may be used only in the parameter list of a \".k\" "
<< "declaration, which is where a klammer's interface is declared once "
<< "for all of its targets. Declare "
<< q_(klammer_name + ".k") << " and give each target's definition as "
<< "an instance (\"::\"), which inherits the declared parameters.";
} else {
ss << "The klammer " << q_(name) << " is applied in the parameter list of "
<< definition_name(klammer_name, target_name) << ".\n\n"
<< "A klammer application in a parameter list is not allowed: it is "
<< "resolved after the parameters are parsed, so the parameter list it "
<< "was meant to contribute is not there when the list is read. An "
<< "option set, declared with a \".o\" target, is how parameters are "
<< "shared between klammers.\n\nDeclared option sets: "
<< option_sets.available() << ".";
}
throw Definition_error(ss.str(), loc);
}
// The default written for each member at the use site:
// @caption_args :number false :caption_side top @
// Only defaults may be given -- names and types belong to the set.
std::map<std::string, std::string> use_site_defaults(
const Option_set& option_set, katom_iter begin, katom_iter end)
{
auto [positional, optional, rest] = argument_split(begin + 1, end - 1, 0);
if (!positional.empty() || active(rest)) {
throw Definition_error(
"The use of the option set " + q_(option_set.m_name) +
" gives a value that is not an option.\n\n"
"A set's names and types are fixed where the set is declared; only a "
"default may be given where it is used, written as \":name value\".",
begin->m_loc);
}
std::map<std::string, std::string> defaults {};
for (const auto& option : optional) {
std::string name = option_name(option[0]);
if (name.size() + 1 != option[0].m_text.size()) {
throw Definition_error(
"The use of the option set " + q_(option_set.m_name) + " gives a type for \":"
+ name + "\".\n\n"
"A set's names and types are declared where the set is; only a default "
"may be given where it is used.",
option[0].m_loc);
}
const Parameter* member = option_set.find(name);
if (member == nullptr) {
throw Definition_error(
"The option set " + q_(option_set.m_name) + " has no parameter \":"
+ name + "\". It declares:\n " + option_set.member_names(),
option[0].m_loc);
}
if (defaults.count(name) > 0) {
throw Definition_error(
"A default for \":" + name + "\" is given more than once in the use of "
"the option set " + q_(option_set.m_name),
option[0].m_loc);
}
std::string value = trim(to_string(katom_list(option.begin() + 1, option.end())));
// A bare option name means the argument type's :alone value, exactly
// as it does where an argument is written.
if (value.empty() && !member->m_argtype.m_alone.empty()) {
value = member->m_argtype.m_alone;
}
// Validated here, at definition time: an invalid default must not
// wait for an application that happens not to supply the argument.
Parameter_set::validate(*member, value, option[0].m_loc);
defaults[name] = value;
}
return defaults;
}
} // namespace
option_set_uses_t expand_option_sets(
katom_list& parameters, const std::string& klammer_name,
const std::string& target_name, Option_set_registry& option_sets)
{
option_set_uses_t uses {};
// Every parameter name in the list, and where it came from, so that a
// collision between two sets (or between a set and a name written here)
// can name both origins rather than just the name.
std::map<std::string, std::string> origin {};
auto declare = [&](const std::string& name, const std::string& from, const Locator& loc) {
auto previous = origin.find(name);
if (previous != origin.end()) {
throw Definition_error(
"The parameter \":" + name + "\" of " + q_(klammer_name) +
" is declared twice:\n " + previous->second + "\n " + from,
loc);
}
origin[name] = from;
};
katom_list result {};
result.reserve(parameters.size());
for (auto k = parameters.begin(); k != parameters.end(); ) {
if (!begin_klammer_apply(*k)) {
if (k->m_type == katom_t::option_name) {
declare(option_name(*k), "declared in the parameter list", k->m_loc);
}
result.push_back(*k);
++k;
continue;
}
std::string name = trim_char(k->m_text, '@');
auto end = application_end(k, parameters.end());
if (target_name != Target_registry::declare_name || !option_sets.has(name)) {
reject_application(name, klammer_name, target_name, option_sets, k->m_loc);
}
const Option_set& option_set = option_sets.get(name, k->m_loc);
std::map<std::string, std::string> defaults = use_site_defaults(option_set, k, end);
// m_members and m_parameters.m_optional are the same members in the
// same order -- one is the katoms as declared, the other what they
// parsed to -- so the parameter's name is taken from the parsed side
// rather than re-derived from the katom text (":number.bool").
for (size_t i = 0; i < option_set.m_members.size(); ++i) {
const katom_list& member = option_set.m_members[i];
const std::string& member_name = option_set.m_parameters.m_optional[i].m_name;
declare(member_name, "from the option set " + q_(name) + ", "
+ option_set.m_loc.desc(), k->m_loc);
if (!result.empty()) {
result.push_back(Katom(" ", katom_t::space, member[0].m_loc));
}
// The name katom comes from the set, so the parameter's location
// is where it is declared; an overriding default comes from here.
result.push_back(member[0]);
auto given = defaults.find(member_name);
if (given == defaults.end()) {
result.insert(result.end(), member.begin() + 1, member.end());
} else if (!given->second.empty()) {
result.push_back(Katom(" ", katom_t::space, k->m_loc));
result.push_back(Katom(given->second, katom_t::text, k->m_loc));
}
uses[member_name] = {name, given != defaults.end()};
}
option_sets.add_user(name, klammer_name);
k = end;
}
parameters = result;
return uses;
}
void stamp_option_set_uses(Parameter_set& parameters, const option_set_uses_t& uses)
{
if (uses.empty()) return;
for (Parameter& parameter : parameters.m_optional) {
auto use = uses.find(parameter.m_name);
if (use == uses.end()) continue;
parameter.m_option_set = use->second.m_set;
parameter.m_default_overridden = use->second.m_overridden;
}
}