Skip to content

feat: add jsonwrite and jsonparse builtins - #712

Merged
ddehilster merged 2 commits into
masterfrom
feat/json-builtins
Aug 4, 2026
Merged

feat: add jsonwrite and jsonparse builtins#712
ddehilster merged 2 commits into
masterfrom
feat/json-builtins

Conversation

@ddehilster

@ddehilsterddehilster commented Aug 4, 2026

Copy link
Copy Markdown
Member

Serializing the knowledge base is where a KB analyzer actually spends its time. Measured on date-time, its output.nlpthirteen lines that call JsonKB() and SaveKB() — was 43.9% of total runtime, all of it inside the 692-line interpreted KBFuncs.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)serialize a concept tree to JSON
jsonparse(json_str, parent)parse JSON into concepts under parent

jsonwrite — a drop-in for JsonKB

Same argument order, byte-identical output, so KBFuncs.nlp's JsonKB becomes a one-line wrapper and an analyzer gets the speedup without touching its output pass:

JsonKB(L("file"),L("con")) {
jsonwrite(L("file"),L("con"));
}

date-time at ×30 input, three runs each:

JsonKB (NLP++)jsonwrite (C++)
output pass1.44–1.61s0.41–0.55s~3.1×
total run1.62–1.81s0.59–0.81s~2.6×

Byte-identical verified at 10,537 bytes (single) and 309,457 bytes (×30).

Two quirks reproduced deliberately, both commented at their definitions:

  • SpacesStr(n) emits max(1,n) indents, not n — so SpacesStr(0) is two spaces.
  • 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, as their own change.

jsonparse — the inverse

Follows 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 won't overwrite an existing .kbb, so editing the .json silently does nothing until you delete it.

Pairs with 3.8.0's readfile:

jsonparse(readfile(G("$apppath") + "/data/config.json"), G("kb"))

Handles nested objects/arrays, all string escapes including \uXXXX with surrogate-pair combining, int/float discrimination, and true/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: jsonwrite synthesizes "id" from a counted concept's name suffix (item1"id":"1"), and jsonparse was storing that as a genuine attribute — so a second write emitted it twice. jsonparse now 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.nlp rewiring 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

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>
@ddehilster
ddehilster merged commit ff88671 into masterAug 4, 2026
7 checks passed
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>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ddehilster