#pragma once #include #include #include #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& 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_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& 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 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();