Deltas between the three renderings of a ShEx schema, with the evidence for each. Written up because the abstract syntax they are all supposed to share is not quite the same in each.
ShEx schemas exist in three syntaxes, and each has its own grammar:
| Syntax | Grammar | Kind |
|---|---|---|
| ShExC | ShEx Semantics § Language | EBNF over a compact text syntax |
| ShExJ | doc/ShExJ.jsg |
JSON Schema Grammar over a JSON tree |
| ShExR | doc/ShExR.shex |
a ShEx schema over an RDF graph |
ShExJ is the pivot: ShExC parses to it, and ShExJ plus
doc/ShExJ-context.jsonld
is ShExR — which is how
bin/mkShExR.js
derives ShExR.ttl and ShExR.ntriples. So a delta between ShExC and
ShExR is a place where that round trip is not total.
start be a ShapeDecl; nothing else doesThe three grammars say:
start ::= "start" '=' inlineShapeExpression ShExC start:shapeExprOrRef ? ShExJ shapeExprOrRef = shapeExpr | shapeDeclRef ; shapeExpr = ShapeOr | ShapeAnd | ShapeNot | NodeConstraint | Shape | ShapeExternal ; sx:start @<#shapeDeclOrExpr> ? ; ShExR <#shapeDeclOrExpr> @<#ShapeDecl> OR @<#shapeExpr>
Since ShEx 2.1 moved id onto ShapeDecl, a ShExJ
shapeExpr cannot itself be a ShapeDecl. ShExR's
shapeDeclOrExpr says it can. Running the same four start
forms through all three grammars:
start is… | ShExC | ShExJ.jsg | ShExR.shex |
|---|---|---|---|
a reference — start = @<S> |
✓ | accepted | accepted |
an inline shapeExpr — start = { } |
✓ | accepted | accepted |
an anonymous ShapeDecl |
no production | rejected | accepted |
an anonymous abstract ShapeDecl |
no production | rejected | accepted |
RDF cannot distinguish reference from containment. In ShExJ a reference is a bare string:
{ "type": "Schema", "start": "http://ex/S",
"shapes": [ { "type": "ShapeDecl", "id": "http://ex/S", "shapeExpr": {…} } ] }
structurally unlike an inline object. In RDF there is no such distinction — the arc is the thing:
<http://ex/sch> a sx:Schema ; sx:start <http://ex/S> ; sx:shapes ( <http://ex/S> ) . <http://ex/S> a sx:ShapeDecl ; sx:shapeExpr [ a sx:Shape ] .
So ShExR must let sx:start reach a ShapeDecl, or it could
not encode start = @<S> at all. The cost is that it also admits an
anonymous inline ShapeDecl, which no ShExC document can write and ShExJ's own
grammar rejects.
The obvious tightening does not work either. You might add AND IRI
to the ShapeDecl branch, forcing references to be IRIs — but
shapeDeclLabel = IRIREF | BNODE,
so blank-node-labelled ShapeDecls are legal, and a reference to one is a
blank-node arc, indistinguishable from inlining it. The ambiguity is
irreducible unless ShExJ drops BNODE from shapeDeclLabel.
startThe same shapeDeclOrExpr sits under every slot where ShExJ writes
shapeExprOrRef:
| ShExR | ShExJ |
|---|---|
sx:start |
Schema.start |
sx:shapeExprs (ShapeOr, ShapeAnd) |
shapeExprs |
sx:shapeExpr (ShapeNot) |
ShapeNot.shapeExpr |
sx:extends |
Shape.extends |
sx:valueExpr |
TripleConstraint.valueExpr |
start is just where it is easiest to see.
start slips past the abstract-shape requirementShExR accepts sx:start [ a sx:ShapeDecl ; sx:abstract true ; … ]. The
specification's abstract-shape requirement is worded over shapeExprRefs:
Every shapeExprRef referer MUST identify at least one non-abstract shape.
An inline abstract ShapeDecl at start is not a ref, so the
requirement's wording does not reach it — leaving a start shape that nothing
can satisfy. This is the one delta with semantic teeth rather than just
surplus expressivity.
inlineShapeExpression restricts start only in ShExCShExC's start takes an inline shape expression. Comparing the
inline and general forms, the only difference is the tail:
shapeDefinition ::= (includeSet | extraPropertySet | "CLOSED")* '{' tripleExpression? '}' annotation* semanticActions inlineShapeDefinition ::= (includeSet | extraPropertySet | "CLOSED")* '{' tripleExpression? '}'
So an inline shape definition may not carry annotations or semantic actions, and per the grammar this is ill-formed:
start = { <p> . } %<a>% # annotation/semAct on an inlineShapeDefinition
while parenthesising reaches the general production through
inlineShapeAtom's
'(' shapeExpression ')' branch:
start = ( { <p> . } %<a>% ) # well-formed
Neither ShExJ nor ShExR has any such restriction — sx:start reaches
<#Shape>, which permits sx:semActs and
sx:annotation unconditionally. The restriction is purely syntactic
and has no counterpart in the abstract syntax, so it is an obligation on
writers: a ShExJ→ShExC serialiser must parenthesise a start
carrying annotations or semantic actions, or it emits invalid ShExC.
shex.js accepts the bare form
above and attaches semActs to the start Shape, so its parser is more
permissive than the published grammar here. That is an implementation/grammar
divergence rather than a syntax/syntax one, but it means the bare form
round-trips in practice and so is likely to appear in the wild.
ShExC and ShExJ, from a shex.js checkout:
node -e "const p=require('./packages/shex-parser');
console.log(JSON.stringify(p.construct('http://ex/',{}).parse('<S> {} start = @<S>').start))"
# "http://ex/S" -- a shapeDeclRef, not an object
npx json-grammar doc/ShExJ.jsg start-shapedecl.json
# rejects { "type":"Schema", "start":{ "type":"ShapeDecl", … } }
ShExR, validating a candidate graph against doc/ShExR.shex:
const shexr = ShExParser.construct('http://www.w3.org/ns/shex',{},{index:true})
.parse(fs.readFileSync('doc/ShExR.shex','utf8'));
new ShExValidator(shexr, rdfjsDB(graph), {})
.validateShapeMap([{node:'http://ex/sch', shape:'http://www.w3.org/ns/shex#Schema'}]);
PREFIX sx: <http://www.w3.org/ns/shex#> <http://ex/sch> a sx:Schema ; sx:start [ a sx:ShapeDecl ; sx:abstract true ; sx:shapeExpr [ a sx:Shape ] ] . # conformant -- though no ShExC or ShExJ document can denote this
What ShExUtil.ShExRtoShExJ does with an anonymous ShapeDecl at
start is not established here: the loader failed earlier
(Cannot read properties of undefined (reading 'validator')) before
reaching the conversion. If it emits start: {type: "ShapeDecl", …},
that is a ShExJ document its own grammar rejects, which would turn this from a
specification gap into a reachable bug.