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)
This commit is contained in:
2026-08-06 13:11:37 +02:00
parent 4306dcd490
commit 6e7596ab2e
37 changed files with 2198 additions and 267 deletions

View File

@@ -41,7 +41,10 @@ parse_name(const Target_registry& targets, const Katom& name_katom)
}
std::tuple<Katom, Parameter_set, katom_list, Locator>
parse_definition_katoms(const std::string& klammer_name, const Argtype_registry& argtypes, katom_iter& begin, katom_iter& end)
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(
@@ -68,7 +71,13 @@ parse_definition_katoms(const std::string& klammer_name, const Argtype_registry&
std::erase_if(parameter_katoms,
[](const Katom& k) { return k.m_type == katom_t::ignored; });
parameter_katoms = trim_whitespace(parameter_katoms);
if ((deftype->m_type == katom_t::klammer_instance ||
// "::" 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 ? "::" : ":::";
@@ -76,7 +85,16 @@ parse_definition_katoms(const std::string& klammer_name, const Argtype_registry&
"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);
@@ -84,11 +102,12 @@ parse_definition_katoms(const std::string& klammer_name, const Argtype_registry&
}
void Klammer::add_target_definition(
const std::string& target_name, const Argtype_registry& argtypes, katom_iter begin, katom_iter end)
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, argtypes, begin, end); // targets, begin, end);
parse_definition_katoms(m_name, target_name, argtypes, option_sets, begin, end);
// msg() << "Klammer " << m_name << " add: " << target_name << "\n";
// parameters.describe_parameters();
@@ -208,7 +227,8 @@ void Klammer::copy_components(
(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::general_name ||
target_name == Target_registry::optionset_name) {
continue;
}
m_parameters = parameters;
@@ -243,7 +263,6 @@ void Klammer::check_for_declaration_and_definitions()
if (def.target != Target_registry::declare_name) {
if (def.deftype == katom_t::klammer_definition ||
def.deftype == katom_t::klammer_default) {
msg() << def << "\n";
definitions.push_back(def);
}
}
@@ -286,7 +305,11 @@ void Klammer::copy_general_klammer_to_undefined(const Target_registry& targets)
}
for (const auto& target_name : targets.m_names) {
// std::cout << "General copy, considering " << target_name << "\n";
if (m_body.count(target_name) == 0 && target_name != Target_registry::declare_name) {
// "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
@@ -299,6 +322,39 @@ void Klammer::copy_general_klammer_to_undefined(const Target_registry& targets)
// 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);
@@ -318,14 +374,24 @@ void Klammer::no_declarations(const Target_registry& targets)
}
}
if (!all_equal<Parameter_set>(all_parameter_sets)) {
// std::cout << " Not all equal\n";
throw Definition_error(
error_list("There is no declaration (.k) target for klammer \"" + m_name + "\"\n"
"but the parameters of all targets are not the same",
m_defs,
"Use a .k klammer to define the parameters and describe the klammer,\n"
"with \"::\" and no parameters for all targets."),
m_defs[0].loc, false);
// 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);
@@ -485,10 +551,39 @@ std::string Klammer::description_text() const
}
// 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();
result += "@" + m_name + signature_text() + description_text() + option_set_text();
result = add_margin(result, margin) + "\n";
return result;
}