Uh oh!
There was an error while loading. Please reload this page.
feat: add jsonwrite and jsonparse builtins - #712
Merged
Conversation
Serializing the knowledge base is where a KB analyzer actually spends its time. Measured on the date-time analyzer, its output.nlp -- thirteen lines that call JsonKB() and SaveKB() -- was 43.9% of total runtime, all of it inside the 692-line interpreted KBFuncs.nlp. Memoizing the indent helper in NLP++ was tried first and bought 4.6%: the cost is not one hot helper but interpreted execution itself, roughly 73us per emitted line. Moving the walk into C++ is the only lever. jsonwrite(file, concept) serialize a concept tree to JSON jsonparse(json_str, parent) parse JSON into concepts under parent jsonwrite is a drop-in for KBFuncs.nlp's JsonKB: same argument order, byte-identical output, so JsonKB becomes a one-line wrapper and every analyzer picks up the speedup without touching its output pass. On date-time at 30x input, over three runs: output pass 1.44-1.61s -> 0.41-0.55s ~3.1x total run 1.62-1.81s -> 0.59-0.81s ~2.6x Output verified byte-identical at 10,537 bytes single and 309,457 bytes at 30x. Byte compatibility means reproducing two quirks deliberately, both commented at their definitions: SpacesStr(n) emits max(1,n) indents rather than n, and SeparateConCount's early exit is a no-op statement so its backward digit scan never stops -- "a1b2" splits as base "a1" count "12". Fixing either would silently change existing analyzers' output; they should be fixed together with their NLP++ originals. jsonparse is the inverse, following the mapping python/json2kbb.py documents. Doing it in the engine removes the Python dependency, which matters for the npm and pypi distributions: a python pass shells out to an interpreter neither bundles. It also removes that script's stale intermediate -- it will not overwrite an existing .kbb, so editing the .json silently does nothing until you delete it. The parser handles nested objects and arrays, all string escapes including \uXXXX with surrogate-pair combining, int/float discrimination, and true/false/null. It requires a top-level object and rejects trailing garbage, so a truncated or doubled document is reported rather than half-loaded. The round-trip fixture earned its keep: jsonwrite synthesizes "id" from a counted concept's name suffix (item1 -> "id":"1"), and jsonparse was storing that as a real attribute, so a second write emitted it twice. jsonparse now drops "id" inside array elements, where the concept name already carries it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bumped in this PR rather than as a follow-up, because the help pages and the KBFuncs.nlp comment in VisualText/visualtext-files#204 already name 3.8.2 as the release that introduces jsonwrite and jsonparse. Leaving the constant at 3.8.1 would have made both of those wrong. A patch bump, following this repo's convention: 3.7.10, 3.7.12, 3.7.13 and 3.7.14 all shipped new features as patch releases within a minor line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
ddehilster added a commit
to VisualText/visualtext-files
that referenced
this pull request
Aug 4, 2026
Engine side: VisualText/nlp-engine#712. spec/KBFuncs.nlp JsonKB becomes a one-line wrapper around the jsonwrite builtin. Its output is byte-identical, so nothing downstream changes, but the pass gets about 3x faster: serializing the KB is where a knowledge-base analyzer spends most of its time. On the date-time analyzer the thirteen-line output pass that calls JsonKB and SaveKB was 43.9% of total runtime, all of it inside interpreted NLP++. Kept as a wrapper rather than editing every analyzer's output.nlp. Note that analyzers carry their OWN copy of KBFuncs.nlp, so an existing analyzer picks this up when its copy is refreshed; newly created ones get it from the template automatically. JsonKBRecurse, JsonAttributes and JsonStr are left in place, with a note saying why: an analyzer may call them directly (JsonStr is useful on its own for escaping a string), and they document exactly what the builtin reproduces byte for byte. New pages jsonwrite.md, jsonparse.md, including the two long-standing quirks the builtin reproduces deliberately for byte compatibility -- the indent for level n is max(1,n) steps, and a concept name with interior digits is split at the wrong place when deciding whether it is a counted sibling. Also records the 3.8.1 python pass fix in the version notes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Serializing the knowledge base is where a KB analyzer actually spends its time. Measured on
date-time, itsoutput.nlp— thirteen lines that callJsonKB()andSaveKB()— was 43.9% of total runtime, all of it inside the 692-line interpretedKBFuncs.nlp.I tried the cheap fix first: memoizing the indent helper in NLP++ bought 4.6%. The cost isn't one hot helper, it's interpreted execution itself (~73µs per emitted line). C++ is the only lever.
jsonwrite(file, concept)jsonparse(json_str, parent)jsonwrite — a drop-in for JsonKB
Same argument order, byte-identical output, so
KBFuncs.nlp'sJsonKBbecomes a one-line wrapper and an analyzer gets the speedup without touching its output pass:date-time at ×30 input, three runs each:
outputpassByte-identical verified at 10,537 bytes (single) and 309,457 bytes (×30).
Two quirks reproduced deliberately, both commented at their definitions:
SpacesStr(n)emitsmax(1,n)indents, notn— soSpacesStr(0)is two spaces.SeparateConCount's early exit is a no-op statement, so its backward digit scan never stops:a1b2splits as basea1/ count12.Fixing either would silently change existing analyzers' output. They should be fixed together with their NLP++ originals, as their own change.
jsonparse — the inverse
Follows the mapping
python/json2kbb.pydocuments. Doing it in the engine removes the Python dependency, which matters for the npm and pypi distributions — a python pass shells out to an interpreter neither bundles. It also removes that script's stale intermediate: it won't overwrite an existing.kbb, so editing the.jsonsilently does nothing until you delete it.Pairs with 3.8.0's
readfile:Handles nested objects/arrays, all string escapes including
\uXXXXwith surrogate-pair combining, int/float discrimination, andtrue/false/null. Requires a top-level object and rejects trailing garbage, so a truncated document is reported rather than half-loaded.The round-trip fixture earned its keep
It caught a real bug:
jsonwritesynthesizes"id"from a counted concept's name suffix (item1→"id":"1"), andjsonparsewas storing that as a genuine attribute — so a second write emitted it twice.jsonparsenow drops"id"inside array elements, where the name already carries it. The fixture asserts the count stays at 2, not 4.Scope
Analyzers carry their own copy of
KBFuncs.nlp(13 in the analyzers repo, 21 bundled in the extension), and those copies already differ from the shared template. So rewiring the template gives newly created analyzers the speedup automatically; an existing analyzer gets it when its copy is refreshed. This is not a silent global upgrade, and I corrected an earlier claim in this description that implied it was.Note on sequencing
The
KBFuncs.nlprewiring is a separate PR in visualtext-files. It must not ship before this engine change is released, or analyzers on an older engine would call a builtin that doesn't exist.🤖 Generated with Claude Code