From 8483618012f24373bf3afc23530284c9bfc9153d Mon Sep 17 00:00:00 2001 From: Brad Arant Date: Wed, 26 Aug 2026 09:39:36 -0700 Subject: [PATCH] Scope custom definitions to their enclosing invocation - and fix two real, previously-latent bugs found while building this Custom tag definitions used to be entirely flat: every - regardless of where in the script it appeared - registered into a single global.tags map, visible everywhere for the rest of the script. Now a defined directly inside another custom tag's own invocation (i.e. nested inside a __dotag instance's own expansion, not just sitting inside an // that happens to be there - those remain transparent) is scoped to that invocation: visible only for the rest of that specific expansion, gone once it finishes. Definitions at the true top level (or inside control-flow tags that are themselves at the top level) still register globally, unchanged. Implementation: Tag gained isCustomTagBoundary (true only on __dotag instances) and localTags (populated only on boundary instances) - both public rather than protected, since __tag.cpp needs to read/write them through an arbitrary ancestor Tag* found by walking parent, and C++ protected access doesn't extend to another object reached through a base-typed pointer. findTagDefinition() walks from `this` up through `parent` looking for the nearest boundary whose localTags has the name, falling back to global.tags - used by both ifTagDefined() (is visible right here?) and __dotag's own constructor (fetch the definition to expand). __tag.cpp's registration does the mirror walk starting from `parent` (skip self, a definition is never itself a boundary). Two real bugs found and fixed along the way, neither introduced by this feature, both confirmed via a hung/broken real jet-2.0 run before diagnosis rather than assumed: 1. __tag.cpp's own base Tag(...) call passed (this, this) as its own parent/local instead of the parent/local it actually received - made the new scope-walk loop on itself forever (since 's own `parent` would just point at itself). Fixed to pass through the real received parent/local; nothing else in this class depended on the old self-referential values (a definition's own container is never evaluated at definition time - evaluate=false - so parent/local were never used for anything else here). 2. __dotag never overrode hasContainer, which Tag::processContainer gates real rendering on - and the base constructor sets hasContainer from whether THIS INVOCATION was self-closing ( vs ...), which is the right question for capturing an invocation's own nested content, but the wrong one for whether the tag's OWN DEFINITION should render. Net effect, confirmed via a real run: always silently rendered nothing for ANY custom tag - only the form ever worked. Fixed by forcing hasContainer=true in __dotag's constructor - by the time it's constructed a valid definition is guaranteed (dispatch only builds __dotag after ifTagDefined() already confirmed one exists), so there's always something to render regardless of invocation syntax. updated to match: its "DEFINED TAGS" section now renders the real tree currently in effect (global at the root, each enclosing custom-tag invocation nested one level deeper, outermost first) instead of a single flat list - if there's no active local scope, that's just the root with nothing nested under it. Verified via real jet-2.0 runs, not just compilation: a tag defined inside another tag's body is invokable from within that body and correctly NOT recognized (falls through to literal-text echo) outside it; the same holds through 3 levels of nesting; the dump tree correctly shows nested levels indented under their own "(inside X)" header. Full regression pass on /, /, , and dump's own flat top-level output - all still work unchanged. --- Tag.cpp | 23 ++++++++++++++++++++--- Tag.h | 26 ++++++++++++++++++++++++++ __dotag.cpp | 30 ++++++++++++++++++++++++++++-- __dump.cpp | 35 +++++++++++++++++++++++++++++------ __tag.cpp | 26 ++++++++++++++++++++++++-- 5 files changed, 127 insertions(+), 13 deletions(-) diff --git a/Tag.cpp b/Tag.cpp index 2d5059b..46167ec 100644 --- a/Tag.cpp +++ b/Tag.cpp @@ -318,11 +318,28 @@ namespace jet { return false; } + bool Tag::findTagDefinition(coreutils::MString name, coreutils::ZString &result) { + Tag *scope = this; + while(scope) { + if(scope->isCustomTagBoundary && scope->localTags.count(name)) { + result = scope->localTags[name]; + return true; + } + scope = scope->parent; + } + if(global.tags.count(name)) { + result = global.tags[name]; + return true; + } + return false; + } + bool Tag::ifTagDefined(coreutils::ZString &in, coreutils::ZString &tag) { in.push(); if(in.ifNext("<")) { tag = in.getTokenInclude("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"); - if(global.tags.count(tag)) { + coreutils::ZString definition; + if(findTagDefinition(tag, definition)) { if(in.ifNext(" ")) { in.pop(); return true; @@ -332,10 +349,10 @@ namespace jet { } else if(in.ifNext(">")) { in.pop(); return true; - } + } in.pop(); } - } + } in.pop(); return false; } diff --git a/Tag.h b/Tag.h index afaed7f..1a34b41 100644 --- a/Tag.h +++ b/Tag.h @@ -46,6 +46,32 @@ namespace jet { // the 22 tag classes' logic. void interpretSpan(coreutils::ZString span, coreutils::MString &out); + // Custom scoping: a Tag defined directly inside + // another custom tag's own invocation (i.e. inside a __dotag + // instance's expansion, not just nested inside //etc. that + // happen to sit there) registers into that invocation's own + // localTags instead of the single flat global.tags map, so it's + // only visible for the rest of that invocation - not the whole + // script. isCustomTagBoundary is true only on __dotag instances + // (set in __dotag's own constructor body); every other tag leaves + // it false and is transparently walked past when searching for an + // enclosing scope, exactly like // already are for + // parent-chain purposes elsewhere. Public (like variables/keywords/ + // name/etc. above) rather than protected - __tag.cpp needs to read + // and write these through an arbitrary ancestor Tag* found by + // walking parent, not just on its own `this`, and protected access + // in C++ only extends to a derived class's own (sub)object, not to + // another object reached through a base-class-typed pointer. + bool isCustomTagBoundary = false; + std::map localTags; + + // Walks from `this` up through `parent` looking for the nearest + // isCustomTagBoundary ancestor whose localTags has `name`, falling + // back to global.tags - used by both ifTagDefined() (is + // visible right here?) and __dotag's own constructor (fetch the + // actual definition to expand). + bool findTagDefinition(coreutils::MString name, coreutils::ZString &result); + protected: bool hasContainer = false; bool hasContainer2 = false; diff --git a/__dotag.cpp b/__dotag.cpp index 1bd3b9d..e81423c 100644 --- a/__dotag.cpp +++ b/__dotag.cpp @@ -2,9 +2,35 @@ #include "Exception.h" namespace jet { - + __dotag::__dotag(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, local) { - coreutils::ZString container3 = global.tags[name]; + // Every invocation of a custom tag is itself a new scope boundary - + // a encountered while expanding this invocation's + // own template (below) registers into THIS instance's localTags, + // not the global map, so it's only visible for the rest of this + // expansion. See Tag.h's isCustomTagBoundary/localTags doc comment. + isCustomTagBoundary = true; + + // The base Tag constructor already set hasContainer from whether + // THIS INVOCATION was self-closing ( vs ...) - + // that's the right question for capturing this invocation's own + // nested content into `container` above, but the wrong one for + // whether the tag's DEFINITION (container3 below) should render: + // a self-closing invocation of a tag whose definition doesn't use + // at all is completely normal and should still + // render. Found this as a real, pre-existing bug while testing + // this scoping feature - always silently rendered nothing + // for ANY custom tag, only worked, since + // processContainer() gates on hasContainer. Force it true here: + // by this point a valid definition is guaranteed to exist (dispatch + // only constructs __dotag after ifTagDefined() already confirmed + // it), so there's always something to render regardless of how + // this invocation was written. + hasContainer = true; + + coreutils::ZString container3; + if(!findTagDefinition(name, container3)) + throw coreutils::Exception("tag definition not found."); processContainer(container3, container); } } diff --git a/__dump.cpp b/__dump.cpp index 7ef143f..477d939 100644 --- a/__dump.cpp +++ b/__dump.cpp @@ -2,6 +2,7 @@ #include "Exception.h" #include #include +#include namespace jet { @@ -33,14 +34,36 @@ namespace jet { for (auto i = global.cookies.data.begin(); i != global.cookies.data.end(); i++) outFile << i->first << "=[" << i->second << "]" << std::endl; - // Unlike variables (which have real global/local/parent scoping), - // custom definitions have only one level - __tag.cpp - // always registers into global.tags regardless of where in the - // script the definition sits, so there's nothing to break out by - // scope here. + // Custom definitions now have real scoping - one + // defined directly inside another custom tag's own invocation is + // only visible for the rest of that invocation (see Tag.h's + // isCustomTagBoundary/localTags doc comment). Render the actual + // tree currently in effect at this point in the script: global at + // the root, then each enclosing custom-tag invocation nested one + // level deeper, outermost first - if there's no active local scope + // (this isn't running inside any custom tag's expansion), + // that's just the root with nothing nested under it. outFile << "*** DEFINED TAGS ***" << std::endl; + outFile << "(global)" << std::endl; for (auto i = global.tags.begin(); i != global.tags.end(); i++) - outFile << i->first << std::endl; + outFile << " " << i->first << std::endl; + + std::vector scopeChain; + for (Tag *scope = this; scope; scope = scope->parent) + if (scope->isCustomTagBoundary) + scopeChain.push_back(scope); + + int depth = 1; + for (auto it = scopeChain.rbegin(); it != scopeChain.rend(); ++it) { + Tag *boundary = *it; + coreutils::MString indent; + for (int d = 0; d < depth; d++) + indent << " "; + outFile << indent << "(inside " << boundary->name << ")" << std::endl; + for (auto i = boundary->localTags.begin(); i != boundary->localTags.end(); i++) + outFile << indent << " " << i->first << std::endl; + depth++; + } outFile.close(); diff --git a/__tag.cpp b/__tag.cpp index 5930660..620bbb2 100644 --- a/__tag.cpp +++ b/__tag.cpp @@ -3,14 +3,36 @@ namespace jet { - __tag::__tag(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, this, this) { + __tag::__tag(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, local) { evaluate = false; output = false; if(!keywordDefined("name")) throw coreutils::Exception("tag must have a name."); if(!hasContainer) throw coreutils::Exception("tag requires a container to process."); - global.tags[keywords["name"]] = container; + + // A defined directly inside another custom tag's own + // invocation (a __dotag instance's expansion) is scoped to that + // invocation - only visible for the rest of it, not the whole + // script. Control-flow tags like / that happen to sit in + // between are transparent (walked past, not boundaries themselves). + // Reaching parent==NULL (true top level) falls back to the original + // single flat global.tags map, unchanged from before. This relies + // on `parent` here actually being the real enclosing tag - the base + // Tag(...) call above used to pass (this, this) instead of the + // parent/local this constructor actually received, which would have + // made this walk loop on itself forever; fixed as part of adding + // this scoping (parent/local weren't needed anywhere else in this + // class - 's own container is never evaluated at definition + // time, evaluate=false above, so nothing depended on the old + // self-referential values). + Tag *scope = parent; + while(scope && !scope->isCustomTagBoundary) + scope = scope->parent; + if(scope) + scope->localTags[keywords["name"]] = container; + else + global.tags[keywords["name"]] = container; } }