// metta_counts.cc — reading a MeTTa SCB instance from C++. // // Source: authored in b3u_use_cases for the metta_bench use case. // // This is the consumer side of the loop the `metta` use case ends at. // The ccsmetta frontend turned your .metta source into a .bfgr; this // loads that binary ONCE and then walks it, counting atoms by kind and // timing the walk. // // It builds against the STANDALONE SCB C++ package alone (namespace // ccsscb, libscbcpp.a) — exactly what a b3u.dev delivery contains — // plus the generated mettaKeyWordDefinition.h from the cpp bundle. No // compiler, no runtime library, no cppcc source tree. // // Two walkers run over the same instance and must agree. That is the // point of having two: a single walker that is wrong is silently // wrong, and a walk over a structure you did not build is exactly the // place a quiet mistake hides. // // Helper — recursive traversal through nested kwn/edp wrappers // Helper2 — the same recursion over flat vectors, no per-call // bounds checks // // Build (paths point at your delivered package and the cpp bundle): // // g++ -std=c++17 -O2 -Wall -fno-strict-aliasing // -I/include -I // metta_counts.cc /lib/libscbcpp.a -o metta_counts // // Usage: // ./metta_counts .metta.bfgr [iterations] (default 100) #include #include #include #include #include #include "SyntaxControlledBinary.h" // The generated KeyWordDefinition header also serves frontend builds: // its tail declares a factory returning cppcc::com::KeyWordsContainer. // Linking the standalone SCB package instead, that type is absent — a // forward declaration satisfies the (never-called) declaration and // keeps the delivered header untouched. namespace cppcc { namespace com { class KeyWordsContainer; } } #include "mettaKeyWordDefinition.h" namespace { using ccsscb::scb::SyntaxControlledBinary; namespace tag = ccsscb::scr::tag; namespace kw = cppcc::metta; template double mean_us(int iters, F&& fn) { using clk = std::chrono::steady_clock; auto t0 = clk::now(); for (int i = 0; i < iters; i++) fn(); auto t1 = clk::now(); auto ns = std::chrono::duration_cast(t1 - t0).count(); return (ns / 1000.0) / iters; } // The MeTTa atom classes. The first three are the ones that do not // exist in SGDL's predefined set — they are what LanguageMeTTa adds, // and seeing them counted here is seeing the lexical extension work // all the way through to a consumer. struct Counts { unsigned long sym_; // mettaSymbol — Lisp-permissive symbols unsigned long var_; // mettaVariable — $-prefixed unsigned long ref_; // mettaSpaceRef — &-prefixed unsigned long ident_; // identifier — the predefined class unsigned long str_; // stringToken unsigned long int_; // integerToken unsigned long term_; // standalone terminals: = : -> [ ] | { } etc. unsigned long list_; // '(' — one per s-expression list unsigned long total() const { return sym_ + var_ + ref_ + ident_ + str_ + int_ + term_; } bool operator==(const Counts& o) const { return sym_ == o.sym_ && var_ == o.var_ && ref_ == o.ref_ && ident_ == o.ident_ && str_ == o.str_ && int_ == o.int_ && term_ == o.term_ && list_ == o.list_; } }; void print_counts(const char* name, double us, const Counts& c) { std::printf(" %-8s %9.2f us/walk atoms=%-5lu lists=%-5lu " "sym=%lu var=%lu ref=%lu id=%lu str=%lu int=%lu term=%lu\n", name, us, c.total(), c.list_, c.sym_, c.var_, c.ref_, c.ident_, c.str_, c.int_, c.term_); } // Tally one leaf by its keyword id. Returns true if the tag was a // leaf (and therefore must not be recursed into — a leaf's fixed[] // holds raw bytes, not child tags). bool tally_leaf(tag::Long kkk, tag::Long nnn, Counts& c) { switch (kkk) { case kw::KW_METTASYMBOL: ++c.sym_; return true; case kw::KW_METTAVARIABLE: ++c.var_; return true; case kw::KW_METTASPACEREF: ++c.ref_; return true; case kw::KW_IDENTIFIER: ++c.ident_; return true; case kw::KW_STRINGTOKEN: ++c.str_; return true; case kw::KW_INTEGERTOKEN: ++c.int_; return true; case kw::KW_FLOATTOKEN: return true; case kw::KW_TEXTTOKEN: return true; case kw::KW_METTACOMMENT: return true; case kw::KW_TERMINALTOKENOFRULE: return true; case kw::KW_TERMTOKEN: // ⚠ The specific terminal is named by nnn, NOT by kkk — kkk is // KW_TERMTOKEN for every one of them. Comparing kkk here (the // obvious mistake, and the one this code made first) silently // counts all 20 parentheses of a small sample as atoms. if (nnn == kw::KW_LEFTPARENTHESISTERMTOKEN) { ++c.list_; return true; } if (nnn == kw::KW_RIGHTPARENTHESISTERMTOKEN) { return true; } ++c.term_; return true; } return false; } // ---- Helper: recursive traversal ------------------------------------ void walk1(SyntaxControlledBinary::Helper& h, tag::Long t, Counts& c) { if (!t) return; tag::Long kkk; tag::Long nnn; tag::setlongedf(&t, kkk, nnn); if (tally_leaf(kkk, nnn, c)) return; if (kkk < 0 || kkk >= static_cast(h.kwns_.size())) return; auto* k = h.kwns_[kkk].k_; if (!k) return; if (nnn < 0 || nnn >= static_cast(h.kwns_[kkk].edps_.size())) return; auto* e = h.kwns_[kkk].edps_[nnn].e_; if (!e) return; tag::Long* uf = h.b_.fixed(e); tag::Long* ud = h.b_.dynamic(e); if (uf) for (tag::Long i = 0; i < e->fl; ++i) walk1(h, uf[i], c); if (ud) for (tag::Long i = 0; i < e->dl; ++i) walk1(h, ud[i], c); } // ---- Helper2: same logic, flat vectors ------------------------------ void walk2(SyntaxControlledBinary::Helper2& h, tag::Long t, Counts& c) { if (!t) return; tag::Long kkk; tag::Long nnn; tag::setlongedf(&t, kkk, nnn); if (tally_leaf(kkk, nnn, c)) return; // Trust the binary — no bounds/null checks. This is the difference // between the two walkers, and the reason they are worth running // together: if the flat path is ever wrong, the checked one says so. SyntaxControlledBinary::edpirType* e = h.edps_[kkk][nnn]; tag::Long* uf = e->fl ? &h.b_.memory_[e->d] : nullptr; tag::Long* ud = e->dl ? &h.b_.memory_[e->d + e->fl] : nullptr; if (uf) for (tag::Long i = 0; i < e->fl; ++i) walk2(h, uf[i], c); if (ud) for (tag::Long i = 0; i < e->dl; ++i) walk2(h, ud[i], c); } } // namespace int main(int argc, char** argv) { if (argc < 2) { std::fprintf(stderr, "usage: metta_counts .metta.bfgr [iterations]\n"); return 2; } std::string bfgr = argv[1]; int iters = argc > 2 ? std::atoi(argv[2]) : 100; if (iters <= 0) iters = 100; std::ifstream probe(bfgr); if (!probe) { std::fprintf(stderr, "FAIL: cannot open %s — run `ccsmetta -lt " ".metta` first\n", bfgr.c_str()); return 1; } probe.close(); // Load ONCE; only the walks are timed. That split is the whole point // of the format: produce the binary once, read it many times. SyntaxControlledBinary bin; bin.readBinaryRaw(bfgr); std::printf("loaded: %s (%zu longs), iterations: %d\n", bfgr.c_str(), bin.memory_.size(), iters); SyntaxControlledBinary::Helper h1(bin); SyntaxControlledBinary::Helper2 h2(bin); Counts c1{}, c2{}; double us1 = mean_us(iters, [&] { c1 = Counts{}; walk1(h1, h1.h_.cntbeg, c1); }); double us2 = mean_us(iters, [&] { c2 = Counts{}; walk2(h2, h2.h_.cntbeg, c2); }); print_counts("Helper", us1, c1); print_counts("Helper2", us2, c2); if (!(c1 == c2)) { std::fprintf(stderr, "FAIL: the two walkers disagree — one of them is " "wrong, and a single walker would not have said so\n"); return 1; } if (c1.total() == 0) { std::fprintf(stderr, "FAIL: zero atoms. If the .bfgr is ~224 bytes the " "frontend was run without -lt and parsed almost " "nothing (it exits 0 either way)\n"); return 1; } std::printf("OK: both walkers agree on %lu atoms\n", c1.total()); return 0; }