Skip to content

ShExMap: materialize by iteration scopes; keys, static checks, provenance, in-place update - #110

Open
ericprud wants to merge 2 commits into
masterfrom
ShExMap
Open

ericprud wants to merge 2 commits into
masterfrom
ShExMap

Conversation

@ericprud

Copy link
Copy Markdown
Collaborator

ShExMap (pyshex.shexmap) maps RDF between two ShEx schemas whose triple constraints share %Map:{ … %} variables. This PR replaces the materializer's frame-cursor model, ported from shex.js, with materialization by iteration scopes, and adds what that makes possible.

Why

With cardinality above one, flattening the binding tree into frames and reading them with a forward-only cursor could not preserve the input's structure: a nested schema did not map to itself, a list of (a, b) pairs could not be transposed into a list of as and a list of bs, and reading a parent's binding a second time inside an item stepped the cursor and mixed items. Those are properties of the model, not bugs in the search (the design notes trace them to the nested-relational result that nest ∘ unnest is the identity only with keys at every level).

What changes

  • Binding tree: an unambiguous grammar (a scope is an object, or an array of the own-bindings object followed by one list per repeated constraint or group, in schema order, empty when nothing matched; lists uniform); each iteration of a repeated shape-valued constraint carries @node, the input node it matched. shex.js's stored bindings still compare equal modulo @node, and its two older layouts are still read.
  • Materializer: evaluates the output schema over the scope tree. A constraint reads its variable from the scope it is evaluated at or an ancestor; reading marks nothing. A repetition iterates the deepest input list its body's variables are bound at, skipping items whose body fails, honouring min and max. A body reading from two unrelated lists is refused by name. Choices yield alternatives, pruned to max_accepts; the one reading the most distinct bindings wins. ThreadedMaterializer keeps its name and results. Every example's expected graph is unchanged; the round-trip law (materialize with the input schema, bind again) holds on all of them.
  • Node identity: a %Map variable on a shape-valued constraint names its node; %Map:{ id(arg, …) %} keys it (variables, @node, or an IRI template <…{v:x}…> percent-encoded as R2RML does), so equal keys merge; unkeyed nodes are minted from root, constraint, depth and scope, so runs agree.
  • Value check: a Map code's value must satisfy the constraint's value expression; a plain literal whose lexical form fits the datatype is retyped, anything else fails the constraint like an unbound variable.
  • Static checks: analyse(input_schema, output_schema) and shexmap --check decide before any data whether a pair maps coherently.
  • Provenance and updates: every triple carries its provenance; Bindings.matched is the subgraph the input schema selected; ThreadedMaterializer.update / materialize(replace=True) / shexmap --into replace what the output schema holds at a root and leave the rest alone.
  • Tests: 260+ ShExMap tests, including a differential run of every example through shex.js's @shexjs/extension-map when SHEXJS_EXTENSION_MAP points at a built checkout (13 of 14 agree; shex.js binds nothing through EXTENDS).

The design notes behind this (problem analysis, literature, the normative draft, per-phase status) are kept outside the repo for now; the corresponding shex.js changes are being prepared separately.

🤖 Generated with Claude Code

…ance, in-place update

The binding tree is now written to a grammar a reader can parse without
guessing (a scope is an object, or an array whose first element is the
own-bindings object -- always written -- followed by one list per repeated
constraint or group, in schema order, empty when nothing matched; a list's
iterations are uniform), and each iteration of a repeated shape-valued
constraint carries "@node", the input node it matched.  A non-repeated
nested shape merges into the scope that matched it.  shex.js's stored
bindings still compare equal once @node is stripped, and its two older
layouts are still read.

The materializer no longer flattens the tree into frames read by a
forward-only cursor.  It evaluates the output schema over the scope tree:
a constraint reads its variable from the scope it is evaluated at or the
nearest ancestor binding it, and reading marks nothing, so a parent's
binding can be read in every item; a repetition iterates the deepest
input list its body's variables are bound at, skipping items whose body
fails, honouring min and max; a body reading from two unrelated lists is
refused by name; OneOf, ShapeOr, optionals and extensions yield
alternatives combined by product and pruned to max_accepts, the one that
read the most distinct bindings winning.  A nested schema now maps to
itself, sibling lists transpose, a parent's binding read at the parent
and in each item no longer mixes items, and the round-trip law holds on
every example; the expected graphs are unchanged.  ThreadedMaterializer
keeps its name and results.

Node identity: a shape-valued constraint with a %Map variable names its
node; %Map:{ id(arg, ...) %} keys it -- variables, @node, or an IRI
template <...{v:x}...> percent-encoded as R2RML does -- so equal keys
merge and id(@node) maps a graph onto itself node for node; other nodes
are minted from the root, constraint, depth and scope, so runs agree and
materializing the same root twice adds nothing.

A value a Map code produces must satisfy the constraint's value
expression; a plain literal whose lexical form fits the datatype is
retyped, anything else fails the constraint like an unbound variable.

analyse(input_schema, output_schema) and shexmap --check decide before
any data whether a pair maps coherently: unrelated lists, "which one?"
reads, unbound and unused variables, double binding sites, id() misuse.

Every triple carries its provenance (constraint, input scope and node,
bindings and statics read, how the object arose); Bindings.matched is the
subgraph the input schema selected; ThreadedMaterializer.update, and
shexmap --into, replace what the output schema holds at a root and leave
the rest alone.  A differential test runs every example through shex.js's
extension-map when SHEXJS_EXTENSION_MAP is set.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The recorded bindings are now what shex.js (branch shexmap-scopes, 4d53c6e1)
writes: the scope layout with @node, one list per repeated expression.  The
example test compares the whole tree against them, blank-node labels aside,
instead of only the frames.  The differential driver reads shex.js's
bindingTree() when the checkout has it, hands @node over as written, and the
EXTENDS example no longer diverges.

normalize() keeps a scope's own bindings as a frame when its lists are all
empty, as shex.js's normalizeBindingTree does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

1 participant