A snapshot of the development tree. The substantial changes since the last one:
COMMAND OUTPUT POLICY. The three commands display text in exactly three cases,
and each owns a stream: LOGGING under "-v" greater than 0 and an ERROR before
termination go to STDERR; OUTPUT THE USER ASKED FOR goes to STDOUT. For ktext
that output is a document, so "ktext doc.kt -d | ..." is now safe -- logging
used to share the stream and land inside the document. A bare command prints
its usage and succeeds rather than failing. Colour is emitted only to a
terminal, per stream, and NO_COLOR is honoured.
"-v 1" reports every decision whose outcome you could not have read off your own
input: the klammerset that was loaded and from which file, a font's directory, a
":files" name's file, how "-o" was expanded. Higher levels are the trace.
The commands no longer warn and continue: an anomaly is an error, described with
its location. Two exceptions remain, each for a stated reason -- a condition
that is expected and temporary by design, and a judgment that is a heuristic
rather than exact.
@cond IS NOW A TRUE SPECIAL FORM, resolved at APPLICATION time rather than when
the file is read. Two consequences for a writer:
* a state variable reaches the predicate. "@@@state Flag :value true @@@
@cond *Flag* | T | F @" renders "T"; it used to see the literal "*Flag*" and
silently take the false branch. The document now behaves like a klammer
body, whose arguments are bound before its conditionals are decided.
* nothing in a discarded branch happens -- it is not read, not evaluated, not
expanded. An @eval in the branch not taken used to run anyway.
Its predicate relation is total and strict: true, True, 1; false, False, 0, and
empty; anything else is an error at the @cond rather than silently false.
@eval REACHING OUTSIDE. ":shell" and ":haskell" now keep the command's standard
error out of the document (it appears under "-v 1") and treat a nonzero exit as
an error naming what the command reported. A command that exits nonzero on
purpose -- "grep" finding no match -- says so with "|| true".
KLAMMER SETS. Several combine: "--klammersets a b c" loads all three in the
order given, sharing one namespace, with the definition modes deciding
collisions. "none" means none and may not be combined with other symbols. A
klammerset with symbol X is declared in a file X/X.k, which is what lets two
sets require the same third set without loading it twice.
TESTS. Four new suites: the kdiag command's interface, the @eval primitive's
contract with the outside world, and verbosity at both tiers. Three suites
that could not run on macOS at all now do.
Assembled from dev commit 6c8ee6c22fca.
365 lines
16 KiB
C++
365 lines
16 KiB
C++
#include <sstream>
|
|
|
|
#include "option_set_registry.h"
|
|
#include "argument_set.h"
|
|
#include "error.h"
|
|
#include "katom_list.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"
|
|
"An option set declares only optional parameters -- names written with "
|
|
"a leading \":\".",
|
|
begin->m_loc, false);
|
|
}
|
|
if (parameters.m_optional.empty()) {
|
|
throw Definition_error(
|
|
"The option set " + q_(name) + " declares no parameters.\n"
|
|
"The form is: @@" + name + ".o :name.argtype default ... : <description> @@",
|
|
begin->m_loc, false);
|
|
}
|
|
|
|
// 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"
|
|
<< "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"
|
|
<< "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"
|
|
<< "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. Declared option sets: "
|
|
<< option_sets.available() << ".";
|
|
}
|
|
throw Definition_error(ss.str(), loc, false);
|
|
}
|
|
|
|
// 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"
|
|
"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, false);
|
|
}
|
|
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"
|
|
"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, false);
|
|
}
|
|
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 + "\".\n It declares: " + option_set.member_names(),
|
|
option[0].m_loc, false);
|
|
}
|
|
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, false);
|
|
}
|
|
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;
|
|
}
|
|
}
|