Files
klammertext/mac/klammer.cpp
Andy Kopra 6e7596ab2e Option sets: a .o target for shared parameters
A named group of optional parameters, declared once and used by several
klammers, so a writer learns one vocabulary instead of a spelling per
klammer.  The "o" target is a pseudo-target beside "k": "k" declares a
klammer's interface and documents it, "o" declares an option interface and
documents it, and neither produces output for any target.

    @@caption_args.o :caption :number.bool true :caption_side.side
    : Arguments that define a caption for a block element @@

    @@code.k :filename @hpos_args :hpos left @ @caption_args :caption_side top @
    | text.literal : A source file displayed verbatim @@

A set is used only in the parameter list of a ".k" declaration -- the one
place a klammer's interface is declared once for all of its targets -- and
is resolved as that list is read.  Names and types come from the set; a
default may be overridden where it is used.  A klammer application in a
parameter list is now a definition-time error.

The SKS gains the sets caption_args and hpos_args (:hpos and :offset), and
@table, @image, @image_grid, @reference and @show gain .k declarations.  A
distance is no longer written as a position: :hpos 4em is rejected, and the
same layout is :hpos left :offset 4em.  Code listings are numbered by
default, like tables and figures.

New engine sources mac/option_set{,_registry}.{h,cpp}; tst/ ships two more
suites, option_set_test.sh and signature_test.sh (twelve in all).

 (from dev 34e536cb0329)
2026-08-06 13:11:37 +02:00

592 lines
22 KiB
C++

#include <algorithm>
#include "klammer.h"
#include "show.h"
#include "log.h"
#include "argument_set.h"
#include "eval.h"
#include "util.h"
#include "character.h"
using namespace std::literals;
std::regex Klammer::name_re = std::regex(R"((\w+)(?:\.(\w+))?)");
std::tuple<std::string, std::string>
parse_name(const Target_registry& targets, const Katom& name_katom)
{
std::string name_with_target = trim_char(name_katom.m_text, '@');
std::smatch match {};
if (!std::regex_match(name_with_target, match, Klammer::name_re)) {
throw Parsing_error(
"The klammer name \"" + name_with_target + "\" is not correctly defined. "
"The form is \"<klammer-name>\" for general klammers or \"<klammer-name>.<target-name>\" "
"for a specialized target. The klammer defined as \"<klammer-name>.k\" specifies the "
"arguments and contains a description of the klammer in the definition body.",
name_katom.m_loc);
}
std::string klammer_name = match[1];
std::string target_name = match[2];
if (target_name.empty()) {
target_name = Target_registry::general_name;
}
if (!targets.has(target_name)) {
throw Target_error(
"The target \"" + target_name + "\" in klammer definition \"" + name_with_target + "\" "
"is not defined. Enter \"kdesc -t\" to see the targets defined by the Standard Klammer Set.",
name_katom.m_loc);
}
return { klammer_name, target_name };
}
std::tuple<Katom, Parameter_set, katom_list, Locator>
parse_definition_katoms(
const std::string& klammer_name, const std::string& target_name,
const Argtype_registry& argtypes, Option_set_registry& option_sets,
katom_iter& begin, katom_iter& end)
{
(void)K::log(3, *begin, *(end - 1));
katom_iter deftype = std::find_if(
begin, end, [](const Katom& k) { return is_deftype(k.m_type); });
if (deftype == end) {
throw Argument_error(
"The klammer definition does not contain a definition separator that specifies\n"
"how the klammer should be defined. The klammer syntax is:\n\n"
" @@<name>[.<target>] <parameters> : <body> @@ definition\n"
" @@<name>[.<target>] :: <body> @@ instance (uses .k parameters)\n"
" @@<name>[.<target>] ::: <body> @@ override existing definition\n"
" @@<name>[.<target>] <parameters> :::: <body> @@ default (can be overridden)",
begin->m_loc, false);
}
katom_list parameter_katoms(begin, deftype);
// Resolve text-removal markers (#, #[...]#, ##) in the parameter list.
// The interior of a @@...@@ definition is held verbatim until now (so a
// literal klammer body receives its content untouched), which means the
// parameter declarations still contain any removal markers the writer
// used. Apply removal here, to the parameters only -- the body is left
// verbatim by add_target_definition.
mark_ignored_katoms(parameter_katoms);
std::erase_if(parameter_katoms,
[](const Katom& k) { return k.m_type == katom_t::ignored; });
parameter_katoms = trim_whitespace(parameter_katoms);
// "::" and ":::" take their parameters from the ".k" declaration, so a
// parameter list written with them is a mistake -- EXCEPT for an option
// set, which has no separate declaration to inherit from: a set is its
// own declaration, so an override restates what it declares.
bool inherits_parameters = target_name != Target_registry::optionset_name;
if (inherits_parameters &&
(deftype->m_type == katom_t::klammer_instance ||
deftype->m_type == katom_t::klammer_override) &&
!parameter_katoms.empty()) {
std::string sym = deftype->m_type == katom_t::klammer_instance ? "::" : ":::";
throw Definition_error(
"The \"" + klammer_name + "\" klammer uses the \"" + sym + "\" symbol but defines parameters.",
begin->m_loc);
}
// Replace the option sets used in the parameter list with the parameters
// they declare. This is where the difference between an option set and a
// klammer lies: a set is resolved HERE, as the parameter list is read,
// rather than in the fixed-point apply loop, so what it contributes is
// present when the list is parsed. Any other klammer application in a
// parameter list is rejected by the same pass.
option_set_uses_t option_set_uses =
expand_option_sets(parameter_katoms, klammer_name, target_name, option_sets);
Parameter_set parameters(parameter_katoms, argtypes);
stamp_option_set_uses(parameters, option_set_uses);
katom_list body_katoms(deftype + 1, end - 1);
body_katoms = trim_whitespace(body_katoms);
return { *deftype, parameters, body_katoms, begin->m_loc };
}
void Klammer::add_target_definition(
const std::string& target_name, const Argtype_registry& argtypes,
Option_set_registry& option_sets, katom_iter begin, katom_iter end)
{
(void)K::log(3, *begin, *(end-1));
auto [deftype, parameters, body, loc] =
parse_definition_katoms(m_name, target_name, argtypes, option_sets, begin, end);
// msg() << "Klammer " << m_name << " add: " << target_name << "\n";
// parameters.describe_parameters();
std::regex variable_re(R"(\*(\w+)\*)");
int i = 0;
variable_map_t varmap {};
for (const auto& k : body) {
std::smatch match {};
std::string txt = k.m_text;
while (std::regex_search(txt, match, variable_re) &&
(k.m_type == katom_t::karg || k.m_type == katom_t::text)) {
//std::cout << " txt: " << txt << "\n";
std::string varname = match[1];
//m_varmap[target_name][varname].push_back(i);
varmap[varname].push_back(i);
txt = std::regex_replace(txt, std::regex(R"(\*)" + varname + R"(\*)"), "");
}
i++;
}
// m_body[target_name] = body;
m_defs.push_back({target_name, deftype.m_initial_type, parameters, body, varmap, begin->m_loc});
m_defloc[target_name] = begin->m_loc;
m_defmode[target_name] = defmode_from_katom(deftype.m_initial_type);
// std::cout << "Klammer " << m_name << "." << target_name << ": " << m_defloc[target_name] << "\n";
}
void Klammer::remove_target_definition(const std::string& target_name)
{
std::erase_if(m_defs,
[&target_name](const auto& def) { return def.target == target_name; });
m_defloc.erase(target_name);
m_defmode.erase(target_name);
m_body.erase(target_name);
m_varmap.erase(target_name);
}
// Rationalize multiple definitions
std::string error_list(const std::string& label, const auto& components, const std::string& after="")
{
std::stringstream ss {};
ss << label << ":\n";
for (const auto& c : components) {
ss << " " << c.loc.desc() << "\n";
}
ss << after;
return ss.str();
}
auto Klammer::target_defs(const std::vector<std::string>& target_names)
{
std::vector<Klammer::components> defs {};
for (const auto& target : target_names) {
auto target_defs = collect_if(
m_defs, [&target](const auto& def) { return def.target == target; });
defs.insert(defs.end(), target_defs.begin(), target_defs.end());
}
return defs;
}
auto Klammer::instance_defs()
{
return collect_if(
m_defs,
[] (const auto& def) {
return def.deftype == katom_t::klammer_instance
|| def.deftype == katom_t::klammer_override; });
}
void Klammer::disallow_instances() //Klammer::components declaration)
{
auto instances = instance_defs();
if (!instances.empty()) {
int icount = instances.size();
std::stringstream ss {};
ss << "There " << to_be(icount) << " " << icount << " "
<< plural("instance", icount) << " (defined by \"::\"), but "
<< "no declarations (defined by a \".k\" target)";
throw Definition_error(
error_list(ss.str(), instances), instances[0].loc, false);
}
}
bool Klammer::copy_to_instances(const Target_registry& targets)
{
auto instances = instance_defs();
if (!instances.empty()) {
auto definitions = collect_if(
m_defs,
[] (const auto& def) {
return def.deftype != katom_t::klammer_instance
&& def.deftype != katom_t::klammer_override; });
int dcount = definitions.size();
if (dcount != 1) {
int icount = instances.size();
std::stringstream ss {};
ss << "There " << to_be(icount) << " " << icount << " "
<< plural("instance", icount) << " (defined by \"::\"), but "
<< dcount << " "<< plural("definition", dcount) << " (defined by \":\")";
throw Definition_error(
error_list(ss.str(), definitions), definitions[0].loc, false);
} else {
copy_components(definitions[0].parameters, m_defs, targets);
return true;
}
} else {
return false;
}
}
void Klammer::copy_components(
const Parameter_set& parameters, const std::vector<Klammer::components>& cs,
const Target_registry& targets)
{
(void)K::log(4);
for (const auto& target_name : targets.m_names) {
if (target_name == Target_registry::declare_name ||
target_name == Target_registry::general_name ||
target_name == Target_registry::optionset_name) {
continue;
}
m_parameters = parameters;
}
for (const auto& c : cs) {
m_body[c.target] = c.body;
m_body_generic[c.target] = (c.target == Target_registry::general_name);
m_varmap[c.target] = c.varmap;
}
}
// Verify that there is no more than one general klammer definition
void Klammer::check_for_multiple_general_klammers()
{
auto general_klammers = target_defs({Target_registry::general_name});
if (general_klammers.size() > 1) {
throw Definition_error(
error_list(
"There is more than one general klammer (a klammer in which no target is defined)",
general_klammers),
general_klammers[0].loc, false);
}
}
void Klammer::check_for_declaration_and_definitions()
{
auto declares = target_defs({Target_registry::declare_name});
if (!declares.empty()) {
std::vector<Klammer::components> definitions {};
for (const auto& def : m_defs) {
if (def.target != Target_registry::declare_name) {
if (def.deftype == katom_t::klammer_definition ||
def.deftype == katom_t::klammer_default) {
definitions.push_back(def);
}
}
}
if (!definitions.empty()) {
auto defsize = definitions.size();
std::string desc = defsize == 1 ? "a definition" :
std::to_string(defsize) + " definitions";
throw Definition_error(
error_list("A klammer has both a declaration (.k) as well as " + desc + "\n(instances are defined by \"::\")",
definitions),
declares[0].loc, false);
}
}
}
// If a general definition exists, use it for targets not defined, but check signatures
void Klammer::copy_general_klammer_to_undefined(const Target_registry& targets)
{
(void)K::log(4);
auto general_klammers = target_defs({Target_registry::general_name});
auto declares = target_defs({Target_registry::declare_name});
// Check matching signatures (though this case already handled)
if (general_klammers.size() == 1) {
if (declares.empty() && !m_parameters.m_katoms.empty()) {
auto general_parameters = general_klammers[0].parameters;
if (general_parameters != m_parameters) {
// m_parameters.describe_parameters();
throw Definition_error(
error_list("The general parameters are different than the defined parameters",
general_klammers),
m_parameters.m_katoms[0].m_loc, false);
}
}
const auto& [target, deftype, parameters, body, varmap, loc] = general_klammers[0];
if (m_parameters.m_katoms.empty()) {
m_parameters = parameters;
}
for (const auto& target_name : targets.m_names) {
// std::cout << "General copy, considering " << target_name << "\n";
// "k" and "o" declare interfaces rather than produce output, so a
// general body is never copied to them.
if (m_body.count(target_name) == 0 &&
target_name != Target_registry::declare_name &&
target_name != Target_registry::optionset_name) {
// std::cout << " Copying to " << target_name << "\n";
m_body[target_name] = body;
m_body_generic[target_name] = true; // general body -> writer content
m_defloc[target_name] = loc;
m_varmap[target_name] = varmap;
}
}
}
}
// Three declaration cases: none, one, many
namespace {
// A compact, plain-text rendering of ONE definition's parameter list, for
// diagnostics that must show how two definitions differ.
// Klammer::signature_text() cannot serve here: it renders the rationalized
// m_parameters, which is exactly what does not exist yet when the per-target
// lists disagree.
std::string parameter_signature(const Parameter_set& parameters)
{
std::string result {};
auto add = [&result](const std::string& s) {
if (!result.empty()) result += " ";
result += s;
};
auto typed = [](const Parameter& p) {
return p.m_argtype.m_name == default_argtype
? p.m_name : p.m_name + "." + p.m_argtype.m_name;
};
bool first = true;
for (const auto& pos : parameters.m_positional) {
add(first ? typed(pos) : "| " + typed(pos));
first = false;
}
for (const auto& rest : parameters.m_rest)
add(typed(rest));
for (const auto& opt : parameters.m_optional)
add(":" + typed(opt)
+ (opt.m_default.empty() ? "" : " " + opt.m_default));
return result.empty() ? "(no parameters)" : result;
}
} // namespace
void Klammer::no_declarations(const Target_registry& targets)
{
(void)K::log(4);
// std::cout << boldblack << "No declarations\n" << black;
check_for_multiple_general_klammers();
std::vector<Klammer::components> defs {};
std::vector<std::string> target_names = targets.applicable();
std::vector<Parameter_set> all_parameter_sets {};
// Are all parameters the same?
for (const auto& def : m_defs) {
if (std::ranges::find(target_names, def.target) != target_names.end()) {
// std::cout << " Found: " << def.target << "\n";
all_parameter_sets.push_back(def.parameters);
} else {
// std::cout << " Not found: " << def.target << "\n";
}
}
if (!all_equal<Parameter_set>(all_parameter_sets)) {
// Show each target's own parameter list, not just its location: with
// more than two targets the designer would otherwise have to diff the
// definitions by hand to find which one drifted.
std::stringstream ss {};
ss << "The parameters of klammer \"" << m_name
<< "\" are not the same for every target,\n"
"and there is no declaration (.k) target to define them once:\n";
for (const auto& def : m_defs) {
if (std::ranges::find(target_names, def.target) == target_names.end())
continue;
std::string target = def.target;
target.resize(std::max(target.size(), size_t(6)), ' ');
ss << " " << target << " " << parameter_signature(def.parameters)
<< "\n " << def.loc.desc() << "\n";
}
ss << "Use a .k target to declare the parameters and describe the klammer,\n"
"and \"::\" with no parameters for each target's definition.";
throw Definition_error(ss.str(), m_defs[0].loc, false);
} else {
// std::cout << " All equal\n";
copy_components(m_defs[0].parameters, m_defs, targets);
}
copy_general_klammer_to_undefined(targets);
}
void Klammer::one_declaration(const Target_registry& targets, const Klammer::components& declare)
{
(void)K::log(4);
// std::cout << boldblack << "One declaration\n" << black;
check_for_multiple_general_klammers();
check_for_declaration_and_definitions();
copy_components(declare.parameters, m_defs, targets);
copy_general_klammer_to_undefined(targets);
}
void Klammer::many_declarations(const std::vector<Klammer::components>& declares)
{
(void)K::log(4);
// std::cout << boldblack << "Many declarations\n" << black;
throw Definition_error(
error_list("More than one declaration (.k) klammer", declares),
declares[0].loc, false);
}
void Klammer::rationalize(const Target_registry& targets)
{
(void)K::log(3, m_name);
auto declares = target_defs({Target_registry::declare_name});
auto declare_count = declares.size();
if (declare_count == 0) {
disallow_instances();
if (!copy_to_instances(targets)) {
no_declarations(targets);
}
} else if (declare_count == 1) {
one_declaration(targets, declares[0]);
} else {
many_declarations(declares);
}
}
std::string klammer_name_from_katom(const std::string& s, const Locator& loc)
{
std::regex rgx(R"(@(\w+).*)");
std::smatch match {};
if (std::regex_match(s, match, rgx)) {
return match[1];
} else {
throw Definition_error("The form of the klammer name " + q_(s) + " is not correct", loc);
}
}
void label(const std::string& s)
{
int w = 13;
std::cout << std::right << std::setw(w) << std::setfill(' ') << s << ": ";
}
void show_args(const std::string& label_text, const std::vector<Argument>& arguments)
{
if (!arguments.empty()) {
label(label_text);
for (const auto& a : arguments) {
std::cout << a << " ";
}
std::cout << '\n';
}
}
strings_t Klammer::get_target_names() const
{
strings_t names {};
for (const auto& [target, body] : m_body) {
std::stringstream ss {};
// ss << name << target.m_loc.m_line;
ss << target;
names.push_back(ss.str());
}
return names; // return join(names, ","s);
}
strings_t Klammer::get_locations()
{
strings_t locs {};
for (auto [target, locator] : m_defloc) {
std::cout << target << right_arrow << locator << "\n";
}
return {};
}
std::string Klammer::signature_text() const
{
std::string result {};
bool has_pos = false;
bool has_opt = false;
for (const auto& pos : m_parameters.m_positional) {
result += pos.m_name;
std::string type = pos.m_argtype.m_name;
if (type != default_argtype) {
result += "." + type;
}
result += " | ";
has_pos = true;
}
if (result.size() >= 2)
result.resize(result.size() - 2);
auto opt_count = m_parameters.m_optional.size();
if (opt_count <= 3 && !has_pos) {
result += " ";
}
for (const auto& opt : m_parameters.m_optional) {
if (opt_count > 3) {
result += "\n :" + opt.m_name;
} else {
result += ":" + opt.m_name;
}
std::string type = opt.m_argtype.m_name;
if (type != default_argtype) {
result += "." + type;
}
// result += ":" + opt.m_name + "." + opt.m_argtype.m_name;
if (!opt.m_default.empty()) {
result += " " + italic_on() + opt.m_default + italic_off();
}
result += " ";
has_opt = true;
}
result = trim_right(result);
if (has_pos) {
result = " " + result;
}
if (opt_count > 3) {
result += "\n";
} else if (has_opt or has_pos) {
result += " ";
}
result += "@\n";
return result;
}
std::string Klammer::description_text() const
{
std::string result = " [" + m_name + ": no description]";
if (m_body.contains("k")) {
result = to_string(m_body.at("k"), true);
result = justify(result, 80, 1);
}
return result;
}
// Which option sets this klammer's parameters came from, and which of their
// defaults it overrode. A reader of the signature sees the effective
// interface; this says how much of it the klammer shares with other klammers,
// which is the reason for declaring a set in the first place.
std::string Klammer::option_set_text() const
{
std::vector<std::string> sets {};
std::map<std::string, std::vector<std::string>> overridden {};
for (const auto& opt : m_parameters.m_optional) {
if (opt.m_option_set.empty()) continue;
if (!is_in(opt.m_option_set, sets)) {
sets.push_back(opt.m_option_set);
}
if (opt.m_default_overridden) {
overridden[opt.m_option_set].push_back(":" + opt.m_name);
}
}
if (sets.empty()) return "";
std::vector<std::string> descriptions {};
for (const auto& set : sets) {
std::string desc = set;
if (overridden.count(set) > 0) {
desc += " (" + join(overridden[set], ", ") + " defaulted here)";
}
descriptions.push_back(desc);
}
return "\n Option sets: " + join(descriptions, ", ");
}
std::string Klammer::describe(int margin) const
{
std::string result {};
result += "@" + m_name + signature_text() + description_text() + option_set_text();
result = add_margin(result, margin) + "\n";
return result;
}