Lumiera  0.pre.03
»edit your freedom«
text-template.hpp
Go to the documentation of this file.
1 /*
2  TEXT-TEMPLATE.hpp - minimalistic text substitution engine
3 
4  Copyright (C)
5  2024, Hermann Vosseler <Ichthyostega@web.de>
6 
7   **Lumiera** is free software; you can redistribute it and/or modify it
8   under the terms of the GNU General Public License as published by the
9   Free Software Foundation; either version 2 of the License, or (at your
10   option) any later version. See the file COPYING for further details.
11 
12 */
13 
14 
163 #ifndef LIB_TEXT_TEMPLATE_H
164 #define LIB_TEXT_TEMPLATE_H
165 
166 
167 #include "lib/error.hpp"
168 #include "lib/nocopy.hpp"
169 #include "lib/index-iter.hpp"
170 #include "lib/iter-explorer.hpp"
171 #include "lib/format-string.hpp"
172 #include "lib/format-util.hpp"
173 #include "lib/regex.hpp"
174 #include "lib/util.hpp"
175 
176 #include <memory>
177 #include <string>
178 #include <vector>
179 #include <stack>
180 #include <map>
181 
182 
183 namespace lib {
184  namespace error = lumiera::error;
185 
186  namespace test { // declared friend for test access
187  class TextTemplate_test;
188  }
189 
190  using std::string;
191  using StrView = std::string_view;
192 
193  using util::_Fmt;
194  using util::isnil;
195  using util::unConst;
196 
197 
198  namespace text_template {
199 
200  //-----------Syntax-for-iteration-control-in-map------
201  const string MATCH_DATA_TOKEN = R"~(([^,;"\s]*)\s*)~";
202  const string MATCH_DELIMITER = R"~((?:^|,|;)\s*)~" ;
203  const regex ACCEPT_DATA_ELM {MATCH_DELIMITER + MATCH_DATA_TOKEN};
204 
205  inline auto
206  iterNestedKeys (string key, StrView const& iterDef)
207  {
208  return explore (util::RegexSearchIter{iterDef, ACCEPT_DATA_ELM})
209  .transform ([key](smatch mat){ return key+"."+string{mat[1]}+"."; });
210  }
211 
212  //-----------Syntax-for-key-value-data-from-string------
213  const string MATCH_BINDING_KEY = R"~(([\w\.]+))~";
214  const string MATCH_BINDING_VAL = R"~(([^,;"\s]+)\s*)~";
215  const string MATCH_QUOTED_VAL = R"~("([^"]+)"\s*)~";
216  const string MATCH_BINDING_TOK = MATCH_BINDING_KEY+"\\s*=\\s*(?:"+MATCH_BINDING_VAL+"|"+MATCH_QUOTED_VAL+")";
217  const regex ACCEPT_BINDING_ELM {MATCH_DELIMITER + MATCH_BINDING_TOK};
218 
219  inline auto
220  iterBindingSeq (string const& dataDef)
221  {
222  return explore (util::RegexSearchIter{dataDef, ACCEPT_BINDING_ELM})
223  .transform ([&](smatch mat){ return std::make_pair (string{mat[1]}
224  ,string{mat[3].matched? mat[3]:mat[2]}); });
225  }
226 
227 
228  //-----------Syntax-for-TextTemplate-tags--------
229  const string MATCH_SINGLE_KEY = "[A-Za-z_]+\\w*";
230  const string MATCH_KEY_PATH = MATCH_SINGLE_KEY+"(?:\\."+MATCH_SINGLE_KEY+")*";
231  const string MATCH_LOGIC_TOK = "if|for";
232  const string MATCH_END_TOK = "end\\s*";
233  const string MATCH_ELSE_TOK = "else";
234  const string MATCH_SYNTAX = "("+MATCH_ELSE_TOK+")|(?:("+MATCH_END_TOK+")?("+MATCH_LOGIC_TOK+")\\s*)?("+MATCH_KEY_PATH+")?";
235  const string MATCH_FIELD = "\\$\\{\\s*(?:"+MATCH_SYNTAX+")\\s*\\}";
236  const string MATCH_ESCAPE = R"~((\\\$))~";
237 
238  const regex ACCEPT_MARKUP { MATCH_ESCAPE+"|"+MATCH_FIELD
239  , regex::ECMAScript|regex::optimize
240  };
241  // Sub-Matches: 1 = ESCAPE; 2 = ELSE; 3 = END; 4 = LOGIC; 5 = KEY;
242 
243  struct TagSyntax
244  {
245  enum Keyword{ ESCAPE
246  , KEYID
247  , IF
248  , END_IF
249  , FOR
250  , END_FOR
251  , ELSE
252  };
253  Keyword syntax{ESCAPE};
254  StrView lead;
255  StrView tail;
256  string key;
257  };
258 
259  inline auto
260  parse (string const& input)
261  {
262  auto classify = [rest=StrView(input)]
263  (smatch mat) mutable -> TagSyntax
264  {
265  REQUIRE (not mat.empty());
266  TagSyntax tag;
267  auto restAhead = mat.length() + mat.suffix().length();
268  auto pre = rest.length() - restAhead;
269  tag.lead = rest.substr(0, pre);
270  rest = rest.substr(tag.lead.length());
271  if (mat[5].matched)
272  tag.key = mat[5];
273  if (mat[1].matched)
274  rest = rest.substr(1); // strip escape
275  else
276  { // not escaped but indeed active field
277  rest = rest.substr(mat.length());
278  if (mat[4].matched)
279  { // detected a logic keyword...
280  if ("if" == mat[4])
281  tag.syntax = mat[3].matched? TagSyntax::END_IF : TagSyntax::IF;
282  else
283  if ("for" == mat[4])
284  tag.syntax = mat[3].matched? TagSyntax::END_FOR : TagSyntax::FOR;
285  else
286  throw error::Logic{_Fmt{"unexpected keyword \"%s\""} % mat[4]};
287  }
288  else
289  if (mat[2].matched)
290  tag.syntax = TagSyntax::ELSE;
291  else
292  if ("end" == mat[5])
293  throw error::Invalid{_Fmt{"unqualified \"end\" without logic-keyword:"
294  " ...%s${end |↯|}"} % tag.lead};
295  else
296  tag.syntax = TagSyntax::KEYID;
297  }
298  tag.tail = rest;
299  return tag;
300  };
301 
302  return explore (util::RegexSearchIter{input, ACCEPT_MARKUP})
303  .transform (classify);
304  }
305 
306 
311  template<class DAT, typename SEL=void>
312  class DataSource;
313 
314  }//(namespace) text_template
315 
316 
317 
318 
319 
320 
321  /*************************************************/
332  {
333  enum Clause {
334  IF, FOR
335  };
336  enum Code {
337  TEXT, KEY, COND, JUMP, ITER, LOOP
338  };
339 
341  using Idx = size_t;
342 
343  template<class SRC>
345 
346  struct ParseCtx
347  {
348  Clause clause;
349  Idx begin{0};
350  Idx after{0};
351  };
352  using ScopeStack = std::stack<ParseCtx, std::vector<ParseCtx>>;
353 
354  struct Action
355  {
356  Code code{TEXT};
357  string val{};
358  Idx refIDX{0};
359 
360  template<class SRC>
361  auto instantiate (InstanceCore<SRC>&) const;
362  };
363 
365  using ActionSeq = std::vector<Action>;
366 
368  class ActionCompiler;
369 
371  template<class SRC>
372  class InstanceCore
373  {
374  using ActionIter = IndexIter<const ActionSeq>;
375  using DataCtxIter = typename SRC::Iter;
376  using NestedCtx = std::pair<DataCtxIter, SRC>;
377  using CtxStack = std::stack<NestedCtx, std::vector<NestedCtx>>;
378  using Value = typename SRC::Value;
379 
380  SRC dataSrc_;
381  ActionIter actionIter_;
382  CtxStack ctxStack_;
383  Value rendered_;
384 
385  public:
386  InstanceCore (ActionSeq const& actions, SRC);
387 
388  bool checkPoint() const;
389  auto& yield() const;
390  void iterNext();
391 
392  Value instantiateNext();
393  Value reInstatiate (Idx =Idx(-1));
394  Value getContent(string key);
395  bool conditional (string key);
396  bool openIteration (string key);
397  bool loopFurther();
398  void focusNested();
399  };
400 
401 
402  ActionSeq actions_;
403 
404  public:
405  TextTemplate(string spec)
406  : actions_{compile (spec)}
407  { }
408 
409  template<class DAT>
410  auto
411  submit (DAT const& data) const;
412 
413  template<class DAT>
414  string
415  render (DAT const& data) const;
416 
417  template<class DAT>
418  static string
419  apply (string spec, DAT const& data);
420 
421  auto keys() const;
422 
424  static ActionSeq compile (string const&);
425  friend class test::TextTemplate_test;
426  };
427 
428 
429 
430 
431  /* ======= Parser / Compiler pipeline ======= */
432 
445  {
446  ScopeStack scope_{};
447 
448  public:
449  template<class PAR>
450  ActionSeq
451  buildActions (PAR&& parseIter)
452  {
453  ActionSeq actions;
454  while (parseIter)
455  compile (parseIter, actions);
456  return actions;
457  }
458 
459  private:
460  template<class PAR>
461  void
462  compile (PAR& parseIter, ActionSeq& actions)
463  {
464  auto currIDX = [&]{ return actions.size(); };
465  auto valid = [&](Idx i){ return 0 < i and i < actions.size(); };
466  auto clause = [](Clause c)-> string { return c==IF? "if" : "for"; };
467  auto scopeClause = [&]{ return scope_.empty()? "??" : clause(scope_.top().clause); };
468 
469  // Support for bracketing constructs (if / for)
470  auto beginIdx = [&]{ return scope_.empty()? 0 : scope_.top().begin; }; // Index of action where scope was opened
471  auto scopeKey = [&]{ return valid(beginIdx())? actions[beginIdx()].val : "";}; // Key controlling the if-/for-Scope
472  auto keyMatch = [&]{ return isnil(parseIter->key) or parseIter->key == scopeKey(); }; // Key matches in opening and closing tag
473  auto clauseMatch = [&](Clause c){ return not scope_.empty() and scope_.top().clause == c; }; // Kind of closing tag matches innermost scope
474  auto scopeMatch = [&](Clause c){ return clauseMatch(c) and keyMatch(); };
475 
476  auto lead = [&]{ return parseIter->lead; };
477  auto clashLead = [&]{ return actions[scope_.top().after - 1].val; }; // (for diagnostics: lead before a conflicting other "else")
478  auto abbrev = [&](auto s){ return s.length()<16? s : s.substr(s.length()-15); }; // (shorten lead display to 15 chars)
479 
480  // Syntax / consistency checks...
481  auto __requireKey = [&](string descr)
482  {
483  if (isnil (parseIter->key))
484  throw error::Invalid{_Fmt{"Tag without key: ...%s${%s |↯|}"}
485  % abbrev(lead()) % descr
486  }; };
487  auto __checkBalanced = [&](Clause c)
488  {
489  if (not scopeMatch(c))
490  throw error::Invalid{_Fmt{"Unbalanced Logic: expect ${end %s %s}"
491  " -- found ...%s${end |↯|%s %s}"}
492  % scopeClause() % scopeKey()
493  % abbrev(lead())
494  % clause(c) % parseIter->key
495  }; };
496  auto __checkInScope = [&] {
497  if (scope_.empty())
498  throw error::Invalid{_Fmt{"Misplaced ...%s|↯|${else}"}
499  % abbrev(lead())};
500  };
501  auto __checkNoDup = [&] {
502  if (scope_.top().after != 0)
503  throw error::Invalid{_Fmt{"Conflicting ...%s${else} ⟷ ...%s|↯|${else}"}
504  % abbrev(clashLead()) % abbrev(lead())};
505  };
506  auto __checkClosed = [&] {
507  if (not scope_.empty())
508  throw error::Invalid{_Fmt{"Unclosed Logic tags: |↯|${end %s %s} missing"}
509  % scopeClause() % scopeKey()};
510  };
511 
512  // Primitives used for code generation....
513  auto add = [&](Code c, string v){ actions.push_back (Action{c,v});};
514  auto addCode = [&](Code c) { add ( c, parseIter->key); }; // add code token and transfer key picked up by parser
515  auto addLead = [&] { add (TEXT, string{parseIter->lead}); }; // add TEXT token to represent the static part before this tag
516  auto openScope = [&](Clause c){ scope_.push (ParseCtx{c, currIDX()}); }; // start nested scope for bracketing construct (if / for)
517  auto closeScope = [&] { scope_.pop(); }; // close innermost nested scope
518 
519  auto linkElseToStart = [&]{ actions[beginIdx()].refIDX = currIDX(); }; // link the start position of the else-branch into opening logic code
520  auto markJumpInScope = [&]{ scope_.top().after = currIDX(); }; // memorise jump before else-branch for later linkage
521  auto linkLoopBack = [&]{ actions.back().refIDX = scope_.top().begin; }; // fill in the back-jump position at loop end
522  auto linkJumpToNext = [&]{ actions[scope_.top().after].refIDX = currIDX(); }; // link jump to the position after the end of the logic bracket
523 
524  auto hasElse = [&]{ return scope_.top().after != 0; }; // a jump code to link was only marked if there was an else tag
525 
526  using text_template::TagSyntax;
527 
528  /* === Code Generation === */
529  switch (parseIter->syntax) {
530  case TagSyntax::ESCAPE:
531  addLead();
532  break;
533  case TagSyntax::KEYID:
534  __requireKey("<placeholder>");
535  addLead();
536  addCode(KEY);
537  break;
538  case TagSyntax::IF:
539  __requireKey("if <conditional>");
540  addLead();
541  openScope(IF);
542  addCode(COND);
543  break;
544  case TagSyntax::END_IF:
545  addLead();
546  __checkBalanced(IF);
547  if (hasElse())
548  linkJumpToNext();
549  else
550  linkElseToStart();
551  closeScope();
552  break;
553  case TagSyntax::FOR:
554  __requireKey("for <data-id>");
555  addLead();
556  openScope(FOR);
557  addCode(ITER);
558  break;
559  case TagSyntax::END_FOR:
560  addLead();
561  __checkBalanced(FOR);
562  if (hasElse())
563  linkJumpToNext();
564  else
565  { // no else-branch; end active loop here
566  addCode(LOOP);
567  linkLoopBack();
568  linkElseToStart(); // jump behind when iteration turns out empty
569  }
570  closeScope();
571  break;
572  case TagSyntax::ELSE:
573  addLead();
574  __checkInScope();
575  __checkNoDup();
576  if (IF == scope_.top().clause)
577  {
578  markJumpInScope();
579  addCode(JUMP);
580  linkElseToStart();
581  }
582  else
583  {
584  addCode(LOOP);
585  linkLoopBack();
586  markJumpInScope();
587  addCode(JUMP);
588  linkElseToStart(); // jump to else-block when iteration turns out empty
589  }
590  break;
591  default:
592  NOTREACHED ("uncovered TagSyntax keyword while compiling a TextTemplate.");
593  }
594 
595  StrView tail = parseIter->tail;
596  ++parseIter;
597  if (not parseIter)
598  {//add final action to supply text after last active tag
599  add (TEXT, string{tail});
600  __checkClosed();
601  }
602  }
603  };
604 
606  TextTemplate::compile (string const& spec)
607  {
608  ActionSeq code = ActionCompiler().buildActions (text_template::parse (spec));
609  if (isnil (code))
610  throw error::Invalid ("TextTemplate spec without active placeholders.");
611  return code;
612  }
613 
614 
615 
616 
617 
618 
619  /* ======= preconfigured data bindings ======= */
620 
621  namespace text_template {
622 
623  template<class DAT, typename SEL>
624  struct DataSource
625  {
626  static_assert (not sizeof(DAT),
627  "unable to bind this data source "
628  "for TextTemplate instantiation");
629 
630  DataSource (DAT const&);
631  };
632 
633  using MapS = std::map<string,string>;
634 
653  template<>
654  struct DataSource<MapS>
655  {
656  MapS const * data_{nullptr};
657  string keyPrefix_{};
658 
659  bool isSubScope() { return not isnil (keyPrefix_); }
660 
661  DataSource() = default;
662  DataSource(MapS const& map)
663  : data_{&map}
664  { }
665 
666 
667  using Value = std::string_view;
668  using Iter = decltype(iterNestedKeys("",""));
669 
670  bool
671  contains (string key)
672  {
673  return (isSubScope() and util::contains (*data_, keyPrefix_+key))
674  or util::contains (*data_, key);
675  }
676 
677  Value
678  retrieveContent (string key)
679  {
680  MapS::const_iterator elm;
681  if (isSubScope())
682  {
683  elm = data_->find (keyPrefix_+key);
684  if (elm == data_->end())
685  elm = data_->find (key);
686  }
687  else
688  elm = data_->find (key);
689  ENSURE (elm != data_->end());
690  return elm->second;
691  }
692 
693  Iter
694  getSequence (string key)
695  {
696  if (not contains(key))
697  return Iter{};
698  else
699  return iterNestedKeys (key, retrieveContent(key));
700  }
701 
702  DataSource
703  openContext (Iter& iter)
704  {
705  REQUIRE (iter);
706  DataSource nested{*this};
707  nested.keyPrefix_ += *iter;
708  return nested;
709  }
710  };
711 
712 
713  using PairS = std::pair<string,string>;
714 
716  template<>
717  struct DataSource<string>
718  : DataSource<MapS>
719  {
720  std::shared_ptr<MapS> spec_;
721 
722  DataSource (string const& dataSpec)
723  : spec_{new MapS}
724  {
725  data_ = spec_.get();
726  explore (iterBindingSeq (dataSpec))
727  .foreach([this](PairS const& bind){ spec_->insert (bind); });
728  }
729 
730  DataSource
731  openContext (Iter& iter)
732  {
733  DataSource nested(*this);
734  auto nestedBase = DataSource<MapS>::openContext (iter);
735  nested.keyPrefix_ = nestedBase.keyPrefix_;
736  return nested;
737  }
738  };
739 
744  template<class STR, typename = meta::enable_if<meta::is_StringLike<STR>> >
745  DataSource(STR const&) -> DataSource<string>;
746 
747  }// namespace text_template
748 
749 
750 
751 
752 
753  /* ======= implementation of the instantiation state ======= */
754 
761  template<class SRC>
762  inline auto
764  {
765  using Result = decltype (core.getContent(val));
766  switch (code) {
767  case TEXT:
768  return Result(val);
769  case KEY:
770  return core.getContent (val);
771  case COND:
772  return core.conditional (val)? core.reInstatiate() // next is the conditional content
773  : core.reInstatiate(refIDX); // points to start of else-block (or after)
774  case JUMP:
775  return core.reInstatiate(refIDX);
776  case ITER:
777  return core.openIteration(val)? core.reInstatiate() // looping initiated => continue with next
778  : core.reInstatiate(refIDX); // points to start of else-block (or after)
779  case LOOP:
780  return core.loopFurther() ? core.reInstatiate(refIDX+1) // start with one after the loop opening
781  : core.reInstatiate(); // continue with next -> jump over else-block
782  default:
783  NOTREACHED ("uncovered Activity verb in activation function.");
784  }
785  }
786 
787 
788 
789  template<class SRC>
791  : dataSrc_{s}
792  , actionIter_{actions}
793  , ctxStack_{}
794  , rendered_{}
795  {
796  rendered_ = instantiateNext();
797  }
798 
804  template<class SRC>
805  inline bool
807  {
808  return bool(actionIter_);
809  }
810 
811  template<class SRC>
812  inline auto&
814  {
815  return unConst(this)->rendered_;
816  }
817 
818  template<class SRC>
819  inline void
821  {
822  ++actionIter_;
823  rendered_ = instantiateNext();
824  }
825 
826 
828  template<class SRC>
829  inline typename SRC::Value
831  {
832  return actionIter_? actionIter_->instantiate(*this)
833  : Value{};
834  }
835 
842  template<class SRC>
843  inline typename SRC::Value
845  {
846  if (nextCode == Idx(-1))
847  ++actionIter_;
848  else
849  actionIter_.setIDX (nextCode);
850  return instantiateNext();
851  }
852 
854  template<class SRC>
855  inline typename SRC::Value
857  {
858  static Value nil{};
859  return dataSrc_.contains(key)? dataSrc_.retrieveContent(key) : nil;
860  }
861 
863  template<class SRC>
864  inline bool
866  {
867  return not util::isNo (string{getContent (key)});
868  }
869 
881  template<class SRC>
882  inline bool
884  {
885  if (conditional (key))
886  if (DataCtxIter dataIter = dataSrc_.getSequence(key))
887  {
888  ctxStack_.push (NestedCtx{move (dataIter)
889  ,dataSrc_});
890  focusNested();
891  return true;
892  }
893  return false;
894  }
895 
902  template<class SRC>
903  inline bool
905  {
906  DataCtxIter& dataIter = ctxStack_.top().first;
907  ++dataIter;
908  if (dataIter)
909  { // open next nested context *from enclosing context*
910  focusNested();
911  return true;
912  }
913  else
914  { // restore original data context
915  using std::swap;
916  swap (dataSrc_, ctxStack_.top().second);
917  ctxStack_.pop();
918  return false;
919  }
920  }
921 
932  template<class SRC>
933  inline void
935  {
936  REQUIRE (not ctxStack_.empty());
937  NestedCtx& innermostScope = ctxStack_.top();
938  DataCtxIter& currentDataItem = innermostScope.first;
939  SRC& parentDataSrc = innermostScope.second;
940 
941  this->dataSrc_ = parentDataSrc.openContext (currentDataItem);
942  }
943 
944 
945 
946 
947 
953  template<class DAT>
954  inline auto
955  TextTemplate::submit (DAT const& data) const
956  {
957  return explore (InstanceCore{actions_, text_template::DataSource(data)});
958  }
959 
961  template<class DAT>
962  inline string
963  TextTemplate::render (DAT const& data) const
964  {
965  return util::join (submit (data), "");
966  }
967 
969  template<class DAT>
970  inline string
971  TextTemplate::apply (string spec, DAT const& data)
972  {
973  return TextTemplate(spec).render (data);
974  }
975 
977  inline auto
979  {
980  return explore (actions_)
981  .filter ([](Action const& a){ return a.code == KEY or a.code == COND or a.code == ITER; })
982  .transform([](Action const& a){ return a.val; });
983  }
984 
985 
986 }// namespace lib
987 #endif /*LIB_TEXT_TEMPLATE_H*/
Binding to a specific data source.
bool checkPoint() const
TextTemplate instantiation: check point on rendered Action.
Result(VAL &&) -> Result< VAL >
deduction guide: allow perfect forwarding of a any result into the ctor call.
auto explore(IT &&srcSeq)
start building a IterExplorer by suitably wrapping the given iterable source.
Definition: run.hpp:40
Adapter for the Map-Data-Source to consume a string spec (for testing)
bool conditional(string key)
retrieve a data value for the key and interpret it as boolean expression
Types marked with this mix-in may be moved but not copied.
Definition: nocopy.hpp:49
Front-end for printf-style string template interpolation.
bool openIteration(string key)
Attempt to open data sequence by evaluating the entrance key.
Text template substitution engine.
auto submit(DAT const &data) const
Instantiate this (pre-compiled) TextTemplate using the given data binding.
A front-end for using printf-style formatting.
Implementation namespace for support and library code.
Iterator »State Core« to process the template instantiation.
Derived specific exceptions within Lumiera&#39;s exception hierarchy.
Definition: error.hpp:190
bool loopFurther()
Possibly continue the iteration within an already established nested scope.
Mix-Ins to allow or prohibit various degrees of copying and cloning.
string render(DAT const &data) const
submit data and materialise rendered results into a single string
Iterator-style access handle to a referred container with subscript index.
Value instantiateNext()
Instantiate next Action token and expose its rendering.
Tiny helper functions and shortcuts to be used everywhere Consider this header to be effectively incl...
represents text content
auto instantiate(InstanceCore< SRC > &) const
Interpret an action token from the compiled text template based on the given data binding and iterati...
Value reInstatiate(Idx=Idx(-1))
relocate to another Action token and continue instantiation there
static ActionSeq compile(string const &)
auto keys() const
diagnostics: query a list of all active keys expected by the template.
Lumiera error handling (C++ interface).
wrapped regex iterator to allow usage in foreach loops
Definition: regex.hpp:40
static string apply(string spec, DAT const &data)
one-shot shorthand: compile a template and apply it to the given data
const string MATCH_DATA_TOKEN
< Parser and DataSource binding for lib::TextTemplate
std::vector< Action > ActionSeq
the text template is compiled into a sequence of Actions
Collection of small helpers and convenience shortcuts for diagnostics & formatting.
void focusNested()
Step down into the innermost data item context, prepared at the top of #ctxStack_.
Convenience wrappers and helpers for dealing with regular expressions.
Value getContent(string key)
retrieve a data value from the data source for the indiated key
Building tree expanding and backtracking evaluations within hierarchical scopes.
size_t Idx
cross-references by index number