150 lines
7.0 KiB
C++
150 lines
7.0 KiB
C++
#pragma once
|
|||
|
|
|
||
|
|
#include <regex>
|
||
|
|
#include <string>
|
||
|
|
#include <vector>
|
||
|
|
|
||
|
|
#include "ktype.h"
|
||
|
|
|
||
|
|
// A regex kept beside the pattern it was built from, so a rule can be
|
||
|
|
// DISPLAYED as written (kdesc --rewrite) as well as applied. It lived in
|
||
|
|
// ktype.h until the rule table moved here, and nothing else ever used it.
|
||
|
|
class Rgx {
|
||
|
|
public:
|
||
|
|
explicit Rgx(std::string pattern)
|
||
|
|
: m_pattern(pattern)
|
||
|
|
, m_regex(std::regex(pattern)) {}
|
||
|
|
std::string m_pattern;
|
||
|
|
std::regex m_regex;
|
||
|
|
};
|
||
|
|
|
||
|
|
// THE KATOM REWRITE RULES: what divides text that is not yet a katom.
|
||
|
|
// Live since 2026-09-02. The table moved here from ktype.h, where it had
|
||
|
|
// been a flat list of tuples, and gained a category per rule; its companion
|
||
|
|
// is katomize.{h,cpp}, which does the dividing.
|
||
|
|
//
|
||
|
|
// WHY CATEGORIES. The rule list is a PRIORITY list: the first rule whose
|
||
|
|
// rewrite changes the word fires, its parts are re-katomized, and the loop
|
||
|
|
// RETURNS. Every later rule that would also have matched is preempted --
|
||
|
|
// and that preemption is the interaction the flat list states nowhere.
|
||
|
|
// Measured over every tracked .k and .kt (dbg/rule_conflicts.cpp): 1,971
|
||
|
|
// words reach the rules and there are 806 preemptions.
|
||
|
|
//
|
||
|
|
// Two facts from that measurement make the categories worth having:
|
||
|
|
//
|
||
|
|
// * TWO THIRDS OF THE INTERACTION IS LOCAL. 532 of the 806 preemptions
|
||
|
|
// are between rules of the SAME category. Grouping puts those pairs
|
||
|
|
// next to each other, where the reader can see them.
|
||
|
|
//
|
||
|
|
// * THE CATEGORY RELATION IS A STRICT ORDER. Cross-category preemption
|
||
|
|
// has no cycles, and -- this is the surprise -- the current declaration
|
||
|
|
// order already violates it nowhere. The order in ktype.h was arrived
|
||
|
|
// at one precedent at a time and is nevertheless a valid topological
|
||
|
|
// sort. Categories therefore DOCUMENT the existing order rather than
|
||
|
|
// changing it, which is why this file is expected to be behaviorally
|
||
|
|
// identical to the current katomizer. Any divergence the comparison
|
||
|
|
// reports is a transcription error here, not a design difference.
|
||
|
|
//
|
||
|
|
// The order between categories, and how much of it is forced:
|
||
|
|
//
|
||
|
|
// caret FORCED first -- it preempts every other category
|
||
|
|
// bar free (only "after caret" is forced)
|
||
|
|
// removal_span before removal_line
|
||
|
|
// karg before klammer
|
||
|
|
// punctuation before klammer
|
||
|
|
// desugar free (only "after caret" is forced)
|
||
|
|
// klammer before removal_line
|
||
|
|
// spacing before removal_line
|
||
|
|
// removal_line FORCED last
|
||
|
|
//
|
||
|
|
// Six constraints in place of thirty positions. Note that removal_span
|
||
|
|
// (#[, ]#, #-) and removal_line (a bare #) are one CHARACTER and two
|
||
|
|
// categories: the specific forms are decided before the general one, which
|
||
|
|
// is longest match stated as an ordering.
|
||
|
|
|
||
|
|
enum class rule_category {
|
||
|
|
caret, // ^| ^' '^ ^X ^...^ ^Xy -- the quoting and character forms
|
||
|
|
bar, // || | -- argument separators
|
||
|
|
removal_span, // #[ ]# #- -- delimited removal and whitespace removal
|
||
|
|
karg, // *name* -- argument substitution in a body
|
||
|
|
punctuation, // @ before . , ; : ? ! ( ) '
|
||
|
|
desugar, // @name-arg -- the abbreviated application
|
||
|
|
klammer, // @name@ and klammers embedded in a word
|
||
|
|
spacing, // #+N -- added whitespace
|
||
|
|
removal_line, // # -- remove to end of line
|
||
|
|
};
|
||
|
|
|
||
|
|
std::string category_name(rule_category category);
|
||
|
|
|
||
|
|
// The category sequence the dispatch follows. This is the whole of the
|
||
|
|
// cross-category ordering decision, in one place.
|
||
|
|
const std::vector<rule_category>& category_order();
|
||
|
|
|
||
|
|
struct Rewrite_rule {
|
||
|
|
rule_category category;
|
||
|
|
std::string description;
|
||
|
|
Rgx pattern;
|
||
|
|
std::string replacement;
|
||
|
|
// Non-null for a rule whose rewrite is not a plain regex_replace. One
|
||
|
|
// rule needs it: the abbreviated application @name-a-b, which today is
|
||
|
|
// a hand-cut special case at the TOP of expand_compound_katom() rather
|
||
|
|
// than an entry in the table at all. Making it a rule in the desugar
|
||
|
|
// category is safe -- no rule of an earlier category can match a word
|
||
|
|
// made only of "@", word characters and hyphens -- and it removes the
|
||
|
|
// oddity that the shortcut is currently reported by "kdiag --rewrite"
|
||
|
|
// as the doing of whichever rule happened to be first in the list.
|
||
|
|
std::string (*special)(const std::string& word) = nullptr;
|
||
|
|
};
|
||
|
|
|
||
|
|
// In DECLARATION order, matching ktype.h's katom_rewrite_rules index for
|
||
|
|
// index, so the two can be diffed. Dispatch order is a separate question:
|
||
|
|
// see rule_order() below.
|
||
|
|
extern const std::vector<Rewrite_rule> rewrite_rules;
|
||
|
|
|
||
|
|
enum class rewrite_order {
|
||
|
|
// The order written in ktype.h. Categories are annotation only, and
|
||
|
|
// the behavior is by construction identical to the current katomizer.
|
||
|
|
as_declared,
|
||
|
|
// Rules grouped so each category is contiguous, in category_order().
|
||
|
|
// Differs from as_declared by the position of ONE rule -- "single
|
||
|
|
// argument for special character", which moves from 15 to just after
|
||
|
|
// the punctuation rules -- and that rule is one of the eight that never
|
||
|
|
// fire. So this too is expected to be identical, and the comparison is
|
||
|
|
// what turns "expected" into "measured".
|
||
|
|
by_category,
|
||
|
|
};
|
||
|
|
|
||
|
|
// Indices into rewrite_rules, in dispatch order. Computed once.
|
||
|
|
const std::vector<size_t>& rule_order(rewrite_order order);
|
||
|
|
|
||
|
|
struct Rewrite_result {
|
||
|
|
bool matched = false;
|
||
|
|
size_t rule = 0; // index into rewrite_rules
|
||
|
|
std::string text {}; // the rewritten word
|
||
|
|
};
|
||
|
|
|
||
|
|
// The first rule that USEFULLY rewrites word: it changes the string, and no
|
||
|
|
// part of the result is the whole input. That second condition is the
|
||
|
|
// termination guard -- a rule that only pads a word with whitespace hands
|
||
|
|
// back the word it was given, and recursing on it exhausts the stack.
|
||
|
|
Rewrite_result find_rewrite(const std::string& word, rewrite_order order = rewrite_order::by_category);
|
||
|
|
|
||
|
|
// Every rule that would usefully rewrite word, in dispatch order. The
|
||
|
|
// first is what find_rewrite() returns; the rest are preempted by it.
|
||
|
|
std::vector<size_t> matching_rules(const std::string& word, rewrite_order order = rewrite_order::by_category);
|
||
|
|
|
||
|
|
// The executable form of the claim this file rests on: whatever fires, no
|
||
|
|
// rule of an EARLIER category also matched. When that holds for every
|
||
|
|
// word, the category order is a complete account of cross-category
|
||
|
|
// interaction and a reader need only understand one category at a time.
|
||
|
|
// Returns true when it holds; otherwise fills why with a description.
|
||
|
|
// This is what a flat priority list cannot offer -- it has no boundary to
|
||
|
|
// state an invariant across.
|
||
|
|
bool category_order_holds(const std::string& word, std::string& why,
|
||
|
|
rewrite_order order = rewrite_order::by_category);
|
||
|
|
|
||
|
|
// The "kdesc --rewrite" listing. It moved here with the table it reads,
|
||
|
|
// and gained the CATEGORY column -- which is the point of the categories
|
||
|
|
// reaching the user rather than only the maintainer: a reader sees the nine
|
||
|
|
// groups and their order instead of thirty positions.
|
||
|
|
void describe_rewrite_patterns();
|