From 20f2b1b90aecd09b7468f3d8cff1c365ed7a7443 Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Sun, 24 Mar 2024 21:42:38 +0100 Subject: [PATCH] Library: complete implementation of code generation ...including the handling of cross jumps / links ...verified by one elaborate example in the tests --- src/lib/text-template.hpp | 97 +++++++++++++--- tests/library/text-template-test.cpp | 86 +++++++++++++++ wiki/thinkPad.ichthyo.mm | 159 ++++++++++++++++----------- 3 files changed, 261 insertions(+), 81 deletions(-) diff --git a/src/lib/text-template.hpp b/src/lib/text-template.hpp index d42208d49..f2ffc22bf 100644 --- a/src/lib/text-template.hpp +++ b/src/lib/text-template.hpp @@ -101,7 +101,8 @@ #include "lib/nocopy.hpp" #include "lib/iter-index.hpp" #include "lib/iter-explorer.hpp" -#include "lib/format-util.hpp"///////////////////OOO use format-string?? +#include "lib/format-string.hpp" +#include "lib/format-util.hpp" #include "lib/regex.hpp" #include "lib/util.hpp" @@ -113,12 +114,14 @@ namespace lib { + namespace error = lumiera::error; using std::optional; using std::nullopt; using std::string; using StrView = std::string_view; + using util::_Fmt; using util::unConst; @@ -183,7 +186,7 @@ namespace lib { if ("for" == mat[4]) tag.syntax = mat[3].matched? TagSyntax::END_FOR : TagSyntax::FOR; else - throw error::Logic("unexpected keyword"); + throw error::Logic(_Fmt{"unexpected keyword \"%s\""} % mat[4]); } else if (mat[2].matched) @@ -334,11 +337,60 @@ namespace lib { void compile (PAR& parseIter, ActionSeq& actions) { - auto add = [&](Code c, string v){ actions.push_back (Action{c,v}); }; - auto addCode = [&](Code c) { add ( c, parseIter->key); }; - auto addLead = [&] { add (TEXT, string{parseIter->lead}); }; - auto openScope = [&](Clause c){ scope_.push (ParseCtx{c, actions.size()});}; + auto currIDX = [&]{ return actions.size(); }; + auto valid = [&](Idx i){ return 0 < i and i < actions.size(); }; + auto clause = [](Clause c)-> string { return c==IF? "if" : "for"; }; + auto scopeClause = [&]{ return scope_.empty()? "??" : clause(scope_.top().clause); }; + // Support for bracketing constructs (if / for) + auto beginIdx = [&]{ return scope_.empty()? 0 : scope_.top().begin; }; // Index of action where scope was opened + auto scopeKey = [&]{ return valid(beginIdx())? actions[beginIdx()].val : "";}; // Key controlling the if-/for-Scope + auto keyMatch = [&]{ return isnil(parseIter->key) or parseIter->key == scopeKey(); }; // Key matches in opening and closing tag + auto clauseMatch = [&](Clause c){ return not scope_.empty() and scope_.top().clause == c; }; // Kind of closing tag matches innermost scope + auto scopeMatch = [&](Clause c){ return clauseMatch(c) and keyMatch(); }; + + auto lead = [&]{ return parseIter->lead; }; + auto clashLead = [&]{ return actions[scope_.top().after - 1].val; }; // (for diagnostics: lead before a conflicting other "else") + auto abbrev = [&](auto s){ return s.length()<16? s : s.substr(s.length()-15); }; // (shorten lead display to 15 chars) + + // Syntax / consistency checks... + auto __checkBalanced = [&](Clause c) + { + if (not scopeMatch(c)) + throw error::Invalid{_Fmt{"Unbalanced Logic: expect ${end %s %s}" + " -- found ...%s${end |↯|%s %s}"} + % scopeClause() % scopeKey() + % abbrev(lead()) + % clause(c) % parseIter->key + }; + }; + auto __checkInScope = [&] { + if (scope_.empty()) + throw error::Invalid{_Fmt{"Misplaced ...%s|↯|${else}"} + % abbrev(lead())}; + }; + auto __checkNoDup = [&] { + if (scope_.top().after != 0) + throw error::Invalid{_Fmt{"Conflicting ...%s${else} ⟷ ...%s|↯|${else}"} + % abbrev(clashLead()) % abbrev(lead())}; + }; + + // Primitives used for code generation.... + auto add = [&](Code c, string v){ actions.push_back (Action{c,v});}; + auto addCode = [&](Code c) { add ( c, parseIter->key); }; // add code token and transfer key picked up by parser + auto addLead = [&] { add (TEXT, string{parseIter->lead}); }; // add TEXT token to represent the static part before this tag + auto openScope = [&](Clause c){ scope_.push (ParseCtx{c, currIDX()}); }; // start nested scope for bracketing construct (if / for) + auto closeScope = [&] { scope_.pop(); }; // close innermost nested scope + + auto linkElseToStart = [&]{ actions[beginIdx()].refIDX = currIDX(); }; // link the start position of the else-branch into opening logic code + auto markJumpInScope = [&]{ scope_.top().after = currIDX(); }; // memorise jump before else-branch for later linkage + auto linkLoopBack = [&]{ actions.back().refIDX = scope_.top().begin; }; // fill in the back-jump position at loop end + auto linkJumpToNext = [&]{ actions[scope_.top().after].refIDX = currIDX(); }; // link jump to the position after the end of the logic bracket + + auto hasElse = [&]{ return scope_.top().after != 0; }; // a jump code to link was only marked if there was an else tag + + + /* === Code Generation === */ switch (parseIter->syntax) { case TagSyntax::ESCAPE: addLead(); @@ -354,31 +406,48 @@ namespace lib { break; case TagSyntax::END_IF: addLead(); - ///////////////////////////////////////////////////OOO verify and pop IF-clause here -// if (scope_.empty() or -// (not isnil(tag.key) scope_.top()) + __checkBalanced(IF); + if (hasElse()) + linkJumpToNext(); + else + linkElseToStart(); + closeScope(); break; case TagSyntax::FOR: addLead(); openScope(FOR); - ///////////////////////////////////////////////////OOO push FOR-clause here addCode(ITER); break; case TagSyntax::END_FOR: addLead(); - ///////////////////////////////////////////////////OOO verify and pop FOR-clause here + __checkBalanced(FOR); + if (hasElse()) + linkJumpToNext(); + else + { // no else-branch; end active loop here + addCode(LOOP); + linkLoopBack(); + linkElseToStart(); // jump behind when iteration turns out empty + } + closeScope(); break; case TagSyntax::ELSE: addLead(); - if (true) /////////////////////////////////////////OOO derive IF or FOR from context + __checkInScope(); + __checkNoDup(); + if (IF == scope_.top().clause) { - ///////////////////////////////////////////////////OOO actual IF-else implementation + markJumpInScope(); addCode(JUMP); + linkElseToStart(); } else { - ///////////////////////////////////////////////////OOO actual FOR-else implementation addCode(LOOP); + linkLoopBack(); + markJumpInScope(); + addCode(JUMP); + linkElseToStart(); // jump to else-block when iteration turns out empty } break; default: diff --git a/tests/library/text-template-test.cpp b/tests/library/text-template-test.cpp index 9faca41e2..0de2f8600 100644 --- a/tests/library/text-template-test.cpp +++ b/tests/library/text-template-test.cpp @@ -236,6 +236,92 @@ for} tail... )~"; auto actions = TextTemplate::ActionCompiler().buildActions(parse(input)); SHOW_EXPR(util::join (explore(actions).transform(render),"▶\n▶")) + CHECK (25 == actions.size()); + + CHECK (actions[ 0].code == TextTemplate::Code::TEXT); + CHECK (actions[ 0].val == "\n Prefix-1 "_expect); // static text prefix + CHECK (actions[ 0].refIDX == 0); + + CHECK (actions[ 1].code == TextTemplate::Code::KEY); // a placeholder to be substituted + CHECK (actions[ 1].val == "some.key"_expect); // use "some.key" for data retrieval + + CHECK (actions[ 2].code == TextTemplate::Code::TEXT); // static text between active fields + CHECK (actions[ 2].val == " next one is "_expect); + + CHECK (actions[ 3].code == TextTemplate::Code::TEXT); // since next tag was escaped, it appears in static segment + CHECK (actions[ 3].val == "\\${escaped}\n Prefix-2 "_expect); + + CHECK (actions[ 4].code == TextTemplate::Code::COND); // start of an if-bracket construct + CHECK (actions[ 4].val == "cond1"_expect); // data marked with "cond1" will be used to determine true/false + CHECK (actions[ 4].refIDX == 7 ); // IDX ≡ 7 marks start of the else-branch + + CHECK (actions[ 5].code == TextTemplate::Code::TEXT); // this static block will only be included if "cond1" evaluates to true + CHECK (actions[ 5].val == " active "_expect); + + CHECK (actions[ 6].code == TextTemplate::Code::JUMP); // unconditional jump at the end of the if-true-block + CHECK (actions[ 6].val == ""_expect); + CHECK (actions[ 6].refIDX == 8 ); // IDX ≡ 8 points to the next element after the conditional construct + + CHECK (actions[ 7].code == TextTemplate::Code::TEXT); // this static (else)-block will be included if "cond1" does not hold + CHECK (actions[ 7].val == " inactive "_expect); + + CHECK (actions[ 8].code == TextTemplate::Code::TEXT); // again a static segment, displayed unconditionally + CHECK (actions[ 8].val == "Prefix-3 "_expect); // Note: no newline, since the closing bracket was placed at line start + + CHECK (actions[ 9].code == TextTemplate::Code::COND); // again a conditional (but this time without else-branch) + CHECK (actions[ 9].val == "cond2"_expect); // data marked with "cond2" will be evaluated as condition + CHECK (actions[ 9].refIDX == 11 ); // IDX ≡ 11 is the alternative route, this time pointing behind the conditional + + CHECK (actions[10].code == TextTemplate::Code::TEXT); // static text block to be displayed as content of the conditional + CHECK (actions[10].val == " active2"_expect); + + CHECK (actions[11].code == TextTemplate::Code::TEXT); // again an unconditional static segment (behind end of preceding conditional) + CHECK (actions[11].val == " more\n Prefix-4 "_expect); + + CHECK (actions[12].code == TextTemplate::Code::ITER); // Start of a for-construct (iteration) + CHECK (actions[12].val == "data"_expect); // data marked with "data" will be used to find and iterate nested elements + CHECK (actions[12].refIDX == 23 ); // IDX ≡ 23 points to the alternative "else" block, in case no iteration takes place + + CHECK (actions[13].code == TextTemplate::Code::TEXT); // static block to appear for each nested "data" element + CHECK (actions[13].val == " fixed "_expect); + + CHECK (actions[14].code == TextTemplate::Code::KEY); // placeholder to be substituted + CHECK (actions[14].val == "embedded"_expect); // _typically_ the data "embedded" will live in the iterated, nested elements + + CHECK (actions[15].code == TextTemplate::Code::TEXT); // again a static block, which however lives within the iterated segment + CHECK (actions[15].val == "\n Pre-5 "_expect); + + CHECK (actions[16].code == TextTemplate::Code::COND); // a nested conditional, thus nested on second level within the iteration construct + CHECK (actions[16].val == "nested"_expect); // data marked with "nested" will control the conditional (typically from iterated data elements) + CHECK (actions[16].refIDX == 19 ); // IDX ≡ 19 points to the else-block of this nested conditional + + CHECK (actions[17].code == TextTemplate::Code::TEXT); // static content to appear as nested if-true-section + CHECK (actions[17].val == "nested-active"_expect); + + CHECK (actions[18].code == TextTemplate::Code::JUMP); // jump code at end of the true-section + CHECK (actions[18].val == ""_expect); + CHECK (actions[18].refIDX == 20 ); // IDX ≡ 20 points behind the end of this nested conditional construct + + CHECK (actions[19].code == TextTemplate::Code::TEXT); // static content comprising the else-section + CHECK (actions[19].val == "nested-inactive"_expect); // Note: no whitespace due to placement of the tag brackets of "else" / "end if" + + CHECK (actions[20].code == TextTemplate::Code::TEXT); // again an unconditional static segment, yet still within the looping construct + CHECK (actions[20].val == "loop-suffix"_expect); + + CHECK (actions[21].code == TextTemplate::Code::LOOP); // the loop-end code, where evaluation will consider the next iteration + CHECK (actions[21].val == ""_expect); + CHECK (actions[21].refIDX == 12 ); // IDX ≡ 12 points back to the opening ITER code + + CHECK (actions[22].code == TextTemplate::Code::JUMP); // if however the iteration is complete, evaluation will jump over the "else" section + CHECK (actions[22].val == ""_expect); + CHECK (actions[22].refIDX == 24 ); + + CHECK (actions[23].code == TextTemplate::Code::TEXT); // this static else-segment will appear whenever no iteration takes place + CHECK (actions[23].val == ""_expect); // Note: in this example there is an ${else}-tag, yet the content is empty + + CHECK (actions[24].code == TextTemplate::Code::TEXT); // a final static segment after the last active tag + CHECK (actions[24].val == " tail...\n"_expect); + CHECK (actions[24].refIDX == 0); } diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 34a00e2a1..0809d0e1b 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -112628,8 +112628,8 @@ std::cout << tmpl.render({"what", "World"}) << s - - + + @@ -112656,16 +112656,16 @@ std::cout << tmpl.render({"what", "World"}) << s - - - - - + + + + + - + - - + + @@ -112678,11 +112678,11 @@ std::cout << tmpl.render({"what", "World"}) << s - - - + + + - + @@ -112706,16 +112706,16 @@ std::cout << tmpl.render({"what", "World"}) << s - - + + - - + + @@ -112733,8 +112733,8 @@ std::cout << tmpl.render({"what", "World"}) << s - - + + @@ -112758,16 +112758,16 @@ std::cout << tmpl.render({"what", "World"}) << s - - - - + + + + - + - - + + @@ -112777,59 +112777,63 @@ std::cout << tmpl.render({"what", "World"}) << s - + - + - + + + - + - + - + + + - + - + - - + + - + - + @@ -112840,11 +112844,11 @@ std::cout << tmpl.render({"what", "World"}) << s - + - + @@ -113060,19 +113064,30 @@ std::cout << tmpl.render({"what", "World"}) << s - - + + + + +

+ naja... +

+

+ Hat teilweise schon was gebracht, denn der Parser ist schön kompakt und klar geworden — aber für den Compiler bin ich genau deshalb in den Urwald geraten — das war eine „schwere Geburt“ — und in mehreren Schritten bin ich letztlich bei einer funktional-imperativen Formulierung angekommen, die jetzt einigermaßen gut lesbar ist... +

+ +
+ - + - - - - + + + + @@ -113087,7 +113102,7 @@ std::cout << tmpl.render({"what", "World"}) << s - + @@ -113156,17 +113171,21 @@ std::cout << tmpl.render({"what", "World"}) << s - + - + + + + + - - - + + + @@ -113175,15 +113194,15 @@ std::cout << tmpl.render({"what", "World"}) << s - - + + - - + + - - + + @@ -113229,16 +113248,19 @@ std::cout << tmpl.render({"what", "World"}) << s - - + + + + + - - + + @@ -113249,7 +113271,7 @@ std::cout << tmpl.render({"what", "World"}) << s - + @@ -113281,6 +113303,9 @@ std::cout << tmpl.render({"what", "World"}) << s + + + @@ -113378,8 +113403,8 @@ std::cout << tmpl.render({"what", "World"}) << s - - + +