diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 34f4c2ca..036705e6 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -623,8 +623,8 @@ jobs: 'tree:_sent _color apples* .' \ 'tree:_sent _color sky* and* _color grass* .' \ 'tree:Blue' \ - 'concept:sent root:_sent sawcallerG:0 backcall:0 colors:red;' \ - 'concept:sent root:_sent sawcallerG:0 backcall:0 colors:crimson;green;' \ + 'concept:sent root:_ROOT sawcallerG:0 backcall:0 colors:red;' \ + 'concept:sent root:_ROOT sawcallerG:0 backcall:0 colors:crimson;green;' \ 'self:0' \ 'missing:0') if [ "$got" != "$want" ]; then diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index fa26fce5..976a9c72 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -551,8 +551,8 @@ jobs: 'tree:_sent _color apples* .' \ 'tree:_sent _color sky* and* _color grass* .' \ 'tree:Blue' \ - 'concept:sent root:_sent sawcallerG:0 backcall:0 colors:red;' \ - 'concept:sent root:_sent sawcallerG:0 backcall:0 colors:crimson;green;' \ + 'concept:sent root:_ROOT sawcallerG:0 backcall:0 colors:red;' \ + 'concept:sent root:_ROOT sawcallerG:0 backcall:0 colors:crimson;green;' \ 'self:0' \ 'missing:0') if [ "$got" != "$want" ]; then diff --git a/.github/workflows/tests/call-analyzer-callee/spec/colors.nlp b/.github/workflows/tests/call-analyzer-callee/spec/colors.nlp index a48595f2..51242b7b 100644 --- a/.github/workflows/tests/call-analyzer-callee/spec/colors.nlp +++ b/.github/workflows/tests/call-analyzer-callee/spec/colors.nlp @@ -20,6 +20,10 @@ G("mine") = "callee"; addnumval(L("out"),"backcall",callanalyzer(pnroot(),L("out"),"call-analyzer")); @@CODE +# As in any analyzer written to run on its own. When called, _ROOT is the +# root the called analyzer is given, not the node's own name. +@NODES _ROOT + @CHECK if (!N("color",1)) fail(); diff --git a/.github/workflows/tests/call-analyzer-callee/spec/mark.nlp b/.github/workflows/tests/call-analyzer-callee/spec/mark.nlp index 18d3079c..3be17949 100644 --- a/.github/workflows/tests/call-analyzer-callee/spec/mark.nlp +++ b/.github/workflows/tests/call-analyzer-callee/spec/mark.nlp @@ -4,6 +4,8 @@ # analyzer reached. ############################################### +@PATH _ROOT + @POST N("callee",1) = 1; diff --git a/.github/workflows/tests/call-analyzer/spec/call.nlp b/.github/workflows/tests/call-analyzer/spec/call.nlp index aad9c39a..3a108062 100644 --- a/.github/workflows/tests/call-analyzer/spec/call.nlp +++ b/.github/workflows/tests/call-analyzer/spec/call.nlp @@ -10,6 +10,12 @@ @CODE G("results") = makeconcept(findroot(),"results"); G("mine") = "caller"; + +# Every attribute name becomes a dictionary word with no attributes, so this +# puts "crimson" in the KB bare -- the way an earlier call or the caller's own +# KB work leaves common words. The called analyzer must still find crimson's +# entry in its lazy shades-full.dict. +addstrval(G("results"),"crimson","bare"); "out.txt" << "loaded-before:" << str(findana("call-analyzer-callee")) << "\n"; @@CODE diff --git a/.github/workflows/tests/call-analyzer/spec/report.nlp b/.github/workflows/tests/call-analyzer/spec/report.nlp index 87c2cf62..56a8cdb2 100644 --- a/.github/workflows/tests/call-analyzer/spec/report.nlp +++ b/.github/workflows/tests/call-analyzer/spec/report.nlp @@ -4,14 +4,15 @@ # NOTE: What each line proves: # loaded-* the called analyzer was loaded by the first call # mine its G() variables are its own -# tree it changed only the sentence nodes it was given: +# tree it changed only the sentence nodes it was given, +# though its passes select @NODES _ROOT and @PATH _ROOT: # colors became _color nodes and * marks each word # it visited; the top-level "Blue" is untouched # _color red/green come from its regular dictionary, loaded # into the shared KB; crimson only from its lazy # shades-full.dict, looked up on the passed text # concept it wrote into this KB's concept directly, saw the -# given node as its root, and could not call back +# root it was given (_ROOT), and could not call back # self an analyzer cannot call itself # missing an unknown analyzer is an error, not a crash # @@ -24,8 +25,8 @@ # tree:_sent _color apples* . # tree:_sent _color sky* and* _color grass* . # tree:Blue -# concept:sent root:_sent sawcallerG:0 backcall:0 colors:red; -# concept:sent root:_sent sawcallerG:0 backcall:0 colors:crimson;green; +# concept:sent root:_ROOT sawcallerG:0 backcall:0 colors:red; +# concept:sent root:_ROOT sawcallerG:0 backcall:0 colors:crimson;green; # self:0 # missing:0 ############################################### diff --git a/cs/libconsh/cg.cpp b/cs/libconsh/cg.cpp index 9bbe5620..af16c661 100644 --- a/cs/libconsh/cg.cpp +++ b/cs/libconsh/cg.cpp @@ -3512,27 +3512,41 @@ return true; CONCEPT *CG::findWordConcept(_TCHAR *str) { -CONCEPT *word; -if (!(word = kbm_->dict_find_word(str)) ) +CONCEPT *word = kbm_->dict_find_word(str); +if (fullKBBs_.empty() && fullDicts_.empty()) + return word; +if (word && findAttrs(word)) + return word; // A dictionary entry already in memory. + +// Not in memory, or in memory with no attributes of its own: try a +// lazily-loaded "*full" dictionary. // 06/10/26. +if (fullMissCache_.count(str)) + { + if (!word) + logMissingWord(str); // Known absent: skip the search but + return word; // still record it for this file's log. + } + +CONCEPT *full = findFullWord(str); +if (!word) + { + if (!full) { - // Not in memory: try a lazily-loaded "*full" dictionary. // 06/10/26. - if (!fullKBBs_.empty() || !fullDicts_.empty()) - { - if (fullMissCache_.count(str)) - { - logMissingWord(str); // Known absent: skip the search but - return 0; // still record it for this file's log. - } - word = findFullWord(str); - if (!word) - { - fullMissCache_.insert(str); // Negative cache (run-wide). - logMissingWord(str); // Record for later KB curation. - } - return word; - } - return 0; + fullMissCache_.insert(str); // Negative cache (run-wide). + logMissingWord(str); // Record for later KB curation. } + return full; + } + +// A bare word says nothing about what the dictionaries hold for it: every +// attribute name is a dictionary word, so any KB work that names one leaves +// it bare -- and under callanalyzer() the KB is shared with other analyzers. +// A dict hit has been added to word itself; a kbb-only hit is linked to it. +// Searched once either way. // 09/13/26 DD. +fullMissCache_.insert(str); +CONCEPT *meaning = 0; +if (full && full != word && !findVal(word, _T("meaning"), meaning)) + addVal(word, _T("meaning"), full); return word; } diff --git a/lite/callana.cpp b/lite/callana.cpp index fd531da8..d3266510 100644 --- a/lite/callana.cpp +++ b/lite/callana.cpp @@ -23,6 +23,9 @@ All rights reserved. // // WHAT RUNS ON EACH CALL. The passes, in order, on the subtree, with these // differences from running on a text of its own: +// - the root they get is a stand-in named _ROOT that holds pnode's +// children for the length of the call (see borrowChildren), so they +// select and traverse exactly as on a text of their own; // - tokenizer passes are skipped, because the tree already exists; // - a dicttok or dicttokz pass looks up the subtree's words instead, // including in the lazy *full files, which only load a word once it is @@ -286,6 +289,42 @@ for (const auto &file : dicts) } } +// Give the called analyzer a root of its own, as it has running on a text: +// named _ROOT, unsealed, with no parent or siblings, spanning node's text and +// holding node's children. @NODES, @PATH and @MULTI select by name and only +// descend into unsealed nodes, and they walk a root's siblings; node itself is +// named for what the caller built, usually sealed, and has siblings in the +// caller's tree. So without this, @NODES _ROOT selects nothing, deeper selects +// never reach inside, and passes can run on the caller's nodes next to node. +static Node *borrowChildren(Parse *parse, Node *node) +{ +Pn *pn = node->getData(); +Sym *sym = ((Htab *) parse->getNLP()->getHtab())->hsym(_T("_ROOT")); +Node *root = Pn::makeTnode(pn->getStart(), pn->getEnd(), + pn->getUstart(), pn->getUend(), PNNODE, pn->getText(), + sym->getStr(), sym, pn->getLine()); +root->getData()->setUnsealed(true); + +Node *children = node->Down(); +root->setDown(children); +if (children) + children->setUp(root); // Only a first child points up. +node->setDown(0); +return root; +} + +// Put the children back under node, as the called analyzer left them, and +// free the borrowed root. Variables set on the root go with it. +static void returnChildren(Node *root, Node *node) +{ +Node *children = root->Down(); +node->setDown(children); +if (children) + children->setUp(node); +root->setDown(0); +Node::DeleteNodeAndData(root); +} + // Run a loaded analyzer's passes on the subtree under node. static bool runCallee(Parse *parse, NLP *callee, Node *node, CONCEPT *con) { @@ -309,7 +348,8 @@ long rulepass = parse->getRulepass(); Dlist *vars = parse->getVars(); std::string appdir = parse->getAppdir(); -Tree subtree(node); // Does not own the nodes. +Node *root = borrowChildren(parse, node); +Tree subtree(root); // Does not own the nodes. parse->setTree(&subtree); parse->setAna(ana); parse->setVars(0); @@ -334,12 +374,13 @@ for (Delt *step = ana->getSeq(); step && ok; step = step->Right()) parse->setCurrpass(num); parse->setRulepass(num); if (isDictTok(algoname)) - ((DICTTok *) algo)->applyToSubtree(parse, node); + ((DICTTok *) algo)->applyToSubtree(parse, root); else ok = algo->Execute(parse, pass); } calls.pop_back(); +returnChildren(root, node); if (parse->getVars()) Dlist::DeleteDlistAndData(parse->getVars()); diff --git a/nlp/main.cpp b/nlp/main.cpp index 615bbac4..0d36ff71 100644 --- a/nlp/main.cpp +++ b/nlp/main.cpp @@ -15,7 +15,7 @@ All rights reserved. #include "lite/nlp_engine.h" #include "version.h" -#define NLP_ENGINE_VERSION "4.1.0" +#define NLP_ENGINE_VERSION "4.1.1" bool cmdReadArgs(int, _TCHAR *argv[], _TCHAR *&, _TCHAR *&, _TCHAR *&, _TCHAR *&, bool &, bool &, bool &, bool &, bool &, bool &, int &); void cmdHelpargs(_TCHAR *);