Skip to content

perf(provenance): prototype optimized side table - #15

Closed
jeswr wants to merge 11 commits into
mainfrom
codex/provenance-side-table
Closed

perf(provenance): prototype optimized side table#15
jeswr wants to merge 11 commits into
mainfrom
codex/provenance-side-table

Conversation

@jeswr

@jeswr jeswr commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Purpose

Prototype an optimized version of the side-table architecture from rdfjs#672 so its complete diff can be reviewed independently.

Architecture

  • retain existing lexer tokens in a parser-owned Map<Term, Token>
  • avoid allocating a second span object per parsed term
  • avoid allocating the per-quad S/P/O/G callback carrier
  • index value-equal quad occurrences with scalar IDs, allocating an array only for duplicates
  • materialize public Range[] objects lazily on lookup

The regular Map is intentional: the provenance result retains every emitted quad, so weak keys do not enable collection during the index lifetime.

Performance

100,000 N-Triples (6,677,780 bytes):

The fresh host was noisy, so the ratio is included as a smoke measurement rather than a precise cross-branch ranking.

Validation

  • node and browser bundles build
  • 22 test suites / 6,886 tests pass with 100% statements, branches, functions, and lines
  • touched files pass ESLint
  • value-equal lookup and duplicate utterances remain supported

Known limitation

Location is still associated through term identity. A custom DataFactory that interns an equal term across multiple lexical positions can therefore make positions ambiguous. Frozen terms themselves are supported because this option does not mutate them.

Eric Prud'hommeaux and others added 11 commits August 3, 2026 19:27
Lexer: an absolute-offset counter and, under the new trackOffsets
option, offsetStart/offsetEnd on each token (default token shape and
hot path unchanged).

Parser: under the new onQuadSpans option, remember each term's source
token span (WeakMap, populated in _readEntity, literal completion and
synthetic-blank-node creation) and report per-position spans for every
emitted quad. Zero cost when the option is absent.

N3ProvenanceParser: wraps Parser to maintain a multiset of quad
*utterances* on the side - Map from a canonical quad key (value-based,
never object identity, so store-reconstructed quads still resolve) to
{quad, subject/predicate/object/graph: Range[]} with absolute character
offsets. Stores stay plain sets of quads at full speed; the multiset
lives in the wrapper.

All existing tests pass unchanged and coverage stays at 100%; new tests
cover utterance multiset semantics, value-keyed lookup, TriG graph
labels, RDF 1.2 annotations and span-less synthetic terms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
apply-provenance.mjs applies the term-span transforms (a payload of
context-anchored replacements generated by diffing this branch against
main, plus the N3ProvenanceParser source) to a pristine N3.js source
tree. Anchors must match exactly once, so upstream drift fails loudly.
By construction, term-provenance == main + this script, byte for byte -
so the instrumentation can also be maintained entirely out-of-tree
against upstream releases if it isn't wanted in-tree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Ted Thibodeau Jr <tthibodeau@openlinksw.com>
rdfjs#613 fixed literal subjects/predicates in N3 mode. Two
consequences for the provenance branch:

- The subject-literal test document `"s" <p> <o> <g> .` only parsed
  before because the old `_completeSubjectLiteral` swallowed the token
  after the literal, shifting `<o>`/`<g>` into predicate/object. With
  the token no longer dropped, the document is an N3-invalid quad. Use
  `"s" <p> <o> .`, which is what the test meant to exercise.

- `_readPredicate`'s new `case 'literal'` is a fourth site that stashes
  `_literalValue` for later completion, so it needs the same
  `_literalSpan` stash as the subject, object, and list-item sites, or
  predicate literals come back span-less. Instrument it and add the
  corresponding payload transform.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Numbers and booleans reach the parser as a single `literal` token whose
`prefix` already carries the datatype, so they skip the
`_literalValue`/`_literalSpan` handshake that the quoted-literal paths
use and were constructed with no span at all: the object of
`<s> <p> 42 .` came back span-less. Note the span directly from the
token at all four such sites (subject, predicate, object, list item);
the token covers exactly the numeric or boolean lexeme, so no offset
arithmetic is needed.

apply-provenance.mjs itself is unchanged -- this is four more
context-anchored replacements in the payload, and the derivation still
reproduces src/ byte-identically from pristine upstream sources.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The anchor spanned the whole function body, including the _emit line
that carries the reifies quad's graph.  That line is unrelated to the
instrumentation -- the transform only wraps the blankNode() and quad()
calls -- but including it made the anchor drift the moment upstream
touched the graph argument (as the fix for rdfjs#676 does).

Trim it to the three lines actually rewritten.  Output on main is
byte-identical, and the payload now applies cleanly on top of the
reifies-graph, lone-reifier and blank-node-annotation fixes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	src/N3Lexer.js
#	src/N3Parser.js
@jeswr jeswr closed this Sep 6, 2026
Sign up for free to 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.

2 participants