eyeleng 1.1.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  - **SRL** — the Shape Rules Language syntax used by the SHACL 1.2 Rules draft.
9
9
  - **RDF Rules** — a Turtle/RDF syntax for rule sets.
10
10
 
11
- Eyeleng is a forward-chaining reasoner over RDF-style triples. It is deliberately small, dependency-free at runtime, readable as ordinary JavaScript, and usable from the CLI, Node.js, and the browser playground.
11
+ Eyeleng is a compact reasoner over RDF-style triples. It still uses forward chaining for ordinary finite materialization, but the default execution mode now includes conservative auto-hybrid planning: selected function-like predicates can be proved just in time by a tabled backward prover when they are demanded by a query or by another rule body. It is deliberately small, dependency-free at runtime, readable as ordinary JavaScript, and usable from the CLI, Node.js, and the browser playground.
12
12
 
13
13
  Eyeleng implements the rules/reasoning surface. It is **not** a SHACL validation engine and does not emit SHACL validation reports.
14
14
 
@@ -76,6 +76,10 @@ then the body matches with `?parent = :alice` and `?child = :bob`, and the head
76
76
 
77
77
  Negation is handled by stratified evaluation: rules are grouped into dependency layers, and recursion through negation is rejected so the result stays deterministic.
78
78
 
79
+ By default, run mode uses **auto-hybrid reasoning**. Ordinary rules are still materialized forward into the closure. Rules that behave like internal functions can instead be proved backward with tabling when their predicates occur as demanded body goals. For example, a Fibonacci helper predicate can be computed just in time for the public `:fib` results, without printing all intermediate helper triples. Use `--no-hybrid` or `{ hybrid: false }` to force pure forward materialization, and use `--hybrid` or `{ hybrid: true }` to force more aggressive backward orientation.
80
+
81
+ For explicit `--query` / `--query-file` use, the query body is the top-level goal. Each triple pattern in that body is then a sub-goal that may be answered by the backward prover. If pure backward proving is not safe for the demanded predicates, query mode can run a hybrid plan and finally fall back to ordinary forward closure.
82
+
79
83
  Recursive rules that create new terms through `SET`/`BIND` or head blank nodes run in relaxed mode by default. Relaxed mode can derive useful finite closures, but termination is not guaranteed; use `--max-iterations` as a safety valve. `--strict` rejects these recursive term-generating cycles at analysis time. Head blank nodes are deterministically skolemized per rule and solution mapping, so the same firing reuses the same witness node instead of creating a fresh one each pass.
80
84
 
81
85
  ## Language surface
@@ -153,7 +157,6 @@ npm run w3c:rules:earl
153
157
  npm run w3c:rdf
154
158
  npm run w3c:rdf:json
155
159
  npm run w3c:rdf:earl
156
- npm run w3c:all
157
160
  ```
158
161
 
159
162
  `npm test` includes the W3C harnesses. When W3C URLs are reachable, progress is printed test by test. In offline environments, remote W3C checks are reported as unreachable unless `EYELENG_W3C_REQUIRED=1` is set. The `*:earl` scripts also print test progress, but write the EARL Turtle only to `reports/` instead of printing the report to the terminal.
@@ -238,28 +241,30 @@ RULE { ?x a :Mortal } WHERE { ?x a :Man }
238
241
  console.log(formatTriples(result.inferred, result.prefixes));
239
242
  ```
240
243
 
241
- Query mode:
244
+ Hybrid and query mode:
242
245
 
243
246
  ```js
244
- const { runQuery, formatBindings } = require('./src/index.js');
247
+ const { run, runQuery, formatBindings } = require('./src/index.js');
248
+
249
+ // Normal run mode defaults to conservative auto-hybrid reasoning.
250
+ // Ordinary output rules still materialize forward, while selected
251
+ // function-like helper predicates can be proved backward on demand.
252
+ const resultWithAutoHybrid = run(source);
245
253
 
254
+ // Force pure forward closure if you want every derivable helper fact materialized.
255
+ const pureForward = run(source, { hybrid: false });
256
+
257
+ // For explicit queries, the whole raw body is the top-level goal.
258
+ // Each triple pattern in that body is a sub-goal for the planner.
246
259
  const result = runQuery(source, '?x :ancestorOf ?y');
247
260
  console.log(formatBindings(result.query.bindings, result.prefixes));
248
261
 
249
- // Query mode defaults to auto. Supported query/rule shapes are proved
250
- // backward with tabling. If full backward proving is not safe but the
251
- // ruleset contains function-like derived predicates, auto mode uses a
252
- // hybrid plan before falling back to plain forward closure.
253
- const justInTime = runQuery(source, ':alice :computedValue ?value', { queryMode: 'backward' });
254
-
255
- // Run mode uses conservative auto-hybrid planning by default. It keeps
256
- // ordinary rules materialized, but can prove demanded function-like predicates
257
- // backward with tabling. Pass { hybrid: false } to force pure forward closure,
258
- // or { hybrid: true } to force aggressive hybrid orientation.
259
- const resultWithAutoHybrid = run(source);
262
+ // queryMode defaults to auto: try tabled backward proving for demanded
263
+ // predicates, then hybrid execution, then ordinary forward closure.
264
+ const justInTime = runQuery(source, ':alice :computedValue ?value');
260
265
 
261
- // The backward planner is demand-driven: irrelevant unsupported rules do not
262
- // prevent a query from using tabled backward proving.
266
+ // Force the tabled backward prover for supported query shapes.
267
+ const backwardOnly = runQuery(source, ':alice :computedValue ?value', { queryMode: 'backward' });
263
268
  ```
264
269
 
265
270
  Imports:
@@ -326,6 +331,7 @@ Examples live in [examples/](./examples/):
326
331
  - `property-paths.srl` — path matching in bodies
327
332
  - `basic-ruleset.ttl` — RDF Rules syntax
328
333
  - `rdf-messages.srl` / `rdf-messages.trig` — RDF Message Log replay
334
+ - `fibonacci.srl` — fast-doubling Fibonacci using just-in-time backward helper predicates
329
335
  - `deep-taxonomy-*.srl` — generated benchmark programs
330
336
 
331
337
  ## Known boundaries
@@ -441,7 +441,7 @@
441
441
  }
442
442
 
443
443
  parseTripleStatement(options = {}) {
444
- const subjectNode = this.parseGraphNode(options);
444
+ const subjectNode = this.parseGraphNode({ ...options, position: 'subject' });
445
445
  const triples = [...subjectNode.triples];
446
446
  triples.push(...this.parsePropertyListForSubject(subjectNode.term, options));
447
447
  return triples;
@@ -455,7 +455,7 @@
455
455
  if (terminators.some((value) => this.checkValue(value)) || this.checkValue('.')) break;
456
456
  const predicate = options.allowPath ? this.parseVerbPathOrSimple(options) : this.parseVerbTerm(options);
457
457
  do {
458
- const objectNode = this.parseGraphNode(options);
458
+ const objectNode = this.parseGraphNode({ ...options, position: 'object' });
459
459
  triples.push(...objectNode.triples);
460
460
  const baseTriple = { s: subject, p: predicate, o: objectNode.term };
461
461
  triples.push(baseTriple);
@@ -522,6 +522,7 @@
522
522
  currentReifier = this.parseOptionalReifier(options);
523
523
  triples.push({ s: currentReifier, p: iri(RDF_REIFIES), o: reified });
524
524
  } else if (this.matchValue('{|')) {
525
+ if (this.checkValue('|}')) throw this.error('Annotation blocks may not be empty');
525
526
  const annotationSubject = currentReifier || this.freshGraphNode(options);
526
527
  triples.push({ s: annotationSubject, p: iri(RDF_REIFIES), o: reified });
527
528
  triples.push(...this.parsePropertyListForSubject(annotationSubject, options, ['|}']));
@@ -571,7 +572,7 @@
571
572
  }
572
573
 
573
574
  parseVerbTerm(options = {}) {
574
- const term = this.parseTerm(options);
575
+ const term = this.parseTerm({ ...options, position: 'predicate' });
575
576
  if (term.type !== 'iri' && term.type !== 'var') throw this.error('Expected IRI or variable as predicate');
576
577
  return term;
577
578
  }
@@ -694,7 +695,10 @@
694
695
  if (token.value === '<<(') return this.parseTripleTermAfterOpen(options);
695
696
  if (token.value === '<<') throw this.error('Use << s p o >> as a graph node reifier; use <<( s p o )>> for a triple term', token);
696
697
  if (token.type === 'word') {
697
- if (token.value === 'a') return iri(RDF_TYPE);
698
+ if (token.value === 'a') {
699
+ if (options.position !== 'predicate') throw this.error('a is only allowed as a predicate', token);
700
+ return iri(RDF_TYPE);
701
+ }
698
702
  if (token.value === 'true') return literal(true, XSD_BOOLEAN);
699
703
  if (token.value === 'false') return literal(false, XSD_BOOLEAN);
700
704
  if (token.value.startsWith('_:')) return blankNode(token.value.slice(2));
@@ -728,7 +732,13 @@
728
732
  return literal(coerceLexicalLiteral(token.value, datatype), datatype, null);
729
733
  }
730
734
  if (this.checkType('word') && /^@[A-Za-z]+(?:-[A-Za-z0-9]+)*(?:--[A-Za-z]+)?$/.test(this.peek().value)) {
731
- const tag = this.advance().value.slice(1).toLowerCase();
735
+ const tagToken = this.advance();
736
+ const rawTag = tagToken.value.slice(1);
737
+ const direction = rawTag.includes('--') ? rawTag.slice(rawTag.lastIndexOf('--') + 2) : null;
738
+ if (direction && direction !== 'ltr' && direction !== 'rtl') {
739
+ throw this.error(`Invalid base direction --${direction}; expected --ltr or --rtl`, tagToken);
740
+ }
741
+ const tag = rawTag.toLowerCase();
732
742
  const [lang, langDir = null] = tag.split('--');
733
743
  return literal(token.value, null, lang, langDir);
734
744
  }
@@ -2507,6 +2517,7 @@
2507
2517
  const syntaxProfile = String(options.profile || options.profileId || '').toLowerCase();
2508
2518
  const rdf12Surface = syntaxProfile === 'turtle' || syntaxProfile === 'trig';
2509
2519
  const implicitStatementNodes = new Set();
2520
+ function implicitStatementNodeKey(term) { return `${term.kind}:${term.value}`; }
2510
2521
 
2511
2522
  function freshBlank() { bnodeCounter += 1; return blank(`b${bnodeCounter}`); }
2512
2523
  function peek(offset = 0) { return tokens[i + offset]; }
@@ -2657,7 +2668,7 @@
2657
2668
  expect('>>');
2658
2669
  const node = reifier || freshBlank();
2659
2670
  out.push(triple(node, iri(RDF_REIFIES), tripleTerm(s, p, o), graph));
2660
- if (node.kind === 'blank') implicitStatementNodes.add(node.value);
2671
+ implicitStatementNodes.add(implicitStatementNodeKey(node));
2661
2672
  return node;
2662
2673
  }
2663
2674
 
@@ -2686,7 +2697,7 @@
2686
2697
  if (accept(']')) return node;
2687
2698
  parsePredicateObjectList(node, out, graph);
2688
2699
  expect(']');
2689
- if (node.kind === 'blank') implicitStatementNodes.add(node.value);
2700
+ if (node.kind === 'blank') implicitStatementNodes.add(implicitStatementNodeKey(node));
2690
2701
  return node;
2691
2702
  }
2692
2703
 
@@ -2764,9 +2775,8 @@
2764
2775
  if (options3.requireDot) expect('.'); else accept('.');
2765
2776
  return;
2766
2777
  }
2767
- if (peek()?.type === '<<' && peek(1)?.type === '(') throw new Error('Triple term cannot be used as a subject');
2768
- const subject = parseTerm(out, graph, { noLiteral: true, noA: true });
2769
- if ((peek()?.type === '.' || peek()?.type === '}' || peek()?.type === undefined) && subject.kind === 'blank' && implicitStatementNodes.has(subject.value)) {
2778
+ const subject = parseTerm(out, graph, { noLiteral: !rdf12Surface, noA: true });
2779
+ if ((peek()?.type === '.' || peek()?.type === '}' || peek()?.type === undefined) && implicitStatementNodes.has(implicitStatementNodeKey(subject))) {
2770
2780
  if (options3.requireDot) expect('.'); else accept('.');
2771
2781
  return;
2772
2782
  }
package/eyeleng.js CHANGED
@@ -710,7 +710,7 @@
710
710
  }
711
711
 
712
712
  parseTripleStatement(options = {}) {
713
- const subjectNode = this.parseGraphNode(options);
713
+ const subjectNode = this.parseGraphNode({ ...options, position: 'subject' });
714
714
  const triples = [...subjectNode.triples];
715
715
  triples.push(...this.parsePropertyListForSubject(subjectNode.term, options));
716
716
  return triples;
@@ -724,7 +724,7 @@
724
724
  if (terminators.some((value) => this.checkValue(value)) || this.checkValue('.')) break;
725
725
  const predicate = options.allowPath ? this.parseVerbPathOrSimple(options) : this.parseVerbTerm(options);
726
726
  do {
727
- const objectNode = this.parseGraphNode(options);
727
+ const objectNode = this.parseGraphNode({ ...options, position: 'object' });
728
728
  triples.push(...objectNode.triples);
729
729
  const baseTriple = { s: subject, p: predicate, o: objectNode.term };
730
730
  triples.push(baseTriple);
@@ -791,6 +791,7 @@
791
791
  currentReifier = this.parseOptionalReifier(options);
792
792
  triples.push({ s: currentReifier, p: iri(RDF_REIFIES), o: reified });
793
793
  } else if (this.matchValue('{|')) {
794
+ if (this.checkValue('|}')) throw this.error('Annotation blocks may not be empty');
794
795
  const annotationSubject = currentReifier || this.freshGraphNode(options);
795
796
  triples.push({ s: annotationSubject, p: iri(RDF_REIFIES), o: reified });
796
797
  triples.push(...this.parsePropertyListForSubject(annotationSubject, options, ['|}']));
@@ -840,7 +841,7 @@
840
841
  }
841
842
 
842
843
  parseVerbTerm(options = {}) {
843
- const term = this.parseTerm(options);
844
+ const term = this.parseTerm({ ...options, position: 'predicate' });
844
845
  if (term.type !== 'iri' && term.type !== 'var') throw this.error('Expected IRI or variable as predicate');
845
846
  return term;
846
847
  }
@@ -963,7 +964,10 @@
963
964
  if (token.value === '<<(') return this.parseTripleTermAfterOpen(options);
964
965
  if (token.value === '<<') throw this.error('Use << s p o >> as a graph node reifier; use <<( s p o )>> for a triple term', token);
965
966
  if (token.type === 'word') {
966
- if (token.value === 'a') return iri(RDF_TYPE);
967
+ if (token.value === 'a') {
968
+ if (options.position !== 'predicate') throw this.error('a is only allowed as a predicate', token);
969
+ return iri(RDF_TYPE);
970
+ }
967
971
  if (token.value === 'true') return literal(true, XSD_BOOLEAN);
968
972
  if (token.value === 'false') return literal(false, XSD_BOOLEAN);
969
973
  if (token.value.startsWith('_:')) return blankNode(token.value.slice(2));
@@ -997,7 +1001,13 @@
997
1001
  return literal(coerceLexicalLiteral(token.value, datatype), datatype, null);
998
1002
  }
999
1003
  if (this.checkType('word') && /^@[A-Za-z]+(?:-[A-Za-z0-9]+)*(?:--[A-Za-z]+)?$/.test(this.peek().value)) {
1000
- const tag = this.advance().value.slice(1).toLowerCase();
1004
+ const tagToken = this.advance();
1005
+ const rawTag = tagToken.value.slice(1);
1006
+ const direction = rawTag.includes('--') ? rawTag.slice(rawTag.lastIndexOf('--') + 2) : null;
1007
+ if (direction && direction !== 'ltr' && direction !== 'rtl') {
1008
+ throw this.error(`Invalid base direction --${direction}; expected --ltr or --rtl`, tagToken);
1009
+ }
1010
+ const tag = rawTag.toLowerCase();
1001
1011
  const [lang, langDir = null] = tag.split('--');
1002
1012
  return literal(token.value, null, lang, langDir);
1003
1013
  }
@@ -2776,6 +2786,7 @@
2776
2786
  const syntaxProfile = String(options.profile || options.profileId || '').toLowerCase();
2777
2787
  const rdf12Surface = syntaxProfile === 'turtle' || syntaxProfile === 'trig';
2778
2788
  const implicitStatementNodes = new Set();
2789
+ function implicitStatementNodeKey(term) { return `${term.kind}:${term.value}`; }
2779
2790
 
2780
2791
  function freshBlank() { bnodeCounter += 1; return blank(`b${bnodeCounter}`); }
2781
2792
  function peek(offset = 0) { return tokens[i + offset]; }
@@ -2926,7 +2937,7 @@
2926
2937
  expect('>>');
2927
2938
  const node = reifier || freshBlank();
2928
2939
  out.push(triple(node, iri(RDF_REIFIES), tripleTerm(s, p, o), graph));
2929
- if (node.kind === 'blank') implicitStatementNodes.add(node.value);
2940
+ implicitStatementNodes.add(implicitStatementNodeKey(node));
2930
2941
  return node;
2931
2942
  }
2932
2943
 
@@ -2955,7 +2966,7 @@
2955
2966
  if (accept(']')) return node;
2956
2967
  parsePredicateObjectList(node, out, graph);
2957
2968
  expect(']');
2958
- if (node.kind === 'blank') implicitStatementNodes.add(node.value);
2969
+ if (node.kind === 'blank') implicitStatementNodes.add(implicitStatementNodeKey(node));
2959
2970
  return node;
2960
2971
  }
2961
2972
 
@@ -3033,9 +3044,8 @@
3033
3044
  if (options3.requireDot) expect('.'); else accept('.');
3034
3045
  return;
3035
3046
  }
3036
- if (peek()?.type === '<<' && peek(1)?.type === '(') throw new Error('Triple term cannot be used as a subject');
3037
- const subject = parseTerm(out, graph, { noLiteral: true, noA: true });
3038
- if ((peek()?.type === '.' || peek()?.type === '}' || peek()?.type === undefined) && subject.kind === 'blank' && implicitStatementNodes.has(subject.value)) {
3047
+ const subject = parseTerm(out, graph, { noLiteral: !rdf12Surface, noA: true });
3048
+ if ((peek()?.type === '.' || peek()?.type === '}' || peek()?.type === undefined) && implicitStatementNodes.has(implicitStatementNodeKey(subject))) {
3039
3049
  if (options3.requireDot) expect('.'); else accept('.');
3040
3050
  return;
3041
3051
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeleng",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "The EYE Logic Engine: a JavaScript implementation of SHACL Rules, including SRL and RDF Rules syntax front-ends.",
5
5
  "keywords": [
6
6
  "SRL",
package/src/parser.js CHANGED
@@ -296,7 +296,7 @@ class Parser {
296
296
  }
297
297
 
298
298
  parseTripleStatement(options = {}) {
299
- const subjectNode = this.parseGraphNode(options);
299
+ const subjectNode = this.parseGraphNode({ ...options, position: 'subject' });
300
300
  const triples = [...subjectNode.triples];
301
301
  triples.push(...this.parsePropertyListForSubject(subjectNode.term, options));
302
302
  return triples;
@@ -310,7 +310,7 @@ class Parser {
310
310
  if (terminators.some((value) => this.checkValue(value)) || this.checkValue('.')) break;
311
311
  const predicate = options.allowPath ? this.parseVerbPathOrSimple(options) : this.parseVerbTerm(options);
312
312
  do {
313
- const objectNode = this.parseGraphNode(options);
313
+ const objectNode = this.parseGraphNode({ ...options, position: 'object' });
314
314
  triples.push(...objectNode.triples);
315
315
  const baseTriple = { s: subject, p: predicate, o: objectNode.term };
316
316
  triples.push(baseTriple);
@@ -377,6 +377,7 @@ class Parser {
377
377
  currentReifier = this.parseOptionalReifier(options);
378
378
  triples.push({ s: currentReifier, p: iri(RDF_REIFIES), o: reified });
379
379
  } else if (this.matchValue('{|')) {
380
+ if (this.checkValue('|}')) throw this.error('Annotation blocks may not be empty');
380
381
  const annotationSubject = currentReifier || this.freshGraphNode(options);
381
382
  triples.push({ s: annotationSubject, p: iri(RDF_REIFIES), o: reified });
382
383
  triples.push(...this.parsePropertyListForSubject(annotationSubject, options, ['|}']));
@@ -426,7 +427,7 @@ class Parser {
426
427
  }
427
428
 
428
429
  parseVerbTerm(options = {}) {
429
- const term = this.parseTerm(options);
430
+ const term = this.parseTerm({ ...options, position: 'predicate' });
430
431
  if (term.type !== 'iri' && term.type !== 'var') throw this.error('Expected IRI or variable as predicate');
431
432
  return term;
432
433
  }
@@ -549,7 +550,10 @@ class Parser {
549
550
  if (token.value === '<<(') return this.parseTripleTermAfterOpen(options);
550
551
  if (token.value === '<<') throw this.error('Use << s p o >> as a graph node reifier; use <<( s p o )>> for a triple term', token);
551
552
  if (token.type === 'word') {
552
- if (token.value === 'a') return iri(RDF_TYPE);
553
+ if (token.value === 'a') {
554
+ if (options.position !== 'predicate') throw this.error('a is only allowed as a predicate', token);
555
+ return iri(RDF_TYPE);
556
+ }
553
557
  if (token.value === 'true') return literal(true, XSD_BOOLEAN);
554
558
  if (token.value === 'false') return literal(false, XSD_BOOLEAN);
555
559
  if (token.value.startsWith('_:')) return blankNode(token.value.slice(2));
@@ -583,7 +587,13 @@ class Parser {
583
587
  return literal(coerceLexicalLiteral(token.value, datatype), datatype, null);
584
588
  }
585
589
  if (this.checkType('word') && /^@[A-Za-z]+(?:-[A-Za-z0-9]+)*(?:--[A-Za-z]+)?$/.test(this.peek().value)) {
586
- const tag = this.advance().value.slice(1).toLowerCase();
590
+ const tagToken = this.advance();
591
+ const rawTag = tagToken.value.slice(1);
592
+ const direction = rawTag.includes('--') ? rawTag.slice(rawTag.lastIndexOf('--') + 2) : null;
593
+ if (direction && direction !== 'ltr' && direction !== 'rtl') {
594
+ throw this.error(`Invalid base direction --${direction}; expected --ltr or --rtl`, tagToken);
595
+ }
596
+ const tag = rawTag.toLowerCase();
587
597
  const [lang, langDir = null] = tag.split('--');
588
598
  return literal(token.value, null, lang, langDir);
589
599
  }
package/src/rdfSyntax.js CHANGED
@@ -1160,6 +1160,7 @@ function parseN3(source, options = {}) {
1160
1160
  const syntaxProfile = String(options.profile || options.profileId || '').toLowerCase();
1161
1161
  const rdf12Surface = syntaxProfile === 'turtle' || syntaxProfile === 'trig';
1162
1162
  const implicitStatementNodes = new Set();
1163
+ function implicitStatementNodeKey(term) { return `${term.kind}:${term.value}`; }
1163
1164
 
1164
1165
  function freshBlank() { bnodeCounter += 1; return blank(`b${bnodeCounter}`); }
1165
1166
  function peek(offset = 0) { return tokens[i + offset]; }
@@ -1310,7 +1311,7 @@ function parseN3(source, options = {}) {
1310
1311
  expect('>>');
1311
1312
  const node = reifier || freshBlank();
1312
1313
  out.push(triple(node, iri(RDF_REIFIES), tripleTerm(s, p, o), graph));
1313
- if (node.kind === 'blank') implicitStatementNodes.add(node.value);
1314
+ implicitStatementNodes.add(implicitStatementNodeKey(node));
1314
1315
  return node;
1315
1316
  }
1316
1317
 
@@ -1339,7 +1340,7 @@ function parseN3(source, options = {}) {
1339
1340
  if (accept(']')) return node;
1340
1341
  parsePredicateObjectList(node, out, graph);
1341
1342
  expect(']');
1342
- if (node.kind === 'blank') implicitStatementNodes.add(node.value);
1343
+ if (node.kind === 'blank') implicitStatementNodes.add(implicitStatementNodeKey(node));
1343
1344
  return node;
1344
1345
  }
1345
1346
 
@@ -1417,9 +1418,8 @@ function parseN3(source, options = {}) {
1417
1418
  if (options3.requireDot) expect('.'); else accept('.');
1418
1419
  return;
1419
1420
  }
1420
- if (peek()?.type === '<<' && peek(1)?.type === '(') throw new Error('Triple term cannot be used as a subject');
1421
- const subject = parseTerm(out, graph, { noLiteral: true, noA: true });
1422
- if ((peek()?.type === '.' || peek()?.type === '}' || peek()?.type === undefined) && subject.kind === 'blank' && implicitStatementNodes.has(subject.value)) {
1421
+ const subject = parseTerm(out, graph, { noLiteral: !rdf12Surface, noA: true });
1422
+ if ((peek()?.type === '.' || peek()?.type === '}' || peek()?.type === undefined) && implicitStatementNodes.has(implicitStatementNodeKey(subject))) {
1423
1423
  if (options3.requireDot) expect('.'); else accept('.');
1424
1424
  return;
1425
1425
  }
package/test/api.test.js CHANGED
@@ -746,6 +746,41 @@ RULE { :test :annotation ?source } WHERE {
746
746
  assert.match(output, /:test :annotation :witness \./);
747
747
  });
748
748
 
749
+ test('DATA blocks accept RDF 1.2 symmetric and standalone reifier syntax', () => {
750
+ const symmetric = parse(`
751
+ PREFIX : <http://example/>
752
+ DATA {
753
+ 123 :q 456 .
754
+ <<( :s :p :o )>> :q <<( :s1 :p1 :o1 )>> .
755
+ }
756
+ `);
757
+ assert.equal(symmetric.data.length, 2);
758
+ assert.equal(symmetric.data[0].s.type, 'literal');
759
+ assert.equal(symmetric.data[1].s.type, 'triple');
760
+
761
+ const standalone = parse(`
762
+ PREFIX : <http://example/>
763
+ DATA { << :a :b :c ~:r >> . }
764
+ `);
765
+ assert.equal(standalone.data.length, 1);
766
+ assert.equal(standalone.data[0].s.value, 'http://example/r');
767
+ assert.equal(standalone.data[0].p.value, 'http://www.w3.org/1999/02/22-rdf-syntax-ns#reifies');
768
+ });
769
+
770
+ test('SRL rejects invalid base directions, subject a, and empty annotations', () => {
771
+ const invalidPatterns = [
772
+ 'RULE { } WHERE { :s :p "abc"@en--LTR }',
773
+ 'RULE { :s :p "abc"@en--LTR } WHERE { ?a ?b ?c }',
774
+ 'RULE { } WHERE { a :p "abc" }',
775
+ 'RULE { a :p "abc" } WHERE { ?a ?b ?c }',
776
+ 'RULE { } WHERE { :s :p :o ~:r {| |} }',
777
+ 'RULE { :s :p :o ~:r {| |} } WHERE { ?a ?b ?c }',
778
+ ];
779
+ for (const pattern of invalidPatterns) {
780
+ assert.throws(() => parse(`PREFIX : <http://example/>\n${pattern}`), Error, pattern);
781
+ }
782
+ });
783
+
749
784
 
750
785
  test('strict conformance allows recursive constant BIND aliases', () => {
751
786
  assert.doesNotThrow(() => compile(`