eyeleng 1.1.2 → 1.2.1

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/playground.html CHANGED
@@ -414,8 +414,9 @@
414
414
 
415
415
  <div class="option-grid">
416
416
  <div>
417
- <label for="example-select">Examples</label>
417
+ <label for="example-select">Load an example</label>
418
418
  <select id="example-select"></select>
419
+ <span class="hint">Examples are listed from the <a href="https://github.com/eyereasoner/eyeleng/tree/main/examples">GitHub repository</a>.</span>
419
420
  </div>
420
421
  <div>
421
422
  <label for="syntax-select">Syntax</label>
@@ -438,7 +439,7 @@
438
439
  <div class="checks" aria-label="Reasoning options">
439
440
  <label class="toggle"><input id="all-output" type="checkbox" /> Print full closure (--all)</label>
440
441
  <label class="toggle"><input id="json-output" type="checkbox" /> JSON output (--json)</label>
441
- <label class="toggle"><input id="trace-output" type="checkbox" /> Include derivation trace (--trace)</label>
442
+ <label class="toggle"><input id="trace-output" type="checkbox" /> Include proof explanations (--prove)</label>
442
443
  <label class="toggle"><input id="strict-mode" type="checkbox" /> Treat warnings as errors (--strict)</label>
443
444
  <label class="toggle"><input id="check-only" type="checkbox" /> Check only, do not run rules</label>
444
445
  <label class="toggle"><input id="show-analysis" type="checkbox" /> Include analysis in JSON</label>
@@ -485,9 +486,9 @@
485
486
  </main>
486
487
 
487
488
  <script>
488
- window.__EYELENG_MODULES__ = {"src/api.js": "'use strict';\n\nconst { parse, parseQuery } = require('./parser.js');\nconst { parseRdfSyntax, parseRdfDocument, rdfDocumentToProgram, looksLikeRdfRules } = require('./rdfSyntax.js');\nconst { evaluate } = require('./engine.js');\nconst { analyze } = require('./analyze.js');\nconst { formatTriples, sortTriples, toJSON, formatTrace, formatBindings } = require('./format.js');\nconst { runQuery, queryResult } = require('./query.js');\n\nfunction parseInput(source, options = {}) {\n if (typeof source !== 'string') return source;\n return looksLikeRdfRules(source, options) ? parseRdfSyntax(source, options) : parse(source, options);\n}\n\nfunction compile(source, options = {}) {\n const parsed = parseInput(source, options);\n const program = options.resolveImports === false ? parsed : resolveImports(parsed, options);\n const analysis = analyze(program);\n const diagnostics = analysis.diagnostics;\n const fatal = analysis.errors.length > 0 || (options.strict && analysis.warnings.length > 0);\n if (fatal && options.throwOnDiagnostics !== false) {\n const details = diagnostics.map((diagnostic) => diagnostic.message).join('; ');\n throw new Error(`${analysis.errors.length > 0 ? 'Analysis failed' : 'Strict mode failed'}: ${details}`);\n }\n return { program, diagnostics, analysis };\n}\n\nfunction resolveImports(program, options = {}, seen = new Set()) {\n if (!program.imports || program.imports.length === 0) return cloneProgram(program);\n const importResolver = options.importResolver;\n if (!importResolver) return cloneProgram(program);\n\n let merged = emptyProgram(program);\n const localKey = program.baseIRI || options.filename || '<input>';\n if (localKey) seen.add(localKey);\n\n for (const target of program.imports) {\n if (seen.has(target)) continue;\n seen.add(target);\n const resolved = importResolver(target, { from: program.baseIRI || options.filename || null, seen });\n if (!resolved) throw new Error(`IMPORTS resolver returned no source for ${target}`);\n const importSource = typeof resolved === 'string' ? resolved : resolved.source;\n const importOptions = typeof resolved === 'string' ? {} : (resolved.options || {});\n const parsedImport = parseInput(importSource, { ...options, ...importOptions, baseIRI: importOptions.baseIRI || target, filename: importOptions.filename || target });\n const imported = resolveImports(parsedImport, { ...options, ...importOptions, importResolver }, seen);\n merged = mergePrograms(merged, imported);\n }\n\n return mergePrograms(merged, program);\n}\n\nfunction emptyProgram(program = {}) {\n return {\n baseIRI: program.baseIRI || null,\n version: program.version || null,\n imports: [],\n prefixes: { ...(program.prefixes || {}) },\n data: [],\n rules: [],\n };\n}\n\nfunction cloneProgram(program) {\n return {\n baseIRI: program.baseIRI || null,\n version: program.version || null,\n imports: (program.imports || []).slice(),\n prefixes: { ...(program.prefixes || {}) },\n data: (program.data || []).slice(),\n rules: (program.rules || []).slice(),\n };\n}\n\nfunction mergePrograms(left, right) {\n return {\n baseIRI: right.baseIRI || left.baseIRI || null,\n version: right.version || left.version || null,\n imports: Array.from(new Set([...(left.imports || []), ...(right.imports || [])])),\n prefixes: { ...(left.prefixes || {}), ...(right.prefixes || {}) },\n data: [...(left.data || []), ...(right.data || [])],\n rules: [...(left.rules || []), ...(right.rules || [])],\n };\n}\n\nfunction run(source, options = {}) {\n const { program, diagnostics, analysis } = compile(source, options);\n const result = evaluate(program, { ...options, analysis });\n result.diagnostics = diagnostics;\n result.analysis = analysis;\n return result;\n}\n\nfunction runToString(source, options = {}) {\n const result = run(source, options);\n const triples = options.all ? result.closure : result.inferred;\n return formatTriples(triples, result.prefixes);\n}\n\nmodule.exports = {\n parse,\n parseQuery,\n parseInput,\n parseRdfSyntax,\n parseRdfDocument,\n rdfDocumentToProgram,\n compile,\n resolveImports,\n mergePrograms,\n analyze,\n evaluate,\n run,\n runToString,\n runQuery,\n queryResult,\n formatTriples,\n formatBindings,\n sortTriples,\n toJSON,\n formatTrace,\n};\n", "src/parser.js": "'use strict';\n\nconst { tokenize, SyntaxErrorWithLocation } = require('./tokenizer.js');\nconst { isBuiltinName } = require('./builtins.js');\nconst {\n iri,\n variable,\n blankNode,\n literal,\n tripleTerm,\n RDF_TYPE,\n RDF_FIRST,\n RDF_REST,\n RDF_NIL,\n RDF_REIFIES,\n XSD_BOOLEAN,\n XSD_INTEGER,\n XSD_DECIMAL,\n XSD_DOUBLE,\n} = require('./term.js');\n\nclass Parser {\n constructor(source, options = {}) {\n this.tokens = Array.isArray(source) ? source : tokenize(source, options.filename);\n this.pos = 0;\n this.baseIRI = options.baseIRI || null;\n this.version = null;\n this.imports = [];\n this.bnodeCounter = 0;\n this.prefixes = {\n rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',\n sh: 'http://www.w3.org/ns/shacl#',\n srl: 'http://www.w3.org/ns/shacl-rules#',\n xsd: 'http://www.w3.org/2001/XMLSchema#',\n ...options.prefixes,\n };\n }\n\n parseProgram() {\n const data = [];\n const rules = [];\n while (!this.is('eof')) {\n if (this.matchWord('PREFIX')) {\n this.parsePrefix(false);\n } else if (this.matchWord('BASE')) {\n this.parseBase(false);\n } else if (this.matchWord('VERSION')) {\n this.parseVersion();\n } else if (this.matchWord('IMPORTS')) {\n this.parseImports();\n } else if (this.matchWord('DATA')) {\n this.expectValue('{');\n data.push(...this.parseTriplesBlock({ allowPath: false, context: 'data' }));\n } else if (this.matchWord('RULE')) {\n rules.push(this.parseRule());\n } else if (this.matchWord('IF')) {\n rules.push(this.parseIfThenRule());\n } else if (this.checkDeclarationKeyword()) {\n rules.push(...this.parseDeclaration());\n } else {\n throw this.error(`Expected PREFIX, BASE, VERSION, IMPORTS, DATA, RULE, IF, TRANSITIVE, SYMMETRIC, or INVERSE; got ${this.peek().value}`);\n }\n }\n return {\n baseIRI: this.baseIRI,\n version: this.version,\n imports: this.imports.slice(),\n prefixes: { ...this.prefixes },\n data,\n rules,\n };\n }\n\n parseBase(wasAtBase = false) {\n const iriToken = this.expectType('iri');\n this.baseIRI = iriToken.value;\n if (wasAtBase) this.consumeOptionalDot();\n }\n\n parsePrefix(wasAtPrefix = false) {\n const nameToken = this.advance();\n if (nameToken.type !== 'word') throw this.error('Expected prefix name', nameToken);\n let name = nameToken.value;\n if (!name.endsWith(':')) throw this.error('Prefix name must end with :', nameToken);\n name = name.slice(0, -1);\n const iriToken = this.expectType('iri');\n this.prefixes[name] = this.resolveIRI(iriToken.value, iriToken);\n if (wasAtPrefix) this.consumeOptionalDot();\n }\n\n parseVersion() {\n const token = this.expectType('string');\n this.version = token.value;\n }\n\n parseImports() {\n const target = this.parseIRIValue();\n this.imports.push(target.value);\n this.consumeOptionalDot();\n }\n\n parseRule() {\n this.expectValue('{');\n const head = this.parseTriplesBlock({ allowPath: false, context: 'head' });\n this.expectWord('WHERE');\n this.expectValue('{');\n const body = this.parseBodyBlockAlreadyOpen();\n return { name: null, head, body, runOnce: body.some((clause) => clause.type === 'set') };\n }\n\n parseIfThenRule() {\n this.expectValue('{');\n const body = this.parseBodyBlockAlreadyOpen();\n this.expectWord('THEN');\n this.expectValue('{');\n const head = this.parseTriplesBlock({ allowPath: false, context: 'head' });\n return { name: null, head, body, runOnce: body.some((clause) => clause.type === 'set') };\n }\n\n checkDeclarationKeyword() {\n return this.checkType('word') && ['TRANSITIVE', 'SYMMETRIC', 'INVERSE'].includes(this.peek().value.toUpperCase());\n }\n\n parseDeclaration() {\n if (this.matchWord('TRANSITIVE')) {\n this.expectValue('(');\n const pred = this.parseIRIValue();\n this.expectValue(')');\n this.consumeOptionalDot();\n return [{\n name: `TRANSITIVE(${pred.lexical})`,\n head: [{ s: variable('x'), p: iri(pred.value), o: variable('z') }],\n body: [\n { type: 'triple', triple: { s: variable('x'), p: iri(pred.value), o: variable('y') } },\n { type: 'triple', triple: { s: variable('y'), p: iri(pred.value), o: variable('z') } },\n ],\n runOnce: false,\n }];\n }\n if (this.matchWord('SYMMETRIC')) {\n this.expectValue('(');\n const pred = this.parseIRIValue();\n this.expectValue(')');\n this.consumeOptionalDot();\n return [{\n name: `SYMMETRIC(${pred.lexical})`,\n head: [{ s: variable('y'), p: iri(pred.value), o: variable('x') }],\n body: [{ type: 'triple', triple: { s: variable('x'), p: iri(pred.value), o: variable('y') } }],\n runOnce: false,\n }];\n }\n if (this.matchWord('INVERSE')) {\n this.expectValue('(');\n const left = this.parseIRIValue();\n this.expectValue(',');\n const right = this.parseIRIValue();\n this.expectValue(')');\n this.consumeOptionalDot();\n return [\n {\n name: `INVERSE(${left.lexical},${right.lexical})#1`,\n head: [{ s: variable('y'), p: iri(right.value), o: variable('x') }],\n body: [{ type: 'triple', triple: { s: variable('x'), p: iri(left.value), o: variable('y') } }],\n runOnce: false,\n },\n {\n name: `INVERSE(${left.lexical},${right.lexical})#2`,\n head: [{ s: variable('y'), p: iri(left.value), o: variable('x') }],\n body: [{ type: 'triple', triple: { s: variable('x'), p: iri(right.value), o: variable('y') } }],\n runOnce: false,\n },\n ];\n }\n throw this.error(`Expected declaration, got ${this.peek().value}`);\n }\n\n parseIRIValue() {\n const token = this.advance();\n if (token.type === 'iri') return { value: this.resolveIRI(token.value, token), lexical: `<${token.value}>` };\n if (token.type === 'word') {\n if (token.value === 'a') return { value: RDF_TYPE, lexical: 'a' };\n if (!token.value.includes(':')) throw this.error(`Expected IRI or prefixed name, got ${token.value}`, token);\n return { value: this.expandPrefixedName(token.value, token), lexical: token.value };\n }\n throw this.error(`Expected IRI or prefixed name, got ${token.value}`, token);\n }\n\n parseTriplesBlock(options = {}) {\n const triples = [];\n while (!this.matchValue('}')) {\n triples.push(...this.parseTripleStatement(options));\n this.consumeOptionalDot();\n }\n return triples;\n }\n\n parseTripleStatement(options = {}) {\n const subjectNode = this.parseGraphNode(options);\n const triples = [...subjectNode.triples];\n triples.push(...this.parsePropertyListForSubject(subjectNode.term, options));\n return triples;\n }\n\n parsePropertyListForSubject(subject, options = {}, terminators = ['}', '|}', ']']) {\n const triples = [];\n let keepParsingPredicates = true;\n\n while (keepParsingPredicates) {\n if (terminators.some((value) => this.checkValue(value)) || this.checkValue('.')) break;\n const predicate = options.allowPath ? this.parseVerbPathOrSimple() : this.parseVerbTerm();\n do {\n const objectNode = this.parseGraphNode(options);\n triples.push(...objectNode.triples);\n const baseTriple = { s: subject, p: predicate, o: objectNode.term };\n triples.push(baseTriple);\n triples.push(...this.parseAnnotationsForTriple(baseTriple, options));\n } while (this.matchValue(','));\n\n if (this.matchValue(';')) {\n keepParsingPredicates = !(this.checkValue('.') || terminators.some((value) => this.checkValue(value)));\n } else {\n keepParsingPredicates = false;\n }\n }\n\n return triples;\n }\n\n parseGraphNode(options = {}) {\n if (this.checkValue('[')) return this.parseBlankNodePropertyList(options);\n if (this.checkValue('(')) return this.parseCollection(options);\n if (this.checkValue('<<')) return this.parseReifiedTripleNode(options);\n return { term: this.parseTerm(options), triples: [] };\n }\n\n parseBlankNodePropertyList(options = {}) {\n this.expectValue('[');\n const node = this.freshGraphNode(options);\n if (this.matchValue(']')) return { term: node, triples: [] };\n const triples = this.parsePropertyListForSubject(node, options, [']']);\n this.expectValue(']');\n return { term: node, triples };\n }\n\n parseCollection(options = {}) {\n this.expectValue('(');\n if (this.matchValue(')')) return { term: iri(RDF_NIL), triples: [] };\n\n const items = [];\n while (!this.checkValue(')')) items.push(this.parseGraphNode(options));\n this.expectValue(')');\n\n const triples = [];\n for (const item of items) triples.push(...item.triples);\n const cells = items.map(() => this.freshGraphNode(options));\n for (let i = 0; i < items.length; i += 1) {\n triples.push({ s: cells[i], p: iri(RDF_FIRST), o: items[i].term });\n triples.push({ s: cells[i], p: iri(RDF_REST), o: i + 1 < cells.length ? cells[i + 1] : iri(RDF_NIL) });\n }\n return { term: cells[0], triples };\n }\n\n freshGraphNode(options = {}) {\n this.bnodeCounter += 1;\n const id = `b${this.bnodeCounter}`;\n return options.context === 'body' ? variable(`__${id}`) : blankNode(id);\n }\n\n parseAnnotationsForTriple(baseTriple, options = {}) {\n const triples = [];\n const reified = tripleTerm(baseTriple.s, baseTriple.p, baseTriple.o);\n let currentReifier = null;\n\n while (this.checkValue('~') || this.checkValue('{|')) {\n if (this.matchValue('~')) {\n currentReifier = this.parseOptionalReifier(options);\n triples.push({ s: currentReifier, p: iri(RDF_REIFIES), o: reified });\n } else if (this.matchValue('{|')) {\n const annotationSubject = currentReifier || this.freshGraphNode(options);\n triples.push({ s: annotationSubject, p: iri(RDF_REIFIES), o: reified });\n triples.push(...this.parsePropertyListForSubject(annotationSubject, options, ['|}']));\n this.expectValue('|}');\n }\n }\n return triples;\n }\n\n parseOptionalReifier(options = {}) {\n if (this.checkValue('{|') || this.checkValue('.') || this.checkValue(';') || this.checkValue(',') || this.checkValue('}') || this.checkValue('|}') || this.checkValue('>>')) {\n return this.freshGraphNode(options);\n }\n return this.parseVarOrReifierId();\n }\n\n parseVarOrReifierId() {\n const token = this.peek();\n if (token.type === 'variable') return this.parseTerm();\n if (token.type === 'iri') return this.parseTerm();\n if (token.type === 'word' && (token.value.startsWith('_:') || token.value.includes(':'))) return this.parseTerm();\n throw this.error(`Expected variable, IRI, or blank node after ~; got ${token.value}`, token);\n }\n\n parseReifiedTripleNode(options = {}) {\n this.expectValue('<<');\n const subjectNode = this.parseReifiedTripleComponent(options);\n const p = this.parseVerbTerm();\n const objectNode = this.parseReifiedTripleComponent(options);\n let reifier = null;\n if (this.matchValue('~')) reifier = this.parseOptionalReifier(options);\n this.expectValue('>>');\n reifier = reifier || this.freshGraphNode(options);\n return {\n term: reifier,\n triples: [\n ...subjectNode.triples,\n ...objectNode.triples,\n { s: reifier, p: iri(RDF_REIFIES), o: tripleTerm(subjectNode.term, p, objectNode.term) },\n ],\n };\n }\n\n parseReifiedTripleComponent(options = {}) {\n if (this.checkValue('<<')) return this.parseReifiedTripleNode(options);\n return { term: this.parseTerm(options), triples: [] };\n }\n\n parseVerbTerm() {\n const term = this.parseTerm();\n if (term.type !== 'iri' && term.type !== 'var') throw this.error('Expected IRI or variable as predicate');\n return term;\n }\n\n parseVerbPathOrSimple() {\n if (this.checkType('variable')) return this.parseTerm();\n return this.parsePathSequence();\n }\n\n parsePathSequence() {\n const parts = [this.parsePathEltOrInverse()];\n while (this.matchValue('/')) parts.push(this.parsePathEltOrInverse());\n return parts.length === 1 ? parts[0] : { type: 'path', kind: 'sequence', parts };\n }\n\n parsePathEltOrInverse() {\n if (this.matchValue('^')) return { type: 'path', kind: 'inverse', path: this.parsePathPrimary() };\n return this.parsePathPrimary();\n }\n\n parsePathPrimary() {\n if (this.matchValue('(')) {\n const path = this.parsePathSequence();\n this.expectValue(')');\n return path;\n }\n const token = this.peek();\n if (token.type === 'iri' || token.type === 'word') {\n const value = this.parseIRIValue();\n return iri(value.value);\n }\n throw this.error(`Expected path IRI, a, ^, or (, got ${token.value}`, token);\n }\n\n parseFilterClause() {\n // SRL FILTER accepts a bracketted expression, a built-in call, or an IRI-named function call.\n // The bracketted-expression form is the familiar FILTER(?x > 10).\n const expr = this.parseExpression();\n return { type: 'filter', expr };\n }\n\n parseBodyBlockAlreadyOpen() {\n const clauses = [];\n while (!this.matchValue('}')) {\n if (this.matchWord('FILTER')) {\n clauses.push(this.parseFilterClause());\n } else if (this.matchWord('SET')) {\n this.expectValue('(');\n const variableToken = this.expectType('variable');\n this.expectValue(':=');\n const expr = this.parseExpression();\n this.expectValue(')');\n clauses.push({ type: 'set', variable: variableToken.value, expr });\n } else if (this.matchWord('NOT')) {\n this.expectValue('{');\n const body = this.parseBodyBasicAlreadyOpen();\n clauses.push({ type: 'not', body });\n } else {\n for (const triple of this.parseTripleStatement({ allowPath: true, context: 'body' })) {\n if (triple.p && triple.p.type === 'path') clauses.push({ type: 'path', triple });\n else clauses.push({ type: 'triple', triple });\n }\n }\n this.consumeOptionalDot();\n }\n return clauses;\n }\n\n parseBodyBasicAlreadyOpen() {\n const clauses = [];\n while (!this.matchValue('}')) {\n if (this.matchWord('FILTER')) {\n clauses.push(this.parseFilterClause());\n } else if (this.matchWord('SET')) {\n throw this.error('SET is not allowed inside NOT blocks by the SRL grammar');\n } else if (this.matchWord('NOT')) {\n throw this.error('Nested NOT is not allowed inside NOT blocks by the SRL grammar');\n } else {\n for (const triple of this.parseTripleStatement({ allowPath: true, context: 'body' })) {\n if (triple.p && triple.p.type === 'path') clauses.push({ type: 'path', triple });\n else clauses.push({ type: 'triple', triple });\n }\n }\n this.consumeOptionalDot();\n }\n return clauses;\n }\n\n parseTerm() {\n const token = this.advance();\n if (token.type === 'operator' && (token.value === '+' || token.value === '-') && this.peek().type === 'number') {\n const numberToken = this.advance();\n return numericLiteral(token.value === '-' ? -numberToken.value : numberToken.value);\n }\n if (token.type === 'variable') return variable(token.value);\n if (token.type === 'iri') return iri(this.resolveIRI(token.value, token));\n if (token.type === 'string') return this.parseLiteralAfterToken(token);\n if (token.type === 'number') return numericLiteral(token.value);\n if (token.value === '<<(') return this.parseTripleTermAfterOpen();\n if (token.value === '<<') throw this.error('Use << s p o >> as a graph node reifier; use <<( s p o )>> for a triple term', token);\n if (token.type === 'word') {\n if (token.value === 'a') return iri(RDF_TYPE);\n if (token.value === 'true') return literal(true, XSD_BOOLEAN);\n if (token.value === 'false') return literal(false, XSD_BOOLEAN);\n if (token.value.startsWith('_:')) return blankNode(token.value.slice(2));\n return iri(this.expandPrefixedName(token.value, token));\n }\n throw this.error(`Expected term, got ${token.value}`, token);\n }\n\n parseTripleTermAfterOpen() {\n const s = this.parseTerm();\n const p = this.parseVerbTerm();\n const o = this.parseTerm();\n this.expectValue(')>>');\n return tripleTerm(s, p, o);\n }\n\n parseReifiedTripleAfterOpen() {\n const s = this.parseTerm();\n const p = this.parseVerbTerm();\n const o = this.parseTerm();\n if (this.matchValue('~')) {\n if (!this.checkValue('>>')) this.parseVarOrReifierId();\n }\n this.expectValue('>>');\n return tripleTerm(s, p, o);\n }\n\n parseLiteralAfterToken(token) {\n if (this.matchValue('^^')) {\n const datatype = this.parseDatatypeIRI();\n return literal(coerceLexicalLiteral(token.value, datatype), datatype, null);\n }\n if (this.checkType('word') && /^@[A-Za-z]+(?:-[A-Za-z0-9]+)*(?:--[A-Za-z]+)?$/.test(this.peek().value)) {\n const tag = this.advance().value.slice(1).toLowerCase();\n const [lang, langDir = null] = tag.split('--');\n return literal(token.value, null, lang, langDir);\n }\n return literal(token.value);\n }\n\n parseDatatypeIRI() {\n const token = this.advance();\n if (token.type === 'iri') return this.resolveIRI(token.value, token);\n if (token.type === 'word') return this.expandPrefixedName(token.value, token);\n throw this.error(`Expected datatype IRI, got ${token.value}`, token);\n }\n\n expandPrefixedName(value, token) {\n const colon = value.indexOf(':');\n if (colon < 0) throw this.error(`Expected IRI, prefixed name, literal, blank node, or variable; got ${value}`, token);\n const prefix = value.slice(0, colon);\n const local = value.slice(colon + 1);\n if (!(prefix in this.prefixes)) throw this.error(`Unknown prefix ${prefix}:`, token);\n return this.prefixes[prefix] + local;\n }\n\n resolveIRI(value, token = null) {\n if (!this.baseIRI || /^[A-Za-z][A-Za-z0-9+.-]*:/.test(value)) return value;\n try {\n return new URL(value, this.baseIRI).href;\n } catch (_) {\n if (token) throw this.error(`Could not resolve IRI ${value} against BASE ${this.baseIRI}`, token);\n return value;\n }\n }\n\n parseExpression(minPrec = 0) {\n let left = this.parseUnaryExpression();\n while (true) {\n const info = this.peekBinaryOperator();\n if (!info || info.prec < minPrec) break;\n this.consumeBinaryOperator(info.op);\n if (info.op === 'IN' || info.op === 'NOT IN') {\n const items = this.parseExpressionListItems();\n left = { type: 'binary', op: info.op, left, right: { type: 'list', items } };\n } else {\n const right = this.parseExpression(info.prec + 1);\n left = { type: 'binary', op: info.op, left, right };\n }\n }\n return left;\n }\n\n parseExpressionListItems() {\n this.expectValue('(');\n const items = [];\n if (!this.checkValue(')')) {\n do { items.push(this.parseExpression()); }\n while (this.matchValue(','));\n }\n this.expectValue(')');\n return items;\n }\n\n peekBinaryOperator() {\n const token = this.peek();\n if (token.type === 'operator') {\n const prec = binaryPrecedence(token.value);\n return prec >= 0 ? { op: token.value, prec } : null;\n }\n if (token.type === 'word' && token.value.toUpperCase() === 'IN') return { op: 'IN', prec: 3 };\n if (token.type === 'word' && token.value.toUpperCase() === 'NOT' && this.peekN(1).type === 'word' && this.peekN(1).value.toUpperCase() === 'IN') return { op: 'NOT IN', prec: 3 };\n return null;\n }\n\n consumeBinaryOperator(op) {\n if (op === 'NOT IN') { this.expectWord('NOT'); this.expectWord('IN'); return; }\n if (op === 'IN') { this.expectWord('IN'); return; }\n this.expectValue(op);\n }\n\n parseUnaryExpression() {\n if (this.peek().type === 'operator' && (this.peek().value === '!' || this.peek().value === '-' || this.peek().value === '+')) {\n const op = this.advance().value;\n return { type: 'unary', op, expr: this.parseUnaryExpression() };\n }\n return this.parsePrimaryExpression();\n }\n\n parsePrimaryExpression() {\n const token = this.advance();\n if (token.type === 'variable') return { type: 'var', name: token.value };\n if (token.type === 'string') return this.parseLiteralExpressionAfterToken(token);\n if (token.type === 'number') return { type: 'literal', value: token.value };\n if (token.type === 'iri') {\n const name = this.resolveIRI(token.value, token);\n if (this.checkValue('(')) return this.parseFunctionCallAfterName(name);\n return { type: 'term', value: iri(name) };\n }\n if (token.value === '<<(') return { type: 'term', value: this.parseTripleTermAfterOpen() };\n if (token.value === '<<') throw this.error('Use <<( s p o )>> for triple terms inside expressions', token);\n if (token.type === 'word') {\n if (token.value === 'true') return { type: 'literal', value: true };\n if (token.value === 'false') return { type: 'literal', value: false };\n if (token.value.startsWith('_:')) return { type: 'term', value: blankNode(token.value.slice(2)) };\n if (this.checkValue('(')) {\n if (token.value.includes(':') && token.value !== 'a') {\n const name = this.expandPrefixedName(token.value, token);\n return this.parseFunctionCallAfterName(name);\n }\n if (isBuiltinName(token.value)) return this.parseFunctionCallAfterName(token.value);\n throw this.error(`Unknown built-in or unprefixed function call ${token.value}; use an IRI such as :${token.value} for custom functions`, token);\n }\n if (token.value.includes(':') || token.value === 'a') {\n const value = token.value === 'a' ? RDF_TYPE : this.expandPrefixedName(token.value, token);\n return { type: 'term', value: iri(value) };\n }\n }\n if (token.value === '(') {\n const expr = this.parseExpression();\n this.expectValue(')');\n return expr;\n }\n throw this.error(`Expected expression, got ${token.value}`, token);\n }\n\n parseFunctionCallAfterName(name) {\n this.expectValue('(');\n const args = [];\n if (!this.checkValue(')')) {\n do { args.push(this.parseExpression()); }\n while (this.matchValue(','));\n }\n this.expectValue(')');\n return { type: 'call', name, args };\n }\n\n parseLiteralExpressionAfterToken(token) {\n const term = this.parseLiteralAfterToken(token);\n if (term.datatype || term.lang) return { type: 'term', value: term };\n return { type: 'literal', value: term.value };\n }\n\n consumeOptionalDot() { this.matchValue('.'); }\n\n matchWord(value) {\n if (this.checkType('word') && this.peek().value.toUpperCase() === value.toUpperCase()) {\n this.advance();\n return true;\n }\n return false;\n }\n\n expectWord(value) {\n if (this.matchWord(value)) return this.previous();\n throw this.error(`Expected ${value}, got ${this.peek().value}`);\n }\n\n matchValue(value) {\n const token = this.peek();\n if ((token.type === 'punct' || token.type === 'operator') && token.value === value) { this.advance(); return true; }\n return false;\n }\n\n expectValue(value) {\n if (this.matchValue(value)) return this.previous();\n throw this.error(`Expected ${value}, got ${this.peek().value}`);\n }\n\n checkValue(value) { const token = this.peek(); return (token.type === 'punct' || token.type === 'operator') && token.value === value; }\n checkType(type) { return this.peek().type === type; }\n is(type) { return this.peek().type === type; }\n\n expectType(type) {\n if (this.peek().type === type) return this.advance();\n throw this.error(`Expected ${type}, got ${this.peek().value}`);\n }\n\n advance() { if (!this.is('eof')) this.pos += 1; return this.previous(); }\n peek() { return this.tokens[this.pos]; }\n peekN(n) { return this.tokens[this.pos + n] || this.tokens[this.tokens.length - 1]; }\n previous() { return this.tokens[this.pos - 1]; }\n error(message, token = this.peek()) { return new SyntaxErrorWithLocation(message, token); }\n}\n\nfunction numericLiteral(value) {\n if (Number.isInteger(value)) return literal(value, XSD_INTEGER);\n return literal(value, XSD_DECIMAL);\n}\n\nfunction coerceLexicalLiteral(value, datatype) {\n if (datatype === XSD_INTEGER) return Number.parseInt(value, 10);\n if (datatype === XSD_DECIMAL || datatype === XSD_DOUBLE) return Number.parseFloat(value);\n if (datatype === XSD_BOOLEAN) return value === 'true' || value === '1';\n return value;\n}\n\nfunction binaryPrecedence(op) {\n return {\n '||': 1,\n '&&': 2,\n '=': 3,\n '!=': 3,\n 'IN': 3,\n 'NOT IN': 3,\n '<': 4,\n '<=': 4,\n '>': 4,\n '>=': 4,\n '+': 5,\n '-': 5,\n '*': 6,\n '/': 6,\n }[op] ?? -1;\n}\n\nfunction parse(source, options = {}) {\n return new Parser(source, options).parseProgram();\n}\n\nfunction parseQuery(source, options = {}) {\n if (/^\\s*(QUERY|SELECT)\\b/i.test(source)) {\n throw new Error('QUERY/SELECT concrete syntax is not part of the SHACL Rules SRL grammar; pass a raw body pattern instead');\n }\n const trimmed = String(source).trim();\n const text = trimmed.startsWith('{') ? `RULE { } WHERE ${trimmed}` : `RULE { } WHERE { ${source} }`;\n const program = new Parser(text, options).parseProgram();\n if (program.rules.length !== 1 || program.data.length !== 0) {\n throw new Error('Expected exactly one raw body pattern');\n }\n return { select: null, body: program.rules[0].body, prefixes: program.prefixes, baseIRI: program.baseIRI };\n}\n\nmodule.exports = { Parser, parse, parseQuery };\n", "src/tokenizer.js": "'use strict';\n\nclass SyntaxErrorWithLocation extends Error {\n constructor(message, token) {\n const suffix = token && token.line ? ` at ${token.filename || '<input>'}:${token.line}:${token.column}` : '';\n super(`${message}${suffix}`);\n this.name = 'SyntaxError';\n this.token = token;\n }\n}\n\nfunction tokenize(source, filename = '<input>') {\n const tokens = [];\n let i = 0;\n let line = 1;\n let column = 1;\n\n function current() { return source[i]; }\n function peek(n = 1) { return source[i + n]; }\n function startsWith(text) { return source.slice(i, i + text.length) === text; }\n function advance() {\n const ch = source[i++];\n if (ch === '\\n') { line += 1; column = 1; }\n else column += 1;\n return ch;\n }\n function token(type, value, startLine, startColumn) {\n tokens.push({ type, value, line: startLine, column: startColumn, filename });\n }\n function syntax(message, startLine, startColumn) {\n throw new SyntaxErrorWithLocation(message, { line: startLine, column: startColumn, filename });\n }\n\n function readNumericLiteral() {\n let value = '';\n while (i < source.length && /[0-9]/.test(current())) value += advance();\n if (current() === '.' && /[0-9]/.test(peek())) {\n value += advance();\n while (i < source.length && /[0-9]/.test(current())) value += advance();\n }\n if (current() === 'e' || current() === 'E') {\n const saveI = i;\n const saveLine = line;\n const saveColumn = column;\n let exponent = advance();\n if (current() === '+' || current() === '-') exponent += advance();\n if (/[0-9]/.test(current())) {\n while (i < source.length && /[0-9]/.test(current())) exponent += advance();\n value += exponent;\n } else {\n i = saveI;\n line = saveLine;\n column = saveColumn;\n }\n }\n return value;\n }\n\n function readEscape(startLine, startColumn) {\n advance(); // consume backslash\n const esc = advance();\n if (esc === 'u' || esc === 'U') {\n const length = esc === 'u' ? 4 : 8;\n let hex = '';\n for (let j = 0; j < length; j += 1) {\n if (!/[0-9A-Fa-f]/.test(current() || '')) syntax(`Invalid \\${esc} escape`, startLine, startColumn);\n hex += advance();\n }\n return String.fromCodePoint(Number.parseInt(hex, 16));\n }\n return escapeValue(esc);\n }\n\n while (i < source.length) {\n const ch = current();\n if (/\\s/.test(ch)) { advance(); continue; }\n if (ch === '#') {\n while (i < source.length && current() !== '\\n') advance();\n continue;\n }\n\n const startLine = line;\n const startColumn = column;\n\n if (startsWith('<<(')) {\n advance(); advance(); advance();\n token('punct', '<<(', startLine, startColumn);\n continue;\n }\n if (startsWith(')>>')) {\n advance(); advance(); advance();\n token('punct', ')>>', startLine, startColumn);\n continue;\n }\n if (startsWith('<<')) {\n advance(); advance();\n token('punct', '<<', startLine, startColumn);\n continue;\n }\n if (startsWith('>>')) {\n advance(); advance();\n token('punct', '>>', startLine, startColumn);\n continue;\n }\n if (startsWith('{|')) {\n advance(); advance();\n token('punct', '{|', startLine, startColumn);\n continue;\n }\n if (startsWith('|}')) {\n advance(); advance();\n token('punct', '|}', startLine, startColumn);\n continue;\n }\n\n if (ch === '<' && looksLikeIRI(source, i)) {\n let value = '';\n advance();\n while (i < source.length && current() !== '>') value += advance();\n if (current() !== '>') syntax('Unterminated IRI', startLine, startColumn);\n advance();\n token('iri', value, startLine, startColumn);\n continue;\n }\n\n if ((ch === '\"' && startsWith('\"\"\"')) || (ch === \"'\" && startsWith(\"'''\"))) {\n const quote = ch;\n advance(); advance(); advance();\n let value = '';\n while (i < source.length && !startsWith(quote.repeat(3))) {\n if (current() === '\\\\') {\n value += readEscape(startLine, startColumn);\n } else {\n value += advance();\n }\n }\n if (!startsWith(quote.repeat(3))) syntax('Unterminated long string literal', startLine, startColumn);\n advance(); advance(); advance();\n token('string', value, startLine, startColumn);\n continue;\n }\n\n if (ch === '\"' || ch === \"'\") {\n const quote = ch;\n let value = '';\n advance();\n while (i < source.length && current() !== quote) {\n if (current() === '\\n' || current() === '\\r') syntax('Unterminated string literal', startLine, startColumn);\n if (current() === '\\\\') {\n value += readEscape(startLine, startColumn);\n } else {\n value += advance();\n }\n }\n if (current() !== quote) syntax('Unterminated string literal', startLine, startColumn);\n advance();\n token('string', value, startLine, startColumn);\n continue;\n }\n\n if (ch === '@') {\n let value = advance();\n while (i < source.length && /[A-Za-z0-9-]/.test(current())) value += advance();\n if (!/^@[A-Za-z]+(?:-[A-Za-z0-9]+)*(?:--[A-Za-z]+)?$/.test(value)) syntax(`Invalid language tag ${value}`, startLine, startColumn);\n token('word', value, startLine, startColumn);\n continue;\n }\n\n if (ch === '?' || ch === '$') {\n let value = advance();\n while (i < source.length && /[A-Za-z0-9_\\-]/.test(current())) value += advance();\n if (value.length === 1) syntax('Expected variable name', startLine, startColumn);\n token('variable', value.slice(1), startLine, startColumn);\n continue;\n }\n\n if (startsNumericLiteral(source, i)) {\n const value = readNumericLiteral();\n token('number', Number(value), startLine, startColumn);\n continue;\n }\n\n const two = ch + peek();\n if ([':=', '!=', '<=', '>=', '&&', '||', '=>', '^^'].includes(two)) {\n advance(); advance();\n token('operator', two, startLine, startColumn);\n continue;\n }\n\n if ('{}()[].,;|'.includes(ch)) {\n token('punct', advance(), startLine, startColumn);\n continue;\n }\n\n if ('=<>+-*/!^~'.includes(ch)) {\n token('operator', advance(), startLine, startColumn);\n continue;\n }\n\n let value = '';\n while (i < source.length) {\n const c = current();\n if (/\\s/.test(c) || '{}()[].,;|'.includes(c) || '=<>+-*/!^~'.includes(c)) break;\n if (c === '#') break;\n value += advance();\n }\n if (value.length === 0) syntax(`Unexpected character ${JSON.stringify(ch)}`, startLine, startColumn);\n\n if (/^[+-]?(?:(?:\\d+\\.\\d*|\\.\\d+)(?:[eE][+-]?\\d+)?|\\d+[eE][+-]?\\d+|\\d+)$/.test(value)) token('number', Number(value), startLine, startColumn);\n else token('word', value, startLine, startColumn);\n }\n\n tokens.push({ type: 'eof', value: '<eof>', line, column, filename });\n return tokens;\n}\n\nfunction startsNumericLiteral(source, i) {\n const ch = source[i];\n const next = source[i + 1];\n if (/[0-9]/.test(ch)) return true;\n if (ch === '.' && /[0-9]/.test(next)) return true;\n return false;\n}\n\nfunction looksLikeIRI(source, i) {\n const next = source[i + 1];\n if (next === undefined || /[\\s=]/.test(next)) return false;\n for (let j = i + 1; j < source.length; j += 1) {\n const c = source[j];\n if (c === '>') return true;\n if (/\\s/.test(c)) return false;\n }\n return false;\n}\n\nfunction escapeValue(esc) {\n const map = { n: '\\n', r: '\\r', t: '\\t', b: '\\b', f: '\\f', '\"': '\"', \"'\": \"'\", '\\\\': '\\\\' };\n return map[esc] ?? esc;\n}\n\nmodule.exports = { tokenize, SyntaxErrorWithLocation };\n", "src/builtins.js": "'use strict';\n\nconst {\n iri,\n blankNode,\n literal,\n tripleTerm,\n termEquals,\n termToPrimitive,\n termToString,\n booleanValue,\n comparePrimitives,\n isIRI,\n isBlank,\n isLiteral,\n isTripleTerm,\n valueToTerm,\n inferDatatype,\n XSD_STRING,\n RDF_NS,\n XSD_INTEGER,\n XSD_DECIMAL,\n XSD_DOUBLE,\n} = require('./term.js');\n\nconst XSD_DATETIME = 'http://www.w3.org/2001/XMLSchema#dateTime';\nconst XSD_DAYTIME_DURATION = 'http://www.w3.org/2001/XMLSchema#dayTimeDuration';\nconst RDF_LANGSTRING = `${RDF_NS}langString`;\nconst RDF_DIRLANGSTRING = `${RDF_NS}dirLangString`;\nconst NUMERIC_DATATYPES = new Set([XSD_INTEGER, XSD_DECIMAL, XSD_DOUBLE]);\n\n// This table is intentionally shaped by the SHACL 1.2 Rules grammar production BuiltInCall.\n// Keys are the canonical spellings used by the draft; lookup is case-insensitive so examples\n// may use SPARQL-style uppercase or lowercase spellings while still being checked against the\n// grammar's finite set of built-ins.\nconst BUILTIN_SIGNATURES = Object.freeze({\n STR: { min: 1, max: 1 },\n LANG: { min: 1, max: 1 },\n LANGMATCHES: { min: 2, max: 2 },\n LANGDIR: { min: 1, max: 1 },\n DATATYPE: { min: 1, max: 1 },\n IRI: { min: 1, max: 1 },\n URI: { min: 1, max: 1 },\n BNODE: { min: 0, max: 1 },\n ABS: { min: 1, max: 1 },\n CEIL: { min: 1, max: 1 },\n FLOOR: { min: 1, max: 1 },\n ROUND: { min: 1, max: 1 },\n CONCAT: { min: 0, max: Infinity },\n SUBSTR: { min: 2, max: 3 },\n STRLEN: { min: 1, max: 1 },\n REPLACE: { min: 3, max: 4 },\n UCASE: { min: 1, max: 1 },\n LCASE: { min: 1, max: 1 },\n ENCODE_FOR_URI: { min: 1, max: 1 },\n CONTAINS: { min: 2, max: 2 },\n STRSTARTS: { min: 2, max: 2 },\n STRENDS: { min: 2, max: 2 },\n STRBEFORE: { min: 2, max: 2 },\n STRAFTER: { min: 2, max: 2 },\n YEAR: { min: 1, max: 1 },\n MONTH: { min: 1, max: 1 },\n DAY: { min: 1, max: 1 },\n HOURS: { min: 1, max: 1 },\n MINUTES: { min: 1, max: 1 },\n SECONDS: { min: 1, max: 1 },\n TIMEZONE: { min: 1, max: 1 },\n TZ: { min: 1, max: 1 },\n NOW: { min: 0, max: 0 },\n UUID: { min: 0, max: 0 },\n STRUUID: { min: 0, max: 0 },\n IF: { min: 3, max: 3, lazy: true },\n STRLANG: { min: 2, max: 2 },\n STRLANGDIR: { min: 3, max: 3 },\n STRDT: { min: 2, max: 2 },\n sameTerm: { min: 2, max: 2 },\n isIRI: { min: 1, max: 1 },\n isURI: { min: 1, max: 1 },\n isBLANK: { min: 1, max: 1 },\n isLITERAL: { min: 1, max: 1 },\n isNUMERIC: { min: 1, max: 1 },\n hasLANG: { min: 1, max: 1 },\n hasLANGDIR: { min: 1, max: 1 },\n REGEX: { min: 2, max: 3 },\n isTRIPLE: { min: 1, max: 1 },\n TRIPLE: { min: 3, max: 3 },\n SUBJECT: { min: 1, max: 1 },\n PREDICATE: { min: 1, max: 1 },\n OBJECT: { min: 1, max: 1 },\n});\n\nconst BUILTIN_BY_LOWER = new Map(Object.keys(BUILTIN_SIGNATURES).map((name) => [name.toLowerCase(), name]));\n\nfunction canonicalBuiltinName(name) {\n return BUILTIN_BY_LOWER.get(String(name).toLowerCase()) || null;\n}\n\nfunction isBuiltinName(name) {\n return canonicalBuiltinName(name) !== null;\n}\n\nfunction builtinNames() {\n return Object.keys(BUILTIN_SIGNATURES);\n}\n\nfunction evalExpression(expr, binding, options = {}) {\n switch (expr.type) {\n case 'literal':\n return expr.value;\n case 'term':\n return expr.value;\n case 'var':\n return binding[expr.name];\n case 'list':\n return expr.items.map((item) => evalExpression(item, binding, options));\n case 'unary': {\n const value = evalExpression(expr.expr, binding, options);\n if (expr.op === '!') return !booleanValue(value);\n if (expr.op === '-') return -Number(termToPrimitive(valueToTermIfNeeded(value)));\n if (expr.op === '+') return Number(termToPrimitive(valueToTermIfNeeded(value)));\n throw new Error(`Unsupported unary operator ${expr.op}`);\n }\n case 'binary': {\n const left = evalExpression(expr.left, binding, options);\n if (expr.op === '&&') return booleanValue(left) && booleanValue(evalExpression(expr.right, binding, options));\n if (expr.op === '||') return booleanValue(left) || booleanValue(evalExpression(expr.right, binding, options));\n const right = evalExpression(expr.right, binding, options);\n return evalBinary(expr.op, left, right);\n }\n case 'call':\n return evalCallExpression(expr, binding, options);\n default:\n throw new Error(`Unsupported expression type ${expr.type}`);\n }\n}\n\nfunction evalCallExpression(expr, binding, options) {\n const canonical = canonicalBuiltinName(expr.name);\n if (canonical === 'IF') {\n validateArity(canonical, expr.args.length);\n const condition = evalExpression(expr.args[0], binding, options);\n return evalExpression(booleanValue(condition) ? expr.args[1] : expr.args[2], binding, options);\n }\n return callBuiltin(expr.name, expr.args.map((arg) => evalExpression(arg, binding, options)), binding, options);\n}\n\nfunction evalBinary(op, left, right) {\n if (op === '=') return termishEquals(left, right);\n if (op === '!=') return !termishEquals(left, right);\n if (op === 'IN' || op === 'NOT IN') {\n const list = Array.isArray(right) ? right : [];\n const found = list.some((item) => termishEquals(left, item));\n return op === 'IN' ? found : !found;\n }\n if (['<', '<=', '>', '>='].includes(op)) {\n const cmp = comparePrimitives(left, right);\n if (op === '<') return cmp < 0;\n if (op === '<=') return cmp <= 0;\n if (op === '>') return cmp > 0;\n if (op === '>=') return cmp >= 0;\n }\n const lp = termToPrimitive(valueToTermIfNeeded(left));\n const rp = termToPrimitive(valueToTermIfNeeded(right));\n if (op === '+') {\n if (typeof lp === 'number' && typeof rp === 'number') return lp + rp;\n return String(lp) + String(rp);\n }\n if (op === '-') return Number(lp) - Number(rp);\n if (op === '*') return Number(lp) * Number(rp);\n if (op === '/') return Number(lp) / Number(rp);\n throw new Error(`Unsupported binary operator ${op}`);\n}\n\nfunction valueToTermIfNeeded(value) {\n return value && value.type ? value : literal(value, inferDatatype(value));\n}\n\nfunction termishEquals(left, right) {\n if (left && left.type && right && right.type) return termEquals(left, right);\n const lp = left && left.type ? termToPrimitive(left) : left;\n const rp = right && right.type ? termToPrimitive(right) : right;\n return lp === rp;\n}\n\nfunction callBuiltin(name, args, binding = {}, options = {}) {\n const injected = options.builtins && (options.builtins[name] || options.builtins[String(name).toLowerCase()]);\n if (injected) return injected(args, { binding, iri, blankNode, literal, tripleTerm, termToString, booleanValue, termToPrimitive });\n\n const canonical = canonicalBuiltinName(name);\n if (!canonical) throw new Error(`Unknown builtin ${name}`);\n validateArity(canonical, args.length);\n const key = canonical.toLowerCase();\n\n if (key === 'str') return termToString(args[0]);\n if (key === 'iri' || key === 'uri') return makeIRI(termToString(args[0]), options);\n if (key === 'bnode') return makeBlankNode(args, options);\n if (key === 'concat') return args.map(termToString).join('');\n if (key === 'lcase') return termToString(args[0]).toLowerCase();\n if (key === 'ucase') return termToString(args[0]).toUpperCase();\n if (key === 'contains') return termToString(args[0]).includes(termToString(args[1]));\n if (key === 'strstarts') return termToString(args[0]).startsWith(termToString(args[1]));\n if (key === 'strends') return termToString(args[0]).endsWith(termToString(args[1]));\n if (key === 'strbefore') {\n const s = termToString(args[0]);\n const needle = termToString(args[1]);\n const index = s.indexOf(needle);\n return index < 0 ? '' : s.slice(0, index);\n }\n if (key === 'strafter') {\n const s = termToString(args[0]);\n const needle = termToString(args[1]);\n const index = s.indexOf(needle);\n return index < 0 ? '' : s.slice(index + needle.length);\n }\n if (key === 'encode_for_uri') return encodeURIComponent(termToString(args[0]));\n if (key === 'regex') return regex(args);\n if (key === 'replace') return replace(args);\n if (key === 'substr') return substr(args);\n if (key === 'sameterm') return termishEquals(args[0], args[1]);\n if (key === 'isiri' || key === 'isuri') return isIRI(args[0]);\n if (key === 'isblank') return isBlank(args[0]);\n if (key === 'isliteral') return isLiteral(args[0]);\n if (key === 'istriple') return isTripleTerm(args[0]);\n if (key === 'isnumeric') return isNumericValue(args[0]);\n if (key === 'datatype') return datatypeOf(args[0]);\n if (key === 'lang') return args[0] && args[0].type === 'literal' ? (args[0].lang || '') : '';\n if (key === 'langmatches') return langMatches(termToString(args[0]), termToString(args[1]));\n if (key === 'haslang') return !!(args[0] && args[0].type === 'literal' && args[0].lang);\n if (key === 'langdir') return args[0] && args[0].type === 'literal' ? (args[0].langDir || '') : '';\n if (key === 'haslangdir') return !!(args[0] && args[0].type === 'literal' && args[0].langDir);\n if (key === 'strlen') return termToString(args[0]).length;\n if (key === 'abs') return Math.abs(Number(termToPrimitive(valueToTermIfNeeded(args[0]))));\n if (key === 'floor') return Math.floor(Number(termToPrimitive(valueToTermIfNeeded(args[0]))));\n if (key === 'ceil') return Math.ceil(Number(termToPrimitive(valueToTermIfNeeded(args[0]))));\n if (key === 'round') return Math.round(Number(termToPrimitive(valueToTermIfNeeded(args[0]))));\n if (key === 'if') return booleanValue(args[0]) ? args[1] : args[2];\n if (key === 'strdt') return literal(termToString(args[0]), termToString(args[1]));\n if (key === 'strlang') return literal(termToString(args[0]), null, termToString(args[1]).toLowerCase());\n if (key === 'strlangdir') return literal(termToString(args[0]), null, termToString(args[1]).toLowerCase(), termToString(args[2]).toLowerCase());\n if (key === 'triple') return tripleTerm(valueToTermIfNeeded(args[0]), valueToTermIfNeeded(args[1]), valueToTermIfNeeded(args[2]));\n if (key === 'subject') return isTripleTerm(args[0]) ? args[0].s : null;\n if (key === 'predicate') return isTripleTerm(args[0]) ? args[0].p : null;\n if (key === 'object') return isTripleTerm(args[0]) ? args[0].o : null;\n if (key === 'year') return datePart(args[0], 'year');\n if (key === 'month') return datePart(args[0], 'month');\n if (key === 'day') return datePart(args[0], 'day');\n if (key === 'hours') return datePart(args[0], 'hours');\n if (key === 'minutes') return datePart(args[0], 'minutes');\n if (key === 'seconds') return datePart(args[0], 'seconds');\n if (key === 'timezone') return timezoneDuration(args[0]);\n if (key === 'tz') return timezoneLexical(args[0]);\n if (key === 'now') return literal((options.now || new Date()).toISOString(), XSD_DATETIME);\n if (key === 'uuid') return iri(`urn:uuid:${freshUuid(options)}`);\n if (key === 'struuid') return freshUuid(options);\n throw new Error(`Unimplemented builtin ${name}`);\n}\n\nfunction validateArity(canonical, actual) {\n const sig = BUILTIN_SIGNATURES[canonical];\n if (!sig) throw new Error(`Unknown builtin ${canonical}`);\n const tooFew = actual < sig.min;\n const tooMany = actual > sig.max;\n if (tooFew || tooMany) {\n const expected = sig.min === sig.max ? `${sig.min}` : `${sig.min}${sig.max === Infinity ? '+' : `..${sig.max}`}`;\n throw new Error(`${canonical} expects ${expected} argument${expected === '1' ? '' : 's'}, got ${actual}`);\n }\n}\n\nfunction makeIRI(value, options) {\n if (options.baseIRI && !/^[A-Za-z][A-Za-z0-9+.-]*:/.test(value)) {\n try { return iri(new URL(value, options.baseIRI).href); } catch (_) { /* fall through */ }\n }\n return iri(value);\n}\n\nfunction makeBlankNode(args, options) {\n if (args.length === 0) return blankNode(freshId(options));\n const label = termToString(args[0]);\n if (!options.__bnodeLabels) options.__bnodeLabels = new Map();\n if (!options.__bnodeLabels.has(label)) options.__bnodeLabels.set(label, label || freshId(options));\n return blankNode(options.__bnodeLabels.get(label));\n}\n\nfunction regex(args) {\n const flags = regexFlags(termToString(args[2] || ''));\n return new RegExp(termToString(args[1]), flags).test(termToString(args[0]));\n}\n\nfunction replace(args) {\n const flags = regexFlags(termToString(args[3] || ''));\n const effectiveFlags = flags.includes('g') ? flags : `${flags}g`;\n return termToString(args[0]).replace(new RegExp(termToString(args[1]), effectiveFlags), termToString(args[2]));\n}\n\nfunction regexFlags(flags) {\n let out = '';\n for (const ch of String(flags)) {\n // JavaScript RegExp has no direct SPARQL/xpath \"x\" free-spacing flag, so ignore it.\n if (ch === 'x') continue;\n if ('imsuyg'.includes(ch) && !out.includes(ch)) out += ch;\n }\n return out;\n}\n\nfunction substr(args) {\n const value = termToString(args[0]);\n const start = Math.max(0, Number(termToPrimitive(valueToTermIfNeeded(args[1]))) - 1);\n if (args.length >= 3) return value.substring(start, start + Number(termToPrimitive(valueToTermIfNeeded(args[2]))));\n return value.substring(start);\n}\n\nfunction datatypeOf(value) {\n const term = valueToTermIfNeeded(value);\n if (term.type !== 'literal') return null;\n if (term.langDir) return iri(RDF_DIRLANGSTRING);\n if (term.lang) return iri(RDF_LANGSTRING);\n return iri(term.datatype || inferDatatype(term.value) || XSD_STRING);\n}\n\nfunction isNumericValue(value) {\n const term = valueToTermIfNeeded(value);\n if (typeof termToPrimitive(term) === 'number') return true;\n return term.type === 'literal' && NUMERIC_DATATYPES.has(term.datatype);\n}\n\nfunction datePart(value, part) {\n const lexical = termToString(value);\n const match = lexical.match(/^(-?\\d{4,})-(\\d{2})-(\\d{2})(?:T(\\d{2}):(\\d{2}):(\\d{2}(?:\\.\\d+)?)(Z|[+-]\\d{2}:?\\d{2})?)?/);\n if (!match) return null;\n const [, year, month, day, hours = '0', minutes = '0', seconds = '0'] = match;\n if (part === 'year') return Number(year);\n if (part === 'month') return Number(month);\n if (part === 'day') return Number(day);\n if (part === 'hours') return Number(hours);\n if (part === 'minutes') return Number(minutes);\n if (part === 'seconds') return Number(seconds);\n return null;\n}\n\nfunction timezoneLexical(value) {\n const lexical = termToString(value);\n const match = lexical.match(/(?:T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?)(Z|[+-]\\d{2}:?\\d{2})$/);\n return match ? match[1] : '';\n}\n\nfunction timezoneDuration(value) {\n const zone = timezoneLexical(value);\n if (!zone) return null;\n if (zone === 'Z') return literal('PT0S', XSD_DAYTIME_DURATION);\n const match = zone.match(/^([+-])(\\d{2}):?(\\d{2})$/);\n if (!match) return null;\n const [, sign, hh, mm] = match;\n const hours = Number(hh);\n const minutes = Number(mm);\n const body = `${hours ? `${hours}H` : ''}${minutes ? `${minutes}M` : ''}` || '0S';\n return literal(`${sign === '-' ? '-' : ''}PT${body}`, XSD_DAYTIME_DURATION);\n}\n\nfunction langMatches(lang, range) {\n if (range === '*') return lang.length > 0;\n return lang.toLowerCase() === range.toLowerCase() || lang.toLowerCase().startsWith(`${range.toLowerCase()}-`);\n}\n\nfunction freshUuid(options) {\n if (typeof options.uuidGenerator === 'function') return String(options.uuidGenerator());\n options.__eyelengUuidCounter = (options.__eyelengUuidCounter || 0) + 1;\n return `00000000-0000-4000-8000-${String(options.__eyelengUuidCounter).padStart(12, '0')}`;\n}\n\nfunction freshId(options) {\n options.__eyelengCounter = (options.__eyelengCounter || 0) + 1;\n return `eyeleng-${options.__eyelengCounter}`;\n}\n\nfunction asTerm(value) {\n return valueToTerm(value);\n}\n\nmodule.exports = {\n BUILTIN_SIGNATURES,\n builtinNames,\n canonicalBuiltinName,\n isBuiltinName,\n validateArity,\n evalExpression,\n booleanValue,\n asTerm,\n callBuiltin,\n evalBinary,\n};\n", "src/term.js": "'use strict';\n\nconst RDF_NS = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';\nconst RDF_TYPE = `${RDF_NS}type`;\nconst RDF_FIRST = `${RDF_NS}first`;\nconst RDF_REST = `${RDF_NS}rest`;\nconst RDF_NIL = `${RDF_NS}nil`;\nconst RDF_REIFIES = `${RDF_NS}reifies`;\nconst XSD_STRING = 'http://www.w3.org/2001/XMLSchema#string';\nconst XSD_BOOLEAN = 'http://www.w3.org/2001/XMLSchema#boolean';\nconst XSD_INTEGER = 'http://www.w3.org/2001/XMLSchema#integer';\nconst XSD_DECIMAL = 'http://www.w3.org/2001/XMLSchema#decimal';\nconst XSD_DOUBLE = 'http://www.w3.org/2001/XMLSchema#double';\n\nfunction iri(value) {\n return { type: 'iri', value: String(value) };\n}\n\nfunction variable(name) {\n return { type: 'var', value: String(name).replace(/^[?$]/, '') };\n}\n\nfunction blankNode(value) {\n return { type: 'blank', value: String(value).replace(/^_:/, '') };\n}\n\nfunction literal(value, datatype = null, lang = null, langDir = null) {\n return { type: 'literal', value, datatype, lang, langDir };\n}\n\nfunction tripleTerm(s, p, o) {\n return { type: 'triple', s, p, o };\n}\n\nfunction isVariable(term) {\n return term && term.type === 'var';\n}\n\nfunction isIRI(term) {\n return term && term.type === 'iri';\n}\n\nfunction isBlank(term) {\n return term && term.type === 'blank';\n}\n\nfunction isLiteral(term) {\n return term && term.type === 'literal';\n}\n\nfunction isTripleTerm(term) {\n return term && term.type === 'triple';\n}\n\nfunction termEquals(a, b) {\n return termKey(a) === termKey(b);\n}\n\nfunction termKey(term) {\n if (!term) return 'null';\n if (term.type === 'iri') return `I:${term.value}`;\n if (term.type === 'blank') return `B:${term.value}`;\n if (term.type === 'var') return `V:${term.value}`;\n if (term.type === 'literal') return `L:${JSON.stringify(term.value)}^^${term.datatype || ''}@${term.lang || ''}--${term.langDir || ''}`;\n if (term.type === 'triple') return `T:${termKey(term.s)} ${termKey(term.p)} ${termKey(term.o)}`;\n return JSON.stringify(term);\n}\n\nfunction tripleKey(triple) {\n return `${termKey(triple.s)} ${termKey(triple.p)} ${termKey(triple.o)}`;\n}\n\nfunction cloneTerm(term) {\n if (!term) return term;\n if (term.type === 'triple') return tripleTerm(cloneTerm(term.s), cloneTerm(term.p), cloneTerm(term.o));\n return { ...term };\n}\n\nfunction valueToTerm(value) {\n if (value && typeof value === 'object' && value.type) return value;\n return literal(value, inferDatatype(value));\n}\n\nfunction inferDatatype(value) {\n if (typeof value === 'boolean') return XSD_BOOLEAN;\n if (typeof value === 'number' && Number.isInteger(value)) return XSD_INTEGER;\n if (typeof value === 'number') return XSD_DECIMAL;\n if (typeof value === 'string') return XSD_STRING;\n return null;\n}\n\nfunction termToPrimitive(term) {\n if (!term) return undefined;\n if (term.type === 'literal') return term.value;\n if (term.type === 'iri') return term.value;\n if (term.type === 'blank') return `_:${term.value}`;\n if (term.type === 'var') return undefined;\n if (term.type === 'triple') return term;\n return term;\n}\n\nfunction termToString(term) {\n const value = termToPrimitive(term);\n if (value === undefined || value === null) return '';\n if (value && value.type === 'triple') return formatTerm(value);\n return String(value);\n}\n\nfunction booleanValue(value) {\n const primitive = value && value.type ? termToPrimitive(value) : value;\n if (primitive === undefined || primitive === null) return false;\n if (typeof primitive === 'boolean') return primitive;\n if (typeof primitive === 'number') return primitive !== 0 && !Number.isNaN(primitive);\n if (typeof primitive === 'string') return primitive.length > 0 && primitive !== 'false';\n return Boolean(primitive);\n}\n\nfunction comparePrimitives(a, b) {\n const av = a && a.type ? termToPrimitive(a) : a;\n const bv = b && b.type ? termToPrimitive(b) : b;\n if (typeof av === 'number' && typeof bv === 'number') return av - bv;\n const as = String(av);\n const bs = String(bv);\n if (as < bs) return -1;\n if (as > bs) return 1;\n return 0;\n}\n\nfunction escapeString(value) {\n return String(value)\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/\\n/g, '\\\\n')\n .replace(/\\r/g, '\\\\r')\n .replace(/\\t/g, '\\\\t')\n .replace(/\"/g, '\\\\\"');\n}\n\nfunction compactIRI(value, prefixes = {}) {\n if (value === RDF_TYPE) return 'a';\n const entries = Object.entries(prefixes)\n .filter(([, iriPrefix]) => iriPrefix && value.startsWith(iriPrefix))\n .sort((a, b) => b[1].length - a[1].length);\n if (entries.length > 0) {\n const [prefix, iriPrefix] = entries[0];\n const local = value.slice(iriPrefix.length);\n if (/^[A-Za-z_][A-Za-z0-9_\\-]*$/.test(local) || /^[A-Za-z0-9_\\-]+$/.test(local)) {\n return `${prefix}:${local}`;\n }\n }\n return `<${value}>`;\n}\n\nfunction formatTerm(term, prefixes = {}) {\n if (term.type === 'iri') return compactIRI(term.value, prefixes);\n if (term.type === 'blank') return `_:${term.value}`;\n if (term.type === 'var') return `?${term.value}`;\n if (term.type === 'triple') return `<<(${formatTerm(term.s, prefixes)} ${formatTerm(term.p, prefixes)} ${formatTerm(term.o, prefixes)})>>`;\n if (term.type === 'literal') {\n const v = term.value;\n if (typeof v === 'number' && Number.isFinite(v) && !term.lang && (!term.datatype || term.datatype === XSD_INTEGER || term.datatype === XSD_DECIMAL || term.datatype === XSD_DOUBLE)) return String(v);\n if (typeof v === 'boolean' && !term.lang && (!term.datatype || term.datatype === XSD_BOOLEAN)) return v ? 'true' : 'false';\n const lexical = `\"${escapeString(v)}\"`;\n if (term.lang) return `${lexical}@${term.lang}${term.langDir ? `--${term.langDir}` : ''}`;\n if (term.datatype && term.datatype !== XSD_STRING) return `${lexical}^^${compactIRI(term.datatype, prefixes)}`;\n return lexical;\n }\n return String(term.value ?? term);\n}\n\nfunction formatTriple(triple, prefixes = {}) {\n return `${formatTerm(triple.s, prefixes)} ${formatTerm(triple.p, prefixes)} ${formatTerm(triple.o, prefixes)} .`;\n}\n\nmodule.exports = {\n RDF_NS,\n RDF_TYPE,\n RDF_FIRST,\n RDF_REST,\n RDF_NIL,\n RDF_REIFIES,\n XSD_STRING,\n XSD_BOOLEAN,\n XSD_INTEGER,\n XSD_DECIMAL,\n XSD_DOUBLE,\n iri,\n variable,\n blankNode,\n literal,\n tripleTerm,\n isVariable,\n isIRI,\n isBlank,\n isLiteral,\n isTripleTerm,\n termEquals,\n termKey,\n tripleKey,\n cloneTerm,\n valueToTerm,\n inferDatatype,\n termToPrimitive,\n termToString,\n booleanValue,\n comparePrimitives,\n compactIRI,\n formatTerm,\n formatTriple,\n};\n", "src/rdfSyntax.js": "'use strict';\n\nconst { tokenize, SyntaxErrorWithLocation } = require('./tokenizer.js');\nconst {\n iri,\n variable,\n blankNode,\n literal,\n tripleTerm,\n termKey,\n termEquals,\n formatTerm,\n RDF_TYPE,\n RDF_FIRST,\n RDF_REST,\n RDF_NIL,\n XSD_BOOLEAN,\n XSD_INTEGER,\n XSD_DECIMAL,\n XSD_DOUBLE,\n} = require('./term.js');\n\nconst RDF_NS = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';\nconst SRL_NS = 'http://www.w3.org/ns/shacl-rules#';\nconst SHNEX_NS = 'http://www.w3.org/ns/shacl-node-expr#';\nconst SPARQL_NS = 'http://www.w3.org/ns/sparql#';\nconst OWL_IMPORTS = 'http://www.w3.org/2002/07/owl#imports';\nconst SRL_RULE_SET = `${SRL_NS}RuleSet`;\nconst SRL_RULE = `${SRL_NS}Rule`;\nconst SRL_DATA = `${SRL_NS}data`;\nconst SRL_RULES = `${SRL_NS}rules`;\nconst SRL_BODY = `${SRL_NS}body`;\nconst SRL_HEAD = `${SRL_NS}head`;\nconst SRL_SUBJECT = `${SRL_NS}subject`;\nconst SRL_PREDICATE = `${SRL_NS}predicate`;\nconst SRL_OBJECT = `${SRL_NS}object`;\nconst SRL_FILTER = `${SRL_NS}filter`;\nconst SRL_EXPR = `${SRL_NS}expr`;\nconst SRL_ASSIGN = `${SRL_NS}assign`;\nconst SRL_ASSIGN_VAR = `${SRL_NS}assignVar`;\nconst SRL_ASSIGN_VALUE = `${SRL_NS}assignValue`;\nconst SRL_NOT = `${SRL_NS}not`;\nconst SRL_VAR_NAME = `${SRL_NS}varName`;\nconst SHNEX_VAR = `${SHNEX_NS}var`;\n\nclass TurtleParser {\n constructor(source, options = {}) {\n this.tokens = Array.isArray(source) ? source : tokenize(source, options.filename || '<rdf>');\n this.pos = 0;\n this.baseIRI = options.baseIRI || null;\n this.bnodeCounter = 0;\n this.prefixes = {\n '': 'http://example/',\n rdf: RDF_NS,\n srl: SRL_NS,\n shnex: SHNEX_NS,\n sparql: SPARQL_NS,\n xsd: 'http://www.w3.org/2001/XMLSchema#',\n owl: 'http://www.w3.org/2002/07/owl#',\n ...options.prefixes,\n };\n this.triples = [];\n this.imports = [];\n }\n\n parseDocument() {\n while (!this.is('eof')) {\n if (this.matchDirective('PREFIX', '@prefix')) this.parsePrefix(this.previous().value.startsWith('@'));\n else if (this.matchDirective('BASE', '@base')) this.parseBase(this.previous().value.startsWith('@'));\n else this.parseTriplesStatement();\n }\n return {\n baseIRI: this.baseIRI,\n prefixes: { ...this.prefixes },\n triples: this.triples,\n imports: this.imports.slice(),\n };\n }\n\n parsePrefix(atStyle = false) {\n const nameToken = this.advance();\n if (nameToken.type !== 'word' || !nameToken.value.endsWith(':')) throw this.error('Expected prefix label ending in :', nameToken);\n const iriToken = this.expectType('iri');\n this.prefixes[nameToken.value.slice(0, -1)] = this.resolveIRI(iriToken.value, iriToken);\n if (atStyle) this.expectValue('.');\n }\n\n parseBase(atStyle = false) {\n const iriToken = this.expectType('iri');\n this.baseIRI = this.resolveIRI(iriToken.value, iriToken);\n if (atStyle) this.expectValue('.');\n }\n\n parseTriplesStatement() {\n const subjectNode = this.parseNode();\n this.triples.push(...subjectNode.triples);\n this.triples.push(...this.parsePredicateObjectList(subjectNode.term, ['.']));\n this.expectValue('.');\n }\n\n parsePredicateObjectList(subject, terminators = [']']) {\n const triples = [];\n while (!terminators.some((value) => this.checkValue(value))) {\n const predicate = this.parseVerb();\n do {\n const objectNode = this.parseNode();\n triples.push(...objectNode.triples);\n triples.push({ s: subject, p: predicate, o: objectNode.term });\n if (predicate.type === 'iri' && predicate.value === OWL_IMPORTS && objectNode.term.type === 'iri') this.imports.push(objectNode.term.value);\n } while (this.matchValue(','));\n if (this.matchValue(';')) {\n while (this.matchValue(';')) { /* tolerate repeated semicolons */ }\n if (terminators.some((value) => this.checkValue(value))) break;\n } else break;\n }\n return triples;\n }\n\n parseNode() {\n if (this.checkValue('[')) return this.parseBlankNodePropertyList();\n if (this.checkValue('(')) return this.parseCollection();\n return { term: this.parseTerm(), triples: [] };\n }\n\n parseBlankNodePropertyList() {\n this.expectValue('[');\n const node = this.freshBlankNode();\n if (this.matchValue(']')) return { term: node, triples: [] };\n const triples = this.parsePredicateObjectList(node, [']']);\n this.expectValue(']');\n return { term: node, triples };\n }\n\n parseCollection() {\n this.expectValue('(');\n if (this.matchValue(')')) return { term: iri(RDF_NIL), triples: [] };\n const items = [];\n while (!this.checkValue(')')) items.push(this.parseNode());\n this.expectValue(')');\n const triples = [];\n for (const item of items) triples.push(...item.triples);\n const cells = items.map(() => this.freshBlankNode());\n for (let i = 0; i < items.length; i += 1) {\n triples.push({ s: cells[i], p: iri(RDF_FIRST), o: items[i].term });\n triples.push({ s: cells[i], p: iri(RDF_REST), o: i + 1 < cells.length ? cells[i + 1] : iri(RDF_NIL) });\n }\n return { term: cells[0], triples };\n }\n\n parseVerb() {\n if (this.checkType('word') && this.peek().value === 'a') { this.advance(); return iri(RDF_TYPE); }\n const term = this.parseTerm();\n if (term.type !== 'iri') throw this.error('Expected IRI as Turtle predicate');\n return term;\n }\n\n parseTerm() {\n const token = this.advance();\n if (token.type === 'operator' && (token.value === '+' || token.value === '-') && this.peek().type === 'number') {\n const numberToken = this.advance();\n return numericLiteral(token.value === '-' ? -numberToken.value : numberToken.value);\n }\n if (token.type === 'iri') return iri(this.resolveIRI(token.value, token));\n if (token.type === 'string') return this.parseLiteralAfterToken(token);\n if (token.type === 'number') return numericLiteral(token.value);\n if (token.value === '<<(') return this.parseTripleTermAfterOpen();\n if (token.type === 'word') {\n const word = token.value.includes(':') || token.value.startsWith('_:') ? this.consumeHyphenatedWord(token.value) : token.value;\n if (word === 'a') return iri(RDF_TYPE);\n if (word === 'true') return literal(true, XSD_BOOLEAN);\n if (word === 'false') return literal(false, XSD_BOOLEAN);\n if (word.startsWith('_:')) return blankNode(word.slice(2));\n if (word.includes(':')) return iri(this.expandPrefixedName(word, token));\n }\n throw this.error(`Expected RDF term, got ${token.value}`, token);\n }\n\n parseTripleTermAfterOpen() {\n const s = this.parseTerm();\n const p = this.parseVerb();\n const o = this.parseTerm();\n this.expectValue(')>>');\n return tripleTerm(s, p, o);\n }\n\n parseLiteralAfterToken(token) {\n if (this.matchValue('^^')) {\n const datatype = this.parseDatatypeIRI();\n return literal(coerceLexicalLiteral(token.value, datatype), datatype, null);\n }\n if (this.checkType('word') && /^@[A-Za-z]+(?:-[A-Za-z0-9]+)*(?:--[A-Za-z]+)?$/.test(this.peek().value)) {\n const tag = this.advance().value.slice(1).toLowerCase();\n const [lang, langDir = null] = tag.split('--');\n return literal(token.value, null, lang, langDir);\n }\n return literal(token.value);\n }\n\n parseDatatypeIRI() {\n const token = this.advance();\n if (token.type === 'iri') return this.resolveIRI(token.value, token);\n if (token.type === 'word' && token.value.includes(':')) return this.expandPrefixedName(token.value, token);\n throw this.error(`Expected datatype IRI, got ${token.value}`, token);\n }\n\n freshBlankNode() {\n this.bnodeCounter += 1;\n return blankNode(`rdf${this.bnodeCounter}`);\n }\n\n consumeHyphenatedWord(value) {\n let out = value;\n while (this.checkValue('-') && (this.peekN(1).type === 'word' || this.peekN(1).type === 'number')) {\n this.advance();\n out += `-${this.advance().value}`;\n }\n return out;\n }\n\n expandPrefixedName(value, token) {\n const colon = value.indexOf(':');\n if (colon < 0) throw this.error(`Expected prefixed name, got ${value}`, token);\n const prefix = value.slice(0, colon);\n const local = value.slice(colon + 1);\n if (!Object.hasOwn(this.prefixes, prefix)) throw this.error(`Unknown prefix ${prefix}:`, token);\n return this.prefixes[prefix] + local;\n }\n\n resolveIRI(value) {\n if (!this.baseIRI || /^[A-Za-z][A-Za-z0-9+.-]*:/.test(value)) return value;\n try { return new URL(value, this.baseIRI).href; } catch (_) { return value; }\n }\n\n matchDirective(...names) {\n if (this.checkType('word')) {\n const value = this.peek().value;\n if (names.some((name) => value.toUpperCase() === name.toUpperCase())) { this.advance(); return true; }\n }\n return false;\n }\n\n previous() { return this.tokens[this.pos - 1]; }\n peek() { return this.tokens[this.pos]; }\n peekN(n) { return this.tokens[this.pos + n]; }\n is(type) { return this.peek().type === type; }\n checkType(type) { return this.peek().type === type; }\n checkValue(value) { return this.peek().value === value; }\n matchValue(value) { if (this.checkValue(value)) { this.advance(); return true; } return false; }\n advance() { return this.tokens[this.pos++]; }\n expectType(type) { const token = this.advance(); if (token.type !== type) throw this.error(`Expected ${type}, got ${token.value}`, token); return token; }\n expectValue(value) { const token = this.advance(); if (token.value !== value) throw this.error(`Expected ${value}, got ${token.value}`, token); return token; }\n error(message, token = this.peek()) { return new SyntaxErrorWithLocation(message, token); }\n}\n\nfunction parseRdfDocument(source, options = {}) {\n return new TurtleParser(source, options).parseDocument();\n}\n\nfunction parseRdfSyntax(source, options = {}) {\n const document = parseRdfDocument(source, options);\n return rdfDocumentToProgram(document, options);\n}\n\nfunction rdfDocumentToProgram(document, options = {}) {\n const graph = new RdfGraph(document.triples, document.prefixes);\n const ruleSetNodes = chooseRuleSets(graph, options.ruleSet);\n if (ruleSetNodes.length === 0) throw new Error('No srl:RuleSet found in RDF Rules syntax input');\n\n const program = {\n baseIRI: document.baseIRI || null,\n version: null,\n imports: options.rdfImportsAsImports ? document.imports.slice() : [],\n prefixes: { ...document.prefixes },\n data: [],\n rules: [],\n rdfSyntax: true,\n ruleSets: ruleSetNodes.map((term) => formatTerm(term, document.prefixes)),\n };\n\n for (const ruleSet of ruleSetNodes) {\n for (const dataList of graph.objects(ruleSet, SRL_DATA)) {\n for (const item of graph.list(dataList)) program.data.push(toDataTriple(item, graph));\n }\n for (const rulesList of graph.objects(ruleSet, SRL_RULES)) {\n for (const ruleNode of graph.list(rulesList)) program.rules.push(toRule(ruleNode, graph));\n }\n }\n return program;\n}\n\nfunction chooseRuleSets(graph, selected) {\n if (selected) {\n const term = graph.parseReference(selected);\n return [term];\n }\n const typed = graph.subjects(RDF_TYPE, iri(SRL_RULE_SET));\n if (typed.length > 0) return uniqueTerms(typed);\n const byData = graph.subjectsWithPredicate(SRL_DATA);\n const byRules = graph.subjectsWithPredicate(SRL_RULES);\n return uniqueTerms([...byData, ...byRules]).filter((term) => graph.objects(term, SRL_RULES).length > 0 || graph.objects(term, SRL_DATA).length > 0);\n}\n\nfunction toDataTriple(item, graph) {\n if (item.type === 'triple') return { s: item.s, p: item.p, o: item.o };\n const triple = toTripleLike(item, graph);\n if ([triple.s, triple.p, triple.o].some((term) => term.type === 'var')) throw new Error('RDF Rules srl:data may not contain variables');\n if (triple.p.type !== 'iri') throw new Error('RDF Rules data triple predicate must be an IRI');\n return triple;\n}\n\nfunction toRule(ruleNode, graph) {\n const bodyLists = graph.objects(ruleNode, SRL_BODY);\n const headLists = graph.objects(ruleNode, SRL_HEAD);\n if (bodyLists.length !== 1 || headLists.length !== 1) throw new Error(`RDF Rule ${graph.label(ruleNode)} must have exactly one srl:body and one srl:head`);\n const body = graph.list(bodyLists[0]).map((item) => toBodyElement(item, graph));\n const head = graph.list(headLists[0]).map((item) => toTripleLike(item, graph));\n return { name: graph.label(ruleNode), head, body, runOnce: body.some((clause) => clause.type === 'set') };\n}\n\nfunction toBodyElement(node, graph) {\n if (hasTripleShape(node, graph)) return { type: 'triple', triple: toTripleLike(node, graph) };\n const filters = graph.objects(node, SRL_FILTER).concat(graph.objects(node, SRL_EXPR));\n if (filters.length > 0) {\n if (filters.length !== 1) throw new Error(`Filter element ${graph.label(node)} must have exactly one srl:filter`);\n return { type: 'filter', expr: toExpression(filters[0], graph) };\n }\n const assigns = graph.objects(node, SRL_ASSIGN);\n if (assigns.length > 0) {\n if (assigns.length !== 1) throw new Error(`Assignment element ${graph.label(node)} must have exactly one srl:assign`);\n const assign = assigns[0];\n const vars = graph.objects(assign, SRL_ASSIGN_VAR);\n const values = graph.objects(assign, SRL_ASSIGN_VALUE);\n if (vars.length !== 1 || values.length !== 1) throw new Error(`Assignment ${graph.label(assign)} must have exactly one srl:assignVar and srl:assignValue`);\n const variableTerm = toVarOrTerm(vars[0], graph);\n if (variableTerm.type !== 'var') throw new Error('srl:assignVar must point to a variable node');\n return { type: 'set', variable: variableTerm.value, expr: toExpression(values[0], graph) };\n }\n const negations = graph.objects(node, SRL_NOT);\n if (negations.length > 0) {\n if (negations.length !== 1) throw new Error(`Negation element ${graph.label(node)} must have exactly one srl:not`);\n const body = graph.list(negations[0]).map((item) => {\n const clause = toBodyElement(item, graph);\n if (clause.type === 'set' || clause.type === 'not') throw new Error('RDF Rules srl:not may contain only triple patterns and filters');\n return clause;\n });\n return { type: 'not', body };\n }\n throw new Error(`Unsupported RDF Rules body element ${graph.label(node)}`);\n}\n\nfunction toTripleLike(node, graph) {\n if (node.type === 'triple') return { s: node.s, p: node.p, o: node.o };\n const subjects = graph.objects(node, SRL_SUBJECT);\n const predicates = graph.objects(node, SRL_PREDICATE);\n const objects = graph.objects(node, SRL_OBJECT);\n if (subjects.length !== 1 || predicates.length !== 1 || objects.length !== 1) {\n throw new Error(`Triple node ${graph.label(node)} must have exactly one srl:subject, srl:predicate and srl:object`);\n }\n return {\n s: toVarOrTerm(subjects[0], graph),\n p: toVarOrTerm(predicates[0], graph),\n o: toVarOrTerm(objects[0], graph),\n };\n}\n\nfunction hasTripleShape(node, graph) {\n return graph.objects(node, SRL_SUBJECT).length > 0 || graph.objects(node, SRL_PREDICATE).length > 0 || graph.objects(node, SRL_OBJECT).length > 0;\n}\n\nfunction toVarOrTerm(node, graph) {\n const varNames = graph.objects(node, SRL_VAR_NAME);\n if (varNames.length > 0) {\n if (varNames.length !== 1 || varNames[0].type !== 'literal') throw new Error(`Variable node ${graph.label(node)} must have exactly one string srl:varName`);\n return variable(String(varNames[0].value));\n }\n return node;\n}\n\nfunction toExpression(node, graph) {\n const varNames = graph.objects(node, SHNEX_VAR).concat(graph.objects(node, SRL_VAR_NAME));\n if (varNames.length > 0) {\n if (varNames.length !== 1 || varNames[0].type !== 'literal') throw new Error(`Expression variable ${graph.label(node)} must name one variable`);\n return { type: 'var', name: String(varNames[0].value) };\n }\n if (node.type === 'literal') {\n if (node.datatype || node.lang) return { type: 'term', value: node };\n return { type: 'literal', value: node.value };\n }\n if (node.type === 'iri' || node.type === 'blank' || node.type === 'triple') {\n const call = graph.functionCall(node);\n if (call) return toFunctionExpression(call.name, call.args.map((arg) => toExpression(arg, graph)));\n if (node.type === 'blank' && graph.hasOutgoing(node)) return { type: 'term', value: node };\n return { type: 'term', value: toVarOrTerm(node, graph) };\n }\n return { type: 'term', value: node };\n}\n\nfunction toFunctionExpression(name, args) {\n if (name.startsWith(SPARQL_NS)) {\n const local = name.slice(SPARQL_NS.length);\n if (local === 'less-than' || local === 'lessThan') return binary('<', args);\n if (local === 'less-than-or-equal' || local === 'lessThanOrEqual') return binary('<=', args);\n if (local === 'greater-than' || local === 'greaterThan') return binary('>', args);\n if (local === 'greater-than-or-equal' || local === 'greaterThanOrEqual') return binary('>=', args);\n if (local === 'equal' || local === 'equals') return binary('=', args);\n if (local === 'not-equal' || local === 'notEqual') return binary('!=', args);\n if (local === 'add') return foldBinary('+', args);\n if (local === 'subtract') return binary('-', args);\n if (local === 'multiply') return foldBinary('*', args);\n if (local === 'divide') return binary('/', args);\n if (local === 'and' || local === 'function-and') return foldBinary('&&', args);\n if (local === 'or' || local === 'function-or') return foldBinary('||', args);\n if (local === 'not') return { type: 'unary', op: '!', expr: args[0] };\n const builtin = sparqlLocalToBuiltin(local);\n return { type: 'call', name: builtin, args };\n }\n return { type: 'call', name, args };\n}\n\nfunction binary(op, args) {\n if (args.length !== 2) throw new Error(`sparql operator ${op} expects 2 arguments`);\n return { type: 'binary', op, left: args[0], right: args[1] };\n}\n\nfunction foldBinary(op, args) {\n if (args.length < 2) throw new Error(`sparql operator ${op} expects at least 2 arguments`);\n return args.slice(1).reduce((left, right) => ({ type: 'binary', op, left, right }), args[0]);\n}\n\nfunction sparqlLocalToBuiltin(local) {\n return local.replace(/-([a-z])/g, (_, ch) => ch.toUpperCase()).replace(/^./, (ch) => ch.toUpperCase());\n}\n\nclass RdfGraph {\n constructor(triples, prefixes = {}) {\n this.triples = triples;\n this.prefixes = prefixes;\n this.bySubject = new Map();\n for (const triple of triples) {\n const key = termKey(triple.s);\n if (!this.bySubject.has(key)) this.bySubject.set(key, []);\n this.bySubject.get(key).push(triple);\n }\n }\n\n objects(subject, predicateIRI) {\n const rows = this.bySubject.get(termKey(subject)) || [];\n return rows.filter((triple) => triple.p.type === 'iri' && triple.p.value === predicateIRI).map((triple) => triple.o);\n }\n\n subjects(predicateIRI, object) {\n return this.triples.filter((triple) => triple.p.type === 'iri' && triple.p.value === predicateIRI && termEquals(triple.o, object)).map((triple) => triple.s);\n }\n\n subjectsWithPredicate(predicateIRI) {\n return this.triples.filter((triple) => triple.p.type === 'iri' && triple.p.value === predicateIRI).map((triple) => triple.s);\n }\n\n hasOutgoing(subject) {\n return (this.bySubject.get(termKey(subject)) || []).length > 0;\n }\n\n list(head) {\n const out = [];\n let node = head;\n const seen = new Set();\n while (!(node.type === 'iri' && node.value === RDF_NIL)) {\n const key = termKey(node);\n if (seen.has(key)) throw new Error(`Cycle in RDF list at ${this.label(node)}`);\n seen.add(key);\n const first = this.objects(node, RDF_FIRST);\n const rest = this.objects(node, RDF_REST);\n if (first.length !== 1 || rest.length !== 1) throw new Error(`Expected RDF list node at ${this.label(node)}`);\n out.push(first[0]);\n node = rest[0];\n }\n return out;\n }\n\n functionCall(node) {\n if (node.type !== 'blank') return null;\n const rows = (this.bySubject.get(termKey(node)) || []).filter((triple) => triple.p.type === 'iri');\n const calls = rows.filter((triple) => triple.p.value.startsWith(SPARQL_NS) || triple.p.value.includes('#') || triple.p.value.includes('/'));\n const viable = calls.filter((triple) => isRdfListHead(triple.o, this));\n if (viable.length !== 1) return null;\n return { name: viable[0].p.value, args: this.list(viable[0].o) };\n }\n\n parseReference(text) {\n if (typeof text !== 'string') return text;\n if (text.startsWith('<') && text.endsWith('>')) return iri(text.slice(1, -1));\n if (text.startsWith('_:')) return blankNode(text.slice(2));\n const colon = text.indexOf(':');\n if (colon >= 0) {\n const prefix = text.slice(0, colon);\n const local = text.slice(colon + 1);\n const ns = this.prefixes[prefix] || (prefix === 'srl' ? SRL_NS : null);\n if (ns) return iri(ns + local);\n }\n return iri(text);\n }\n\n label(term) { return formatTerm(term, this.prefixes); }\n}\n\nfunction isRdfListHead(term, graph) {\n return (term.type === 'iri' && term.value === RDF_NIL) || graph.objects(term, RDF_FIRST).length === 1;\n}\n\nfunction uniqueTerms(terms) {\n const seen = new Set();\n const out = [];\n for (const term of terms) {\n const key = termKey(term);\n if (!seen.has(key)) { seen.add(key); out.push(term); }\n }\n return out;\n}\n\nfunction numericLiteral(value) {\n if (Number.isInteger(value)) return literal(value, XSD_INTEGER);\n if (String(value).includes('e') || String(value).includes('E')) return literal(value, XSD_DOUBLE);\n return literal(value, XSD_DECIMAL);\n}\n\nfunction coerceLexicalLiteral(value, datatype) {\n if (datatype === XSD_INTEGER) return Number.parseInt(value, 10);\n if (datatype === XSD_DECIMAL || datatype === XSD_DOUBLE) return Number(value);\n if (datatype === XSD_BOOLEAN) return value === true || value === 'true' || value === '1';\n return value;\n}\n\nfunction looksLikeRdfRules(source, options = {}) {\n if (options.syntax === 'rdf') return true;\n if (options.syntax === 'srl') return false;\n if (options.filename && /\\.(ttl|trig|nt|n3)$/i.test(options.filename)) return true;\n return /\\bsrl:RuleSet\\b|\\bsrl:rules\\b|http:\\/\\/www\\.w3\\.org\\/ns\\/shacl-rules#RuleSet/.test(source);\n}\n\nmodule.exports = {\n parseRdfDocument,\n parseRdfSyntax,\n rdfDocumentToProgram,\n looksLikeRdfRules,\n TurtleParser,\n RdfGraph,\n constants: {\n SRL_NS,\n SHNEX_NS,\n SPARQL_NS,\n SRL_RULE_SET,\n SRL_RULE,\n },\n};\n", "src/engine.js": "'use strict';\n\nconst { TripleStore, bindingKey, instantiateTriple } = require('./store.js');\nconst { tripleKey, termEquals } = require('./term.js');\nconst { evalExpression, booleanValue, asTerm } = require('./builtins.js');\nconst { analyze } = require('./analyze.js');\n\nfunction evaluate(program, options = {}) {\n const maxIterations = options.maxIterations ?? 1000;\n const evalOptions = { ...options, baseIRI: options.baseIRI || program.baseIRI || null, now: options.now || new Date(), __bnodeLabels: options.__bnodeLabels || new Map() };\n const store = new TripleStore(program.data);\n const inputKeys = new Set(program.data.map(tripleKey));\n const inferred = [];\n const trace = [];\n let iterations = 0;\n let ruleApplications = 0;\n const perRule = program.rules.map((rule, index) => ({\n name: rule.name || `rule#${index + 1}`,\n applications: 0,\n added: 0,\n runOnce: !!rule.runOnce,\n }));\n\n const analysis = options.analysis || analyze(program);\n if (analysis.errors && analysis.errors.length > 0 && !options.ignoreAnalysisErrors) {\n throw new Error(`Analysis failed: ${analysis.errors.map((error) => error.message).join('; ')}`);\n }\n const layerIndexes = analysis.dependency && analysis.dependency.layerIndexes\n ? analysis.dependency.layerIndexes\n : [program.rules.map((_, index) => index)];\n const recursiveLayerFlags = computeRecursiveLayerFlags(\n layerIndexes,\n analysis.dependency ? analysis.dependency.edges : [],\n );\n\n for (let layerIndex = 0; layerIndex < layerIndexes.length; layerIndex += 1) {\n const layer = layerIndexes[layerIndex];\n const ordinary = layer.filter((ruleIndex) => !program.rules[ruleIndex].runOnce);\n const runOnce = layer.filter((ruleIndex) => program.rules[ruleIndex].runOnce);\n\n const ordinaryResult = runRulesToFixpoint(program, store, ordinary, {\n ...evalOptions,\n maxIterations,\n inputKeys,\n inferred,\n trace,\n perRule,\n layer: layerIndex + 1,\n startingIterations: iterations,\n recursiveLayer: recursiveLayerFlags[layerIndex],\n });\n iterations = ordinaryResult.iterations;\n ruleApplications += ordinaryResult.ruleApplications;\n\n if (runOnce.length > 0) {\n iterations += 1;\n for (const ruleIndex of runOnce) {\n const added = applyRuleOnce(program, store, ruleIndex, {\n ...evalOptions,\n inputKeys,\n inferred,\n trace,\n perRule,\n layer: layerIndex + 1,\n iteration: iterations,\n });\n ruleApplications += added.applications;\n }\n }\n }\n\n return {\n baseIRI: program.baseIRI,\n version: program.version || null,\n imports: program.imports || [],\n prefixes: program.prefixes,\n input: program.data.slice(),\n inferred,\n closure: store.values(),\n iterations,\n layers: layerIndexes.map((layer) => layer.map((ruleIndex) => perRule[ruleIndex].name)),\n ruleApplications,\n perRule,\n trace,\n };\n}\n\nfunction runRulesToFixpoint(program, store, ruleIndexes, context) {\n if (ruleIndexes.length === 0) return { iterations: context.startingIterations, ruleApplications: 0 };\n\n // A stratum may contain only acyclic rule components. Such rules only need a\n // single pass after lower strata have reached their fixpoints; spending an\n // extra no-change pass per layer makes deep taxonomies look non-terminating.\n if (!context.recursiveLayer) {\n const iteration = context.startingIterations + 1;\n let ruleApplications = 0;\n for (const ruleIndex of ruleIndexes) {\n const applied = applyRuleOnce(program, store, ruleIndex, {\n ...context,\n iteration,\n });\n ruleApplications += applied.applications;\n }\n return { iterations: iteration, ruleApplications };\n }\n\n let iterations = context.startingIterations;\n let localIterations = 0;\n let ruleApplications = 0;\n\n while (localIterations < context.maxIterations) {\n localIterations += 1;\n iterations += 1;\n let addedInIteration = 0;\n\n for (const ruleIndex of ruleIndexes) {\n const applied = applyRuleOnce(program, store, ruleIndex, {\n ...context,\n iteration: iterations,\n });\n addedInIteration += applied.added;\n ruleApplications += applied.applications;\n }\n\n if (addedInIteration === 0) break;\n }\n\n if (localIterations >= context.maxIterations) {\n throw new Error(`Reached maxIterations=${context.maxIterations} within layer ${context.layer}; rules may not terminate`);\n }\n\n return { iterations, ruleApplications };\n}\n\nfunction computeRecursiveLayerFlags(layerIndexes, edges = []) {\n const flags = Array(layerIndexes.length).fill(false);\n const layerOfRule = new Map();\n for (let layerIndex = 0; layerIndex < layerIndexes.length; layerIndex += 1) {\n for (const ruleIndex of layerIndexes[layerIndex]) layerOfRule.set(ruleIndex, layerIndex);\n }\n for (const edge of edges) {\n const fromLayer = layerOfRule.get(edge.from);\n if (fromLayer === undefined) continue;\n if (fromLayer === layerOfRule.get(edge.to)) flags[fromLayer] = true;\n }\n return flags;\n}\n\nfunction applyRuleOnce(program, store, ruleIndex, context) {\n const rule = program.rules[ruleIndex];\n const bindings = evaluateBody(rule.body, store, {}, context);\n let applications = 0;\n let added = 0;\n if (bindings.length > 0) {\n applications += bindings.length;\n context.perRule[ruleIndex].applications += bindings.length;\n }\n for (const binding of bindings) {\n for (const head of rule.head) {\n const triple = instantiateTriple(head, binding);\n if (!triple) continue;\n if (store.add(triple)) {\n added += 1;\n context.perRule[ruleIndex].added += 1;\n if (!context.inputKeys.has(tripleKey(triple))) context.inferred.push(triple);\n if (context.trace) {\n context.trace.push({\n layer: context.layer,\n iteration: context.iteration,\n rule: rule.name || `rule#${ruleIndex + 1}`,\n triple,\n binding,\n });\n }\n }\n }\n }\n return { applications, added };\n}\n\nfunction evaluateBody(clauses, store, initialBinding = {}, options = {}) {\n let bindings = [initialBinding];\n for (const clause of clauses) {\n const next = [];\n for (const binding of bindings) {\n if (clause.type === 'triple') {\n for (const matched of store.match(clause.triple, binding)) next.push(matched);\n } else if (clause.type === 'path') {\n for (const matched of store.matchPath(clause.triple, binding)) next.push(matched);\n } else if (clause.type === 'filter') {\n try {\n if (booleanValue(evalExpression(clause.expr, binding, options))) next.push(binding);\n } catch (_) {\n // SPARQL-style FILTER errors reject the current solution.\n }\n } else if (clause.type === 'set') {\n try {\n const value = asTerm(evalExpression(clause.expr, binding, options));\n if (!binding[clause.variable]) next.push({ ...binding, [clause.variable]: value });\n else if (termEquals(binding[clause.variable], value)) next.push(binding);\n } catch (_) {\n // The SRL evaluation sketch drops a solution when assignment evaluation errors.\n }\n } else if (clause.type === 'not') {\n const found = evaluateBody(clause.body, store, binding, options);\n if (found.length === 0) next.push(binding);\n } else {\n throw new Error(`Unsupported body clause ${clause.type}`);\n }\n }\n bindings = uniqueBindings(next);\n if (bindings.length === 0) break;\n }\n return bindings;\n}\n\nfunction uniqueBindings(bindings) {\n const seen = new Set();\n const out = [];\n for (const binding of bindings) {\n const key = bindingKey(binding);\n if (!seen.has(key)) {\n seen.add(key);\n out.push(binding);\n }\n }\n return out;\n}\n\nmodule.exports = { evaluate, evaluateBody, uniqueBindings };\n", "src/store.js": "'use strict';\n\nconst { tripleKey, termKey, termEquals, cloneTerm } = require('./term.js');\n\nclass TripleStore {\n constructor(triples = []) {\n this.map = new Map();\n this.byPredicate = new Map();\n this.byPredicateSubject = new Map();\n this.byPredicateObject = new Map();\n for (const triple of triples) this.add(triple);\n }\n\n add(triple) {\n const normalized = normalizeTriple(triple);\n const key = tripleKey(normalized);\n if (this.map.has(key)) return false;\n this.map.set(key, normalized);\n const predicate = termKey(normalized.p);\n const subject = termKey(normalized.s);\n const object = termKey(normalized.o);\n addIndex(this.byPredicate, predicate, key, normalized);\n addNestedIndex(this.byPredicateSubject, predicate, subject, key, normalized);\n addNestedIndex(this.byPredicateObject, predicate, object, key, normalized);\n return true;\n }\n\n has(triple) {\n return this.map.has(tripleKey(normalizeTriple(triple)));\n }\n\n values() {\n return Array.from(this.map.values());\n }\n\n size() {\n return this.map.size;\n }\n\n candidates(pattern, binding = {}) {\n const p = instantiateTerm(pattern.p, binding);\n if (p && p.type !== 'var') {\n const predicate = termKey(p);\n const s = instantiateTerm(pattern.s, binding);\n const o = instantiateTerm(pattern.o, binding);\n const bySubject = s && s.type !== 'var' ? nestedLookup(this.byPredicateSubject, predicate, termKey(s)) : null;\n const byObject = o && o.type !== 'var' ? nestedLookup(this.byPredicateObject, predicate, termKey(o)) : null;\n if (bySubject && byObject) return smallerValues(bySubject, byObject);\n if (bySubject) return Array.from(bySubject.values());\n if (byObject) return Array.from(byObject.values());\n const indexed = this.byPredicate.get(predicate);\n return indexed ? Array.from(indexed.values()) : [];\n }\n return this.values();\n }\n\n match(pattern, binding = {}) {\n const out = [];\n for (const triple of this.candidates(pattern, binding)) {\n const matched = matchTriple(pattern, triple, binding);\n if (matched) out.push(matched);\n }\n return out;\n }\n\n matchPath(pattern, binding = {}) {\n const out = [];\n for (const pair of pathPairs(this, pattern.p)) {\n let next = mergeBindingTerm(binding, pattern.s, pair.s);\n if (!next) continue;\n next = mergeBindingTerm(next, pattern.o, pair.o);\n if (next) out.push(next);\n }\n return out;\n }\n}\n\nfunction addIndex(index, key, tripleKeyValue, triple) {\n if (!index.has(key)) index.set(key, new Map());\n index.get(key).set(tripleKeyValue, triple);\n}\n\nfunction addNestedIndex(index, outerKey, innerKey, tripleKeyValue, triple) {\n if (!index.has(outerKey)) index.set(outerKey, new Map());\n const inner = index.get(outerKey);\n if (!inner.has(innerKey)) inner.set(innerKey, new Map());\n inner.get(innerKey).set(tripleKeyValue, triple);\n}\n\nfunction nestedLookup(index, outerKey, innerKey) {\n const inner = index.get(outerKey);\n return inner ? inner.get(innerKey) || null : null;\n}\n\nfunction smallerValues(left, right) {\n const small = left.size <= right.size ? left : right;\n const large = small === left ? right : left;\n const out = [];\n for (const [key, triple] of small) if (large.has(key)) out.push(triple);\n return out;\n}\n\nfunction normalizeTriple(triple) {\n return { s: cloneTerm(triple.s), p: cloneTerm(triple.p), o: cloneTerm(triple.o) };\n}\n\nfunction bindingKey(binding) {\n return Object.keys(binding).sort().map((name) => `${name}=${termKey(binding[name])}`).join(';');\n}\n\nfunction mergeBindingTerm(binding, patternTerm, dataTerm) {\n if (!patternTerm || !dataTerm) return null;\n if (patternTerm.type === 'var') {\n const name = patternTerm.value;\n if (!binding[name]) return { ...binding, [name]: dataTerm };\n return termEquals(binding[name], dataTerm) ? binding : null;\n }\n if (patternTerm.type === 'triple') {\n if (dataTerm.type !== 'triple') return null;\n let next = mergeBindingTerm(binding, patternTerm.s, dataTerm.s);\n if (!next) return null;\n next = mergeBindingTerm(next, patternTerm.p, dataTerm.p);\n if (!next) return null;\n return mergeBindingTerm(next, patternTerm.o, dataTerm.o);\n }\n return termEquals(patternTerm, dataTerm) ? binding : null;\n}\n\nfunction matchTriple(pattern, triple, binding = {}) {\n let next = mergeBindingTerm(binding, pattern.s, triple.s);\n if (!next) return null;\n next = mergeBindingTerm(next, pattern.p, triple.p);\n if (!next) return null;\n next = mergeBindingTerm(next, pattern.o, triple.o);\n return next;\n}\n\nfunction instantiateTerm(term, binding) {\n if (term.type === 'var') return binding[term.value] || null;\n if (term.type === 'triple') {\n const s = instantiateTerm(term.s, binding);\n const p = instantiateTerm(term.p, binding);\n const o = instantiateTerm(term.o, binding);\n if (!s || !p || !o) return null;\n return { type: 'triple', s, p, o };\n }\n return term;\n}\n\nfunction instantiateTriple(pattern, binding) {\n const s = instantiateTerm(pattern.s, binding);\n const p = instantiateTerm(pattern.p, binding);\n const o = instantiateTerm(pattern.o, binding);\n if (!s || !p || !o) return null;\n if (p.type !== 'iri') return null;\n return { s, p, o };\n}\n\nfunction pathPairs(store, path) {\n if (!path || path.type !== 'path') {\n return store.match({ s: { type: 'var', value: '__s' }, p: path, o: { type: 'var', value: '__o' } })\n .map((binding) => ({ s: binding.__s, o: binding.__o }));\n }\n\n if (path.kind === 'iri') {\n return pathPairs(store, path.iri);\n }\n\n if (path.kind === 'inverse') {\n return pathPairs(store, path.path).map((pair) => ({ s: pair.o, o: pair.s }));\n }\n\n if (path.kind === 'sequence') {\n let pairs = pathPairs(store, path.parts[0]);\n for (const part of path.parts.slice(1)) {\n const right = pathPairs(store, part);\n const joined = [];\n for (const leftPair of pairs) {\n for (const rightPair of right) {\n if (termEquals(leftPair.o, rightPair.s)) joined.push({ s: leftPair.s, o: rightPair.o });\n }\n }\n pairs = uniquePairs(joined);\n }\n return pairs;\n }\n\n throw new Error(`Unsupported path kind ${path.kind}`);\n}\n\nfunction uniquePairs(pairs) {\n const seen = new Set();\n const out = [];\n for (const pair of pairs) {\n const key = `${termKey(pair.s)} ${termKey(pair.o)}`;\n if (!seen.has(key)) {\n seen.add(key);\n out.push(pair);\n }\n }\n return out;\n}\n\nmodule.exports = {\n TripleStore,\n normalizeTriple,\n bindingKey,\n matchTriple,\n instantiateTerm,\n instantiateTriple,\n pathPairs,\n};\n", "src/analyze.js": "'use strict';\n\nconst { compactIRI, iri, variable, termEquals } = require('./term.js');\n\nfunction analyze(program) {\n const diagnostics = [];\n const dependency = dependencyGraph(program);\n\n program.rules.forEach((rule, index) => {\n const name = ruleName(rule, index);\n const label = displayRuleName(name, program.prefixes || {});\n const bound = boundVariables(rule.body);\n const positive = positiveVariables(rule.body);\n const head = new Set();\n for (const triple of rule.head) collectTripleVars(triple, head);\n\n for (const variable of head) {\n if (!bound.has(variable)) {\n diagnostics.push({\n code: 'unsafe-head-variable',\n severity: 'warning',\n rule: name,\n message: `${label} has unbound head variable ?${variable}`,\n });\n }\n }\n\n for (const triple of rule.head) {\n if (triple.p.type !== 'iri' && triple.p.type !== 'var') {\n diagnostics.push({\n code: 'invalid-head-predicate',\n severity: 'error',\n rule: name,\n message: `${label} has a non-IRI/non-variable predicate in the head`,\n });\n }\n }\n\n diagnostics.push(...sequentialWellFormednessDiagnostics(rule.body, name, label));\n\n if (rule.runOnce && recursiveRuleIndexes(dependency).has(index)) {\n diagnostics.push({\n code: 'recursive-assignment-rule',\n severity: 'warning',\n rule: name,\n message: `${label} contains SET and is recursive; assignment rules are run once in Eyeleng`,\n });\n }\n\n });\n\n for (const cycle of dependency.unstratifiedCycles) {\n diagnostics.push({\n code: 'unstratified-negation',\n severity: 'error',\n rules: cycle.rules,\n message: `Unstratified negation through ${cycle.rules.map((name) => displayRuleName(name, program.prefixes || {})).join(' -> ')} using ${cycle.predicate ? compactIRI(cycle.predicate, program.prefixes || {}) : '*'}`,\n });\n }\n\n return {\n warnings: diagnostics.filter((diagnostic) => diagnostic.severity === 'warning'),\n errors: diagnostics.filter((diagnostic) => diagnostic.severity === 'error'),\n diagnostics,\n dependency,\n };\n}\n\nfunction ruleName(rule, index) {\n return rule.name || `rule#${index + 1}`;\n}\n\nfunction displayRuleName(name, prefixes = {}) {\n return /^https?:/.test(name) ? compactIRI(name, prefixes) : name;\n}\n\nfunction dependencyGraph(program) {\n const rules = program.rules.map((rule, index) => {\n const positivePatterns = bodyTriplePatterns(rule.body, false);\n const negativePatterns = bodyTriplePatterns(rule.body, true);\n return {\n index,\n name: ruleName(rule, index),\n headTemplates: rule.head.slice(),\n positivePatterns,\n negativePatterns,\n headPredicates: new Set(rule.head.map((triple) => predicateIRI(triple)).filter(Boolean)),\n positivePredicates: new Set(positivePatterns.flatMap((triple) => predicateIRIs(triple))),\n negativePredicates: new Set(negativePatterns.flatMap((triple) => predicateIRIs(triple))),\n runOnce: !!rule.runOnce,\n };\n });\n\n const edgeMap = new Map();\n function addEdge(from, to, negative, predicate) {\n const label = predicate || '*';\n const key = `${from.index}->${to.index}:${label}`;\n const existing = edgeMap.get(key);\n if (existing) {\n existing.negative = existing.negative || negative;\n return;\n }\n edgeMap.set(key, { from: from.index, to: to.index, negative, predicate });\n }\n\n const headIndex = buildHeadTemplateIndex(rules);\n\n for (const from of rules) {\n for (const pattern of from.positivePatterns) {\n for (const candidate of candidateHeadTemplates(headIndex, pattern)) {\n if (canPossiblyGenerate(candidate.template, pattern)) addEdge(from, rules[candidate.ruleIndex], false, dependencyPredicateLabel(pattern));\n }\n }\n for (const pattern of from.negativePatterns) {\n for (const candidate of candidateHeadTemplates(headIndex, pattern)) {\n if (canPossiblyGenerate(candidate.template, pattern)) addEdge(from, rules[candidate.ruleIndex], true, dependencyPredicateLabel(pattern));\n }\n }\n }\n\n const edges = Array.from(edgeMap.values()).sort((a, b) => a.from - b.from || a.to - b.to || String(a.predicate || '').localeCompare(String(b.predicate || '')));\n\n const components = stronglyConnectedComponents(rules.length, edges);\n const componentOf = new Map();\n components.forEach((component, index) => {\n for (const ruleIndex of component) componentOf.set(ruleIndex, index);\n });\n\n const unstratifiedCycles = [];\n const seen = new Set();\n for (const edge of edges) {\n if (!edge.negative) continue;\n if (edge.from === edge.to && rules[edge.from] && rules[edge.from].runOnce) continue;\n if (componentOf.get(edge.from) !== componentOf.get(edge.to)) continue;\n const component = components[componentOf.get(edge.from)];\n const key = `${component.slice().sort((a, b) => a - b).join(',')}|${edge.predicate || '*'}`;\n if (seen.has(key)) continue;\n seen.add(key);\n unstratifiedCycles.push({\n predicate: edge.predicate,\n rules: component.map((ruleIndex) => rules[ruleIndex].name),\n });\n }\n\n const layers = stratificationLayers(rules.length, components, componentOf, edges);\n\n return {\n rules: rules.map((rule) => ({\n index: rule.index,\n name: rule.name,\n headPredicates: Array.from(rule.headPredicates),\n positivePredicates: Array.from(rule.positivePredicates),\n negativePredicates: Array.from(rule.negativePredicates),\n runOnce: rule.runOnce,\n })),\n edges,\n components: components.map((component) => component.map((ruleIndex) => rules[ruleIndex].name)),\n layers: layers.map((layer) => layer.map((ruleIndex) => rules[ruleIndex].name)),\n layerIndexes: layers,\n unstratifiedCycles,\n };\n}\n\nfunction buildHeadTemplateIndex(rules) {\n const templates = [];\n const positions = ['s', 'p', 'o'];\n const byPosition = {\n s: new Map(),\n p: new Map(),\n o: new Map(),\n };\n const flexibleByPosition = {\n s: new Set(),\n p: new Set(),\n o: new Set(),\n };\n\n for (const rule of rules) {\n for (const template of rule.headTemplates) {\n const entry = { id: templates.length, ruleIndex: rule.index, template };\n templates.push(entry);\n for (const position of positions) {\n const key = fixedTermIndexKey(template[position]);\n if (key === null) flexibleByPosition[position].add(entry.id);\n else {\n let bucket = byPosition[position].get(key);\n if (!bucket) {\n bucket = new Set();\n byPosition[position].set(key, bucket);\n }\n bucket.add(entry.id);\n }\n }\n }\n }\n\n return { templates, byPosition, flexibleByPosition };\n}\n\nfunction candidateHeadTemplates(index, pattern) {\n const positions = ['s', 'p', 'o'];\n let selected = null;\n\n for (const position of positions) {\n const key = fixedTermIndexKey(pattern[position]);\n if (key === null) continue;\n const exact = index.byPosition[position].get(key) || null;\n const flexible = index.flexibleByPosition[position];\n const estimatedSize = (exact ? exact.size : 0) + flexible.size;\n if (selected === null || estimatedSize < selected.estimatedSize) selected = { exact, flexible, estimatedSize };\n if (estimatedSize === 0) break;\n }\n\n if (selected === null) return index.templates;\n const ids = [];\n if (selected.exact) for (const id of selected.exact) ids.push(id);\n for (const id of selected.flexible) ids.push(id);\n const out = [];\n const seen = new Set();\n for (const id of ids) {\n if (seen.has(id)) continue;\n seen.add(id);\n out.push(index.templates[id]);\n }\n return out;\n}\n\nfunction fixedTermIndexKey(term) {\n if (!term) return null;\n if (term.type === 'var') return null;\n if (term.type === 'path') return null;\n if (term.type === 'triple' && containsVariableTerm(term)) return null;\n return termIndexKey(term);\n}\n\nfunction containsVariableTerm(term) {\n if (!term) return false;\n if (term.type === 'var') return true;\n if (term.type === 'triple') return containsVariableTerm(term.s) || containsVariableTerm(term.p) || containsVariableTerm(term.o);\n if (term.type === 'path') {\n if (term.kind === 'inverse') return containsVariableTerm(term.path);\n if (term.kind === 'sequence') return term.parts.some(containsVariableTerm);\n }\n return false;\n}\n\nfunction termIndexKey(term) {\n if (!term) return 'null';\n if (term.type === 'iri') return `I:${term.value}`;\n if (term.type === 'blank') return `B:${term.value}`;\n if (term.type === 'literal') return `L:${JSON.stringify(term.value)}^^${term.datatype || ''}@${term.lang || ''}--${term.langDir || ''}`;\n if (term.type === 'triple') return `T:${termIndexKey(term.s)} ${termIndexKey(term.p)} ${termIndexKey(term.o)}`;\n return JSON.stringify(term);\n}\n\nfunction unionSets(a, b) {\n const out = new Set();\n if (a) for (const value of a) out.add(value);\n if (b) for (const value of b) out.add(value);\n return out;\n}\n\nfunction allTemplateIds(length) {\n const out = new Set();\n for (let i = 0; i < length; i += 1) out.add(i);\n return out;\n}\n\nfunction stratificationLayers(ruleCount, components, componentOf, edges) {\n if (ruleCount === 0) return [];\n const outgoing = Array.from({ length: components.length }, () => new Set());\n const indegree = Array(components.length).fill(0);\n\n for (const edge of edges) {\n const dependent = componentOf.get(edge.from);\n const dependency = componentOf.get(edge.to);\n if (dependent === dependency) continue;\n // Rule edge means \"from depends on to\". Evaluation must run dependency before dependent.\n if (!outgoing[dependency].has(dependent)) {\n outgoing[dependency].add(dependent);\n indegree[dependent] += 1;\n }\n }\n\n let ready = [];\n for (let i = 0; i < indegree.length; i += 1) if (indegree[i] === 0) ready.push(i);\n const layers = [];\n const emitted = new Set();\n while (ready.length > 0) {\n ready.sort((a, b) => componentMin(components[a]) - componentMin(components[b]));\n const layerComponents = ready;\n ready = [];\n const layer = [];\n for (const componentIndex of layerComponents) {\n emitted.add(componentIndex);\n layer.push(...components[componentIndex]);\n for (const next of outgoing[componentIndex]) {\n indegree[next] -= 1;\n if (indegree[next] === 0) ready.push(next);\n }\n }\n layers.push(layer.sort((a, b) => a - b));\n }\n\n if (emitted.size !== components.length) return [Array.from({ length: ruleCount }, (_, i) => i)];\n return layers;\n}\n\n\nfunction componentMin(component) {\n let min = Infinity;\n for (const value of component) if (value < min) min = value;\n return min;\n}\n\nfunction recursiveRuleIndexes(dependency) {\n const out = new Set();\n for (const component of dependency.components) {\n if (component.length <= 1) continue;\n for (const name of component) {\n const rule = dependency.rules.find((item) => item.name === name);\n if (rule) out.add(rule.index);\n }\n }\n for (const edge of dependency.edges) {\n const rule = dependency.rules.find((item) => item.index === edge.from);\n if (edge.from === edge.to && !(edge.negative && rule && rule.runOnce)) out.add(edge.from);\n }\n return out;\n}\n\nfunction stronglyConnectedComponents(size, edges) {\n const adjacency = Array.from({ length: size }, () => []);\n const reverse = Array.from({ length: size }, () => []);\n for (const edge of edges) {\n adjacency[edge.from].push(edge.to);\n reverse[edge.to].push(edge.from);\n }\n\n const visited = Array(size).fill(false);\n const order = [];\n for (let start = 0; start < size; start += 1) {\n if (visited[start]) continue;\n const stack = [[start, 0]];\n visited[start] = true;\n while (stack.length > 0) {\n const frame = stack[stack.length - 1];\n const v = frame[0];\n let nextIndex = frame[1];\n if (nextIndex < adjacency[v].length) {\n const w = adjacency[v][nextIndex];\n frame[1] = nextIndex + 1;\n if (!visited[w]) {\n visited[w] = true;\n stack.push([w, 0]);\n }\n } else {\n order.push(v);\n stack.pop();\n }\n }\n }\n\n const assigned = Array(size).fill(false);\n const components = [];\n for (let i = order.length - 1; i >= 0; i -= 1) {\n const start = order[i];\n if (assigned[start]) continue;\n const component = [];\n const stack = [start];\n assigned[start] = true;\n while (stack.length > 0) {\n const v = stack.pop();\n component.push(v);\n for (const w of reverse[v]) {\n if (!assigned[w]) {\n assigned[w] = true;\n stack.push(w);\n }\n }\n }\n components.push(component.sort((a, b) => a - b));\n }\n return components;\n}\n\nfunction sequentialWellFormednessDiagnostics(clauses, ruleNameValue, label) {\n const diagnostics = [];\n\n function visit(items, initialBound, scopeLabel) {\n const bound = new Set(initialBound);\n for (const clause of items) {\n if (clause.type === 'triple' || clause.type === 'path') {\n collectTripleVars(clause.triple, bound);\n } else if (clause.type === 'filter') {\n for (const variable of expressionVariables(clause.expr)) {\n if (!bound.has(variable)) {\n diagnostics.push({\n code: 'unbound-filter-variable',\n severity: 'error',\n rule: ruleNameValue,\n message: `${label} FILTER uses ?${variable} before it is bound${scopeLabel}`,\n });\n }\n }\n } else if (clause.type === 'set') {\n if (bound.has(clause.variable)) {\n diagnostics.push({\n code: 'assignment-variable-already-bound',\n severity: 'error',\n rule: ruleNameValue,\n message: `${label} SET assigns ?${clause.variable}, but that variable is already bound${scopeLabel}`,\n });\n }\n for (const variable of expressionVariables(clause.expr)) {\n if (!bound.has(variable)) {\n diagnostics.push({\n code: 'unbound-assignment-variable',\n severity: 'error',\n rule: ruleNameValue,\n message: `${label} SET expression uses ?${variable} before it is bound${scopeLabel}`,\n });\n }\n }\n bound.add(clause.variable);\n } else if (clause.type === 'not') {\n visit(clause.body, bound, ' inside NOT');\n }\n }\n return bound;\n }\n\n visit(clauses, new Set(), '');\n return diagnostics;\n}\n\nfunction bodyTriplePatterns(clauses, wantNegative, inNegativeContext = false) {\n const out = [];\n for (const clause of clauses) {\n if ((clause.type === 'triple' || clause.type === 'path') && wantNegative === inNegativeContext) {\n if (clause.type === 'path') out.push(...pathTriplePatterns(clause.triple));\n else out.push(clause.triple);\n } else if (clause.type === 'not') {\n out.push(...bodyTriplePatterns(clause.body, wantNegative, true));\n }\n }\n return out;\n}\n\nfunction pathTriplePatterns(triple) {\n const predicates = predicateIRIs(triple);\n if (predicates.length === 0) return [];\n return predicates.map((predicate, index) => ({\n s: variable(`__path_s_${index}`),\n p: iri(predicate),\n o: variable(`__path_o_${index}`),\n }));\n}\n\nfunction dependencyPredicateLabel(pattern) {\n return pattern && pattern.p && pattern.p.type === 'iri' ? pattern.p.value : null;\n}\n\nfunction canPossiblyGenerate(template, pattern) {\n if (!template || !pattern) return false;\n if (!compatibleTerm(template.s, pattern.s)) return false;\n if (!compatibleTerm(template.p, pattern.p)) return false;\n if (!compatibleTerm(template.o, pattern.o)) return false;\n\n const constraints = new Map();\n if (!recordTemplateVariableConstraints(template.s, pattern.s, constraints)) return false;\n if (!recordTemplateVariableConstraints(template.p, pattern.p, constraints)) return false;\n if (!recordTemplateVariableConstraints(template.o, pattern.o, constraints)) return false;\n return true;\n}\n\nfunction compatibleTerm(templateTerm, patternTerm) {\n if (!templateTerm || !patternTerm) return false;\n if (templateTerm.type === 'var' || patternTerm.type === 'var') return true;\n if (templateTerm.type === 'triple' || patternTerm.type === 'triple') {\n if (templateTerm.type !== 'triple' || patternTerm.type !== 'triple') return false;\n return compatibleTerm(templateTerm.s, patternTerm.s)\n && compatibleTerm(templateTerm.p, patternTerm.p)\n && compatibleTerm(templateTerm.o, patternTerm.o);\n }\n return termEquals(templateTerm, patternTerm);\n}\n\nfunction recordTemplateVariableConstraints(templateTerm, patternTerm, constraints) {\n if (!templateTerm || !patternTerm) return false;\n if (templateTerm.type === 'var') {\n const existing = constraints.get(templateTerm.value);\n if (!existing) {\n constraints.set(templateTerm.value, patternTerm);\n return true;\n }\n return possiblySameTerm(existing, patternTerm);\n }\n if (templateTerm.type === 'triple' && patternTerm.type === 'triple') {\n return recordTemplateVariableConstraints(templateTerm.s, patternTerm.s, constraints)\n && recordTemplateVariableConstraints(templateTerm.p, patternTerm.p, constraints)\n && recordTemplateVariableConstraints(templateTerm.o, patternTerm.o, constraints);\n }\n return true;\n}\n\nfunction possiblySameTerm(a, b) {\n if (!a || !b) return false;\n if (a.type === 'var' || b.type === 'var') return true;\n if (a.type === 'triple' || b.type === 'triple') {\n if (a.type !== 'triple' || b.type !== 'triple') return false;\n return possiblySameTerm(a.s, b.s) && possiblySameTerm(a.p, b.p) && possiblySameTerm(a.o, b.o);\n }\n return termEquals(a, b);\n}\n\nfunction bodyPredicates(clauses, wantNegative, inNegativeContext = false) {\n const out = [];\n for (const clause of clauses) {\n if ((clause.type === 'triple' || clause.type === 'path') && wantNegative === inNegativeContext) {\n out.push(...predicateIRIs(clause.triple));\n } else if (clause.type === 'not') {\n out.push(...bodyPredicates(clause.body, wantNegative, true));\n }\n }\n return out;\n}\n\nfunction predicateIRI(triple) {\n return triple && triple.p && triple.p.type === 'iri' ? triple.p.value : null;\n}\n\nfunction predicateIRIs(triple) {\n if (!triple || !triple.p) return [];\n if (triple.p.type === 'iri') return [triple.p.value];\n if (triple.p.type === 'path') return pathPredicateIRIs(triple.p);\n return [];\n}\n\nfunction pathPredicateIRIs(path) {\n if (!path) return [];\n if (path.type === 'iri') return [path.value];\n if (path.type !== 'path') return [];\n if (path.kind === 'inverse') return pathPredicateIRIs(path.path);\n if (path.kind === 'sequence') return path.parts.flatMap(pathPredicateIRIs);\n if (path.kind === 'iri') return pathPredicateIRIs(path.iri);\n return [];\n}\n\nfunction boundVariables(clauses) {\n const vars = new Set();\n for (const clause of clauses) {\n if (clause.type === 'triple' || clause.type === 'path') collectTripleVars(clause.triple, vars);\n if (clause.type === 'set') vars.add(clause.variable);\n }\n return vars;\n}\n\nfunction positiveVariables(clauses) {\n const vars = new Set();\n for (const clause of clauses) {\n if (clause.type === 'triple' || clause.type === 'path') collectTripleVars(clause.triple, vars);\n if (clause.type === 'set') vars.add(clause.variable);\n if (clause.type === 'filter') for (const v of expressionVariables(clause.expr)) vars.add(v);\n }\n return vars;\n}\n\nfunction bodyVariables(clauses) {\n const vars = new Set();\n for (const clause of clauses) {\n if (clause.type === 'triple' || clause.type === 'path') collectTripleVars(clause.triple, vars);\n if (clause.type === 'set') {\n vars.add(clause.variable);\n for (const v of expressionVariables(clause.expr)) vars.add(v);\n }\n if (clause.type === 'filter') for (const v of expressionVariables(clause.expr)) vars.add(v);\n if (clause.type === 'not') for (const v of bodyVariables(clause.body)) vars.add(v);\n }\n return vars;\n}\n\nfunction collectTripleVars(triple, vars) {\n for (const term of [triple.s, triple.p, triple.o]) collectTermVars(term, vars);\n}\n\nfunction collectTermVars(term, vars) {\n if (!term) return;\n if (term.type === 'var') vars.add(term.value);\n if (term.type === 'triple') {\n collectTermVars(term.s, vars);\n collectTermVars(term.p, vars);\n collectTermVars(term.o, vars);\n }\n if (term.type === 'path') {\n if (term.kind === 'inverse') collectTermVars(term.path, vars);\n if (term.kind === 'sequence') for (const part of term.parts) collectTermVars(part, vars);\n }\n}\n\nfunction expressionVariables(expr, vars = new Set()) {\n if (!expr) return vars;\n if (expr.type === 'var') vars.add(expr.name);\n else if (expr.type === 'unary') expressionVariables(expr.expr, vars);\n else if (expr.type === 'binary') {\n expressionVariables(expr.left, vars);\n expressionVariables(expr.right, vars);\n } else if (expr.type === 'call') {\n for (const arg of expr.args) expressionVariables(arg, vars);\n } else if (expr.type === 'list') {\n for (const item of expr.items) expressionVariables(item, vars);\n } else if (expr.type === 'term') {\n collectTermVars(expr.value, vars);\n }\n return vars;\n}\n\nmodule.exports = {\n analyze,\n dependencyGraph,\n stratificationLayers,\n boundVariables,\n positiveVariables,\n bodyVariables,\n collectTripleVars,\n expressionVariables,\n pathPredicateIRIs,\n bodyTriplePatterns,\n canPossiblyGenerate,\n};\n", "src/format.js": "'use strict';\n\nconst { formatTriple, formatTerm } = require('./term.js');\n\nfunction sortTriples(triples, prefixes = {}) {\n return triples\n .map((triple) => ({ triple, text: formatTriple(triple, prefixes) }))\n .sort((a, b) => a.text.localeCompare(b.text))\n .map((entry) => entry.triple);\n}\n\nfunction formatTriples(triples, prefixes = {}) {\n return triples\n .map((triple) => formatTriple(triple, prefixes))\n .sort((a, b) => a.localeCompare(b))\n .join('\\n');\n}\n\nfunction formatTrace(trace, prefixes = {}) {\n return trace.map((entry) => `#${entry.iteration} ${entry.rule} => ${formatTriple(entry.triple, prefixes)}`).join('\\n');\n}\n\nfunction formatBindings(bindings, prefixes = {}, select = null) {\n const columns = select && select.length > 0 ? select : inferColumns(bindings);\n return bindings\n .slice()\n .sort((a, b) => formatBinding(a, prefixes, columns).localeCompare(formatBinding(b, prefixes, columns)))\n .map((binding) => formatBinding(binding, prefixes, columns))\n .join('\\n');\n}\n\nfunction formatBinding(binding, prefixes = {}, columns = null) {\n const names = columns || Object.keys(binding).sort();\n if (names.length === 0) return 'true';\n return names.map((name) => `?${name} = ${binding[name] ? formatTerm(binding[name], prefixes) : 'UNDEF'}`).join('; ');\n}\n\nfunction inferColumns(bindings) {\n const columns = new Set();\n for (const binding of bindings) for (const name of Object.keys(binding)) columns.add(name);\n return Array.from(columns).sort();\n}\n\nfunction toJSON(result, options = {}) {\n const triples = options.all ? result.closure : result.inferred;\n const json = {\n baseIRI: result.baseIRI || null,\n iterations: result.iterations,\n ruleApplications: result.ruleApplications,\n perRule: result.perRule,\n prefixes: result.prefixes,\n diagnostics: result.diagnostics || [],\n triples: sortTriples(triples, result.prefixes),\n trace: options.trace ? result.trace : undefined,\n };\n if (result.query) json.query = result.query;\n if (result.analysis && options.analysis) json.analysis = result.analysis;\n return json;\n}\n\nmodule.exports = { sortTriples, formatTriples, formatTrace, formatBindings, formatBinding, toJSON };\n", "src/query.js": "'use strict';\n\nconst { parseQuery } = require('./parser.js');\nconst { TripleStore, bindingKey } = require('./store.js');\nconst { evaluateBody } = require('./engine.js');\n\nfunction queryResult(result, querySpec, options = {}) {\n const store = new TripleStore(result.closure || []);\n const bindings = evaluateBody(querySpec.body, store, {}, options);\n const select = normalizeSelect(querySpec.select, bindings);\n return {\n baseIRI: result.baseIRI,\n prefixes: result.prefixes,\n select,\n bindings: projectBindings(bindings, select),\n };\n}\n\nfunction runQuery(source, querySource = null, options = {}) {\n const { run, compile } = require('./api.js');\n const { program, diagnostics } = compile(source, options);\n const result = run(program, options);\n result.diagnostics = diagnostics;\n\n let querySpec;\n if (querySource) querySpec = parseQuery(querySource, { ...options, prefixes: program.prefixes, baseIRI: program.baseIRI });\n else throw new Error('No query supplied. Use --query or --query-file with a raw body pattern.');\n\n const query = queryResult(result, querySpec, options);\n return { ...result, query };\n}\n\nfunction normalizeSelect(select, bindings) {\n if (select && select.length > 0) return select.slice();\n const vars = new Set();\n for (const binding of bindings) for (const key of Object.keys(binding)) vars.add(key);\n return Array.from(vars).sort();\n}\n\nfunction projectBindings(bindings, select) {\n const seen = new Set();\n const out = [];\n for (const binding of bindings) {\n const projected = {};\n for (const name of select) if (binding[name]) projected[name] = binding[name];\n const key = bindingKey(projected);\n if (!seen.has(key)) {\n seen.add(key);\n out.push(projected);\n }\n }\n return out;\n}\n\nmodule.exports = { runQuery, queryResult, parseQuery, normalizeSelect, projectBindings };\n"};
489
- window.__EYELENG_MAPPINGS__ = {"src/tokenizer.js": {}, "src/term.js": {}, "src/builtins.js": {"./term.js": "src/term.js"}, "src/parser.js": {"./tokenizer.js": "src/tokenizer.js", "./builtins.js": "src/builtins.js", "./term.js": "src/term.js"}, "src/rdfSyntax.js": {"./tokenizer.js": "src/tokenizer.js", "./term.js": "src/term.js"}, "src/store.js": {"./term.js": "src/term.js"}, "src/analyze.js": {"./term.js": "src/term.js"}, "src/engine.js": {"./store.js": "src/store.js", "./term.js": "src/term.js", "./builtins.js": "src/builtins.js", "./analyze.js": "src/analyze.js"}, "src/format.js": {"./term.js": "src/term.js"}, "src/query.js": {"./parser.js": "src/parser.js", "./store.js": "src/store.js", "./engine.js": "src/engine.js", "./api.js": "src/api.js"}, "src/api.js": {"./parser.js": "src/parser.js", "./rdfSyntax.js": "src/rdfSyntax.js", "./engine.js": "src/engine.js", "./analyze.js": "src/analyze.js", "./format.js": "src/format.js", "./query.js": "src/query.js"}};
490
- window.__EYELENG_EXAMPLES__ = {"family.srl": "PREFIX : <http://example/>\n\nDATA {\n :A :fatherOf :X .\n :B :motherOf :X .\n :C :motherOf :A .\n}\n\nRULE { ?x :childOf ?y } WHERE { ?y :fatherOf ?x }\nRULE { ?x :childOf ?y } WHERE { ?y :motherOf ?x }\nRULE { ?x :descendedFrom ?y } WHERE { ?x :childOf ?y }\nRULE { ?x :descendedFrom ?y } WHERE { ?x :childOf ?z . ?z :descendedFrom ?y }\n", "spec-2-2-recursion.srl": "PREFIX : <http://example.com/>\n\nDATA {\n :A :fatherOf :X .\n :B :motherOf :X .\n :C :motherOf :A .\n}\n\nRULE { ?x :childOf ?y } WHERE { ?y :fatherOf ?x }\nRULE { ?x :childOf ?y } WHERE { ?y :motherOf ?x }\nRULE { ?x :descendedFrom ?y } WHERE { ?x :childOf ?y }\nRULE { ?x :descendedFrom ?y } WHERE { ?x :childOf ?z . ?z :descendedFrom ?y }\n", "bmi.srl": "# Adapted from eyeling/examples/bmi.n3\n# Normalizes metric input, computes BMI, assigns an adult BMI category, and\n# derives a healthy-weight band for the given height.\n\nPREFIX : <http://example/eyeling/bmi/>\n\nDATA {\n :Input :unitSystem \"metric\" ; :weight 72.0 ; :height 178.0 .\n}\n\nRULE { :Case :weightKg ?weight ; :heightM ?heightM }\nWHERE {\n :Input :unitSystem \"metric\" ; :weight ?weight ; :height ?heightCm .\n SET(?heightM := ?heightCm / 100.0)\n}\n\nRULE {\n :Case :heightSquared ?heightSquared ;\n :bmi ?bmi ;\n :bmiRounded ?bmiRounded ;\n :healthyMinKg ?healthyMin ;\n :healthyMaxKg ?healthyMax ;\n :healthyMinKgRounded ?healthyMinRounded ;\n :healthyMaxKgRounded ?healthyMaxRounded .\n}\nWHERE {\n :Case :weightKg ?weight ; :heightM ?heightM .\n SET(?heightSquared := ?heightM * ?heightM)\n SET(?bmi := ?weight / ?heightSquared)\n SET(?bmiRounded := ROUND(?bmi * 100.0) / 100.0)\n SET(?healthyMin := 18.5 * ?heightSquared)\n SET(?healthyMax := 24.9 * ?heightSquared)\n SET(?healthyMinRounded := ROUND(?healthyMin * 10.0) / 10.0)\n SET(?healthyMaxRounded := ROUND(?healthyMax * 10.0) / 10.0)\n}\n\nRULE { :Decision :category \"Underweight\" }\nWHERE { :Case :bmi ?bmi . FILTER(?bmi < 18.5) }\n\nRULE { :Decision :category \"Normal\" }\nWHERE { :Case :bmi ?bmi . FILTER(?bmi >= 18.5 && ?bmi < 25.0) }\n\nRULE { :Decision :category \"Overweight\" }\nWHERE { :Case :bmi ?bmi . FILTER(?bmi >= 25.0 && ?bmi < 30.0) }\n\nRULE { :Decision :category \"Obesity I\" }\nWHERE { :Case :bmi ?bmi . FILTER(?bmi >= 30.0 && ?bmi < 35.0) }\n\nRULE { :Decision :category \"Obesity II\" }\nWHERE { :Case :bmi ?bmi . FILTER(?bmi >= 35.0 && ?bmi < 40.0) }\n\nRULE { :Decision :category \"Obesity III\" }\nWHERE { :Case :bmi ?bmi . FILTER(?bmi >= 40.0) }\n\nRULE {\n :Answer :bmi ?bmiRounded ;\n :category ?category ;\n :healthyMinKg ?healthyMinRounded ;\n :healthyMaxKg ?healthyMaxRounded .\n}\nWHERE {\n :Case :bmiRounded ?bmiRounded ;\n :healthyMinKgRounded ?healthyMinRounded ;\n :healthyMaxKgRounded ?healthyMaxRounded .\n :Decision :category ?category .\n}\n", "stratified-negation.srl": "PREFIX : <http://example/>\n\nDATA {\n :alice a :Person ; :blocked true .\n :bob a :Person .\n :carol a :Person ; :flagged true .\n}\n\n# This rule is intentionally written before the producer of :blocked.\n# Stratified evaluation must still run the :blocked-producing rule first.\nRULE { ?x :eligible true }\nWHERE { ?x a :Person . NOT { ?x :blocked true } }\n\nRULE { ?x :blocked true }\nWHERE { ?x :flagged true }\n", "spec-builtins.srl": "PREFIX : <http://example/>\nPREFIX xsd: <http://www.w3.org/2001/XMLSchema#>\n\nDATA {\n :event :when \"2026-05-15T10:20:30Z\"^^xsd:dateTime .\n}\n\nRULE { :event :year ?year ; :month ?month ; :blank ?blank ; :tripleSubject ?subject }\nWHERE {\n :event :when ?when .\n SET(?year := YEAR(?when))\n SET(?month := MONTH(?when))\n SET(?blank := BNODE(\"event\"))\n SET(?triple := TRIPLE(:subject, :predicate, :object))\n SET(?subject := SUBJECT(?triple))\n}\n", "spec-4-2-rdf-rules-syntax.ttl": "# Captured from the SHACL 1.2 Rules draft section 4.2.\n# This is RDF Rules syntax in Turtle and is executable by Eyeleng.\n\nPREFIX : <http://example/>\nPREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>\nPREFIX sh: <http://www.w3.org/ns/shacl#>\nPREFIX srl: <http://www.w3.org/ns/shacl-rules#>\nPREFIX sparql: <http://www.w3.org/ns/sparql#>\n\n:ruleSet-1\n rdf:type srl:RuleSet;\n srl:data (\n [ srl:subject :x ; srl:predicate :p ; srl:object 1 ]\n [ srl:subject :x ; srl:predicate :q ; srl:object 2 ]\n );\n srl:rules (\n [\n rdf:type srl:Rule;\n srl:head (\n [ srl:subject [ srl:varName \"x\" ] ; srl:predicate :bothPositive ; srl:object true ]\n ) ;\n srl:body (\n [ srl:subject [ srl:varName \"x\" ]; srl:predicate :p ; srl:object [ srl:varName \"v1\" ] ]\n [ srl:expr [ sparql:greaterThan ( [ srl:varName \"v1\" ] 0 ) ] ]\n [ srl:subject [ srl:varName \"x\" ] ; srl:predicate :q ; srl:object [ srl:varName \"v2\" ] ]\n [ srl:expr [ sparql:greaterThan ( [ srl:varName \"v2\" ] 0 ) ] ]\n );\n ]\n [\n rdf:type srl:Rule;\n srl:head (\n [ srl:subject [ srl:varName \"x\" ] ; srl:predicate :oneIsZero ; srl:object true ]\n ) ;\n srl:body (\n [ srl:subject [ srl:varName \"x\" ] ; srl:predicate :p ; srl:object [ srl:varName \"v1\" ] ]\n [ srl:subject [ srl:varName \"x\" ] ; srl:predicate :q ; srl:object [ srl:varName \"v2\" ] ]\n [ srl:filter [ sparql:function-or (\n [ sparql:equals ( [ srl:varName \"v1\" ] 0 ) ]\n [ sparql:equals ( [ srl:varName \"v2\" ] 0 ) ]\n ) ]\n ]\n );\n ]\n ) .\n", "basic-ruleset.ttl": "PREFIX : <http://example/>\nPREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>\nPREFIX srl: <http://www.w3.org/ns/shacl-rules#>\n\n:familyRules a srl:RuleSet ;\n srl:data (\n [ srl:subject :alice ; srl:predicate :parentOf ; srl:object :bob ]\n [ srl:subject :bob ; srl:predicate :parentOf ; srl:object :carol ]\n ) ;\n srl:rules (\n [ a srl:Rule ;\n srl:body (\n [ srl:subject [ srl:varName \"x\" ] ; srl:predicate :parentOf ; srl:object [ srl:varName \"y\" ] ]\n ) ;\n srl:head (\n [ srl:subject [ srl:varName \"y\" ] ; srl:predicate :childOf ; srl:object [ srl:varName \"x\" ] ]\n )\n ]\n [ a srl:Rule ;\n srl:body (\n [ srl:subject [ srl:varName \"x\" ] ; srl:predicate :parentOf ; srl:object [ srl:varName \"y\" ] ]\n ) ;\n srl:head (\n [ srl:subject [ srl:varName \"x\" ] ; srl:predicate :ancestorOf ; srl:object [ srl:varName \"y\" ] ]\n )\n ]\n [ a srl:Rule ;\n srl:body (\n [ srl:subject [ srl:varName \"x\" ] ; srl:predicate :parentOf ; srl:object [ srl:varName \"y\" ] ]\n [ srl:subject [ srl:varName \"y\" ] ; srl:predicate :ancestorOf ; srl:object [ srl:varName \"z\" ] ]\n ) ;\n srl:head (\n [ srl:subject [ srl:varName \"x\" ] ; srl:predicate :ancestorOf ; srl:object [ srl:varName \"z\" ] ]\n )\n ]\n ) .\n"};
489
+ window.__EYELENG_MODULES__ = {"src/api.js":"'use strict';\n\nconst { parse, parseQuery } = require('./parser.js');\nconst { parseRdfSyntax, parseRdfDocument, rdfDocumentToProgram, looksLikeRdfRules } = require('./rdfSyntax.js');\nconst { parseRdfMessageLog, looksLikeRdfMessageLog } = require('./rdfMessages.js');\nconst { evaluate } = require('./engine.js');\nconst { analyze } = require('./analyze.js');\nconst { formatTriples, sortTriples, toJSON, formatTrace, formatProof, formatBindings } = require('./format.js');\nconst { runQuery, queryResult, queryProgram, queryRunOptions, shouldUseHybridForQuery } = require('./query.js');\nconst { resultTriples } = require('./output.js');\n\nfunction parseInput(source, options = {}) {\n if (typeof source !== 'string') return source;\n if (looksLikeRdfMessageLog(source, options)) return parseRdfMessageLog(source, options);\n return looksLikeRdfRules(source, options) ? parseRdfSyntax(source, options) : parse(source, options);\n}\n\nfunction compile(source, options = {}) {\n const parsed = parseInput(source, options);\n const program = options.resolveImports === false ? parsed : resolveImports(parsed, options);\n const analysis = analyze(program, options);\n const diagnostics = analysis.diagnostics;\n const fatal = analysis.errors.length > 0 || (options.strict && analysis.warnings.length > 0);\n if (fatal && options.throwOnDiagnostics !== false) {\n const details = diagnostics.map((diagnostic) => diagnostic.message).join('; ');\n throw new Error(`${analysis.errors.length > 0 ? 'Analysis failed' : 'Strict mode failed'}: ${details}`);\n }\n return { program, diagnostics, analysis };\n}\n\nfunction resolveImports(program, options = {}, seen = new Set()) {\n if (!program.imports || program.imports.length === 0) return cloneProgram(program);\n const importResolver = options.importResolver;\n if (!importResolver) return cloneProgram(program);\n\n let merged = emptyProgram(program);\n const localKey = program.baseIRI || options.filename || '<input>';\n if (localKey) seen.add(localKey);\n\n for (const target of program.imports) {\n if (seen.has(target)) continue;\n seen.add(target);\n const resolved = importResolver(target, { from: program.baseIRI || options.filename || null, seen });\n if (!resolved) throw new Error(`IMPORTS resolver returned no source for ${target}`);\n const importSource = typeof resolved === 'string' ? resolved : resolved.source;\n const importOptions = typeof resolved === 'string' ? {} : (resolved.options || {});\n const parsedImport = parseInput(importSource, { ...options, ...importOptions, baseIRI: importOptions.baseIRI || target, filename: importOptions.filename || target });\n const imported = resolveImports(parsedImport, { ...options, ...importOptions, importResolver }, seen);\n merged = mergePrograms(merged, imported);\n }\n\n return mergePrograms(merged, program);\n}\n\nfunction emptyProgram(program = {}) {\n return {\n baseIRI: program.baseIRI || null,\n version: program.version || null,\n imports: [],\n prefixes: { ...(program.prefixes || {}) },\n data: [],\n rules: [],\n };\n}\n\nfunction cloneProgram(program) {\n return {\n baseIRI: program.baseIRI || null,\n version: program.version || null,\n imports: (program.imports || []).slice(),\n prefixes: { ...(program.prefixes || {}) },\n data: (program.data || []).slice(),\n rules: (program.rules || []).slice(),\n };\n}\n\nfunction mergePrograms(left, right) {\n return {\n baseIRI: right.baseIRI || left.baseIRI || null,\n version: right.version || left.version || null,\n imports: Array.from(new Set([...(left.imports || []), ...(right.imports || [])])),\n prefixes: { ...(left.prefixes || {}), ...(right.prefixes || {}) },\n data: [...(left.data || []), ...(right.data || [])],\n rules: [...(left.rules || []), ...(right.rules || [])],\n };\n}\n\nfunction run(source, options = {}) {\n const { program, diagnostics, analysis } = compile(source, options);\n const result = evaluate(program, { ...options, analysis });\n result.diagnostics = diagnostics;\n result.analysis = analysis;\n return result;\n}\n\nfunction runToString(source, options = {}) {\n const { program, diagnostics, analysis } = compile(source, options);\n const result = evaluate(program, { ...options, analysis });\n result.diagnostics = diagnostics;\n result.analysis = analysis;\n const triples = resultTriples(result, program, options);\n return formatTriples(triples, result.prefixes);\n}\n\nmodule.exports = {\n parse,\n parseQuery,\n parseInput,\n parseRdfSyntax,\n parseRdfDocument,\n parseRdfMessageLog,\n looksLikeRdfMessageLog,\n rdfDocumentToProgram,\n compile,\n resolveImports,\n mergePrograms,\n analyze,\n evaluate,\n run,\n runToString,\n runQuery,\n queryResult,\n queryProgram,\n queryRunOptions,\n shouldUseHybridForQuery,\n formatTriples,\n formatBindings,\n sortTriples,\n toJSON,\n formatTrace,\n formatProof,\n resultTriples,\n};\n","src/parser.js":"'use strict';\n\nconst { tokenize, SyntaxErrorWithLocation } = require('./tokenizer.js');\nconst { parseN3 } = require('./rdfSyntax.js');\nconst { isBuiltinName } = require('./builtins.js');\nconst { ruleNeedsRunOnce } = require('./assignments.js');\nconst {\n iri,\n variable,\n blankNode,\n literal,\n tripleTerm,\n RDF_TYPE,\n RDF_FIRST,\n RDF_REST,\n RDF_NIL,\n RDF_REIFIES,\n XSD_STRING,\n XSD_BOOLEAN,\n XSD_INTEGER,\n XSD_DECIMAL,\n XSD_DOUBLE,\n} = require('./term.js');\n\nclass Parser {\n constructor(source, options = {}) {\n this.tokens = Array.isArray(source) ? source : tokenize(source, options);\n this.pos = 0;\n this.options = options;\n this.baseIRI = options.baseIRI || null;\n this.version = null;\n this.imports = [];\n this.bnodeCounter = 0;\n this.prefixes = {\n rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',\n sh: 'http://www.w3.org/ns/shacl#',\n srl: 'http://www.w3.org/ns/shacl-rules#',\n xsd: 'http://www.w3.org/2001/XMLSchema#',\n ...options.prefixes,\n };\n }\n\n parseProgram() {\n const data = [];\n const rules = [];\n while (!this.is('eof')) {\n if (this.matchWord('PREFIX')) {\n this.parsePrefix(false);\n } else if (this.matchWord('BASE')) {\n this.parseBase(false);\n } else if (this.matchWord('VERSION')) {\n this.parseVersion();\n } else if (this.matchWord('IMPORTS')) {\n this.parseImports();\n } else if (this.matchWord('DATA')) {\n this.expectValue('{');\n data.push(...this.parseDataBlockWithRdfSyntax());\n } else if (this.matchWord('RULE')) {\n rules.push(this.parseRule());\n } else if (this.matchWord('IF')) {\n rules.push(this.parseIfThenRule());\n } else if (this.checkDeclarationKeyword()) {\n rules.push(...this.parseDeclaration());\n } else {\n throw this.error(`Expected PREFIX, BASE, VERSION, IMPORTS, DATA, RULE, IF, TRANSITIVE, SYMMETRIC, or INVERSE; got ${this.peek().value}`);\n }\n }\n return {\n baseIRI: this.baseIRI,\n version: this.version,\n imports: this.imports.slice(),\n prefixes: { ...this.prefixes },\n data,\n rules,\n };\n }\n\n parseBase(wasAtBase = false) {\n const iriToken = this.expectType('iri');\n this.baseIRI = iriToken.value;\n if (wasAtBase) this.consumeOptionalDot();\n }\n\n parsePrefix(wasAtPrefix = false) {\n const nameToken = this.advance();\n if (nameToken.type !== 'word') throw this.error('Expected prefix name', nameToken);\n let name = nameToken.value;\n if (!name.endsWith(':')) throw this.error('Prefix name must end with :', nameToken);\n name = name.slice(0, -1);\n if (this.strictGrammar() && !isValidPNPrefix(name)) throw this.error(`Invalid prefix name ${nameToken.value}`, nameToken);\n const iriToken = this.expectType('iri');\n this.prefixes[name] = this.resolveIRI(iriToken.value, iriToken);\n if (wasAtPrefix) this.consumeOptionalDot();\n }\n\n parseVersion() {\n const token = this.expectType('string');\n if (this.strictGrammar()) {\n if (token.long) throw this.error('VERSION must use a short string literal', token);\n if (token.value !== '1.2') throw this.error('VERSION must be the SHACL Rules version label \\\"1.2\\\"', token);\n }\n this.version = token.value;\n }\n\n parseImports() {\n const target = this.parseIRIValue();\n this.imports.push(target.value);\n this.consumeOptionalDot();\n }\n\n parseRule() {\n this.expectValue('{');\n const head = this.parseTriplesBlock({ allowPath: false, context: 'head' });\n this.expectWord('WHERE');\n this.expectValue('{');\n const body = this.parseBodyBlockAlreadyOpen();\n return { name: null, head, body, runOnce: ruleNeedsRunOnce(head, body, this.options) };\n }\n\n parseIfThenRule() {\n this.expectValue('{');\n const body = this.parseBodyBlockAlreadyOpen();\n this.expectWord('THEN');\n this.expectValue('{');\n const head = this.parseTriplesBlock({ allowPath: false, context: 'head' });\n return { name: null, head, body, runOnce: ruleNeedsRunOnce(head, body, this.options) };\n }\n\n checkDeclarationKeyword() {\n return this.checkType('word') && ['TRANSITIVE', 'SYMMETRIC', 'INVERSE'].includes(this.peek().value.toUpperCase());\n }\n\n parseDeclaration() {\n if (this.matchWord('TRANSITIVE')) {\n this.expectValue('(');\n const pred = this.parseIRIValue();\n this.expectValue(')');\n this.consumeOptionalDot();\n return [{\n name: `TRANSITIVE(${pred.lexical})`,\n head: [{ s: variable('x'), p: iri(pred.value), o: variable('z') }],\n body: [\n { type: 'triple', triple: { s: variable('x'), p: iri(pred.value), o: variable('y') } },\n { type: 'triple', triple: { s: variable('y'), p: iri(pred.value), o: variable('z') } },\n ],\n runOnce: false,\n }];\n }\n if (this.matchWord('SYMMETRIC')) {\n this.expectValue('(');\n const pred = this.parseIRIValue();\n this.expectValue(')');\n this.consumeOptionalDot();\n return [{\n name: `SYMMETRIC(${pred.lexical})`,\n head: [{ s: variable('y'), p: iri(pred.value), o: variable('x') }],\n body: [{ type: 'triple', triple: { s: variable('x'), p: iri(pred.value), o: variable('y') } }],\n runOnce: false,\n }];\n }\n if (this.matchWord('INVERSE')) {\n this.expectValue('(');\n const left = this.parseIRIValue();\n this.expectValue(',');\n const right = this.parseIRIValue();\n this.expectValue(')');\n this.consumeOptionalDot();\n return [\n {\n name: `INVERSE(${left.lexical},${right.lexical})#1`,\n head: [{ s: variable('y'), p: iri(right.value), o: variable('x') }],\n body: [{ type: 'triple', triple: { s: variable('x'), p: iri(left.value), o: variable('y') } }],\n runOnce: false,\n },\n {\n name: `INVERSE(${left.lexical},${right.lexical})#2`,\n head: [{ s: variable('y'), p: iri(left.value), o: variable('x') }],\n body: [{ type: 'triple', triple: { s: variable('x'), p: iri(right.value), o: variable('y') } }],\n runOnce: false,\n },\n ];\n }\n throw this.error(`Expected declaration, got ${this.peek().value}`);\n }\n\n parseIRIValue() {\n const token = this.advance();\n if (token.type === 'iri') return { value: this.resolveIRI(token.value, token), lexical: `<${token.value}>` };\n if (token.type === 'word') {\n if (token.value === 'a') return { value: RDF_TYPE, lexical: 'a' };\n if (!token.value.includes(':')) throw this.error(`Expected IRI or prefixed name, got ${token.value}`, token);\n return { value: this.expandPrefixedName(token.value, token), lexical: token.value };\n }\n throw this.error(`Expected IRI or prefixed name, got ${token.value}`, token);\n }\n\n parseTriplesBlock(options = {}) {\n const triples = [];\n while (!this.matchValue('}')) {\n triples.push(...this.parseTripleStatement(options));\n this.consumeOptionalDot();\n }\n return triples;\n }\n\n parseDataBlockWithRdfSyntax() {\n const blockSource = this.collectBalancedDataBlockSource();\n const program = parseN3(blockSource, {\n profile: 'trig',\n base: this.baseIRI || '',\n prefixes: this.prefixes,\n });\n return (program.facts || []).map((triple) => this.convertRdfSyntaxTriple(triple));\n }\n\n collectBalancedDataBlockSource() {\n const tokens = [];\n let depth = 1;\n while (!this.is('eof')) {\n const token = this.advance();\n if (token.value === '{') {\n depth += 1;\n tokens.push(token);\n } else if (token.value === '}') {\n depth -= 1;\n if (depth === 0) return this.tokensToRdfSource(tokens);\n tokens.push(token);\n } else {\n tokens.push(token);\n }\n }\n throw this.error('Unterminated DATA block');\n }\n\n tokensToRdfSource(tokens) {\n const parts = [];\n for (let i = 0; i < tokens.length; i += 1) {\n const token = tokens[i];\n if (token.type === 'eof') continue;\n if ((token.value === '+' || token.value === '-') && tokens[i + 1] && tokens[i + 1].type === 'number') {\n parts.push(`${token.value}${this.tokenToRdfSource(tokens[i + 1])}`);\n i += 1;\n } else {\n parts.push(this.tokenToRdfSource(token));\n }\n }\n return parts.join(' ');\n }\n\n tokenToRdfSource(token) {\n if (token.type === 'iri') return `<${String(token.value).replace(/>/g, '\\\\>')}>`;\n if (token.type === 'string') return JSON.stringify(token.value);\n if (token.type === 'variable') return `?${token.value}`;\n if (token.type === 'number') return String(token.value);\n return String(token.value);\n }\n\n convertRdfSyntaxTriple(triple) {\n const out = {\n s: this.convertRdfSyntaxTerm(triple.s),\n p: this.convertRdfSyntaxTerm(triple.p),\n o: this.convertRdfSyntaxTerm(triple.o),\n };\n if (out.s.type === 'var' || out.p.type === 'var' || out.o.type === 'var') {\n throw this.error('DATA blocks may not contain variables');\n }\n if (out.p.type !== 'iri') {\n throw this.error('DATA predicates must be IRIs');\n }\n if (triple.graph) out.graph = this.convertRdfSyntaxTerm(triple.graph);\n return out;\n }\n\n convertRdfSyntaxTerm(term) {\n if (!term) return null;\n if (term.type) return term;\n if (term.kind === 'iri') return iri(term.value);\n if (term.kind === 'blank') return blankNode(term.value);\n if (term.kind === 'var') return variable(term.name || term.value);\n if (term.kind === 'literal') {\n return literal(\n coerceLexicalLiteral(term.value, term.datatype),\n term.datatype === XSD_STRING ? null : (term.datatype || null),\n term.language || null,\n term.langDir || null,\n );\n }\n if (term.kind === 'triple') {\n return tripleTerm(\n this.convertRdfSyntaxTerm(term.s),\n this.convertRdfSyntaxTerm(term.p),\n this.convertRdfSyntaxTerm(term.o),\n );\n }\n throw this.error(`Unsupported RDF term kind ${term.kind || typeof term}`);\n }\n\n parseTripleStatement(options = {}) {\n const subjectNode = this.parseGraphNode({ ...options, position: 'subject' });\n const triples = [...subjectNode.triples];\n triples.push(...this.parsePropertyListForSubject(subjectNode.term, options));\n return triples;\n }\n\n parsePropertyListForSubject(subject, options = {}, terminators = ['}', '|}', ']']) {\n const triples = [];\n let keepParsingPredicates = true;\n\n while (keepParsingPredicates) {\n if (terminators.some((value) => this.checkValue(value)) || this.checkValue('.')) break;\n const predicate = options.allowPath ? this.parseVerbPathOrSimple(options) : this.parseVerbTerm(options);\n do {\n const objectNode = this.parseGraphNode({ ...options, position: 'object' });\n triples.push(...objectNode.triples);\n const baseTriple = { s: subject, p: predicate, o: objectNode.term };\n triples.push(baseTriple);\n triples.push(...this.parseAnnotationsForTriple(baseTriple, options));\n } while (this.matchValue(','));\n\n if (this.matchValue(';')) {\n keepParsingPredicates = !(this.checkValue('.') || terminators.some((value) => this.checkValue(value)));\n } else {\n keepParsingPredicates = false;\n }\n }\n\n return triples;\n }\n\n parseGraphNode(options = {}) {\n if (this.checkValue('[')) return this.parseBlankNodePropertyList(options);\n if (this.checkValue('(')) return this.parseCollection(options);\n if (this.checkValue('<<')) return this.parseReifiedTripleNode(options);\n return { term: this.parseTerm(options), triples: [] };\n }\n\n parseBlankNodePropertyList(options = {}) {\n this.expectValue('[');\n const node = this.freshGraphNode(options);\n if (this.matchValue(']')) return { term: node, triples: [] };\n const triples = this.parsePropertyListForSubject(node, options, [']']);\n this.expectValue(']');\n return { term: node, triples };\n }\n\n parseCollection(options = {}) {\n this.expectValue('(');\n if (this.matchValue(')')) return { term: iri(RDF_NIL), triples: [] };\n\n const items = [];\n while (!this.checkValue(')')) items.push(this.parseGraphNode(options));\n this.expectValue(')');\n\n const triples = [];\n for (const item of items) triples.push(...item.triples);\n const cells = items.map(() => this.freshGraphNode(options));\n for (let i = 0; i < items.length; i += 1) {\n triples.push({ s: cells[i], p: iri(RDF_FIRST), o: items[i].term });\n triples.push({ s: cells[i], p: iri(RDF_REST), o: i + 1 < cells.length ? cells[i + 1] : iri(RDF_NIL) });\n }\n return { term: cells[0], triples };\n }\n\n freshGraphNode(options = {}) {\n this.bnodeCounter += 1;\n const id = `b${this.bnodeCounter}`;\n return options.context === 'body' ? variable(`__${id}`) : blankNode(id);\n }\n\n parseAnnotationsForTriple(baseTriple, options = {}) {\n const triples = [];\n const reified = tripleTerm(baseTriple.s, baseTriple.p, baseTriple.o);\n let currentReifier = null;\n\n while (this.checkValue('~') || this.checkValue('{|')) {\n if (this.matchValue('~')) {\n currentReifier = this.parseOptionalReifier(options);\n triples.push({ s: currentReifier, p: iri(RDF_REIFIES), o: reified });\n } else if (this.matchValue('{|')) {\n if (this.checkValue('|}')) throw this.error('Annotation blocks may not be empty');\n const annotationSubject = currentReifier || this.freshGraphNode(options);\n triples.push({ s: annotationSubject, p: iri(RDF_REIFIES), o: reified });\n triples.push(...this.parsePropertyListForSubject(annotationSubject, options, ['|}']));\n this.expectValue('|}');\n }\n }\n return triples;\n }\n\n parseOptionalReifier(options = {}) {\n if (this.checkValue('{|') || this.checkValue('.') || this.checkValue(';') || this.checkValue(',') || this.checkValue('}') || this.checkValue('|}') || this.checkValue('>>')) {\n return this.freshGraphNode(options);\n }\n return this.parseVarOrReifierId();\n }\n\n parseVarOrReifierId() {\n const token = this.peek();\n if (token.type === 'variable') return this.parseTerm();\n if (token.type === 'iri') return this.parseTerm();\n if (token.type === 'word' && (token.value.startsWith('_:') || token.value.includes(':'))) return this.parseTerm();\n throw this.error(`Expected variable, IRI, or blank node after ~; got ${token.value}`, token);\n }\n\n parseReifiedTripleNode(options = {}) {\n this.expectValue('<<');\n const subjectNode = this.parseReifiedTripleComponent(options);\n const p = this.parseVerbTerm(options);\n const objectNode = this.parseReifiedTripleComponent(options);\n let reifier = null;\n if (this.matchValue('~')) reifier = this.parseOptionalReifier(options);\n this.expectValue('>>');\n reifier = reifier || this.freshGraphNode(options);\n return {\n term: reifier,\n triples: [\n ...subjectNode.triples,\n ...objectNode.triples,\n { s: reifier, p: iri(RDF_REIFIES), o: tripleTerm(subjectNode.term, p, objectNode.term) },\n ],\n };\n }\n\n parseReifiedTripleComponent(options = {}) {\n if (this.checkValue('<<')) return this.parseReifiedTripleNode(options);\n return { term: this.parseTerm(options), triples: [] };\n }\n\n parseVerbTerm(options = {}) {\n const term = this.parseTerm({ ...options, position: 'predicate' });\n if (term.type !== 'iri' && term.type !== 'var') throw this.error('Expected IRI or variable as predicate');\n return term;\n }\n\n parseVerbPathOrSimple(options = {}) {\n if (this.checkType('variable')) return this.parseTerm(options);\n return this.parsePathSequence();\n }\n\n parsePathSequence() {\n const parts = [this.parsePathEltOrInverse()];\n while (this.matchValue('/')) parts.push(this.parsePathEltOrInverse());\n return parts.length === 1 ? parts[0] : { type: 'path', kind: 'sequence', parts };\n }\n\n parsePathEltOrInverse() {\n if (this.matchValue('^')) return { type: 'path', kind: 'inverse', path: this.parsePathPrimary() };\n return this.parsePathPrimary();\n }\n\n parsePathPrimary() {\n if (this.matchValue('(')) {\n const path = this.parsePathSequence();\n this.expectValue(')');\n return path;\n }\n const token = this.peek();\n if (token.type === 'iri' || token.type === 'word') {\n const value = this.parseIRIValue();\n return iri(value.value);\n }\n throw this.error(`Expected path IRI, a, ^, or (, got ${token.value}`, token);\n }\n\n parseFilterClause() {\n // SRL FILTER accepts a bracketted expression, a built-in call, or an IRI-named function call.\n // The bracketted-expression form is the familiar FILTER(?x > 10).\n const expr = this.parseExpression();\n return { type: 'filter', expr };\n }\n\n parseSetClause() {\n this.expectValue('(');\n const variableToken = this.expectType('variable');\n this.expectValue(':=');\n const expr = this.parseExpression();\n this.expectValue(')');\n return { type: 'set', variable: variableToken.value, expr };\n }\n\n parseBindClause() {\n this.expectValue('(');\n const expr = this.parseExpression();\n this.expectWord('AS');\n const variableToken = this.expectType('variable');\n this.expectValue(')');\n return { type: 'bind', variable: variableToken.value, expr };\n }\n\n parseBodyBlockAlreadyOpen() {\n const clauses = [];\n while (!this.matchValue('}')) {\n if (this.matchWord('FILTER')) {\n clauses.push(this.parseFilterClause());\n } else if (this.matchWord('SET')) {\n clauses.push(this.parseSetClause());\n } else if (this.matchWord('BIND')) {\n if (this.strictGrammar()) throw this.error('BIND is not part of the SHACL 1.2 Rules grammar; use SET');\n clauses.push(this.parseBindClause());\n } else if (this.matchWord('NOT')) {\n this.expectValue('{');\n const body = this.parseBodyBasicAlreadyOpen();\n clauses.push({ type: 'not', body });\n } else {\n for (const triple of this.parseTripleStatement({ allowPath: true, context: 'body' })) {\n if (triple.p && triple.p.type === 'path') clauses.push({ type: 'path', triple });\n else clauses.push({ type: 'triple', triple });\n }\n }\n this.consumeOptionalDot();\n }\n return clauses;\n }\n\n parseBodyBasicAlreadyOpen() {\n const clauses = [];\n while (!this.matchValue('}')) {\n if (this.matchWord('FILTER')) {\n clauses.push(this.parseFilterClause());\n } else if (this.matchWord('SET')) {\n throw this.error('SET is not allowed inside NOT blocks by the SRL grammar');\n } else if (this.matchWord('BIND')) {\n throw this.error('BIND is not allowed inside NOT blocks by the SRL grammar');\n } else if (this.matchWord('NOT')) {\n throw this.error('Nested NOT is not allowed inside NOT blocks by the SRL grammar');\n } else {\n for (const triple of this.parseTripleStatement({ allowPath: true, context: 'body' })) {\n if (triple.p && triple.p.type === 'path') clauses.push({ type: 'path', triple });\n else clauses.push({ type: 'triple', triple });\n }\n }\n this.consumeOptionalDot();\n }\n return clauses;\n }\n\n parseTerm(options = {}) {\n const token = this.advance();\n if (token.type === 'operator' && (token.value === '+' || token.value === '-') && this.peek().type === 'number') {\n const numberToken = this.advance();\n return numericLiteral(token.value === '-' ? -numberToken.value : numberToken.value);\n }\n if (token.type === 'variable') {\n if (options.context === 'data') throw this.error('DATA blocks may not contain variables', token);\n return variable(token.value);\n }\n if (token.type === 'iri') return iri(this.resolveIRI(token.value, token));\n if (token.type === 'string') return this.parseLiteralAfterToken(token);\n if (token.type === 'number') return numericLiteral(token.value);\n if (token.value === '<<(') return this.parseTripleTermAfterOpen(options);\n if (token.value === '<<') throw this.error('Use << s p o >> as a graph node reifier; use <<( s p o )>> for a triple term', token);\n if (token.type === 'word') {\n if (token.value === 'a') {\n if (options.position !== 'predicate') throw this.error('a is only allowed as a predicate', token);\n return iri(RDF_TYPE);\n }\n if (token.value === 'true') return literal(true, XSD_BOOLEAN);\n if (token.value === 'false') return literal(false, XSD_BOOLEAN);\n if (token.value.startsWith('_:')) return blankNode(token.value.slice(2));\n return iri(this.expandPrefixedName(token.value, token));\n }\n throw this.error(`Expected term, got ${token.value}`, token);\n }\n\n parseTripleTermAfterOpen(options = {}) {\n const s = this.parseTerm(options);\n const p = this.parseVerbTerm(options);\n const o = this.parseTerm(options);\n this.expectValue(')>>');\n return tripleTerm(s, p, o);\n }\n\n parseReifiedTripleAfterOpen(options = {}) {\n const s = this.parseTerm(options);\n const p = this.parseVerbTerm(options);\n const o = this.parseTerm(options);\n if (this.matchValue('~')) {\n if (!this.checkValue('>>')) this.parseVarOrReifierId();\n }\n this.expectValue('>>');\n return tripleTerm(s, p, o);\n }\n\n parseLiteralAfterToken(token) {\n if (this.matchValue('^^')) {\n const datatype = this.parseDatatypeIRI();\n return literal(coerceLexicalLiteral(token.value, datatype), datatype, null);\n }\n if (this.checkType('word') && /^@[A-Za-z]+(?:-[A-Za-z0-9]+)*(?:--[A-Za-z]+)?$/.test(this.peek().value)) {\n const tagToken = this.advance();\n const rawTag = tagToken.value.slice(1);\n const direction = rawTag.includes('--') ? rawTag.slice(rawTag.lastIndexOf('--') + 2) : null;\n if (direction && direction !== 'ltr' && direction !== 'rtl') {\n throw this.error(`Invalid base direction --${direction}; expected --ltr or --rtl`, tagToken);\n }\n const tag = rawTag.toLowerCase();\n const [lang, langDir = null] = tag.split('--');\n return literal(token.value, null, lang, langDir);\n }\n return literal(token.value);\n }\n\n parseDatatypeIRI() {\n const token = this.advance();\n if (token.type === 'iri') return this.resolveIRI(token.value, token);\n if (token.type === 'word') return this.expandPrefixedName(token.value, token);\n throw this.error(`Expected datatype IRI, got ${token.value}`, token);\n }\n\n expandPrefixedName(value, token) {\n const colon = value.indexOf(':');\n if (colon < 0) throw this.error(`Expected IRI, prefixed name, literal, blank node, or variable; got ${value}`, token);\n const prefix = value.slice(0, colon);\n const local = value.slice(colon + 1);\n if (this.strictGrammar()) validatePrefixedName(prefix, local, value, token, (message, errToken) => this.error(message, errToken));\n if (!(prefix in this.prefixes)) throw this.error(`Unknown prefix ${prefix}:`, token);\n return this.prefixes[prefix] + decodePNLocalEscapes(local);\n }\n\n resolveIRI(value, token = null) {\n if (!this.baseIRI || /^[A-Za-z][A-Za-z0-9+.-]*:/.test(value)) return value;\n try {\n return new URL(value, this.baseIRI).href;\n } catch (_) {\n if (token) throw this.error(`Could not resolve IRI ${value} against BASE ${this.baseIRI}`, token);\n return value;\n }\n }\n\n parseExpression(minPrec = 0) {\n let left = this.parseUnaryExpression();\n while (true) {\n const info = this.peekBinaryOperator();\n if (!info || info.prec < minPrec) break;\n this.consumeBinaryOperator(info.op);\n if (info.op === 'IN' || info.op === 'NOT IN') {\n const items = this.parseExpressionListItems();\n left = { type: 'binary', op: info.op, left, right: { type: 'list', items } };\n } else {\n const right = this.parseExpression(info.prec + 1);\n left = { type: 'binary', op: info.op, left, right };\n }\n }\n return left;\n }\n\n parseExpressionListItems() {\n this.expectValue('(');\n const items = [];\n if (!this.checkValue(')')) {\n do { items.push(this.parseExpression()); }\n while (this.matchValue(','));\n }\n this.expectValue(')');\n return items;\n }\n\n peekBinaryOperator() {\n const token = this.peek();\n if (token.type === 'operator') {\n const prec = binaryPrecedence(token.value);\n return prec >= 0 ? { op: token.value, prec } : null;\n }\n if (token.type === 'word' && token.value.toUpperCase() === 'IN') return { op: 'IN', prec: 3 };\n if (token.type === 'word' && token.value.toUpperCase() === 'NOT' && this.peekN(1).type === 'word' && this.peekN(1).value.toUpperCase() === 'IN') return { op: 'NOT IN', prec: 3 };\n return null;\n }\n\n consumeBinaryOperator(op) {\n if (op === 'NOT IN') { this.expectWord('NOT'); this.expectWord('IN'); return; }\n if (op === 'IN') { this.expectWord('IN'); return; }\n this.expectValue(op);\n }\n\n parseUnaryExpression() {\n if (this.peek().type === 'operator' && (this.peek().value === '!' || this.peek().value === '-' || this.peek().value === '+')) {\n const op = this.advance().value;\n return { type: 'unary', op, expr: this.parseUnaryExpression() };\n }\n return this.parsePrimaryExpression();\n }\n\n parsePrimaryExpression() {\n const token = this.advance();\n if (token.type === 'variable') return { type: 'var', name: token.value };\n if (token.type === 'string') return this.parseLiteralExpressionAfterToken(token);\n if (token.type === 'number') return { type: 'literal', value: token.value };\n if (token.type === 'iri') {\n const name = this.resolveIRI(token.value, token);\n if (this.checkValue('(')) return this.parseFunctionCallAfterName(name);\n return { type: 'term', value: iri(name) };\n }\n if (token.value === '<<(') return { type: 'term', value: this.parseTripleTermAfterOpen() };\n if (token.value === '<<') throw this.error('Use <<( s p o )>> for triple terms inside expressions', token);\n if (token.type === 'word') {\n if (token.value === 'true') return { type: 'literal', value: true };\n if (token.value === 'false') return { type: 'literal', value: false };\n if (token.value.startsWith('_:')) return { type: 'term', value: blankNode(token.value.slice(2)) };\n if (this.checkValue('(')) {\n if (token.value.includes(':') && token.value !== 'a') {\n const name = this.expandPrefixedName(token.value, token);\n return this.parseFunctionCallAfterName(name);\n }\n if (isBuiltinName(token.value)) return this.parseFunctionCallAfterName(token.value);\n throw this.error(`Unknown built-in or unprefixed function call ${token.value}; use an IRI such as :${token.value} for custom functions`, token);\n }\n if (token.value.includes(':') || token.value === 'a') {\n const value = token.value === 'a' ? RDF_TYPE : this.expandPrefixedName(token.value, token);\n return { type: 'term', value: iri(value) };\n }\n }\n if (token.value === '(') {\n const expr = this.parseExpression();\n this.expectValue(')');\n return expr;\n }\n throw this.error(`Expected expression, got ${token.value}`, token);\n }\n\n parseFunctionCallAfterName(name) {\n this.expectValue('(');\n const args = [];\n if (!this.checkValue(')')) {\n do { args.push(this.parseExpression()); }\n while (this.matchValue(','));\n }\n this.expectValue(')');\n return { type: 'call', name, args };\n }\n\n parseLiteralExpressionAfterToken(token) {\n const term = this.parseLiteralAfterToken(token);\n if (term.datatype || term.lang) return { type: 'term', value: term };\n return { type: 'literal', value: term.value };\n }\n\n consumeOptionalDot() { this.matchValue('.'); }\n\n matchWord(value) {\n if (this.checkType('word') && this.peek().value.toUpperCase() === value.toUpperCase()) {\n this.advance();\n return true;\n }\n return false;\n }\n\n expectWord(value) {\n if (this.matchWord(value)) return this.previous();\n throw this.error(`Expected ${value}, got ${this.peek().value}`);\n }\n\n matchValue(value) {\n const token = this.peek();\n if ((token.type === 'punct' || token.type === 'operator') && token.value === value) { this.advance(); return true; }\n return false;\n }\n\n expectValue(value) {\n if (this.matchValue(value)) return this.previous();\n throw this.error(`Expected ${value}, got ${this.peek().value}`);\n }\n\n checkValue(value) { const token = this.peek(); return (token.type === 'punct' || token.type === 'operator') && token.value === value; }\n checkType(type) { return this.peek().type === type; }\n is(type) { return this.peek().type === type; }\n\n expectType(type) {\n if (this.peek().type === type) return this.advance();\n throw this.error(`Expected ${type}, got ${this.peek().value}`);\n }\n\n advance() { if (!this.is('eof')) this.pos += 1; return this.previous(); }\n peek() { return this.tokens[this.pos]; }\n peekN(n) { return this.tokens[this.pos + n] || this.tokens[this.tokens.length - 1]; }\n previous() { return this.tokens[this.pos - 1]; }\n strictGrammar() { return !!this.options.strictGrammar; }\n error(message, token = this.peek()) { return new SyntaxErrorWithLocation(message, token && token.filename ? token : { ...token, filename: this.options.filename || '<input>' }); }\n}\n\n\nfunction isPnCharsBase(ch) {\n if (!ch) return false;\n return /[A-Za-z]/.test(ch) || ch.codePointAt(0) >= 0x00C0;\n}\n\nfunction isPnCharsU(ch) {\n return isPnCharsBase(ch) || ch === '_';\n}\n\nfunction isPnChars(ch) {\n return isPnCharsU(ch) || /[0-9-]/.test(ch) || ch === '\\u00B7' || /[\\u0300-\\u036F\\u203F-\\u2040]/u.test(ch);\n}\n\nfunction isValidPNPrefix(prefix) {\n if (prefix === '') return true;\n const chars = Array.from(prefix);\n if (!isPnCharsBase(chars[0])) return false;\n if (chars.length > 1 && chars.at(-1) === '.') return false;\n return chars.slice(1).every((ch) => isPnChars(ch) || ch === '.');\n}\n\nfunction plxLength(text, index) {\n const ch = text[index];\n if (ch === '%' && /[0-9A-Fa-f]/.test(text[index + 1] || '') && /[0-9A-Fa-f]/.test(text[index + 2] || '')) return 3;\n if (ch === '\\\\' && /[_~.!$&'()*+,;=/?#@%-]/.test(text[index + 1] || '')) return 2;\n return 0;\n}\n\nfunction isPNLocalStartAt(text, index) {\n const ch = text[index];\n return isPnCharsU(ch) || /[0-9:]/.test(ch || '') || plxLength(text, index) > 0;\n}\n\nfunction isPNLocalBodyAt(text, index) {\n const ch = text[index];\n return isPnChars(ch) || ch === '.' || ch === ':' || plxLength(text, index) > 0;\n}\n\nfunction isPNLocalEndAt(text, index) {\n const ch = text[index];\n return isPnChars(ch) || ch === ':' || plxLength(text, index) > 0;\n}\n\nfunction validatePNLocal(local) {\n if (local === '') return true;\n if (!isPNLocalStartAt(local, 0)) return false;\n let lastStart = 0;\n for (let i = 0; i < local.length;) {\n const len = plxLength(local, i) || 1;\n if (i > 0 && !isPNLocalBodyAt(local, i)) return false;\n lastStart = i;\n i += len;\n }\n return isPNLocalEndAt(local, lastStart);\n}\n\nfunction validatePrefixedName(prefix, local, value, token, makeError) {\n if (!isValidPNPrefix(prefix)) throw makeError(`Invalid prefixed name ${value}: invalid prefix`, token);\n if (!validatePNLocal(local)) throw makeError(`Invalid prefixed name ${value}: invalid local name`, token);\n}\n\nfunction decodePNLocalEscapes(local) {\n return String(local).replace(/\\\\([_~.!$&'()*+,;=/?#@%-])/g, '$1');\n}\n\nfunction numericLiteral(value) {\n if (Number.isInteger(value)) return literal(value, XSD_INTEGER);\n return literal(value, XSD_DECIMAL);\n}\n\nfunction parseIntegerLiteral(value) {\n const text = String(value);\n const asNumber = Number.parseInt(text, 10);\n return Number.isSafeInteger(asNumber) && String(asNumber) === text.replace(/^\\+/, '') ? asNumber : BigInt(text);\n}\n\nfunction coerceLexicalLiteral(value, datatype) {\n if (datatype === XSD_INTEGER) return parseIntegerLiteral(value);\n if (datatype === XSD_DECIMAL || datatype === XSD_DOUBLE) return Number.parseFloat(value);\n if (datatype === XSD_BOOLEAN) return value === 'true' || value === '1';\n return value;\n}\n\nfunction binaryPrecedence(op) {\n return {\n '||': 1,\n '&&': 2,\n '=': 3,\n '!=': 3,\n 'IN': 3,\n 'NOT IN': 3,\n '<': 4,\n '<=': 4,\n '>': 4,\n '>=': 4,\n '+': 5,\n '-': 5,\n '*': 6,\n '/': 6,\n }[op] ?? -1;\n}\n\nfunction parse(source, options = {}) {\n return new Parser(source, options).parseProgram();\n}\n\nfunction parseQuery(source, options = {}) {\n if (/^\\s*(QUERY|SELECT)\\b/i.test(source)) {\n throw new Error('QUERY/SELECT concrete syntax is not part of the SHACL Rules SRL grammar; pass a raw body pattern instead');\n }\n const trimmed = String(source).trim();\n const text = trimmed.startsWith('{') ? `RULE { } WHERE ${trimmed}` : `RULE { } WHERE { ${source} }`;\n const program = new Parser(text, options).parseProgram();\n if (program.rules.length !== 1 || program.data.length !== 0) {\n throw new Error('Expected exactly one raw body pattern');\n }\n return { select: null, body: program.rules[0].body, prefixes: program.prefixes, baseIRI: program.baseIRI };\n}\n\nmodule.exports = { Parser, parse, parseQuery };\n","src/tokenizer.js":"'use strict';\n\nclass SyntaxErrorWithLocation extends Error {\n constructor(message, token) {\n const suffix = token && token.line ? ` at ${token.filename || '<input>'}:${token.line}:${token.column}` : '';\n super(`${message}${suffix}`);\n this.name = 'SyntaxError';\n this.token = token;\n }\n}\n\nfunction tokenize(source, filenameOrOptions = '<input>') {\n const options = typeof filenameOrOptions === 'object' && filenameOrOptions !== null ? filenameOrOptions : { filename: filenameOrOptions };\n const filename = options.filename || '<input>';\n const strictGrammar = !!options.strictGrammar;\n const tokens = [];\n let i = 0;\n let line = 1;\n let column = 1;\n\n function current() { return source[i]; }\n function peek(n = 1) { return source[i + n]; }\n function startsWith(text) { return source.startsWith(text, i); }\n function advance() {\n const ch = source[i++];\n if (ch === '\\n') { line += 1; column = 1; }\n else column += 1;\n return ch;\n }\n function token(type, value, startLine, startColumn, extra = {}) {\n tokens.push({ type, value, line: startLine, column: startColumn, ...extra });\n }\n function syntax(message, startLine, startColumn) {\n throw new SyntaxErrorWithLocation(message, { line: startLine, column: startColumn, filename });\n }\n\n function readNumericLiteral() {\n let value = '';\n const start = i;\n while (i < source.length && isDigitCode(source.charCodeAt(i))) { i += 1; column += 1; }\n if (source[i] === '.' && isDigitCode(source.charCodeAt(i + 1))) {\n i += 1; column += 1;\n while (i < source.length && isDigitCode(source.charCodeAt(i))) { i += 1; column += 1; }\n }\n value = source.slice(start, i);\n if (current() === 'e' || current() === 'E') {\n const saveI = i;\n const saveLine = line;\n const saveColumn = column;\n let exponent = advance();\n if (current() === '+' || current() === '-') exponent += advance();\n if (isDigitCode(source.charCodeAt(i))) {\n while (i < source.length && isDigitCode(source.charCodeAt(i))) exponent += advance();\n value += exponent;\n } else {\n i = saveI;\n line = saveLine;\n column = saveColumn;\n }\n }\n return value;\n }\n\n function readEscape(startLine, startColumn) {\n advance(); // consume backslash\n const esc = advance();\n if (esc === 'u' || esc === 'U') {\n const length = esc === 'u' ? 4 : 8;\n let hex = '';\n for (let j = 0; j < length; j += 1) {\n if (!isHexCode(source.charCodeAt(i))) syntax(`Invalid \\\\${esc} escape`, startLine, startColumn);\n hex += advance();\n }\n const codePoint = Number.parseInt(hex, 16);\n try { return String.fromCodePoint(codePoint); }\n catch { syntax(`Invalid \\\\${esc} escape`, startLine, startColumn); }\n }\n if (strictGrammar && !Object.hasOwn(escapeMap, esc)) syntax(`Invalid escape \\\\${esc}`, startLine, startColumn);\n return escapeValue(esc);\n }\n\n function readIriChar(startLine, startColumn) {\n if (current() === '\\\\') {\n advance();\n const esc = advance();\n if (esc !== 'u' && esc !== 'U') syntax(`Invalid IRI escape \\\\${esc}`, startLine, startColumn);\n const length = esc === 'u' ? 4 : 8;\n let hex = '';\n for (let j = 0; j < length; j += 1) {\n if (!isHexCode(source.charCodeAt(i))) syntax(`Invalid \\\\${esc} escape`, startLine, startColumn);\n hex += advance();\n }\n const codePoint = Number.parseInt(hex, 16);\n try { return String.fromCodePoint(codePoint); }\n catch { syntax(`Invalid \\\\${esc} escape`, startLine, startColumn); }\n }\n const c = current();\n if (strictGrammar && (/[\\u0000-\\u0020]/.test(c) || /[<>\"{}|^`]/.test(c))) syntax(`Invalid character in IRI reference ${JSON.stringify(c)}`, startLine, startColumn);\n return advance();\n }\n\n while (i < source.length) {\n const ch = current();\n if (isWhitespaceCode(source.charCodeAt(i))) { advance(); continue; }\n if (ch === '#') {\n while (i < source.length && current() !== '\\n') advance();\n continue;\n }\n\n const startLine = line;\n const startColumn = column;\n\n if (startsWith('<<(')) {\n advance(); advance(); advance();\n token('punct', '<<(', startLine, startColumn);\n continue;\n }\n if (startsWith(')>>')) {\n advance(); advance(); advance();\n token('punct', ')>>', startLine, startColumn);\n continue;\n }\n if (startsWith('<<')) {\n advance(); advance();\n token('punct', '<<', startLine, startColumn);\n continue;\n }\n if (startsWith('>>')) {\n advance(); advance();\n token('punct', '>>', startLine, startColumn);\n continue;\n }\n if (startsWith('{|')) {\n advance(); advance();\n token('punct', '{|', startLine, startColumn);\n continue;\n }\n if (startsWith('|}')) {\n advance(); advance();\n token('punct', '|}', startLine, startColumn);\n continue;\n }\n\n if (ch === '<' && looksLikeIRI(source, i)) {\n let value = '';\n advance();\n while (i < source.length && current() !== '>') value += readIriChar(startLine, startColumn);\n if (current() !== '>') syntax('Unterminated IRI', startLine, startColumn);\n advance();\n token('iri', value, startLine, startColumn);\n continue;\n }\n\n if ((ch === '\"' && startsWith('\"\"\"')) || (ch === \"'\" && startsWith(\"'''\"))) {\n const quote = ch;\n advance(); advance(); advance();\n let value = '';\n while (i < source.length && !startsWith(quote.repeat(3))) {\n if (current() === '\\\\') {\n value += readEscape(startLine, startColumn);\n } else {\n value += advance();\n }\n }\n if (!startsWith(quote.repeat(3))) syntax('Unterminated long string literal', startLine, startColumn);\n advance(); advance(); advance();\n token('string', value, startLine, startColumn, { long: true, quote });\n continue;\n }\n\n if (ch === '\"' || ch === \"'\") {\n const quote = ch;\n let value = '';\n advance();\n while (i < source.length && current() !== quote) {\n if (current() === '\\n' || current() === '\\r') syntax('Unterminated string literal', startLine, startColumn);\n if (current() === '\\\\') {\n value += readEscape(startLine, startColumn);\n } else {\n value += advance();\n }\n }\n if (current() !== quote) syntax('Unterminated string literal', startLine, startColumn);\n advance();\n token('string', value, startLine, startColumn, { long: false, quote });\n continue;\n }\n\n if (ch === '@') {\n const wordStart = i;\n advance();\n while (i < source.length && isLangTagCode(source.charCodeAt(i))) { i += 1; column += 1; }\n const value = source.slice(wordStart, i);\n if (!/^@[A-Za-z]+(?:-[A-Za-z0-9]+)*(?:--[A-Za-z]+)?$/.test(value)) syntax(`Invalid language tag ${value}`, startLine, startColumn);\n token('word', value, startLine, startColumn);\n continue;\n }\n\n if (ch === '?' || ch === '$') {\n const varStart = i;\n advance();\n while (i < source.length && isVarNameCode(source.charCodeAt(i))) { i += 1; column += 1; }\n if (i - varStart === 1) syntax('Expected variable name', startLine, startColumn);\n token('variable', source.slice(varStart + 1, i), startLine, startColumn);\n continue;\n }\n\n if (startsNumericLiteral(source, i)) {\n const value = readNumericLiteral();\n token('number', Number(value), startLine, startColumn);\n continue;\n }\n\n const two = ch + peek();\n if ([':=', '!=', '<=', '>=', '&&', '||', '=>', '^^'].includes(two)) {\n advance(); advance();\n token('operator', two, startLine, startColumn);\n continue;\n }\n\n if ('{}()[].,;|'.includes(ch)) {\n token('punct', advance(), startLine, startColumn);\n continue;\n }\n\n if ('=<>+-*/!^~'.includes(ch)) {\n token('operator', advance(), startLine, startColumn);\n continue;\n }\n\n const wordStart = i;\n while (i < source.length) {\n const c = source[i];\n if (c === '\\\\' && source[i + 1] !== undefined) {\n i += 2;\n column += 2;\n continue;\n }\n const code = source.charCodeAt(i);\n if (isWhitespaceCode(code) || '{}()[],;|'.includes(c) || '=<>+-*/!^~'.includes(c)) break;\n if (c === '.') {\n const n = source[i + 1];\n if (n === undefined || isWhitespaceCode(n.charCodeAt(0)) || '{}()[],;|'.includes(n) || '=<>+-*/!^~'.includes(n)) break;\n }\n if (c === '#') break;\n i += 1;\n column += 1;\n }\n if (i === wordStart) syntax(`Unexpected character ${JSON.stringify(ch)}`, startLine, startColumn);\n\n const value = source.slice(wordStart, i);\n if (/^[+-]?(?:(?:\\d+\\.\\d*|\\.\\d+)(?:[eE][+-]?\\d+)?|\\d+[eE][+-]?\\d+|\\d+)$/.test(value)) token('number', Number(value), startLine, startColumn);\n else token('word', value, startLine, startColumn);\n }\n\n tokens.push({ type: 'eof', value: '<eof>', line, column, filename });\n return tokens;\n}\n\n\nfunction isDigitCode(code) {\n return code >= 48 && code <= 57;\n}\n\nfunction isHexCode(code) {\n return (code >= 48 && code <= 57) || (code >= 65 && code <= 70) || (code >= 97 && code <= 102);\n}\n\nfunction isWhitespaceCode(code) {\n return code === 32 || code === 9 || code === 10 || code === 13 || code === 12;\n}\n\nfunction isLangTagCode(code) {\n return (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || (code >= 48 && code <= 57) || code === 45;\n}\n\nfunction isVarNameCode(code) {\n return (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || (code >= 48 && code <= 57) || code === 95 || code === 45;\n}\n\nfunction startsNumericLiteral(source, i) {\n const ch = source[i];\n const next = source[i + 1];\n if (isDigitCode(ch.charCodeAt(0))) return true;\n if (ch === '.' && next !== undefined && isDigitCode(next.charCodeAt(0))) return true;\n return false;\n}\n\nfunction looksLikeIRI(source, i) {\n const next = source[i + 1];\n if (next === undefined || /[\\s=]/.test(next)) return false;\n for (let j = i + 1; j < source.length; j += 1) {\n const c = source[j];\n if (c === '>') return true;\n if (/\\s/.test(c)) return false;\n }\n return false;\n}\n\nconst escapeMap = { n: '\\n', r: '\\r', t: '\\t', b: '\\b', f: '\\f', '\"': '\"', \"'\": \"'\", '\\\\': '\\\\' };\n\nfunction escapeValue(esc) {\n return escapeMap[esc] ?? esc;\n}\n\nmodule.exports = { tokenize, SyntaxErrorWithLocation };\n","src/rdfSyntax.js":"'use strict';\n\nconst { tokenize, SyntaxErrorWithLocation } = require('./tokenizer.js');\nconst { ruleNeedsRunOnce } = require('./assignments.js');\nconst {\n iri,\n variable,\n blankNode,\n literal,\n tripleTerm,\n termKey,\n termEquals,\n formatTerm,\n RDF_TYPE,\n RDF_FIRST,\n RDF_REST,\n RDF_NIL,\n XSD_BOOLEAN,\n XSD_INTEGER,\n XSD_DECIMAL,\n XSD_DOUBLE,\n} = require('./term.js');\n\nconst RDF_NS = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';\nconst SRL_NS = 'http://www.w3.org/ns/shacl-rules#';\nconst SHNEX_NS = 'http://www.w3.org/ns/shacl-node-expr#';\nconst SPARQL_NS = 'http://www.w3.org/ns/sparql#';\nconst OWL_IMPORTS = 'http://www.w3.org/2002/07/owl#imports';\nconst SRL_RULE_SET = `${SRL_NS}RuleSet`;\nconst SRL_RULE = `${SRL_NS}Rule`;\nconst SRL_DATA = `${SRL_NS}data`;\nconst SRL_RULES = `${SRL_NS}rules`;\nconst SRL_BODY = `${SRL_NS}body`;\nconst SRL_HEAD = `${SRL_NS}head`;\nconst SRL_SUBJECT = `${SRL_NS}subject`;\nconst SRL_PREDICATE = `${SRL_NS}predicate`;\nconst SRL_OBJECT = `${SRL_NS}object`;\nconst SRL_FILTER = `${SRL_NS}filter`;\nconst SRL_EXPR = `${SRL_NS}expr`;\nconst SRL_ASSIGN = `${SRL_NS}assign`;\nconst SRL_ASSIGN_VAR = `${SRL_NS}assignVar`;\nconst SRL_ASSIGN_VALUE = `${SRL_NS}assignValue`;\nconst SRL_NOT = `${SRL_NS}not`;\nconst SRL_VAR_NAME = `${SRL_NS}varName`;\nconst SHNEX_VAR = `${SHNEX_NS}var`;\n\nclass TurtleParser {\n constructor(source, options = {}) {\n this.tokens = Array.isArray(source) ? source : tokenize(source, options.filename || '<rdf>');\n this.pos = 0;\n this.baseIRI = options.baseIRI || null;\n this.bnodeCounter = 0;\n this.prefixes = {\n '': 'http://example/',\n rdf: RDF_NS,\n srl: SRL_NS,\n shnex: SHNEX_NS,\n sparql: SPARQL_NS,\n xsd: 'http://www.w3.org/2001/XMLSchema#',\n owl: 'http://www.w3.org/2002/07/owl#',\n ...options.prefixes,\n };\n this.triples = [];\n this.imports = [];\n }\n\n parseDocument() {\n while (!this.is('eof')) {\n if (this.matchDirective('PREFIX', '@prefix')) this.parsePrefix(this.previous().value.startsWith('@'));\n else if (this.matchDirective('BASE', '@base')) this.parseBase(this.previous().value.startsWith('@'));\n else this.parseTriplesStatement();\n }\n return {\n baseIRI: this.baseIRI,\n prefixes: { ...this.prefixes },\n triples: this.triples,\n imports: this.imports.slice(),\n };\n }\n\n parsePrefix(atStyle = false) {\n const nameToken = this.advance();\n if (nameToken.type !== 'word' || !nameToken.value.endsWith(':')) throw this.error('Expected prefix label ending in :', nameToken);\n const iriToken = this.expectType('iri');\n this.prefixes[nameToken.value.slice(0, -1)] = this.resolveIRI(iriToken.value, iriToken);\n if (atStyle) this.expectValue('.');\n }\n\n parseBase(atStyle = false) {\n const iriToken = this.expectType('iri');\n this.baseIRI = this.resolveIRI(iriToken.value, iriToken);\n if (atStyle) this.expectValue('.');\n }\n\n parseTriplesStatement() {\n const subjectNode = this.parseNode();\n this.triples.push(...subjectNode.triples);\n this.triples.push(...this.parsePredicateObjectList(subjectNode.term, ['.']));\n this.expectValue('.');\n }\n\n parsePredicateObjectList(subject, terminators = [']']) {\n const triples = [];\n while (!terminators.some((value) => this.checkValue(value))) {\n const predicate = this.parseVerb();\n do {\n const objectNode = this.parseNode();\n triples.push(...objectNode.triples);\n triples.push({ s: subject, p: predicate, o: objectNode.term });\n if (predicate.type === 'iri' && predicate.value === OWL_IMPORTS && objectNode.term.type === 'iri') this.imports.push(objectNode.term.value);\n } while (this.matchValue(','));\n if (this.matchValue(';')) {\n while (this.matchValue(';')) { /* tolerate repeated semicolons */ }\n if (terminators.some((value) => this.checkValue(value))) break;\n } else break;\n }\n return triples;\n }\n\n parseNode() {\n if (this.checkValue('[')) return this.parseBlankNodePropertyList();\n if (this.checkValue('(')) return this.parseCollection();\n return { term: this.parseTerm(), triples: [] };\n }\n\n parseBlankNodePropertyList() {\n this.expectValue('[');\n const node = this.freshBlankNode();\n if (this.matchValue(']')) return { term: node, triples: [] };\n const triples = this.parsePredicateObjectList(node, [']']);\n this.expectValue(']');\n return { term: node, triples };\n }\n\n parseCollection() {\n this.expectValue('(');\n if (this.matchValue(')')) return { term: iri(RDF_NIL), triples: [] };\n const items = [];\n while (!this.checkValue(')')) items.push(this.parseNode());\n this.expectValue(')');\n const triples = [];\n for (const item of items) triples.push(...item.triples);\n const cells = items.map(() => this.freshBlankNode());\n for (let i = 0; i < items.length; i += 1) {\n triples.push({ s: cells[i], p: iri(RDF_FIRST), o: items[i].term });\n triples.push({ s: cells[i], p: iri(RDF_REST), o: i + 1 < cells.length ? cells[i + 1] : iri(RDF_NIL) });\n }\n return { term: cells[0], triples };\n }\n\n parseVerb() {\n if (this.checkType('word') && this.peek().value === 'a') { this.advance(); return iri(RDF_TYPE); }\n const term = this.parseTerm();\n if (term.type !== 'iri') throw this.error('Expected IRI as Turtle predicate');\n return term;\n }\n\n parseTerm() {\n const token = this.advance();\n if (token.type === 'operator' && (token.value === '+' || token.value === '-') && this.peek().type === 'number') {\n const numberToken = this.advance();\n return numericLiteral(token.value === '-' ? -numberToken.value : numberToken.value);\n }\n if (token.type === 'iri') return iri(this.resolveIRI(token.value, token));\n if (token.type === 'string') return this.parseLiteralAfterToken(token);\n if (token.type === 'number') return numericLiteral(token.value);\n if (token.value === '<<(') return this.parseTripleTermAfterOpen();\n if (token.type === 'word') {\n const word = token.value.includes(':') || token.value.startsWith('_:') ? this.consumeHyphenatedWord(token.value) : token.value;\n if (word === 'a') return iri(RDF_TYPE);\n if (word === 'true') return literal(true, XSD_BOOLEAN);\n if (word === 'false') return literal(false, XSD_BOOLEAN);\n if (word.startsWith('_:')) return blankNode(word.slice(2));\n if (word.includes(':')) return iri(this.expandPrefixedName(word, token));\n }\n throw this.error(`Expected RDF term, got ${token.value}`, token);\n }\n\n parseTripleTermAfterOpen() {\n const s = this.parseTerm();\n const p = this.parseVerb();\n const o = this.parseTerm();\n this.expectValue(')>>');\n return tripleTerm(s, p, o);\n }\n\n parseLiteralAfterToken(token) {\n if (this.matchValue('^^')) {\n const datatype = this.parseDatatypeIRI();\n return literal(coerceLexicalLiteral(token.value, datatype), datatype, null);\n }\n if (this.checkType('word') && /^@[A-Za-z]+(?:-[A-Za-z0-9]+)*(?:--[A-Za-z]+)?$/.test(this.peek().value)) {\n const tag = this.advance().value.slice(1).toLowerCase();\n const [lang, langDir = null] = tag.split('--');\n return literal(token.value, null, lang, langDir);\n }\n return literal(token.value);\n }\n\n parseDatatypeIRI() {\n const token = this.advance();\n if (token.type === 'iri') return this.resolveIRI(token.value, token);\n if (token.type === 'word' && token.value.includes(':')) return this.expandPrefixedName(token.value, token);\n throw this.error(`Expected datatype IRI, got ${token.value}`, token);\n }\n\n freshBlankNode() {\n this.bnodeCounter += 1;\n return blankNode(`rdf${this.bnodeCounter}`);\n }\n\n consumeHyphenatedWord(value) {\n let out = value;\n while (this.checkValue('-') && (this.peekN(1).type === 'word' || this.peekN(1).type === 'number')) {\n this.advance();\n out += `-${this.advance().value}`;\n }\n return out;\n }\n\n expandPrefixedName(value, token) {\n const colon = value.indexOf(':');\n if (colon < 0) throw this.error(`Expected prefixed name, got ${value}`, token);\n const prefix = value.slice(0, colon);\n const local = value.slice(colon + 1);\n if (!Object.hasOwn(this.prefixes, prefix)) throw this.error(`Unknown prefix ${prefix}:`, token);\n return this.prefixes[prefix] + local;\n }\n\n resolveIRI(value) {\n if (!this.baseIRI || /^[A-Za-z][A-Za-z0-9+.-]*:/.test(value)) return value;\n try { return new URL(value, this.baseIRI).href; } catch (_) { return value; }\n }\n\n matchDirective(...names) {\n if (this.checkType('word')) {\n const value = this.peek().value;\n if (names.some((name) => value.toUpperCase() === name.toUpperCase())) { this.advance(); return true; }\n }\n return false;\n }\n\n previous() { return this.tokens[this.pos - 1]; }\n peek() { return this.tokens[this.pos]; }\n peekN(n) { return this.tokens[this.pos + n]; }\n is(type) { return this.peek().type === type; }\n checkType(type) { return this.peek().type === type; }\n checkValue(value) { return this.peek().value === value; }\n matchValue(value) { if (this.checkValue(value)) { this.advance(); return true; } return false; }\n advance() { return this.tokens[this.pos++]; }\n expectType(type) { const token = this.advance(); if (token.type !== type) throw this.error(`Expected ${type}, got ${token.value}`, token); return token; }\n expectValue(value) { const token = this.advance(); if (token.value !== value) throw this.error(`Expected ${value}, got ${token.value}`, token); return token; }\n error(message, token = this.peek()) { return new SyntaxErrorWithLocation(message, token); }\n}\n\nfunction parseRdfDocument(source, options = {}) {\n return new TurtleParser(source, options).parseDocument();\n}\n\nfunction parseRdfSyntax(source, options = {}) {\n const document = parseRdfDocument(source, options);\n return rdfDocumentToProgram(document, options);\n}\n\nfunction rdfDocumentToProgram(document, options = {}) {\n const graph = new RdfGraph(document.triples, document.prefixes);\n const ruleSetNodes = chooseRuleSets(graph, options.ruleSet);\n if (ruleSetNodes.length === 0) throw new Error('No srl:RuleSet found in RDF Rules syntax input');\n\n const program = {\n baseIRI: document.baseIRI || null,\n version: null,\n imports: options.rdfImportsAsImports ? document.imports.slice() : [],\n prefixes: { ...document.prefixes },\n data: [],\n rules: [],\n rdfSyntax: true,\n options: { shacl12Conformance: !!options.shacl12Conformance },\n ruleSets: ruleSetNodes.map((term) => formatTerm(term, document.prefixes)),\n };\n\n for (const ruleSet of ruleSetNodes) {\n for (const dataList of graph.objects(ruleSet, SRL_DATA)) {\n for (const item of graph.list(dataList)) program.data.push(toDataTriple(item, graph));\n }\n for (const rulesList of graph.objects(ruleSet, SRL_RULES)) {\n for (const ruleNode of graph.list(rulesList)) program.rules.push(toRule(ruleNode, graph, options));\n }\n }\n return program;\n}\n\nfunction chooseRuleSets(graph, selected) {\n if (selected) {\n const term = graph.parseReference(selected);\n return [term];\n }\n const typed = graph.subjects(RDF_TYPE, iri(SRL_RULE_SET));\n if (typed.length > 0) return uniqueTerms(typed);\n const byData = graph.subjectsWithPredicate(SRL_DATA);\n const byRules = graph.subjectsWithPredicate(SRL_RULES);\n return uniqueTerms([...byData, ...byRules]).filter((term) => graph.objects(term, SRL_RULES).length > 0 || graph.objects(term, SRL_DATA).length > 0);\n}\n\nfunction toDataTriple(item, graph) {\n if (item.type === 'triple') return { s: item.s, p: item.p, o: item.o };\n const triple = toTripleLike(item, graph);\n if ([triple.s, triple.p, triple.o].some((term) => term.type === 'var')) throw new Error('RDF Rules srl:data may not contain variables');\n if (triple.p.type !== 'iri') throw new Error('RDF Rules data triple predicate must be an IRI');\n return triple;\n}\n\nfunction toRule(ruleNode, graph, options = {}) {\n const bodyLists = graph.objects(ruleNode, SRL_BODY);\n const headLists = graph.objects(ruleNode, SRL_HEAD);\n if (bodyLists.length !== 1 || headLists.length !== 1) throw new Error(`RDF Rule ${graph.label(ruleNode)} must have exactly one srl:body and one srl:head`);\n const body = graph.list(bodyLists[0]).map((item) => toBodyElement(item, graph));\n const head = graph.list(headLists[0]).map((item) => toTripleLike(item, graph));\n return { name: graph.label(ruleNode), head, body, runOnce: ruleNeedsRunOnce(head, body, options) };\n}\n\nfunction toBodyElement(node, graph) {\n if (hasTripleShape(node, graph)) return { type: 'triple', triple: toTripleLike(node, graph) };\n const filters = graph.objects(node, SRL_FILTER).concat(graph.objects(node, SRL_EXPR));\n if (filters.length > 0) {\n if (filters.length !== 1) throw new Error(`Filter element ${graph.label(node)} must have exactly one srl:filter`);\n return { type: 'filter', expr: toExpression(filters[0], graph) };\n }\n const assigns = graph.objects(node, SRL_ASSIGN);\n if (assigns.length > 0) {\n if (assigns.length !== 1) throw new Error(`Assignment element ${graph.label(node)} must have exactly one srl:assign`);\n const assign = assigns[0];\n const vars = graph.objects(assign, SRL_ASSIGN_VAR);\n const values = graph.objects(assign, SRL_ASSIGN_VALUE);\n if (vars.length !== 1 || values.length !== 1) throw new Error(`Assignment ${graph.label(assign)} must have exactly one srl:assignVar and srl:assignValue`);\n const variableTerm = toVarOrTerm(vars[0], graph);\n if (variableTerm.type !== 'var') throw new Error('srl:assignVar must point to a variable node');\n return { type: 'set', variable: variableTerm.value, expr: toExpression(values[0], graph) };\n }\n const negations = graph.objects(node, SRL_NOT);\n if (negations.length > 0) {\n if (negations.length !== 1) throw new Error(`Negation element ${graph.label(node)} must have exactly one srl:not`);\n const body = graph.list(negations[0]).map((item) => {\n const clause = toBodyElement(item, graph);\n if (clause.type === 'set' || clause.type === 'not') throw new Error('RDF Rules srl:not may contain only triple patterns and filters');\n return clause;\n });\n return { type: 'not', body };\n }\n throw new Error(`Unsupported RDF Rules body element ${graph.label(node)}`);\n}\n\nfunction toTripleLike(node, graph) {\n if (node.type === 'triple') return { s: node.s, p: node.p, o: node.o };\n const subjects = graph.objects(node, SRL_SUBJECT);\n const predicates = graph.objects(node, SRL_PREDICATE);\n const objects = graph.objects(node, SRL_OBJECT);\n if (subjects.length !== 1 || predicates.length !== 1 || objects.length !== 1) {\n throw new Error(`Triple node ${graph.label(node)} must have exactly one srl:subject, srl:predicate and srl:object`);\n }\n return {\n s: toVarOrTerm(subjects[0], graph),\n p: toVarOrTerm(predicates[0], graph),\n o: toVarOrTerm(objects[0], graph),\n };\n}\n\nfunction hasTripleShape(node, graph) {\n return graph.objects(node, SRL_SUBJECT).length > 0 || graph.objects(node, SRL_PREDICATE).length > 0 || graph.objects(node, SRL_OBJECT).length > 0;\n}\n\nfunction toVarOrTerm(node, graph) {\n const varNames = graph.objects(node, SRL_VAR_NAME);\n if (varNames.length > 0) {\n if (varNames.length !== 1 || varNames[0].type !== 'literal') throw new Error(`Variable node ${graph.label(node)} must have exactly one string srl:varName`);\n return variable(String(varNames[0].value));\n }\n return node;\n}\n\nfunction toExpression(node, graph) {\n const varNames = graph.objects(node, SHNEX_VAR).concat(graph.objects(node, SRL_VAR_NAME));\n if (varNames.length > 0) {\n if (varNames.length !== 1 || varNames[0].type !== 'literal') throw new Error(`Expression variable ${graph.label(node)} must name one variable`);\n return { type: 'var', name: String(varNames[0].value) };\n }\n if (node.type === 'literal') {\n if (node.datatype || node.lang) return { type: 'term', value: node };\n return { type: 'literal', value: node.value };\n }\n if (node.type === 'iri' || node.type === 'blank' || node.type === 'triple') {\n const call = graph.functionCall(node);\n if (call) return toFunctionExpression(call.name, call.args.map((arg) => toExpression(arg, graph)));\n if (node.type === 'blank' && graph.hasOutgoing(node)) return { type: 'term', value: node };\n return { type: 'term', value: toVarOrTerm(node, graph) };\n }\n return { type: 'term', value: node };\n}\n\nfunction toFunctionExpression(name, args) {\n if (name.startsWith(SPARQL_NS)) {\n const local = name.slice(SPARQL_NS.length);\n if (local === 'less-than' || local === 'lessThan') return binary('<', args);\n if (local === 'less-than-or-equal' || local === 'lessThanOrEqual') return binary('<=', args);\n if (local === 'greater-than' || local === 'greaterThan') return binary('>', args);\n if (local === 'greater-than-or-equal' || local === 'greaterThanOrEqual') return binary('>=', args);\n if (local === 'equal' || local === 'equals') return binary('=', args);\n if (local === 'not-equal' || local === 'notEqual') return binary('!=', args);\n if (local === 'add') return foldBinary('+', args);\n if (local === 'subtract') return binary('-', args);\n if (local === 'multiply') return foldBinary('*', args);\n if (local === 'divide') return binary('/', args);\n if (local === 'and' || local === 'function-and') return foldBinary('&&', args);\n if (local === 'or' || local === 'function-or') return foldBinary('||', args);\n if (local === 'not') return { type: 'unary', op: '!', expr: args[0] };\n const builtin = sparqlLocalToBuiltin(local);\n return { type: 'call', name: builtin, args };\n }\n return { type: 'call', name, args };\n}\n\nfunction binary(op, args) {\n if (args.length !== 2) throw new Error(`sparql operator ${op} expects 2 arguments`);\n return { type: 'binary', op, left: args[0], right: args[1] };\n}\n\nfunction foldBinary(op, args) {\n if (args.length < 2) throw new Error(`sparql operator ${op} expects at least 2 arguments`);\n return args.slice(1).reduce((left, right) => ({ type: 'binary', op, left, right }), args[0]);\n}\n\nfunction sparqlLocalToBuiltin(local) {\n return local.replace(/-([a-z])/g, (_, ch) => ch.toUpperCase()).replace(/^./, (ch) => ch.toUpperCase());\n}\n\nclass RdfGraph {\n constructor(triples, prefixes = {}) {\n this.triples = triples;\n this.prefixes = prefixes;\n this.bySubject = new Map();\n for (const triple of triples) {\n const key = termKey(triple.s);\n if (!this.bySubject.has(key)) this.bySubject.set(key, []);\n this.bySubject.get(key).push(triple);\n }\n }\n\n objects(subject, predicateIRI) {\n const rows = this.bySubject.get(termKey(subject)) || [];\n return rows.filter((triple) => triple.p.type === 'iri' && triple.p.value === predicateIRI).map((triple) => triple.o);\n }\n\n subjects(predicateIRI, object) {\n return this.triples.filter((triple) => triple.p.type === 'iri' && triple.p.value === predicateIRI && termEquals(triple.o, object)).map((triple) => triple.s);\n }\n\n subjectsWithPredicate(predicateIRI) {\n return this.triples.filter((triple) => triple.p.type === 'iri' && triple.p.value === predicateIRI).map((triple) => triple.s);\n }\n\n hasOutgoing(subject) {\n return (this.bySubject.get(termKey(subject)) || []).length > 0;\n }\n\n list(head) {\n const out = [];\n let node = head;\n const seen = new Set();\n while (!(node.type === 'iri' && node.value === RDF_NIL)) {\n const key = termKey(node);\n if (seen.has(key)) throw new Error(`Cycle in RDF list at ${this.label(node)}`);\n seen.add(key);\n const first = this.objects(node, RDF_FIRST);\n const rest = this.objects(node, RDF_REST);\n if (first.length !== 1 || rest.length !== 1) throw new Error(`Expected RDF list node at ${this.label(node)}`);\n out.push(first[0]);\n node = rest[0];\n }\n return out;\n }\n\n functionCall(node) {\n if (node.type !== 'blank') return null;\n const rows = (this.bySubject.get(termKey(node)) || []).filter((triple) => triple.p.type === 'iri');\n const calls = rows.filter((triple) => triple.p.value.startsWith(SPARQL_NS) || triple.p.value.includes('#') || triple.p.value.includes('/'));\n const viable = calls.filter((triple) => isRdfListHead(triple.o, this));\n if (viable.length !== 1) return null;\n return { name: viable[0].p.value, args: this.list(viable[0].o) };\n }\n\n parseReference(text) {\n if (typeof text !== 'string') return text;\n if (text.startsWith('<') && text.endsWith('>')) return iri(text.slice(1, -1));\n if (text.startsWith('_:')) return blankNode(text.slice(2));\n const colon = text.indexOf(':');\n if (colon >= 0) {\n const prefix = text.slice(0, colon);\n const local = text.slice(colon + 1);\n const ns = this.prefixes[prefix] || (prefix === 'srl' ? SRL_NS : null);\n if (ns) return iri(ns + local);\n }\n return iri(text);\n }\n\n label(term) { return formatTerm(term, this.prefixes); }\n}\n\nfunction isRdfListHead(term, graph) {\n return (term.type === 'iri' && term.value === RDF_NIL) || graph.objects(term, RDF_FIRST).length === 1;\n}\n\nfunction uniqueTerms(terms) {\n const seen = new Set();\n const out = [];\n for (const term of terms) {\n const key = termKey(term);\n if (!seen.has(key)) { seen.add(key); out.push(term); }\n }\n return out;\n}\n\nfunction numericLiteral(value) {\n if (Number.isInteger(value)) return literal(value, XSD_INTEGER);\n if (String(value).includes('e') || String(value).includes('E')) return literal(value, XSD_DOUBLE);\n return literal(value, XSD_DECIMAL);\n}\n\nfunction parseIntegerLiteral(value) {\n const text = String(value);\n const asNumber = Number.parseInt(text, 10);\n return Number.isSafeInteger(asNumber) && String(asNumber) === text.replace(/^\\+/, '') ? asNumber : BigInt(text);\n}\n\nfunction coerceLexicalLiteral(value, datatype) {\n if (datatype === XSD_INTEGER) return parseIntegerLiteral(value);\n if (datatype === XSD_DECIMAL || datatype === XSD_DOUBLE) return Number(value);\n if (datatype === XSD_BOOLEAN) return value === true || value === 'true' || value === '1';\n return value;\n}\n\nfunction looksLikeRdfRules(source, options = {}) {\n if (options.syntax === 'rdf') return true;\n if (options.syntax === 'srl') return false;\n if (options.filename && /\\.(ttl|trig|nt|n3)$/i.test(options.filename)) return true;\n return /\\bsrl:RuleSet\\b|\\bsrl:rules\\b|http:\\/\\/www\\.w3\\.org\\/ns\\/shacl-rules#RuleSet/.test(source);\n}\n\nmodule.exports = {\n parseRdfDocument,\n parseRdfSyntax,\n rdfDocumentToProgram,\n looksLikeRdfRules,\n TurtleParser,\n RdfGraph,\n constants: {\n SRL_NS,\n SHNEX_NS,\n SPARQL_NS,\n SRL_RULE_SET,\n SRL_RULE,\n },\n};\n\n\n// ---- Grammar-hardened RDF 1.1 / RDF 1.2 syntax helpers ----\n// These functions are used by the W3C RDF manifest harness and are kept in\n// rdfSyntax.js beside the existing Turtle/RDF-Rules front-end instead of in a\n// separate monolithic test file. They intentionally keep an internal test\n// graph representation because the W3C manifests exercise syntax, datasets,\n// triple terms, and RDF 1.2 annotation isomorphism independently from the SRL\n// rule engine representation.\nconst rdfW3cSyntax = (() => {\n// Grammar-hardened RDF syntax code shared by the RDF Rules front-end and W3C manifest harness.\nfunction iri(value) {\n if (!value) throw new Error('iri(value) requires a non-empty value');\n return Object.freeze({ kind: 'iri', value: String(value) });\n}\nfunction literal(value, datatype = null, language = null, langDir = null) {\n return Object.freeze({ kind: 'literal', value: String(value), datatype, language, langDir });\n}\nfunction blank(value) {\n const clean = String(value || '').replace(/^_:/, '');\n if (!clean) throw new Error('blank(value) requires a name');\n return Object.freeze({ kind: 'blank', value: clean });\n}\nfunction tripleTerm(s, p, o) { return Object.freeze({ kind: 'triple', s, p, o }); }\nfunction variable(name) {\n const clean = String(name || '').replace(/^\\?/, '');\n if (!clean) throw new Error('variable(name) requires a name');\n return Object.freeze({ kind: 'var', name: clean });\n}\nfunction triple(s, p, o, graph = null) { return Object.freeze({ s, p, o, graph }); }\nfunction termKey(term) {\n if (!term) return 'default';\n switch (term.kind) {\n case 'iri': return `I:${term.value}`;\n case 'literal': return `L:${JSON.stringify(term.value)}^^${term.datatype || ''}@${term.language || ''}`;\n case 'blank': return `B:${term.value}`;\n case 'var': return `V:${term.name}`;\n case 'triple': return `T:${termKey(term.s)} ${termKey(term.p)} ${termKey(term.o)}`;\n default: throw new Error(`Unsupported term kind: ${term.kind}`);\n }\n}\nfunction tripleKey(t) { return `${termKey(t.s)} ${termKey(t.p)} ${termKey(t.o)} ${termKey(t.graph)}`; }\nclass Rule { constructor({ id, body = [], head = [], profile = 'n3-rules-subset-v0' } = {}) { this.id = id; this.body = body; this.head = head; this.profile = profile; } }\n\nconst RDF_NS = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';\nconst RDF_TYPE = `${RDF_NS}type`;\nconst RDF_FIRST = `${RDF_NS}first`;\nconst RDF_REST = `${RDF_NS}rest`;\nconst RDF_NIL = `${RDF_NS}nil`;\nconst RDF_REIFIES = `${RDF_NS}reifies`;\nconst RDF_LANG_STRING = `${RDF_NS}langString`;\nconst RDF_DIR_LANG_STRING = `${RDF_NS}dirLangString`;\nconst XSD_BOOLEAN = 'http://www.w3.org/2001/XMLSchema#boolean';\nconst XSD_INTEGER = 'http://www.w3.org/2001/XMLSchema#integer';\nconst XSD_DECIMAL = 'http://www.w3.org/2001/XMLSchema#decimal';\nconst XSD_DOUBLE = 'http://www.w3.org/2001/XMLSchema#double';\nconst XSD_STRING = 'http://www.w3.org/2001/XMLSchema#string';\n\n// ---- N-Triples / N-Quads parser ----\nconst { parseNQuads, termToNQuads, tripleToNQuads, triplesToNQuads } = (() => {\n\nfunction isWs(ch) { return ch === ' ' || ch === '\\t'; }\nfunction isLineEnd(ch) { return ch === '\\n' || ch === '\\r'; }\nfunction isHex(text) { return /^[0-9A-Fa-f]+$/.test(text); }\n\nfunction decodeCodePoint(hex, token) {\n const code = Number.parseInt(hex, 16);\n if (!Number.isFinite(code) || code < 0 || code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) {\n throw new Error(`Invalid Unicode escape in ${token}`);\n }\n return String.fromCodePoint(code);\n}\n\nfunction decodeIriEscapes(value, token = 'IRI') {\n let out = '';\n for (let i = 0; i < value.length; i += 1) {\n const ch = value[i];\n if (ch !== '\\\\') {\n if (/[<>\"{}|^`\\u0000-\\u0020]/.test(ch)) throw new Error(`Invalid character in ${token}`);\n out += ch;\n continue;\n }\n const esc = value[++i];\n if (esc === 'u') {\n const hex = value.slice(i + 1, i + 5);\n if (hex.length !== 4 || !isHex(hex)) throw new Error(`Invalid Unicode escape in ${token}`);\n out += decodeCodePoint(hex, token);\n i += 4;\n } else if (esc === 'U') {\n const hex = value.slice(i + 1, i + 9);\n if (hex.length !== 8 || !isHex(hex)) throw new Error(`Invalid Unicode escape in ${token}`);\n out += decodeCodePoint(hex, token);\n i += 8;\n } else {\n throw new Error(`Invalid IRI escape \\\\${esc} in ${token}`);\n }\n }\n return out;\n}\n\nfunction decodeLiteralEscapes(value, token = 'literal') {\n let out = '';\n for (let i = 0; i < value.length; i += 1) {\n const ch = value[i];\n if (ch !== '\\\\') {\n if (ch === '\\n' || ch === '\\r') throw new Error(`Raw line break in ${token}`);\n out += ch;\n continue;\n }\n const esc = value[++i];\n if (!esc) throw new Error(`Trailing escape in ${token}`);\n if (esc === 't') out += '\\t';\n else if (esc === 'b') out += '\\b';\n else if (esc === 'n') out += '\\n';\n else if (esc === 'r') out += '\\r';\n else if (esc === 'f') out += '\\f';\n else if (esc === '\"') out += '\"';\n else if (esc === \"'\") out += \"'\";\n else if (esc === '\\\\') out += '\\\\';\n else if (esc === 'u') {\n const hex = value.slice(i + 1, i + 5);\n if (hex.length !== 4 || !isHex(hex)) throw new Error(`Invalid Unicode escape in ${token}`);\n out += decodeCodePoint(hex, token);\n i += 4;\n } else if (esc === 'U') {\n const hex = value.slice(i + 1, i + 9);\n if (hex.length !== 8 || !isHex(hex)) throw new Error(`Invalid Unicode escape in ${token}`);\n out += decodeCodePoint(hex, token);\n i += 8;\n } else {\n throw new Error(`Invalid escape \\\\${esc} in ${token}`);\n }\n }\n return out;\n}\n\nfunction stripNqComment(line) {\n let inString = false;\n let inIri = false;\n let escaped = false;\n for (let i = 0; i < line.length; i += 1) {\n const ch = line[i];\n if (escaped) { escaped = false; continue; }\n if (ch === '\\\\') { escaped = true; continue; }\n if (!inIri && ch === '\"') { inString = !inString; continue; }\n if (!inString && ch === '<' && line[i + 1] !== '<') { inIri = true; continue; }\n if (!inString && inIri && ch === '>') { inIri = false; continue; }\n if (!inString && !inIri && ch === '#') return line.slice(0, i);\n }\n return line;\n}\n\nfunction validateAbsoluteIri(value, position) {\n if (!/^[A-Za-z][A-Za-z0-9+.-]*:/.test(value)) throw new Error(`${position} must be absolute`);\n return value;\n}\n\nfunction validateBlankLabel(value) {\n // RDF blank node labels follow PN_CHARS-style rules. This deliberately accepts\n // Unicode letters and leading underscores; it still rejects empty labels,\n // labels ending in '.', and doubled dots because those are common false\n // positives when a compact statement terminator is adjacent to a blank node.\n if (!value || value.endsWith('.') || value.includes('..')) throw new Error(`Invalid blank node label _: ${value}`);\n if (!/^[\\p{L}\\p{N}_](?:[\\p{L}\\p{N}._\\-\\u00B7\\u0300-\\u036F\\u203F-\\u2040]*[\\p{L}\\p{N}_\\-\\u00B7\\u0300-\\u036F\\u203F-\\u2040])?$/u.test(value)) {\n throw new Error(`Invalid blank node label _: ${value}`);\n }\n return value;\n}\n\nfunction validateLang(value) {\n // LANG_DIR uses BCP47-style language tags. Keep this intentionally strict enough\n // for the W3C syntax tests: each subtag is 1..8 alphanumeric chars, starting alpha.\n if (!value || value.includes('--') || !/^[A-Za-z]{1,8}(?:-[A-Za-z0-9]{1,8})*$/.test(value)) throw new Error(`Invalid language tag @${value}`);\n return value;\n}\n\nclass LineReader {\n constructor(line, lineNumber) {\n this.line = line;\n this.lineNumber = lineNumber;\n this.i = 0;\n }\n\n eof() { return this.i >= this.line.length; }\n peek(offset = 0) { return this.line[this.i + offset]; }\n startsWith(value) { return this.line.startsWith(value, this.i); }\n skipWs() { while (isWs(this.peek())) this.i += 1; }\n\n expect(value) {\n if (!this.startsWith(value)) throw new Error(`Expected ${value} on line ${this.lineNumber}, got ${this.line.slice(this.i, this.i + 20) || 'end of line'}`);\n this.i += value.length;\n }\n\n readIri(position = 'IRI') {\n this.expect('<');\n let raw = '';\n while (!this.eof()) {\n const ch = this.peek();\n if (ch === '>') { this.i += 1; return validateAbsoluteIri(decodeIriEscapes(raw, position), position); }\n raw += ch;\n this.i += 1;\n }\n throw new Error(`Unterminated ${position} on line ${this.lineNumber}`);\n }\n\n readBlank() {\n this.expect('_:');\n const start = this.i;\n while (!this.eof()) {\n const ch = this.peek();\n if (!/[\\p{L}\\p{N}._\\-\\u00B7\\u0300-\\u036F\\u203F-\\u2040]/u.test(ch)) break;\n if (ch === '.') {\n const next = this.peek(1);\n if (!next || isWs(next) || next === '<' || next === '_' || next === '\"' || next === '#') break;\n }\n this.i += 1;\n }\n return blank(validateBlankLabel(this.line.slice(start, this.i)));\n }\n\n readLiteral() {\n this.expect('\"');\n let raw = '';\n let escaped = false;\n while (!this.eof()) {\n const ch = this.peek();\n this.i += 1;\n if (escaped) { raw += `\\\\${ch}`; escaped = false; continue; }\n if (ch === '\\\\') { escaped = true; continue; }\n if (ch === '\"') {\n const value = decodeLiteralEscapes(raw, 'literal');\n let language = null;\n let datatype = XSD_STRING;\n if (this.peek() === '@') {\n this.i += 1;\n const start = this.i;\n while (!this.eof() && /[A-Za-z0-9-]/.test(this.peek())) this.i += 1;\n let rawLang = this.line.slice(start, this.i);\n if (!rawLang) throw new Error('Invalid language tag: missing');\n if (rawLang.endsWith('--ltr') || rawLang.endsWith('--rtl')) rawLang = rawLang.slice(0, -5);\n language = validateLang(rawLang);\n datatype = null;\n } else if (this.startsWith('^^')) {\n this.i += 2;\n this.skipWs();\n datatype = this.readIri('datatype IRI');\n if (datatype === RDF_LANG_STRING || datatype === RDF_DIR_LANG_STRING) {\n throw new Error(`Datatype ${datatype} requires LANG_DIR syntax, not ^^`);\n }\n }\n // RDF 1.2 base direction suffix, e.g. --ltr / --rtl. The core term model does not preserve it yet;\n // accepting it is enough for syntax tests and keeps eval comparison conservative for now.\n if (this.startsWith('--ltr') || this.startsWith('--rtl')) {\n if (!language) throw new Error('Base direction requires a language tag');\n this.i += 5;\n }\n return literal(value, datatype, language);\n }\n raw += ch;\n }\n throw new Error(`Unterminated literal on line ${this.lineNumber}`);\n }\n\n readTerm(position = 'term') {\n this.skipWs();\n if (this.startsWith('<<')) return this.readTripleTerm();\n if (this.peek() === '<') return iri(this.readIri(position));\n if (this.startsWith('_:')) return this.readBlank();\n if (this.peek() === '\"') return this.readLiteral();\n throw new Error(`Expected RDF term for ${position}, got ${this.line.slice(this.i, this.i + 20) || 'end of line'}`);\n }\n\n readSubjectOrGraph(position) {\n const term = this.readTerm(position);\n if (term.kind === 'literal') throw new Error(`N-Quads ${position} cannot be a literal`);\n if (term.kind === 'triple' && (position === 'subject' || position === 'graph')) throw new Error(`N-Quads ${position} cannot be a triple term`);\n return term;\n }\n\n readPredicate() {\n this.skipWs();\n if (this.peek() !== '<') throw new Error(`N-Quads predicate must be an IRI, got ${this.line.slice(this.i, this.i + 20) || 'end of line'}`);\n return iri(this.readIri('predicate'));\n }\n\n readTripleTerm() {\n this.expect('<<');\n this.skipWs();\n // RDF 1.2 N-Triples/N-Quads triple terms use parenthesized triples: <<( s p o )>>.\n // The older unparenthesized RDF-star form is a reified-triple syntax form and is not\n // accepted as a plain subject/object term by the RDF 1.2 syntax manifests.\n this.expect('(');\n this.skipWs();\n const s = this.readSubjectOrGraph('triple-term subject');\n this.skipWs();\n const p = this.readPredicate();\n this.skipWs();\n const o = this.readTerm('triple-term object');\n this.skipWs();\n this.expect(')');\n this.skipWs();\n this.expect('>>');\n return tripleTerm(s, p, o);\n }\n}\n\nfunction parseLine(line, lineNumber, format) {\n const clean = stripNqComment(line).trim();\n if (!clean) return null;\n const r = new LineReader(clean, lineNumber);\n const s = r.readSubjectOrGraph('subject');\n r.skipWs();\n const p = r.readPredicate();\n r.skipWs();\n const o = r.readTerm('object');\n r.skipWs();\n let g = null;\n if (r.peek() !== '.') {\n if (format === 'ntriples') throw new Error(`N-Triples line ${lineNumber} has too many terms before .`);\n g = r.readSubjectOrGraph('graph');\n r.skipWs();\n }\n if (r.peek() !== '.') throw new Error(`N-Quads line ${lineNumber} must end with .`);\n r.i += 1;\n r.skipWs();\n if (!r.eof()) throw new Error(`Unexpected trailing content on N-Quads line ${lineNumber}: ${clean.slice(r.i)}`);\n return triple(s, p, o, g);\n}\n\nfunction parseNQuads(source, options = {}) {\n const facts = [];\n const prefixes = { ...(options.prefixes || {}) };\n const format = options.format || (options.profileId === 'ntriples-graph-v0' ? 'ntriples' : 'nquads');\n const lines = String(source || '').split(/\\r\\n|\\n|\\r/);\n for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {\n const fact = parseLine(lines[lineIndex], lineIndex + 1, format);\n if (fact) facts.push(fact);\n }\n return {\n profile: options.profileId || (format === 'ntriples' ? 'ntriples-graph-v0' : 'nquads-dataset-v0'),\n prefixes,\n base: options.base || '',\n imports: [],\n facts,\n rules: [],\n queries: [],\n expectations: [],\n };\n}\n\nfunction escapeIri(value) {\n return String(value).replace(/[\\\\>\\u0000-\\u0020]/g, (ch) => `\\\\u${ch.charCodeAt(0).toString(16).padStart(4, '0')}`);\n}\n\nfunction escapeLiteral(value) {\n return String(value)\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, '\\\\n')\n .replace(/\\r/g, '\\\\r')\n .replace(/\\t/g, '\\\\t')\n .replace(/\\u0008/g, '\\\\b')\n .replace(/\\u000c/g, '\\\\f');\n}\n\nfunction termToNQuads(term) {\n if (!term) return '';\n switch (term.kind) {\n case 'iri':\n return `<${escapeIri(term.value)}>`;\n case 'blank':\n return `_:${term.value}`;\n case 'literal': {\n let out = `\"${escapeLiteral(term.value)}\"`;\n if (term.language) out += `@${term.language}`;\n else if (term.datatype && term.datatype !== XSD_STRING) out += `^^<${escapeIri(term.datatype)}>`;\n return out;\n }\n case 'triple':\n return `<< ${termToNQuads(term.s)} ${termToNQuads(term.p)} ${termToNQuads(term.o)} >>`;\n default:\n throw new Error(`Cannot serialize ${term.kind} as N-Quads`);\n }\n}\n\nfunction tripleToNQuads(value) {\n const terms = [termToNQuads(value.s), termToNQuads(value.p), termToNQuads(value.o)];\n if (value.graph) terms.push(termToNQuads(value.graph));\n return `${terms.join(' ')} .`;\n}\n\nfunction triplesToNQuads(triples) {\n return Array.from(new Set(Array.from(triples || []).map(tripleToNQuads))).sort().join('\\n');\n}\nreturn { parseNQuads, termToNQuads, tripleToNQuads, triplesToNQuads };\n})();\n\n// ---- Turtle / TriG parser ----\nconst { parseN3 } = (() => {\n\nconst DEFAULT_PREFIXES = Object.freeze({\n rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',\n xsd: 'http://www.w3.org/2001/XMLSchema#',\n log: 'http://www.w3.org/2000/10/swap/log#',\n});\n\n\nfunction isWs(ch) { return /\\s/.test(ch || ''); }\nfunction isPunct(ch) { return '{}.;,()[]|'.includes(ch || ''); }\nfunction isHex(text) { return /^[0-9A-Fa-f]+$/.test(text); }\nfunction isAbsoluteIri(value) { return /^[A-Za-z][A-Za-z0-9+.-]*:/.test(value || ''); }\nfunction resolveIriReference(value, base) {\n if (isAbsoluteIri(value)) return value;\n if (!base) return value;\n try {\n const url = new URL(value, base);\n let href = url.href;\n // The RDF IRI-resolution tests expect bare authority references such as //g\n // to remain http://g, not to gain the URL API's cosmetic trailing slash.\n if (/^\\/\\/[^/?#]+$/.test(value) && href.endsWith('/')) href = href.slice(0, -1);\n if (/^file:\\/\\/[^/?#]+$/.test(value) && href.endsWith('/')) href = href.slice(0, -1);\n return href;\n } catch { return `${base}${value}`; }\n}\nfunction validateBlankLabel(value) {\n const clean = String(value || '').replace(/^_:/, '');\n if (!clean || clean.endsWith('.') || clean.includes('..')) throw new Error(`Invalid blank node label _: ${clean}`);\n // BLANK_NODE_LABEL follows the PN_CHARS family; ':' is only for prefixed names, not blank labels.\n if (/[\\s<>\"{}|^`\\\\:]/u.test(clean)) throw new Error(`Invalid blank node label _: ${clean}`);\n if (/^[\\-.]/u.test(clean)) throw new Error(`Invalid blank node label _: ${clean}`);\n return clean;\n}\nfunction validateIriReference(value) {\n if (/[<>\\\"{}|^`\\u0000-\\u0020]/.test(value)) throw new Error('Invalid character in IRIREF');\n return value;\n}\nfunction validatePrefixedLocal(raw, decoded) {\n if (!raw) return decoded;\n if (raw.startsWith('-') || raw.startsWith('\\\\-') || raw.startsWith('.')) throw new Error(`Invalid prefixed name local ${raw}`);\n for (let i = 0; i < raw.length; i += 1) {\n const ch = raw[i];\n if (ch === '\\\\') {\n const esc = raw[i + 1];\n if (!esc || !'_~.-!$&\\'()*+,;=/?#@%'.includes(esc)) throw new Error(`Invalid prefixed name local escape ${raw}`);\n i += 1;\n continue;\n }\n if (ch === '%') {\n const hex = raw.slice(i + 1, i + 3);\n if (hex.length !== 2 || !isHex(hex)) throw new Error(`Invalid percent escape in prefixed name local ${raw}`);\n i += 2;\n continue;\n }\n if (ch === '~' || ch === '^') throw new Error(`Invalid prefixed name local ${raw}`);\n }\n return decoded;\n}\nfunction decodePrefixedLocal(raw) {\n let out = '';\n for (let i = 0; i < raw.length; i += 1) {\n const ch = raw[i];\n if (ch === '\\\\') { out += raw[i + 1] || ''; i += 1; }\n else out += ch;\n }\n return out;\n}\nfunction codePoint(hex, label) {\n const n = Number.parseInt(hex, 16);\n if (!Number.isFinite(n) || n < 0 || n > 0x10ffff || (n >= 0xd800 && n <= 0xdfff)) throw new Error(`Invalid Unicode escape in ${label}`);\n return String.fromCodePoint(n);\n}\nfunction decodeEscapes(text, label, iriMode = false) {\n let out = '';\n for (let i = 0; i < text.length; i += 1) {\n const ch = text[i];\n if (ch !== '\\\\') { out += ch; continue; }\n const esc = text[++i];\n if (!esc) throw new Error(`Trailing escape in ${label}`);\n if (esc === 'u') {\n const hex = text.slice(i + 1, i + 5); if (hex.length !== 4 || !isHex(hex)) throw new Error(`Invalid Unicode escape in ${label}`);\n out += codePoint(hex, label); i += 4;\n } else if (esc === 'U') {\n const hex = text.slice(i + 1, i + 9); if (hex.length !== 8 || !isHex(hex)) throw new Error(`Invalid Unicode escape in ${label}`);\n out += codePoint(hex, label); i += 8;\n } else if (!iriMode && 'tbnrf\"\\''.includes(esc)) {\n out += { t: '\\t', b: '\\b', n: '\\n', r: '\\r', f: '\\f', '\"': '\"', \"'\": \"'\" }[esc] ?? esc;\n } else if (!iriMode && esc === '\\\\') out += '\\\\';\n else if (iriMode) throw new Error(`Invalid escape \\\\${esc} in ${label}`);\n else throw new Error(`Invalid escape \\\\${esc} in ${label}`);\n }\n return out;\n}\n\nclass Tokenizer {\n constructor(source) { this.source = String(source || ''); this.i = 0; this.tokens = []; }\n eof() { return this.i >= this.source.length; }\n peek(offset = 0) { return this.source[this.i + offset]; }\n startsWith(value) { return this.source.startsWith(value, this.i); }\n push(type, value, extra = {}) { this.tokens.push({ type, value, ...extra }); }\n skipComment() { while (!this.eof() && this.peek() !== '\\n' && this.peek() !== '\\r') this.i += 1; }\n readIri() {\n this.i += 1;\n let raw = '';\n while (!this.eof()) {\n const ch = this.peek();\n if (ch === '>') { this.i += 1; this.push('iri', validateIriReference(decodeEscapes(raw, 'IRI', true))); return; }\n raw += ch; this.i += 1;\n }\n throw new Error('Unterminated IRIREF');\n }\n readString() {\n const quote = this.peek();\n const long = this.source.startsWith(quote.repeat(3), this.i);\n this.i += long ? 3 : 1;\n let raw = '';\n let escaped = false;\n while (!this.eof()) {\n const ch = this.peek();\n if (!escaped && long && this.source.startsWith(quote.repeat(3), this.i)) { this.i += 3; this.push(long ? 'longString' : 'string', decodeEscapes(raw, 'string'), { long }); return; }\n if (!escaped && !long && ch === quote) { this.i += 1; this.push('string', decodeEscapes(raw, 'string'), { long: false }); return; }\n if (!long && (ch === '\\n' || ch === '\\r')) throw new Error('Raw line break in short string');\n raw += ch;\n this.i += 1;\n escaped = !escaped && ch === '\\\\';\n if (ch !== '\\\\') escaped = false;\n }\n throw new Error('Unterminated string');\n }\n readBare() {\n const start = this.i;\n if (this.startsWith('_:')) {\n this.i += 2;\n while (!this.eof()) {\n const ch = this.peek();\n // BLANK_NODE_LABEL uses PN_CHARS, including broad Unicode ranges that are\n // not all JavaScript \\p{L}/\\p{N}. Tokenize generously up to a real\n // Turtle delimiter, then validate the label separately. Keep ':' as a\n // boundary so compact forms such as _:s:p tokenize as blank-node _:s\n // followed by predicate :p.\n if (isWs(ch) || '<>\\\"{}|^`\\\\;,)[]'.includes(ch) || ch === ':' || ch === '#') break;\n if (ch === '.') {\n const next = this.source[this.i + 1];\n if (!next || isWs(next) || '{};,)[]'.includes(next)) break;\n }\n this.i += 1;\n }\n this.push('bare', this.source.slice(start, this.i));\n return;\n }\n while (!this.eof()) {\n const ch = this.peek();\n if (isWs(ch)) break;\n if (ch === '\\\\' && this.source[this.i + 1]) { this.i += 2; continue; }\n if (ch === '<' || ch === '>' || ch === '\"' || ch === \"'\") break;\n if (ch === '.') {\n const next = this.source[this.i + 1];\n if (!next || isWs(next) || '{};,)[]'.includes(next)) break;\n } else if (isPunct(ch)) break;\n if (ch === '#') break;\n if (ch === '^' && this.peek(1) === '^') break;\n if (ch === '=' && this.peek(1) === '>') break;\n this.i += 1;\n }\n this.push('bare', this.source.slice(start, this.i));\n }\n tokenize() {\n while (!this.eof()) {\n const ch = this.peek();\n if (isWs(ch)) { this.i += 1; continue; }\n if (ch === '#') { this.skipComment(); continue; }\n if (this.startsWith('@prefix') && (isWs(this.source[this.i + 7]) || this.source[this.i + 7] === ':')) { this.push('bare', '@prefix'); this.i += 7; continue; }\n if (this.startsWith('@base') && isWs(this.source[this.i + 5])) { this.push('bare', '@base'); this.i += 5; continue; }\n if (this.startsWith('@version') && isWs(this.source[this.i + 8])) { this.push('bare', '@version'); this.i += 8; continue; }\n if (this.startsWith('=>')) { this.push('=>', '=>'); this.i += 2; continue; }\n if (this.startsWith('^^')) { this.push('^^', '^^'); this.i += 2; continue; }\n if (this.startsWith('<<')) { this.push('<<', '<<'); this.i += 2; continue; }\n if (this.startsWith('>>')) { this.push('>>', '>>'); this.i += 2; continue; }\n if (ch === '<') { this.readIri(); continue; }\n if (ch === '\"' || ch === \"'\") { this.readString(); continue; }\n if (ch === '.' && /[0-9]/.test(this.peek(1) || '')) { this.readBare(); continue; }\n if (ch === '~') { this.readBare(); continue; }\n if (isPunct(ch)) { this.push(ch, ch); this.i += 1; continue; }\n this.readBare();\n }\n return this.tokens;\n }\n}\n\nfunction parseN3(source, options = {}) {\n const tokens = new Tokenizer(source).tokenize();\n let i = 0;\n let base = options.base || '';\n const prefixes = { ...DEFAULT_PREFIXES, ...(options.prefixes || {}) };\n const facts = [];\n const rules = [];\n let bnodeCounter = 0;\n const bnodes = new Map();\n const syntaxProfile = String(options.profile || options.profileId || '').toLowerCase();\n const rdf12Surface = syntaxProfile === 'turtle' || syntaxProfile === 'trig';\n const implicitStatementNodes = new Set();\n function implicitStatementNodeKey(term) { return `${term.kind}:${term.value}`; }\n\n function freshBlank() { bnodeCounter += 1; return blank(`b${bnodeCounter}`); }\n function peek(offset = 0) { return tokens[i + offset]; }\n function next() { return tokens[i++]; }\n function eof() { return i >= tokens.length; }\n function accept(value) { if (peek()?.value === value || peek()?.type === value) { i += 1; return true; } return false; }\n function expect(value) { const t = next(); if (!t || (t.value !== value && t.type !== value)) throw new Error(`Expected ${value}, got ${t?.value || 'end of input'}`); return t; }\n function error(msg) { throw new Error(msg); }\n\n function parseIriValueFromBare(token) {\n if (token === 'a') return RDF_TYPE;\n if (token.startsWith('_:')) error(`Blank node label ${token} cannot be used as IRI`);\n const split = token.indexOf(':');\n if (split >= 0) {\n const prefix = token.slice(0, split);\n let local = token.slice(split + 1);\n if (!(prefix in prefixes)) throw new Error(`Unknown prefix ${prefix}:`);\n // Turtle permits reserved escaped characters in local names.\n local = validatePrefixedLocal(local, decodePrefixedLocal(local));\n return prefixes[prefix] + local;\n }\n throw new Error(`Expected IRI or prefixed name, got ${token}`);\n }\n\n function parseIriLike() {\n const t = next();\n if (!t) error('Unexpected end of input while reading IRI');\n if (t.type === 'iri') return resolveIriReference(t.value, base);\n if (t.type === 'bare') return parseIriValueFromBare(t.value);\n throw new Error(`Expected IRI or prefixed name, got ${t.value}`);\n }\n\n function parseBlankLabel(label) {\n const key = validateBlankLabel(label);\n if (!bnodes.has(key)) bnodes.set(key, blank(key));\n return bnodes.get(key);\n }\n\n function parseNumber(token) {\n if (/^[+-]?\\d+$/.test(token)) return literal(token, XSD_INTEGER);\n if (/^[+-]?(?:\\d+\\.\\d*|\\.\\d+)$/.test(token)) return literal(token, XSD_DECIMAL);\n if (/^[+-]?(?:(?:\\d+\\.\\d*)|(?:\\.\\d+)|\\d+)[eE][+-]?\\d+$/.test(token)) return literal(token, XSD_DOUBLE);\n return null;\n }\n\n function parseTerm(out = facts, graph = null, options2 = {}) {\n const t = peek();\n if (!t) error('Unexpected end of input while reading Turtle term');\n if (t.type === '<<') return parseTripleTerm(out, graph, options2);\n if (t.type === 'iri') { next(); return iri(resolveIriReference(t.value, base)); }\n if (t.type === 'string' || t.type === 'longString') {\n if (options2.noLiteral) throw new Error('Literal is not allowed here');\n return parseLiteral();\n }\n if (t.type === '[') {\n // ANON is a BlankNode and is permitted in rtSubject/ttSubject, even where\n // blankNodePropertyList is not. Keep [ ... ] rejected in those positions.\n if (options2.noCompound) {\n if (peek(1)?.type === ']') { expect('['); expect(']'); return freshBlank(); }\n throw new Error('Compound blank node expression is not allowed here');\n }\n return parseBlankNodePropertyList(out, graph);\n }\n if (t.type === '(') {\n if (options2.noCompound) throw new Error('Collection is not allowed here');\n return parseCollection(out, graph);\n }\n if (t.type === 'bare') {\n next();\n if (t.value.startsWith('?')) {\n if (rdf12Surface) throw new Error(`Variables are not allowed in Turtle/TriG: ${t.value}`);\n return variable(t.value.slice(1));\n }\n if (t.value.startsWith('_:')) return parseBlankLabel(t.value);\n if (t.value === 'a' && options2.noA) throw new Error('a is only allowed as a predicate');\n if (t.value === 'true' || t.value === 'false') {\n if (options2.noLiteral) throw new Error('Literal is not allowed here');\n return literal(t.value, XSD_BOOLEAN);\n }\n const num = parseNumber(t.value);\n if (num) {\n if (options2.noLiteral) throw new Error('Literal is not allowed here');\n return num;\n }\n return iri(parseIriValueFromBare(t.value));\n }\n throw new Error(`Expected IRI or prefixed name, got ${t.value}`);\n }\n\n function parseLiteral() {\n const t = next(); if (!t || (t.type !== 'string' && t.type !== 'longString')) throw new Error(`Expected string, got ${t?.value || 'end of input'}`);\n if (peek()?.type === 'bare' && peek().value.startsWith('@')) {\n let lang = next().value.slice(1);\n let langDir = null;\n if (lang.endsWith('--ltr') || lang.endsWith('--rtl')) {\n langDir = lang.slice(-3);\n lang = lang.slice(0, -5);\n }\n if (!lang || lang.includes('--') || !/^[A-Za-z]+(?:-[A-Za-z0-9]+)*$/.test(lang)) throw new Error(`Invalid language tag @${lang}`);\n if (peek()?.type === 'bare' && ['--ltr', '--rtl'].includes(peek().value)) langDir = next().value.slice(2);\n return literal(t.value, null, lang, langDir);\n }\n if (accept('^^')) return literal(t.value, parseIriLike());\n if (peek()?.type === 'bare' && ['--ltr', '--rtl'].includes(peek().value)) throw new Error('Base direction requires a language tag');\n return literal(t.value, XSD_STRING);\n }\n\n function parseReifierToken(out, graph) {\n const t = peek();\n if (!t || t.type !== 'bare' || !t.value.startsWith('~')) return null;\n next();\n const suffix = t.value.slice(1);\n if (suffix) {\n if (suffix.startsWith('_:')) return parseBlankLabel(suffix);\n return iri(parseIriValueFromBare(suffix));\n }\n const n = peek();\n if (n && (n.type === 'iri' || (n.type === 'bare' && (n.value.startsWith('_:') || n.value.includes(':') || n.value === 'a')))) {\n const term = parseTerm(out, graph, { noLiteral: true, noCompound: true, noTripleTerm: true });\n if (term.kind !== 'iri' && term.kind !== 'blank') throw new Error('Reifier must be an IRI or blank node');\n return term;\n }\n return freshBlank();\n }\n\n function parseTripleTerm(out, graph, options2 = {}) {\n expect('<<');\n const parenthesized = accept('(');\n if (parenthesized) {\n if (options2.noTripleTerm) throw new Error('Triple term is not allowed here');\n const s = parseTerm(out, graph, { noLiteral: true, noCompound: true, noReifiedTriple: true });\n if (s.kind === 'triple') throw new Error('Triple term subject cannot be a triple term');\n const p = iri(parseIriLike());\n const o = parseTerm(out, graph, { noCompound: true, noReifiedTriple: true });\n if (o.kind !== 'iri' && o.kind !== 'blank' && o.kind !== 'literal' && o.kind !== 'triple') throw new Error('Invalid triple term object');\n expect(')');\n expect('>>');\n return tripleTerm(s, p, o);\n }\n if (options2.noReifiedTriple) throw new Error('Reified triple is not allowed here');\n const s = parseTerm(out, graph, { noLiteral: true, noCompound: true, noTripleTerm: true });\n if (s.kind !== 'iri' && s.kind !== 'blank') throw new Error('Invalid reified triple subject');\n const p = iri(parseIriLike());\n const o = parseTerm(out, graph, { noCompound: true });\n if (o.kind !== 'iri' && o.kind !== 'blank' && o.kind !== 'literal' && o.kind !== 'triple') throw new Error('Invalid reified triple object');\n let reifier = null;\n if (peek()?.type === 'bare' && peek().value.startsWith('~')) reifier = parseReifierToken(out, graph);\n expect('>>');\n const node = reifier || freshBlank();\n out.push(triple(node, iri(RDF_REIFIES), tripleTerm(s, p, o), graph));\n implicitStatementNodes.add(implicitStatementNodeKey(node));\n return node;\n }\n\n function parseCollection(out, graph) {\n expect('(');\n if (accept(')')) return iri(RDF_NIL);\n const head = freshBlank();\n let current = head;\n while (true) {\n const item = parseTerm(out, graph);\n out.push(triple(current, iri(RDF_FIRST), item, graph));\n if (accept(')')) {\n out.push(triple(current, iri(RDF_REST), iri(RDF_NIL), graph));\n break;\n }\n const rest = freshBlank();\n out.push(triple(current, iri(RDF_REST), rest, graph));\n current = rest;\n }\n return head;\n }\n\n function parseBlankNodePropertyList(out, graph) {\n expect('[');\n const node = freshBlank();\n if (accept(']')) return node;\n parsePredicateObjectList(node, out, graph);\n expect(']');\n if (node.kind === 'blank') implicitStatementNodes.add(implicitStatementNodeKey(node));\n return node;\n }\n\n function parseAnnotationBlock(reifier, out, graph = null) {\n expect('{');\n expect('|');\n if (peek()?.type === '|') {\n // Empty annotation blocks are rejected by the RDF 1.2 syntax tests.\n throw new Error('Empty annotation block');\n }\n parsePredicateObjectList(reifier, out, graph);\n expect('|');\n expect('}');\n }\n\n function ensureReifierForTriple(assertedTriple, out, graph = null) {\n const reifier = freshBlank();\n out.push(triple(reifier, iri(RDF_REIFIES), tripleTerm(assertedTriple.s, assertedTriple.p, assertedTriple.o), graph));\n return reifier;\n }\n\n function parseObjectList(subject, predicate, out, graph = null) {\n while (true) {\n const object = parseTerm(out, graph, { noA: true });\n const asserted = triple(subject, predicate, object, graph);\n out.push(asserted);\n let pendingReifier = null;\n while (true) {\n if (peek()?.type === 'bare' && peek().value.startsWith('~')) {\n pendingReifier = parseReifierToken(out, graph);\n out.push(triple(pendingReifier, iri(RDF_REIFIES), tripleTerm(asserted.s, asserted.p, asserted.o), graph));\n continue;\n }\n if (peek()?.type === '{' && peek(1)?.type === '|') {\n const blockReifier = pendingReifier || ensureReifierForTriple(asserted, out, graph);\n parseAnnotationBlock(blockReifier, out, graph);\n pendingReifier = null;\n continue;\n }\n break;\n }\n if (!accept(',')) break;\n }\n }\n\n function parsePredicateObjectList(subject, out, graph = null) {\n while (true) {\n const predicate = iri(parseIriLike());\n parseObjectList(subject, predicate, out, graph);\n if (!accept(';')) break;\n while (accept(';')) {}\n if ([']', '.', '}', '|'].includes(peek()?.type)) break;\n }\n }\n\n function parseGraphLabel(out, inheritedGraph = null) {\n if (peek()?.type === '[' && peek(1)?.type === ']') { expect('['); expect(']'); return freshBlank(); }\n if (peek()?.type === '(') throw new Error('GRAPH name must be an IRI or blank node');\n const graph = parseTerm(out, inheritedGraph, { noLiteral: true, noCompound: true, noTripleTerm: true, noReifiedTriple: true, noA: true });\n if (graph.kind !== 'iri' && graph.kind !== 'blank') throw new Error('GRAPH name must be an IRI or blank node');\n return graph;\n }\n\n function parseGraphBlock(out, inheritedGraph = null) {\n expect('GRAPH');\n const graph = parseGraphLabel(out, inheritedGraph);\n parseFormula(graph, out);\n }\n\n function parseTripleStatement(out, graph = null, options3 = {}) {\n if (String(peek()?.value || '').toUpperCase() === 'GRAPH') {\n if (syntaxProfile === 'turtle') throw new Error('GRAPH blocks are not Turtle');\n if (graph) throw new Error('GRAPH blocks cannot be nested inside a graph block');\n parseGraphBlock(out, graph);\n if (options3.requireDot) expect('.'); else accept('.');\n return;\n }\n const subject = parseTerm(out, graph, { noLiteral: !rdf12Surface, noA: true });\n if ((peek()?.type === '.' || peek()?.type === '}' || peek()?.type === undefined) && implicitStatementNodes.has(implicitStatementNodeKey(subject))) {\n if (options3.requireDot) expect('.'); else accept('.');\n return;\n }\n parsePredicateObjectList(subject, out, graph);\n if (options3.requireDot) expect('.'); else accept('.');\n }\n\n function parseFormula(graph = null, target = null) {\n expect('{');\n const triples = target || [];\n while (peek()?.type !== '}') parseTripleStatement(triples, graph);\n expect('}');\n return triples;\n }\n\n function parseBase() {\n const directive = next();\n const iriToken = next();\n if (iriToken?.type !== 'iri') throw new Error(`Expected base IRI, got ${iriToken?.value}`);\n base = resolveIriReference(iriToken.value, base);\n if (String(directive.value || '').startsWith('@')) expect('.');\n }\n\n function parsePrefix() {\n const directive = next();\n const label = next();\n if (label?.type !== 'bare' || !label.value.endsWith(':')) throw new Error(`Expected prefix label ending with :, got ${label?.value}`);\n const prefixLabel = label.value.slice(0, -1);\n if (prefixLabel.endsWith('.') || prefixLabel.includes('..')) throw new Error(`Invalid prefix label ${prefixLabel}`);\n const iriToken = next();\n if (iriToken?.type !== 'iri') throw new Error(`Expected prefix IRI, got ${iriToken?.value}`);\n prefixes[prefixLabel] = resolveIriReference(iriToken.value, base);\n if (String(directive.value || '').startsWith('@')) expect('.');\n }\n\n function isSimpleGraphLabelStart(t) {\n return t && (t.type === 'iri' || (t.type === 'bare' && (t.value.startsWith('_:') || t.value.includes(':'))));\n }\n\n while (!eof()) {\n const token = peek();\n const lowerValue = String(token.value || '').toLowerCase();\n if (token.value === '@base' || (!String(token.value || '').startsWith('@') && lowerValue === 'base')) parseBase();\n else if (token.value === '@prefix' || (!String(token.value || '').startsWith('@') && lowerValue === 'prefix')) parsePrefix();\n else if ((!String(token.value || '').startsWith('@') && String(token.value || '').toUpperCase() === 'VERSION') || token.value === '@version') {\n const directive = next();\n const v = next();\n if (!v || v.type !== 'string') throw new Error('VERSION requires a short quoted string');\n if (String(directive.value || '').startsWith('@')) expect('.');\n }\n else if (token.type === '{') {\n if (syntaxProfile === 'turtle') throw new Error('Turtle does not allow top-level graph/formula blocks');\n const body = parseFormula();\n if (accept('=>')) {\n const head = parseFormula();\n accept('.');\n rules.push(new Rule({ id: `n3${rules.length + 1}`, body, head, profile: 'n3-rules-subset-v0' }));\n } else {\n facts.push(...body);\n accept('.');\n }\n } else if (String(token.value || '').toUpperCase() === 'GRAPH') {\n parseGraphBlock(facts);\n if (accept('.')) throw new Error('GRAPH block must not be followed by .');\n } else if (token.type === '[' && peek(1)?.type === ']' && peek(2)?.type === '{') {\n if (syntaxProfile === 'turtle') throw new Error('Turtle does not allow graph labels');\n const graph = parseGraphLabel(facts, null);\n parseFormula(graph, facts);\n accept('.');\n } else if (isSimpleGraphLabelStart(token) && peek(1)?.type === '{') {\n if (syntaxProfile === 'turtle') throw new Error('Turtle does not allow graph labels');\n const graph = parseGraphLabel(facts, null);\n parseFormula(graph, facts);\n accept('.');\n } else {\n parseTripleStatement(facts, null, { requireDot: syntaxProfile === 'turtle' });\n }\n }\n\n return { profile: 'n3-rules-subset-v0', prefixes, base, facts, rules };\n}\nreturn { parseN3 };\n})();\n\n\nreturn {\n parseNQuads,\n termToNQuads,\n tripleToNQuads,\n triplesToNQuads,\n parseN3,\n};\n})();\n\nObject.assign(module.exports, rdfW3cSyntax);\n","src/assignments.js":"'use strict';\n\n// Most SET expressions are deterministic and can safely participate in the\n// ordinary fixpoint loop. Only genuinely fresh generators need run-once\n// evaluation, otherwise a recursive rule such as SET(?x := UUID()) would keep\n// creating new terms forever.\nfunction assignmentsNeedRunOnce(clauses = [], options = {}) {\n if (options.shacl12Conformance) {\n return clauses.some((clause) => clause.type === 'set' || clause.type === 'bind');\n }\n const hasSet = clauses.some((clause) => clause.type === 'set');\n const hasNegation = clauses.some((clause) => clause.type === 'not');\n return (hasSet && hasNegation)\n || clauses.some((clause) => (clause.type === 'set' || clause.type === 'bind') && expressionIsVolatile(clause.expr));\n}\n\nfunction ruleNeedsRunOnce(head = [], body = [], options = {}) {\n return assignmentsNeedRunOnce(body, options) || head.some(tripleHasBlankNode);\n}\n\nfunction tripleHasBlankNode(triple) {\n return termHasBlankNode(triple && triple.s)\n || termHasBlankNode(triple && triple.p)\n || termHasBlankNode(triple && triple.o);\n}\n\nfunction termHasBlankNode(term) {\n if (!term) return false;\n if (term.type === 'blank') return true;\n if (term.type === 'triple') return termHasBlankNode(term.s) || termHasBlankNode(term.p) || termHasBlankNode(term.o);\n return false;\n}\n\nfunction expressionIsVolatile(expr) {\n if (!expr) return false;\n switch (expr.type) {\n case 'call': {\n const name = localName(expr.name).toLowerCase();\n if (name === 'uuid' || name === 'struuid') return true;\n if (name === 'bnode' && (!expr.args || expr.args.length === 0)) return true;\n return (expr.args || []).some(expressionIsVolatile);\n }\n case 'binary':\n return expressionIsVolatile(expr.left) || expressionIsVolatile(expr.right);\n case 'unary':\n return expressionIsVolatile(expr.expr);\n case 'list':\n return (expr.items || []).some(expressionIsVolatile);\n default:\n return false;\n }\n}\n\nfunction localName(name) {\n const text = String(name || '');\n const hash = text.lastIndexOf('#');\n const slash = text.lastIndexOf('/');\n const colon = text.lastIndexOf(':');\n const index = Math.max(hash, slash, colon);\n return index >= 0 ? text.slice(index + 1) : text;\n}\n\nmodule.exports = { assignmentsNeedRunOnce, ruleNeedsRunOnce, expressionIsVolatile, tripleHasBlankNode, termHasBlankNode };\n","src/term.js":"'use strict';\n\nconst RDF_NS = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';\nconst RDF_TYPE = `${RDF_NS}type`;\nconst RDF_FIRST = `${RDF_NS}first`;\nconst RDF_REST = `${RDF_NS}rest`;\nconst RDF_NIL = `${RDF_NS}nil`;\nconst RDF_REIFIES = `${RDF_NS}reifies`;\nconst XSD_STRING = 'http://www.w3.org/2001/XMLSchema#string';\nconst XSD_BOOLEAN = 'http://www.w3.org/2001/XMLSchema#boolean';\nconst XSD_INTEGER = 'http://www.w3.org/2001/XMLSchema#integer';\nconst XSD_DECIMAL = 'http://www.w3.org/2001/XMLSchema#decimal';\nconst XSD_DOUBLE = 'http://www.w3.org/2001/XMLSchema#double';\n\nfunction iri(value) {\n return { type: 'iri', value: String(value) };\n}\n\nfunction variable(name) {\n const value = String(name);\n return { type: 'var', value: value[0] === '?' || value[0] === '$' ? value.slice(1) : value };\n}\n\nfunction blankNode(value) {\n const label = String(value);\n return { type: 'blank', value: label.startsWith('_:') ? label.slice(2) : label };\n}\n\nfunction literal(value, datatype = null, lang = null, langDir = null) {\n return { type: 'literal', value, datatype, lang, langDir };\n}\n\nfunction tripleTerm(s, p, o) {\n return { type: 'triple', s, p, o };\n}\n\nfunction isVariable(term) {\n return term && term.type === 'var';\n}\n\nfunction isIRI(term) {\n return term && term.type === 'iri';\n}\n\nfunction isBlank(term) {\n return term && term.type === 'blank';\n}\n\nfunction isLiteral(term) {\n return term && term.type === 'literal';\n}\n\nfunction isTripleTerm(term) {\n return term && term.type === 'triple';\n}\n\nfunction termEquals(a, b) {\n return termKey(a) === termKey(b);\n}\n\nfunction literalKeyValue(value) {\n if (typeof value === 'bigint') return `${value.toString()}n`;\n return JSON.stringify(value);\n}\n\nfunction isNumericPrimitive(value) {\n return typeof value === 'number' || typeof value === 'bigint';\n}\n\nfunction compareNumericPrimitives(a, b) {\n if (typeof a === 'bigint' && typeof b === 'bigint') {\n if (a < b) return -1;\n if (a > b) return 1;\n return 0;\n }\n if (typeof a === 'bigint' && typeof b === 'number' && Number.isInteger(b) && Number.isSafeInteger(b)) {\n const bi = BigInt(b);\n if (a < bi) return -1;\n if (a > bi) return 1;\n return 0;\n }\n if (typeof a === 'number' && typeof b === 'bigint' && Number.isInteger(a) && Number.isSafeInteger(a)) {\n const ai = BigInt(a);\n if (ai < b) return -1;\n if (ai > b) return 1;\n return 0;\n }\n const diff = Number(a) - Number(b);\n if (diff < 0) return -1;\n if (diff > 0) return 1;\n return 0;\n}\n\nfunction termKey(term) {\n if (!term) return 'null';\n if (term.type === 'iri') return `I:${term.value}`;\n if (term.type === 'blank') return `B:${term.value}`;\n if (term.type === 'var') return `V:${term.value}`;\n if (term.type === 'literal') return `L:${literalKeyValue(term.value)}^^${term.datatype || ''}@${term.lang || ''}--${term.langDir || ''}`;\n if (term.type === 'triple') return `T:${termKey(term.s)} ${termKey(term.p)} ${termKey(term.o)}`;\n return JSON.stringify(term);\n}\n\nfunction tripleKey(triple) {\n return `${termKey(triple.s)} ${termKey(triple.p)} ${termKey(triple.o)}`;\n}\n\nfunction cloneTerm(term) {\n if (!term) return term;\n if (term.type === 'triple') return tripleTerm(cloneTerm(term.s), cloneTerm(term.p), cloneTerm(term.o));\n return { ...term };\n}\n\nfunction valueToTerm(value) {\n if (value && typeof value === 'object' && value.type) return value;\n return literal(value, inferDatatype(value));\n}\n\nfunction inferDatatype(value) {\n if (typeof value === 'boolean') return XSD_BOOLEAN;\n if (typeof value === 'bigint') return XSD_INTEGER;\n if (typeof value === 'number' && Number.isInteger(value)) return XSD_INTEGER;\n if (typeof value === 'number') return XSD_DECIMAL;\n if (typeof value === 'string') return XSD_STRING;\n return null;\n}\n\nfunction termToPrimitive(term) {\n if (!term) return undefined;\n if (term.type === 'literal') return term.value;\n if (term.type === 'iri') return term.value;\n if (term.type === 'blank') return `_:${term.value}`;\n if (term.type === 'var') return undefined;\n if (term.type === 'triple') return term;\n return term;\n}\n\nfunction termToString(term) {\n const value = termToPrimitive(term);\n if (value === undefined || value === null) return '';\n if (value && value.type === 'triple') return formatTerm(value);\n return String(value);\n}\n\nfunction booleanValue(value) {\n const primitive = value && value.type ? termToPrimitive(value) : value;\n if (primitive === undefined || primitive === null) return false;\n if (typeof primitive === 'boolean') return primitive;\n if (typeof primitive === 'bigint') return primitive !== 0n;\n if (typeof primitive === 'number') return primitive !== 0 && !Number.isNaN(primitive);\n if (typeof primitive === 'string') return primitive.length > 0 && primitive !== 'false';\n return Boolean(primitive);\n}\n\nfunction comparePrimitives(a, b) {\n const av = a && a.type ? termToPrimitive(a) : a;\n const bv = b && b.type ? termToPrimitive(b) : b;\n if (isNumericPrimitive(av) && isNumericPrimitive(bv)) return compareNumericPrimitives(av, bv);\n const as = String(av);\n const bs = String(bv);\n if (as < bs) return -1;\n if (as > bs) return 1;\n return 0;\n}\n\nfunction escapeString(value) {\n return String(value)\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/\\n/g, '\\\\n')\n .replace(/\\r/g, '\\\\r')\n .replace(/\\t/g, '\\\\t')\n .replace(/\"/g, '\\\\\"');\n}\n\nfunction compactIRI(value, prefixes = {}) {\n if (value === RDF_TYPE) return 'a';\n const entries = Object.entries(prefixes)\n .filter(([, iriPrefix]) => iriPrefix && value.startsWith(iriPrefix))\n .sort((a, b) => b[1].length - a[1].length);\n if (entries.length > 0) {\n const [prefix, iriPrefix] = entries[0];\n const local = value.slice(iriPrefix.length);\n if (/^[A-Za-z_][A-Za-z0-9_\\-]*$/.test(local) || /^[A-Za-z0-9_\\-]+$/.test(local)) {\n return `${prefix}:${local}`;\n }\n }\n return `<${value}>`;\n}\n\nfunction formatTerm(term, prefixes = {}) {\n if (term.type === 'iri') return compactIRI(term.value, prefixes);\n if (term.type === 'blank') return `_:${term.value}`;\n if (term.type === 'var') return `?${term.value}`;\n if (term.type === 'triple') return `<<(${formatTerm(term.s, prefixes)} ${formatTerm(term.p, prefixes)} ${formatTerm(term.o, prefixes)})>>`;\n if (term.type === 'literal') {\n const v = term.value;\n if (typeof v === 'bigint' && !term.lang && (!term.datatype || term.datatype === XSD_INTEGER)) return String(v);\n if (typeof v === 'number' && Number.isFinite(v) && !term.lang && (!term.datatype || term.datatype === XSD_INTEGER || term.datatype === XSD_DECIMAL || term.datatype === XSD_DOUBLE)) return String(v);\n if (typeof v === 'boolean' && !term.lang && (!term.datatype || term.datatype === XSD_BOOLEAN)) return v ? 'true' : 'false';\n const lexical = `\"${escapeString(v)}\"`;\n if (term.lang) return `${lexical}@${term.lang}${term.langDir ? `--${term.langDir}` : ''}`;\n if (term.datatype && term.datatype !== XSD_STRING) return `${lexical}^^${compactIRI(term.datatype, prefixes)}`;\n return lexical;\n }\n return String(term.value ?? term);\n}\n\nfunction formatTriple(triple, prefixes = {}) {\n return `${formatTerm(triple.s, prefixes)} ${formatTerm(triple.p, prefixes)} ${formatTerm(triple.o, prefixes)} .`;\n}\n\nmodule.exports = {\n RDF_NS,\n RDF_TYPE,\n RDF_FIRST,\n RDF_REST,\n RDF_NIL,\n RDF_REIFIES,\n XSD_STRING,\n XSD_BOOLEAN,\n XSD_INTEGER,\n XSD_DECIMAL,\n XSD_DOUBLE,\n iri,\n variable,\n blankNode,\n literal,\n tripleTerm,\n isVariable,\n isIRI,\n isBlank,\n isLiteral,\n isTripleTerm,\n termEquals,\n termKey,\n tripleKey,\n cloneTerm,\n valueToTerm,\n inferDatatype,\n termToPrimitive,\n termToString,\n booleanValue,\n comparePrimitives,\n compactIRI,\n formatTerm,\n formatTriple,\n};\n","src/builtins.js":"'use strict';\n\nconst {\n iri,\n blankNode,\n literal,\n tripleTerm,\n termEquals,\n termToPrimitive,\n termToString,\n booleanValue,\n comparePrimitives,\n isIRI,\n isBlank,\n isLiteral,\n isTripleTerm,\n valueToTerm,\n inferDatatype,\n XSD_STRING,\n RDF_NS,\n XSD_INTEGER,\n XSD_DECIMAL,\n XSD_DOUBLE,\n} = require('./term.js');\n\nconst XSD_DATETIME = 'http://www.w3.org/2001/XMLSchema#dateTime';\nconst XSD_DAYTIME_DURATION = 'http://www.w3.org/2001/XMLSchema#dayTimeDuration';\nconst RDF_LANGSTRING = `${RDF_NS}langString`;\nconst RDF_DIRLANGSTRING = `${RDF_NS}dirLangString`;\nconst NUMERIC_DATATYPES = new Set([XSD_INTEGER, XSD_DECIMAL, XSD_DOUBLE]);\nconst MAX_SAFE_INTEGER_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);\nconst MIN_SAFE_INTEGER_BIGINT = BigInt(Number.MIN_SAFE_INTEGER);\n\n// This table is intentionally shaped by the SHACL 1.2 Rules grammar production BuiltInCall.\n// Keys are the canonical spellings used by the draft; lookup is case-insensitive so examples\n// may use SPARQL-style uppercase or lowercase spellings while still being checked against the\n// grammar's finite set of built-ins.\nconst BUILTIN_SIGNATURES = Object.freeze({\n STR: { min: 1, max: 1 },\n LANG: { min: 1, max: 1 },\n LANGMATCHES: { min: 2, max: 2 },\n LANGDIR: { min: 1, max: 1 },\n DATATYPE: { min: 1, max: 1 },\n IRI: { min: 1, max: 1 },\n URI: { min: 1, max: 1 },\n BNODE: { min: 0, max: 1 },\n ABS: { min: 1, max: 1 },\n CEIL: { min: 1, max: 1 },\n FLOOR: { min: 1, max: 1 },\n ROUND: { min: 1, max: 1 },\n CONCAT: { min: 0, max: Infinity },\n SUBSTR: { min: 2, max: 3 },\n STRLEN: { min: 1, max: 1 },\n REPLACE: { min: 3, max: 4 },\n UCASE: { min: 1, max: 1 },\n LCASE: { min: 1, max: 1 },\n ENCODE_FOR_URI: { min: 1, max: 1 },\n CONTAINS: { min: 2, max: 2 },\n STRSTARTS: { min: 2, max: 2 },\n STRENDS: { min: 2, max: 2 },\n STRBEFORE: { min: 2, max: 2 },\n STRAFTER: { min: 2, max: 2 },\n YEAR: { min: 1, max: 1 },\n MONTH: { min: 1, max: 1 },\n DAY: { min: 1, max: 1 },\n HOURS: { min: 1, max: 1 },\n MINUTES: { min: 1, max: 1 },\n SECONDS: { min: 1, max: 1 },\n TIMEZONE: { min: 1, max: 1 },\n TZ: { min: 1, max: 1 },\n NOW: { min: 0, max: 0 },\n UUID: { min: 0, max: 0 },\n STRUUID: { min: 0, max: 0 },\n IF: { min: 3, max: 3, lazy: true },\n STRLANG: { min: 2, max: 2 },\n STRLANGDIR: { min: 3, max: 3 },\n STRDT: { min: 2, max: 2 },\n sameTerm: { min: 2, max: 2 },\n isIRI: { min: 1, max: 1 },\n isURI: { min: 1, max: 1 },\n isBLANK: { min: 1, max: 1 },\n isLITERAL: { min: 1, max: 1 },\n isNUMERIC: { min: 1, max: 1 },\n hasLANG: { min: 1, max: 1 },\n hasLANGDIR: { min: 1, max: 1 },\n REGEX: { min: 2, max: 3 },\n isTRIPLE: { min: 1, max: 1 },\n TRIPLE: { min: 3, max: 3 },\n SUBJECT: { min: 1, max: 1 },\n PREDICATE: { min: 1, max: 1 },\n OBJECT: { min: 1, max: 1 },\n});\n\nconst BUILTIN_BY_LOWER = new Map(Object.keys(BUILTIN_SIGNATURES).map((name) => [name.toLowerCase(), name]));\n\nfunction canonicalBuiltinName(name) {\n return BUILTIN_BY_LOWER.get(String(name).toLowerCase()) || null;\n}\n\nfunction isBuiltinName(name) {\n return canonicalBuiltinName(name) !== null;\n}\n\nfunction builtinNames() {\n return Object.keys(BUILTIN_SIGNATURES);\n}\n\nfunction evalExpression(expr, binding, options = {}) {\n switch (expr.type) {\n case 'literal':\n return expr.value;\n case 'term':\n return expr.value;\n case 'var':\n return binding[expr.name];\n case 'list':\n return expr.items.map((item) => evalExpression(item, binding, options));\n case 'unary': {\n const value = evalExpression(expr.expr, binding, options);\n if (expr.op === '!') return !booleanValue(value);\n if (expr.op === '-') return negateNumeric(termToPrimitive(valueToTermIfNeeded(value)));\n if (expr.op === '+') return unaryPlusNumeric(termToPrimitive(valueToTermIfNeeded(value)));\n throw new Error(`Unsupported unary operator ${expr.op}`);\n }\n case 'binary': {\n const left = evalExpression(expr.left, binding, options);\n if (expr.op === '&&') return booleanValue(left) && booleanValue(evalExpression(expr.right, binding, options));\n if (expr.op === '||') return booleanValue(left) || booleanValue(evalExpression(expr.right, binding, options));\n const right = evalExpression(expr.right, binding, options);\n return evalBinary(expr.op, left, right);\n }\n case 'call':\n return evalCallExpression(expr, binding, options);\n default:\n throw new Error(`Unsupported expression type ${expr.type}`);\n }\n}\n\nfunction evalCallExpression(expr, binding, options) {\n const canonical = canonicalBuiltinName(expr.name);\n if (canonical === 'IF') {\n validateArity(canonical, expr.args.length);\n const condition = evalExpression(expr.args[0], binding, options);\n return evalExpression(booleanValue(condition) ? expr.args[1] : expr.args[2], binding, options);\n }\n return callBuiltin(expr.name, expr.args.map((arg) => evalExpression(arg, binding, options)), binding, options);\n}\n\nfunction evalBinary(op, left, right) {\n if (op === '=') return termishEquals(left, right);\n if (op === '!=') return !termishEquals(left, right);\n if (op === 'IN' || op === 'NOT IN') {\n const list = Array.isArray(right) ? right : [];\n const found = list.some((item) => termishEquals(left, item));\n return op === 'IN' ? found : !found;\n }\n if (['<', '<=', '>', '>='].includes(op)) {\n const cmp = comparePrimitives(left, right);\n if (op === '<') return cmp < 0;\n if (op === '<=') return cmp <= 0;\n if (op === '>') return cmp > 0;\n if (op === '>=') return cmp >= 0;\n }\n const lp = termToPrimitive(valueToTermIfNeeded(left));\n const rp = termToPrimitive(valueToTermIfNeeded(right));\n if (op === '+') {\n if (isNumericPrimitive(lp) && isNumericPrimitive(rp)) return addNumeric(lp, rp);\n return String(lp) + String(rp);\n }\n if (op === '-') return subtractNumeric(lp, rp);\n if (op === '*') return multiplyNumeric(lp, rp);\n if (op === '/') return Number(lp) / Number(rp);\n throw new Error(`Unsupported binary operator ${op}`);\n}\n\n\nfunction isNumericPrimitive(value) {\n return typeof value === 'number' || typeof value === 'bigint';\n}\n\nfunction isIntegerPrimitive(value) {\n return typeof value === 'bigint' || (typeof value === 'number' && Number.isInteger(value));\n}\n\nfunction toBigIntInteger(value) {\n if (typeof value === 'bigint') return value;\n if (typeof value === 'number' && Number.isInteger(value) && Number.isSafeInteger(value)) return BigInt(value);\n throw new Error(`Cannot convert ${String(value)} to BigInt safely`);\n}\n\nfunction fromIntegerResult(value) {\n if (value <= MAX_SAFE_INTEGER_BIGINT && value >= MIN_SAFE_INTEGER_BIGINT) return Number(value);\n return value;\n}\n\nfunction addNumeric(left, right) {\n if (isIntegerPrimitive(left) && isIntegerPrimitive(right)) {\n if (typeof left === 'bigint' || typeof right === 'bigint') return fromIntegerResult(toBigIntInteger(left) + toBigIntInteger(right));\n const result = left + right;\n if (Number.isSafeInteger(result)) return result;\n return toBigIntInteger(left) + toBigIntInteger(right);\n }\n return Number(left) + Number(right);\n}\n\nfunction subtractNumeric(left, right) {\n if (isIntegerPrimitive(left) && isIntegerPrimitive(right)) {\n if (typeof left === 'bigint' || typeof right === 'bigint') return fromIntegerResult(toBigIntInteger(left) - toBigIntInteger(right));\n const result = left - right;\n if (Number.isSafeInteger(result)) return result;\n return toBigIntInteger(left) - toBigIntInteger(right);\n }\n return Number(left) - Number(right);\n}\n\nfunction multiplyNumeric(left, right) {\n if (isIntegerPrimitive(left) && isIntegerPrimitive(right)) {\n if (typeof left === 'bigint' || typeof right === 'bigint') return fromIntegerResult(toBigIntInteger(left) * toBigIntInteger(right));\n const result = left * right;\n if (Number.isSafeInteger(result)) return result;\n return toBigIntInteger(left) * toBigIntInteger(right);\n }\n return Number(left) * Number(right);\n}\n\nfunction negateNumeric(value) {\n if (typeof value === 'bigint') return -value;\n return -Number(value);\n}\n\nfunction unaryPlusNumeric(value) {\n if (typeof value === 'bigint') return value;\n return Number(value);\n}\n\nfunction valueToTermIfNeeded(value) {\n return value && value.type ? value : literal(value, inferDatatype(value));\n}\n\nfunction termishEquals(left, right) {\n if (left && left.type && right && right.type) return termEquals(left, right);\n const lp = left && left.type ? termToPrimitive(left) : left;\n const rp = right && right.type ? termToPrimitive(right) : right;\n return lp === rp;\n}\n\nfunction callBuiltin(name, args, binding = {}, options = {}) {\n const injected = options.builtins && (options.builtins[name] || options.builtins[String(name).toLowerCase()]);\n if (injected) return injected(args, { binding, iri, blankNode, literal, tripleTerm, termToString, booleanValue, termToPrimitive });\n\n if (localName(name).toLowerCase() === 'sudoku') {\n if (args.length !== 1) throw new Error(`SUDOKU expects 1 argument, got ${args.length}`);\n return solveSudoku(termToString(args[0]));\n }\n\n const canonical = canonicalBuiltinName(name);\n if (!canonical) throw new Error(`Unknown builtin ${name}`);\n validateArity(canonical, args.length);\n const key = canonical.toLowerCase();\n\n if (key === 'str') return termToString(args[0]);\n if (key === 'iri' || key === 'uri') return makeIRI(termToString(args[0]), options);\n if (key === 'bnode') return makeBlankNode(args, options);\n if (key === 'concat') return args.map(termToString).join('');\n if (key === 'lcase') return termToString(args[0]).toLowerCase();\n if (key === 'ucase') return termToString(args[0]).toUpperCase();\n if (key === 'contains') return termToString(args[0]).includes(termToString(args[1]));\n if (key === 'strstarts') return termToString(args[0]).startsWith(termToString(args[1]));\n if (key === 'strends') return termToString(args[0]).endsWith(termToString(args[1]));\n if (key === 'strbefore') {\n const s = termToString(args[0]);\n const needle = termToString(args[1]);\n const index = s.indexOf(needle);\n return index < 0 ? '' : s.slice(0, index);\n }\n if (key === 'strafter') {\n const s = termToString(args[0]);\n const needle = termToString(args[1]);\n const index = s.indexOf(needle);\n return index < 0 ? '' : s.slice(index + needle.length);\n }\n if (key === 'encode_for_uri') return encodeURIComponent(termToString(args[0]));\n if (key === 'regex') return regex(args);\n if (key === 'replace') return replace(args);\n if (key === 'substr') return substr(args);\n if (key === 'sameterm') return termishEquals(args[0], args[1]);\n if (key === 'isiri' || key === 'isuri') return isIRI(args[0]);\n if (key === 'isblank') return isBlank(args[0]);\n if (key === 'isliteral') return isLiteral(args[0]);\n if (key === 'istriple') return isTripleTerm(args[0]);\n if (key === 'isnumeric') return isNumericValue(args[0]);\n if (key === 'datatype') return datatypeOf(args[0]);\n if (key === 'lang') return args[0] && args[0].type === 'literal' ? (args[0].lang || '') : '';\n if (key === 'langmatches') return langMatches(termToString(args[0]), termToString(args[1]));\n if (key === 'haslang') return !!(args[0] && args[0].type === 'literal' && args[0].lang);\n if (key === 'langdir') return args[0] && args[0].type === 'literal' ? (args[0].langDir || '') : '';\n if (key === 'haslangdir') return !!(args[0] && args[0].type === 'literal' && args[0].langDir);\n if (key === 'strlen') return termToString(args[0]).length;\n if (key === 'abs') return Math.abs(Number(termToPrimitive(valueToTermIfNeeded(args[0]))));\n if (key === 'floor') return Math.floor(Number(termToPrimitive(valueToTermIfNeeded(args[0]))));\n if (key === 'ceil') return Math.ceil(Number(termToPrimitive(valueToTermIfNeeded(args[0]))));\n if (key === 'round') return Math.round(Number(termToPrimitive(valueToTermIfNeeded(args[0]))));\n if (key === 'if') return booleanValue(args[0]) ? args[1] : args[2];\n if (key === 'strdt') return literal(termToString(args[0]), termToString(args[1]));\n if (key === 'strlang') return literal(termToString(args[0]), null, termToString(args[1]).toLowerCase());\n if (key === 'strlangdir') return literal(termToString(args[0]), null, termToString(args[1]).toLowerCase(), termToString(args[2]).toLowerCase());\n if (key === 'triple') return tripleTerm(valueToTermIfNeeded(args[0]), valueToTermIfNeeded(args[1]), valueToTermIfNeeded(args[2]));\n if (key === 'subject') return isTripleTerm(args[0]) ? args[0].s : null;\n if (key === 'predicate') return isTripleTerm(args[0]) ? args[0].p : null;\n if (key === 'object') return isTripleTerm(args[0]) ? args[0].o : null;\n if (key === 'year') return datePart(args[0], 'year');\n if (key === 'month') return datePart(args[0], 'month');\n if (key === 'day') return datePart(args[0], 'day');\n if (key === 'hours') return datePart(args[0], 'hours');\n if (key === 'minutes') return datePart(args[0], 'minutes');\n if (key === 'seconds') return datePart(args[0], 'seconds');\n if (key === 'timezone') return timezoneDuration(args[0]);\n if (key === 'tz') return timezoneLexical(args[0]);\n if (key === 'now') return literal((options.now || new Date()).toISOString(), XSD_DATETIME);\n if (key === 'uuid') return iri(`urn:uuid:${freshUuid(options)}`);\n if (key === 'struuid') return freshUuid(options);\n throw new Error(`Unimplemented builtin ${name}`);\n}\n\n\nfunction localName(name) {\n const text = String(name || '');\n const hash = text.lastIndexOf('#');\n const slash = text.lastIndexOf('/');\n const colon = text.lastIndexOf(':');\n const index = Math.max(hash, slash, colon);\n return index >= 0 ? text.slice(index + 1) : text;\n}\n\nfunction solveSudoku(puzzle) {\n const text = String(puzzle || '').trim();\n if (!/^[0-9.]{81}$/.test(text)) throw new Error('SUDOKU expects an 81-character puzzle string containing digits or dots');\n const cells = Array.from(text, (ch) => (ch === '.' ? 0 : Number(ch)));\n const peers = sudokuPeers();\n\n for (let i = 0; i < 81; i += 1) {\n const value = cells[i];\n if (value === 0) continue;\n for (const peer of peers[i]) {\n if (cells[peer] === value) throw new Error('SUDOKU puzzle has conflicting givens');\n }\n }\n\n const solved = solveSudokuCells(cells, peers);\n if (!solved) return '';\n return solved.join('');\n}\n\nfunction solveSudokuCells(cells, peers) {\n let bestIndex = -1;\n let bestCandidates = null;\n\n for (let i = 0; i < 81; i += 1) {\n if (cells[i] !== 0) continue;\n const candidates = sudokuCandidates(cells, peers[i]);\n if (candidates.length === 0) return null;\n if (!bestCandidates || candidates.length < bestCandidates.length) {\n bestIndex = i;\n bestCandidates = candidates;\n if (candidates.length === 1) break;\n }\n }\n\n if (bestIndex < 0) return cells;\n\n for (const value of bestCandidates) {\n const next = cells.slice();\n next[bestIndex] = value;\n const solved = solveSudokuCells(next, peers);\n if (solved) return solved;\n }\n return null;\n}\n\nfunction sudokuCandidates(cells, peers) {\n const used = new Set();\n for (const peer of peers) if (cells[peer] !== 0) used.add(cells[peer]);\n const out = [];\n for (let value = 1; value <= 9; value += 1) if (!used.has(value)) out.push(value);\n return out;\n}\n\nlet SUDOKU_PEERS = null;\nfunction sudokuPeers() {\n if (SUDOKU_PEERS) return SUDOKU_PEERS;\n SUDOKU_PEERS = Array.from({ length: 81 }, (_, index) => {\n const row = Math.floor(index / 9);\n const col = index % 9;\n const boxRow = Math.floor(row / 3) * 3;\n const boxCol = Math.floor(col / 3) * 3;\n const peers = new Set();\n for (let c = 0; c < 9; c += 1) peers.add(row * 9 + c);\n for (let r = 0; r < 9; r += 1) peers.add(r * 9 + col);\n for (let r = boxRow; r < boxRow + 3; r += 1) {\n for (let c = boxCol; c < boxCol + 3; c += 1) peers.add(r * 9 + c);\n }\n peers.delete(index);\n return Array.from(peers);\n });\n return SUDOKU_PEERS;\n}\n\nfunction validateArity(canonical, actual) {\n const sig = BUILTIN_SIGNATURES[canonical];\n if (!sig) throw new Error(`Unknown builtin ${canonical}`);\n const tooFew = actual < sig.min;\n const tooMany = actual > sig.max;\n if (tooFew || tooMany) {\n const expected = sig.min === sig.max ? `${sig.min}` : `${sig.min}${sig.max === Infinity ? '+' : `..${sig.max}`}`;\n throw new Error(`${canonical} expects ${expected} argument${expected === '1' ? '' : 's'}, got ${actual}`);\n }\n}\n\nfunction makeIRI(value, options) {\n if (options.baseIRI && !/^[A-Za-z][A-Za-z0-9+.-]*:/.test(value)) {\n try { return iri(new URL(value, options.baseIRI).href); } catch (_) { /* fall through */ }\n }\n return iri(value);\n}\n\nfunction makeBlankNode(args, options) {\n if (args.length === 0) return blankNode(freshId(options));\n const label = termToString(args[0]);\n if (!options.__bnodeLabels) options.__bnodeLabels = new Map();\n if (!options.__bnodeLabels.has(label)) options.__bnodeLabels.set(label, label || freshId(options));\n return blankNode(options.__bnodeLabels.get(label));\n}\n\nfunction regex(args) {\n const flags = regexFlags(termToString(args[2] || ''));\n return new RegExp(termToString(args[1]), flags).test(termToString(args[0]));\n}\n\nfunction replace(args) {\n const flags = regexFlags(termToString(args[3] || ''));\n const effectiveFlags = flags.includes('g') ? flags : `${flags}g`;\n return termToString(args[0]).replace(new RegExp(termToString(args[1]), effectiveFlags), termToString(args[2]));\n}\n\nfunction regexFlags(flags) {\n let out = '';\n for (const ch of String(flags)) {\n // JavaScript RegExp has no direct SPARQL/xpath \"x\" free-spacing flag, so ignore it.\n if (ch === 'x') continue;\n if ('imsuyg'.includes(ch) && !out.includes(ch)) out += ch;\n }\n return out;\n}\n\nfunction substr(args) {\n const value = termToString(args[0]);\n const start = Math.max(0, Number(termToPrimitive(valueToTermIfNeeded(args[1]))) - 1);\n if (args.length >= 3) return value.substring(start, start + Number(termToPrimitive(valueToTermIfNeeded(args[2]))));\n return value.substring(start);\n}\n\nfunction datatypeOf(value) {\n const term = valueToTermIfNeeded(value);\n if (term.type !== 'literal') return null;\n if (term.langDir) return iri(RDF_DIRLANGSTRING);\n if (term.lang) return iri(RDF_LANGSTRING);\n return iri(term.datatype || inferDatatype(term.value) || XSD_STRING);\n}\n\nfunction isNumericValue(value) {\n const term = valueToTermIfNeeded(value);\n if (typeof termToPrimitive(term) === 'bigint') return true;\n if (typeof termToPrimitive(term) === 'number') return true;\n return term.type === 'literal' && NUMERIC_DATATYPES.has(term.datatype);\n}\n\nfunction datePart(value, part) {\n const lexical = termToString(value);\n const match = lexical.match(/^(-?\\d{4,})-(\\d{2})-(\\d{2})(?:T(\\d{2}):(\\d{2}):(\\d{2}(?:\\.\\d+)?)(Z|[+-]\\d{2}:?\\d{2})?)?/);\n if (!match) return null;\n const [, year, month, day, hours = '0', minutes = '0', seconds = '0'] = match;\n if (part === 'year') return Number(year);\n if (part === 'month') return Number(month);\n if (part === 'day') return Number(day);\n if (part === 'hours') return Number(hours);\n if (part === 'minutes') return Number(minutes);\n if (part === 'seconds') return Number(seconds);\n return null;\n}\n\nfunction timezoneLexical(value) {\n const lexical = termToString(value);\n const match = lexical.match(/(?:T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?)(Z|[+-]\\d{2}:?\\d{2})$/);\n return match ? match[1] : '';\n}\n\nfunction timezoneDuration(value) {\n const zone = timezoneLexical(value);\n if (!zone) return null;\n if (zone === 'Z') return literal('PT0S', XSD_DAYTIME_DURATION);\n const match = zone.match(/^([+-])(\\d{2}):?(\\d{2})$/);\n if (!match) return null;\n const [, sign, hh, mm] = match;\n const hours = Number(hh);\n const minutes = Number(mm);\n const body = `${hours ? `${hours}H` : ''}${minutes ? `${minutes}M` : ''}` || '0S';\n return literal(`${sign === '-' ? '-' : ''}PT${body}`, XSD_DAYTIME_DURATION);\n}\n\nfunction langMatches(lang, range) {\n if (range === '*') return lang.length > 0;\n return lang.toLowerCase() === range.toLowerCase() || lang.toLowerCase().startsWith(`${range.toLowerCase()}-`);\n}\n\nfunction freshUuid(options) {\n if (typeof options.uuidGenerator === 'function') return String(options.uuidGenerator());\n options.__eyelengUuidCounter = (options.__eyelengUuidCounter || 0) + 1;\n return `00000000-0000-4000-8000-${String(options.__eyelengUuidCounter).padStart(12, '0')}`;\n}\n\nfunction freshId(options) {\n options.__eyelengCounter = (options.__eyelengCounter || 0) + 1;\n return `eyeleng-${options.__eyelengCounter}`;\n}\n\nfunction asTerm(value) {\n return valueToTerm(value);\n}\n\nmodule.exports = {\n BUILTIN_SIGNATURES,\n builtinNames,\n canonicalBuiltinName,\n isBuiltinName,\n validateArity,\n evalExpression,\n booleanValue,\n asTerm,\n callBuiltin,\n evalBinary,\n};\n","src/rdfMessages.js":"'use strict';\n\nconst { parseRdfDocument } = require('./rdfSyntax.js');\nconst {\n iri,\n blankNode,\n literal,\n tripleTerm,\n RDF_TYPE,\n RDF_FIRST,\n RDF_REST,\n RDF_NIL,\n XSD_INTEGER,\n} = require('./term.js');\n\nconst RDF_MESSAGE_VERSION_RE = /^\\s*(?:@version|VERSION)\\s+([\"'])(?:1\\.1|1\\.2|1\\.2-basic)-messages\\1\\s*\\.?\\s*(?:#.*)?$/im;\nconst RDF_MESSAGE_VERSION_LINE_RE = /^\\s*(?:@version|VERSION)\\s+([\"'])(?:1\\.1|1\\.2|1\\.2-basic)-messages\\1\\s*\\.?\\s*(?:#.*)?$/i;\nconst RDF_DIRECTIVE_LINE_RE = /^\\s*(?:@?(?:prefix|base)\\b|PREFIX\\b|BASE\\b)/i;\nconst RDF_MESSAGE_DELIMITER_LINE_RE = /^\\s*(?:MESSAGE\\b|@message\\s*\\.?)\\s*(?:#.*)?$/i;\n\nconst EYMSG_NS = 'https://eyereasoner.github.io/eyeling/vocab/message#';\nconst LOG_NS = 'http://www.w3.org/2000/10/swap/log#';\nconst EYMSG = Object.freeze({\n RDFMessageStream: `${EYMSG_NS}RDFMessageStream`,\n MessageEnvelope: `${EYMSG_NS}MessageEnvelope`,\n envelope: `${EYMSG_NS}envelope`,\n firstEnvelope: `${EYMSG_NS}firstEnvelope`,\n lastEnvelope: `${EYMSG_NS}lastEnvelope`,\n orderedEnvelopes: `${EYMSG_NS}orderedEnvelopes`,\n messageCount: `${EYMSG_NS}messageCount`,\n offset: `${EYMSG_NS}offset`,\n nextEnvelope: `${EYMSG_NS}nextEnvelope`,\n payloadGraph: `${EYMSG_NS}payloadGraph`,\n payloadKind: `${EYMSG_NS}payloadKind`,\n payloadTriple: `${EYMSG_NS}payloadTriple`,\n tripleCount: `${EYMSG_NS}tripleCount`,\n empty: `${EYMSG_NS}empty`,\n nonEmpty: `${EYMSG_NS}nonEmpty`,\n});\nconst LOG_NAME_OF = `${LOG_NS}nameOf`;\n\nfunction simpleHashText(value) {\n let h = 0x811c9dc5;\n const text = String(value || '');\n for (let i = 0; i < text.length; i += 1) {\n h ^= text.charCodeAt(i);\n h = Math.imul(h, 0x01000193) >>> 0;\n }\n return h.toString(16).padStart(8, '0');\n}\n\nfunction looksLikeRdfMessageLog(source, options = {}) {\n return !!options.rdfMessages || RDF_MESSAGE_VERSION_RE.test(String(source || ''));\n}\n\nfunction splitPreservingLineEndings(text) {\n return String(text || '').match(/.*(?:\\r\\n|\\n|\\r)|.+$/g) || [];\n}\n\nfunction isOnlyWhitespaceAndComments(text) {\n return splitPreservingLineEndings(text).every((line) => {\n const hash = line.indexOf('#');\n const body = hash >= 0 ? line.slice(0, hash) : line;\n return body.trim() === '';\n });\n}\n\nfunction stripMessageVersionLines(text) {\n return splitPreservingLineEndings(text).filter((line) => !RDF_MESSAGE_VERSION_LINE_RE.test(line)).join('');\n}\n\nfunction stripDirectiveLines(text) {\n return splitPreservingLineEndings(text).filter((line) => !RDF_DIRECTIVE_LINE_RE.test(line) && !RDF_MESSAGE_VERSION_LINE_RE.test(line)).join('');\n}\n\nfunction collectDirectiveLines(text) {\n const seen = new Set();\n const out = [];\n for (const line of splitPreservingLineEndings(text)) {\n if (!RDF_DIRECTIVE_LINE_RE.test(line)) continue;\n const key = line.trim();\n if (!key || seen.has(key)) continue;\n seen.add(key);\n out.push(line.endsWith('\\n') || line.endsWith('\\r') ? line : `${line}\\n`);\n }\n return out;\n}\n\nfunction readStringAt(source, index) {\n const quote = source[index];\n const long = source.startsWith(quote.repeat(3), index);\n let i = index + (long ? 3 : 1);\n while (i < source.length) {\n if (source[i] === '\\\\') { i += 2; continue; }\n if (long && source.startsWith(quote.repeat(3), i)) return { end: i + 3 };\n if (!long && source[i] === quote) return { end: i + 1 };\n i += 1;\n }\n return { end: source.length };\n}\n\nfunction readIriAt(source, index) {\n let i = index + 1;\n while (i < source.length) {\n if (source[i] === '\\\\') { i += 2; continue; }\n if (source[i] === '>') return { end: i + 1 };\n i += 1;\n }\n return { end: source.length };\n}\n\nfunction skipWsAndComments(source, index) {\n let i = index;\n while (i < source.length) {\n if (/\\s/.test(source[i])) { i += 1; continue; }\n if (source[i] === '#') {\n while (i < source.length && source[i] !== '\\n' && source[i] !== '\\r') i += 1;\n continue;\n }\n break;\n }\n return i;\n}\n\nfunction isWordChar(ch) { return !!ch && /[A-Za-z0-9_\\-]/.test(ch); }\nfunction startsWordAt(source, word, index) {\n return source.slice(index, index + word.length).toUpperCase() === word && !isWordChar(source[index - 1]) && !isWordChar(source[index + word.length]);\n}\n\nfunction findMessageDirectiveAt(source, index) {\n if (startsWordAt(source, 'MESSAGE', index)) return { start: index, end: index + 'MESSAGE'.length };\n if (source.slice(index, index + 8).toLowerCase() === '@message' && !isWordChar(source[index + 8])) {\n let end = index + 8;\n end = skipWsAndComments(source, end);\n if (source[end] === '.') end += 1;\n return { start: index, end };\n }\n return null;\n}\n\nfunction splitRdfMessageLog(source) {\n const text = stripMessageVersionLines(source);\n const chunks = [];\n let i = 0;\n let start = 0;\n let braceDepth = 0;\n let bracketDepth = 0;\n let parenDepth = 0;\n let statementStart = true;\n let sawDelimiter = false;\n\n while (i < text.length) {\n const ch = text[i];\n if (ch === '\"' || ch === \"'\") { i = readStringAt(text, i).end; statementStart = false; continue; }\n if (ch === '<' && !text.startsWith('<<', i)) { i = readIriAt(text, i).end; statementStart = false; continue; }\n if (ch === '#') { while (i < text.length && text[i] !== '\\n' && text[i] !== '\\r') i += 1; statementStart = true; continue; }\n\n if (statementStart && braceDepth === 0 && bracketDepth === 0 && parenDepth === 0) {\n const termStart = skipWsAndComments(text, i);\n const directive = findMessageDirectiveAt(text, termStart);\n if (directive) {\n chunks.push(text.slice(start, termStart));\n start = directive.end;\n i = directive.end;\n statementStart = true;\n sawDelimiter = true;\n continue;\n }\n if (termStart !== i) { i = termStart; continue; }\n }\n\n if (ch === '{') braceDepth += 1;\n else if (ch === '}' && braceDepth > 0) braceDepth -= 1;\n else if (ch === '[') bracketDepth += 1;\n else if (ch === ']' && bracketDepth > 0) bracketDepth -= 1;\n else if (ch === '(') parenDepth += 1;\n else if (ch === ')' && parenDepth > 0) parenDepth -= 1;\n\n if (ch === '.' && braceDepth === 0 && bracketDepth === 0 && parenDepth === 0) statementStart = true;\n else if (ch === '\\n' || ch === '\\r') statementStart = true;\n else if (!/\\s/.test(ch)) statementStart = false;\n i += 1;\n }\n\n const tail = text.slice(start);\n if (!sawDelimiter || !isOnlyWhitespaceAndComments(tail)) chunks.push(tail);\n return chunks;\n}\n\nfunction rewriteMessageBlankLabels(source, messageIndex) {\n const prefix = `msg${String(messageIndex).padStart(3, '0')}_`;\n let out = '';\n let i = 0;\n while (i < source.length) {\n const ch = source[i];\n if (ch === '\"' || ch === \"'\") {\n const end = readStringAt(source, i).end;\n out += source.slice(i, end);\n i = end;\n continue;\n }\n if (ch === '<' && !source.startsWith('<<', i)) {\n const end = readIriAt(source, i).end;\n out += source.slice(i, end);\n i = end;\n continue;\n }\n if (ch === '#') {\n while (i < source.length) {\n const c = source[i++]; out += c;\n if (c === '\\n' || c === '\\r') break;\n }\n continue;\n }\n if (source.startsWith('_:', i)) {\n let j = i + 2;\n while (j < source.length && !/\\s/.test(source[j]) && !'{}[](),;.<>'.includes(source[j])) j += 1;\n const label = source.slice(i + 2, j);\n if (label) {\n out += `_:${prefix}${label.replace(/[^A-Za-z0-9_\\-]/g, '_')}`;\n i = j;\n continue;\n }\n }\n out += ch;\n i += 1;\n }\n return out;\n}\n\nfunction messageChunkHasRdf(chunk) {\n return !isOnlyWhitespaceAndComments(stripDirectiveLines(chunk));\n}\n\nfunction listTriples(headTerm, items, data, makeBlank) {\n if (items.length === 0) return iri(RDF_NIL);\n const cells = items.map(() => makeBlank());\n for (let i = 0; i < cells.length; i += 1) {\n data.push({ s: cells[i], p: iri(RDF_FIRST), o: items[i] });\n data.push({ s: cells[i], p: iri(RDF_REST), o: i + 1 < cells.length ? cells[i + 1] : iri(RDF_NIL) });\n }\n return headTerm || cells[0];\n}\n\nfunction parseRdfMessageLog(source, options = {}) {\n const text = String(source || '');\n const directives = collectDirectiveLines(text);\n const chunks = splitRdfMessageLog(text);\n // Keep generated message-log IRIs stable across machines and checkout paths.\n // The previous seed used baseIRI/filename, which made golden outputs depend on\n // absolute local paths such as file:///home/.../examples/rdf-messages.trig.\n // A caller that needs a location-specific identity can still pass\n // options.messageBaseIRI explicitly.\n const hash = simpleHashText(text);\n const base = options.messageBaseIRI || `urn:eyeleng:message-log:${hash}`;\n const stream = iri(`${base}#stream`);\n const envelopes = chunks.map((unused, index) => iri(`${base}#m${String(index + 1).padStart(3, '0')}`));\n const payloads = chunks.map((unused, index) => iri(`${base}#m${String(index + 1).padStart(3, '0')}/payload`));\n const data = [];\n let bnodeCounter = 0;\n const makeBlank = () => blankNode(`msg${++bnodeCounter}`);\n const prefixes = {\n rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',\n xsd: 'http://www.w3.org/2001/XMLSchema#',\n eymsg: EYMSG_NS,\n log: LOG_NS,\n };\n\n data.push({ s: stream, p: iri(RDF_TYPE), o: iri(EYMSG.RDFMessageStream) });\n data.push({ s: stream, p: iri(EYMSG.messageCount), o: literal(chunks.length, XSD_INTEGER) });\n if (envelopes.length > 0) {\n data.push({ s: stream, p: iri(EYMSG.orderedEnvelopes), o: listTriples(null, envelopes, data, makeBlank) });\n data.push({ s: stream, p: iri(EYMSG.firstEnvelope), o: envelopes[0] });\n data.push({ s: stream, p: iri(EYMSG.lastEnvelope), o: envelopes[envelopes.length - 1] });\n }\n\n for (let i = 0; i < chunks.length; i += 1) {\n const envelope = envelopes[i];\n const payload = payloads[i];\n const rawChunk = chunks[i];\n const chunk = rewriteMessageBlankLabels(rawChunk, i + 1);\n const hasBody = messageChunkHasRdf(chunk);\n const bodySource = `${directives.join('')}\\n${stripDirectiveLines(chunk)}`;\n const parsed = hasBody ? parseRdfDocument(bodySource, { ...options, filename: `${options.filename || '<message>'}#m${i + 1}`, baseIRI: options.baseIRI }) : { triples: [], prefixes: {} };\n Object.assign(prefixes, parsed.prefixes || {});\n const payloadTriples = parsed.triples || [];\n const tripleTerms = payloadTriples.map((t) => tripleTerm(t.s, t.p, t.o));\n\n data.push({ s: stream, p: iri(EYMSG.envelope), o: envelope });\n data.push({ s: envelope, p: iri(RDF_TYPE), o: iri(EYMSG.MessageEnvelope) });\n data.push({ s: envelope, p: iri(EYMSG.offset), o: literal(i + 1, XSD_INTEGER) });\n data.push({ s: envelope, p: iri(EYMSG.payloadKind), o: iri(hasBody ? EYMSG.nonEmpty : EYMSG.empty) });\n data.push({ s: envelope, p: iri(EYMSG.tripleCount), o: literal(payloadTriples.length, XSD_INTEGER) });\n if (i + 1 < envelopes.length) data.push({ s: envelope, p: iri(EYMSG.nextEnvelope), o: envelopes[i + 1] });\n if (hasBody) {\n data.push({ s: envelope, p: iri(EYMSG.payloadGraph), o: payload });\n data.push({ s: payload, p: iri(LOG_NAME_OF), o: listTriples(null, tripleTerms, data, makeBlank) });\n for (const term of tripleTerms) data.push({ s: payload, p: iri(EYMSG.payloadTriple), o: term });\n if (options.includeMessageFacts) data.push(...payloadTriples);\n }\n }\n\n return {\n baseIRI: options.baseIRI || null,\n version: '1.2-messages',\n imports: [],\n prefixes,\n data,\n rules: [],\n };\n}\n\nmodule.exports = {\n EYMSG_NS,\n EYMSG,\n LOG_NS,\n LOG_NAME_OF,\n looksLikeRdfMessageLog,\n splitRdfMessageLog,\n parseRdfMessageLog,\n};\n","src/engine.js":"'use strict';\n\nconst { TripleStore, bindingKey, instantiateTerm } = require('./store.js');\nconst { tripleKey, termKey, termEquals, blankNode, tripleTerm } = require('./term.js');\nconst { evalExpression, booleanValue, asTerm } = require('./builtins.js');\nconst { analyze } = require('./analyze.js');\nconst { BackwardProver, preferredBackwardPredicates, ruleIsBackwardOriented } = require('./backward.js');\n\nfunction evaluate(program, options = {}) {\n const maxIterations = options.maxIterations ?? 10000;\n const evalOptions = { ...options, baseIRI: options.baseIRI || program.baseIRI || null, now: options.now || new Date(), __bnodeLabels: options.__bnodeLabels || new Map() };\n const store = new TripleStore(program.data);\n const inputKeys = new Set(program.data.map(tripleKey));\n const inferred = [];\n const trace = [];\n let iterations = 0;\n let ruleApplications = 0;\n const perRule = program.rules.map((rule, index) => ({\n name: rule.name || `rule#${index + 1}`,\n applications: 0,\n added: 0,\n runOnce: !!rule.runOnce,\n backward: false,\n }));\n\n const analysis = options.analysis || analyze(program, options);\n if (analysis.errors && analysis.errors.length > 0 && !options.ignoreAnalysisErrors) {\n throw new Error(`Analysis failed: ${analysis.errors.map((error) => error.message).join('; ')}`);\n }\n const layerIndexes = analysis.dependency && analysis.dependency.layerIndexes\n ? analysis.dependency.layerIndexes\n : [program.rules.map((_, index) => index)];\n const recursiveLayerFlags = computeRecursiveLayerFlags(\n layerIndexes,\n analysis.dependency ? analysis.dependency.edges : [],\n );\n const relaxedRecursiveRunOnce = options.relaxedRecursion === false\n ? new Set()\n : recursiveTermGenerationRuleIndexes(analysis);\n const useHybrid = options.hybrid !== false && !options.shacl12Conformance;\n const hybridBackwardPredicates = useHybrid || options.backwardBodyCalls\n ? preferredBackwardPredicates(program, options)\n : new Set();\n const hybridBackwardRules = new Set();\n if (hybridBackwardPredicates.size > 0) {\n for (let ruleIndex = 0; ruleIndex < program.rules.length; ruleIndex += 1) {\n if (ruleIsBackwardOriented(program.rules[ruleIndex], hybridBackwardPredicates)) hybridBackwardRules.add(ruleIndex);\n }\n }\n const hybridStats = hybridBackwardPredicates.size > 0 ? emptyBackwardStats() : null;\n for (const ruleIndex of hybridBackwardRules) perRule[ruleIndex].backward = true;\n const baseContext = {\n ...evalOptions,\n maxIterations,\n inputKeys,\n inferred,\n trace,\n perRule,\n layer: 0,\n iteration: 0,\n startingIterations: 0,\n recursiveLayer: false,\n hybridBackwardPredicates,\n hybridBackwardRules,\n hybridStats,\n };\n\n for (let layerIndex = 0; layerIndex < layerIndexes.length; layerIndex += 1) {\n const layer = layerIndexes[layerIndex];\n const forwardLayer = hybridBackwardRules.size > 0 ? layer.filter((ruleIndex) => !hybridBackwardRules.has(ruleIndex)) : layer;\n const ordinary = forwardLayer.filter((ruleIndex) => !program.rules[ruleIndex].runOnce || relaxedRecursiveRunOnce.has(ruleIndex));\n const runOnce = forwardLayer.filter((ruleIndex) => program.rules[ruleIndex].runOnce && !relaxedRecursiveRunOnce.has(ruleIndex));\n\n if (runOnce.length > 0) {\n iterations += 1;\n for (const ruleIndex of runOnce) {\n baseContext.layer = layerIndex + 1;\n baseContext.iteration = iterations;\n const added = applyRuleOnce(program, store, ruleIndex, baseContext);\n ruleApplications += added.applications;\n }\n }\n\n baseContext.layer = layerIndex + 1;\n baseContext.startingIterations = iterations;\n baseContext.recursiveLayer = recursiveLayerFlags[layerIndex];\n const ordinaryResult = runRulesToFixpoint(program, store, ordinary, baseContext);\n iterations = ordinaryResult.iterations;\n ruleApplications += ordinaryResult.ruleApplications;\n }\n\n return {\n baseIRI: program.baseIRI,\n version: program.version || null,\n imports: program.imports || [],\n prefixes: program.prefixes,\n input: program.data.slice(),\n inferred,\n closure: store.values(),\n iterations,\n layers: layerIndexes.map((layer) => layer.map((ruleIndex) => perRule[ruleIndex].name)),\n ruleApplications,\n perRule,\n trace,\n hybridStats,\n };\n}\n\nfunction runRulesToFixpoint(program, store, ruleIndexes, context) {\n if (ruleIndexes.length === 0) return { iterations: context.startingIterations, ruleApplications: 0 };\n\n // A stratum may contain only acyclic rule components. Such rules only need a\n // single pass after lower strata have reached their fixpoints; spending an\n // extra no-change pass per layer makes deep taxonomies look non-terminating.\n if (!context.recursiveLayer) {\n const iteration = context.startingIterations + 1;\n let ruleApplications = 0;\n for (const ruleIndex of ruleIndexes) {\n context.iteration = iteration;\n const applied = applyRuleOnce(program, store, ruleIndex, context);\n ruleApplications += applied.applications;\n }\n return { iterations: iteration, ruleApplications };\n }\n\n let iterations = context.startingIterations;\n let localIterations = 0;\n let ruleApplications = 0;\n\n while (localIterations < context.maxIterations) {\n localIterations += 1;\n iterations += 1;\n let addedInIteration = 0;\n\n for (const ruleIndex of ruleIndexes) {\n context.iteration = iterations;\n const applied = applyRuleOnce(program, store, ruleIndex, context);\n addedInIteration += applied.added;\n ruleApplications += applied.applications;\n }\n\n if (addedInIteration === 0) break;\n }\n\n if (localIterations >= context.maxIterations) {\n throw new Error(`Reached maxIterations=${context.maxIterations} within layer ${context.layer}; rules may not terminate`);\n }\n\n return { iterations, ruleApplications };\n}\n\nfunction computeRecursiveLayerFlags(layerIndexes, edges = []) {\n const flags = Array(layerIndexes.length).fill(false);\n const layerOfRule = new Map();\n for (let layerIndex = 0; layerIndex < layerIndexes.length; layerIndex += 1) {\n for (const ruleIndex of layerIndexes[layerIndex]) layerOfRule.set(ruleIndex, layerIndex);\n }\n for (const edge of edges) {\n const fromLayer = layerOfRule.get(edge.from);\n if (fromLayer === undefined) continue;\n if (fromLayer === layerOfRule.get(edge.to)) flags[fromLayer] = true;\n }\n return flags;\n}\n\n\nfunction applyRuleOnce(program, store, ruleIndex, context) {\n const rule = program.rules[ruleIndex];\n let applications = 0;\n let added = 0;\n const dedupeBindings = rule.body.some((clause) => clause.type === 'path');\n const seenBindings = dedupeBindings ? new Set() : null;\n const headBlankLabels = collectHeadBlankLabels(rule.head);\n\n const bodyContext = prepareBodyContext(program, store, context);\n const bodyBindings = rule.body.length === 1 && rule.body[0].type === 'triple' && !shouldUseBackwardForTriple(rule.body[0].triple, {}, bodyContext)\n ? store.match(rule.body[0].triple, {})\n : evaluateBodyStream(rule.body, store, {}, bodyContext);\n\n for (const binding of bodyBindings) {\n if (seenBindings) {\n const key = bindingKey(binding);\n if (seenBindings.has(key)) continue;\n seenBindings.add(key);\n }\n applications += 1;\n context.perRule[ruleIndex].applications += 1;\n\n const headBlankMap = headBlankLabels.size > 0 ? new Map() : null;\n const skolemKey = headBlankMap ? skolemizationKey(ruleIndex, binding) : null;\n for (const head of rule.head) {\n const triple = instantiateHeadTriple(head, binding, headBlankLabels, headBlankMap, skolemKey);\n if (!triple) continue;\n if (store.add(triple)) {\n added += 1;\n context.perRule[ruleIndex].added += 1;\n if (!context.inputKeys.has(tripleKey(triple))) context.inferred.push(triple);\n if (context.trace) {\n context.trace.push({\n layer: context.layer,\n iteration: context.iteration,\n rule: rule.name || `rule#${ruleIndex + 1}`,\n triple,\n binding,\n uses: proofUses(rule.body, binding),\n });\n }\n }\n }\n }\n\n if (bodyContext.backwardProver && context.hybridStats) mergeBackwardStats(context.hybridStats, bodyContext.backwardProver.stats);\n return { applications, added };\n}\n\nfunction proofUses(body, binding) {\n return body\n .filter((clause) => clause.type === 'triple')\n .map((clause) => ({\n s: instantiateTerm(clause.triple.s, binding),\n p: instantiateTerm(clause.triple.p, binding),\n o: instantiateTerm(clause.triple.o, binding),\n }))\n .filter((triple) => ![triple.s, triple.p, triple.o].some((term) => term && term.type === 'var'));\n}\n\nfunction prepareBodyContext(program, store, context) {\n if (!context.hybridBackwardPredicates || context.hybridBackwardPredicates.size === 0) return context;\n return {\n ...context,\n backwardProver: new BackwardProver(program, {\n ...context,\n store,\n allowedPredicates: context.hybridBackwardPredicates,\n }),\n };\n}\n\nfunction emptyBackwardStats() {\n return { mode: 'hybrid', goals: 0, facts: 0, rules: 0, memoHits: 0, memoStores: 0, maxDepth: 0 };\n}\n\nfunction mergeBackwardStats(total, item) {\n if (!total || !item) return;\n total.goals += item.goals || 0;\n total.facts += item.facts || 0;\n total.rules += item.rules || 0;\n total.memoHits += item.memoHits || 0;\n total.memoStores += item.memoStores || 0;\n total.maxDepth = Math.max(total.maxDepth || 0, item.maxDepth || 0);\n}\n\nfunction recursiveTermGenerationRuleIndexes(analysis) {\n const out = new Set();\n if (!analysis || !analysis.dependency || !analysis.diagnostics) return out;\n const byName = new Map((analysis.dependency.rules || []).map((rule) => [rule.name, rule.index]));\n for (const diagnostic of analysis.diagnostics) {\n if (diagnostic.code !== 'recursive-assignment-rule') continue;\n if (byName.has(diagnostic.rule)) out.add(byName.get(diagnostic.rule));\n }\n return out;\n}\n\nfunction instantiateHeadTriple(pattern, binding, headBlankLabels, headBlankMap, skolemKey) {\n const s = instantiateHeadTerm(pattern.s, binding, headBlankLabels, headBlankMap, skolemKey);\n const p = instantiateHeadTerm(pattern.p, binding, headBlankLabels, headBlankMap, skolemKey);\n const o = instantiateHeadTerm(pattern.o, binding, headBlankLabels, headBlankMap, skolemKey);\n if (!s || !p || !o) return null;\n if (p.type !== 'iri') return null;\n return { s, p, o };\n}\n\nfunction instantiateHeadTerm(term, binding, headBlankLabels, headBlankMap, skolemKey) {\n if (term.type === 'var') return binding[term.value] || null;\n if (term.type === 'blank' && headBlankLabels.has(term.value)) {\n let label = headBlankMap.get(term.value);\n if (!label) {\n label = `sk_${deterministicSkolemIdFromKey(`${skolemKey}|${term.value}`).replace(/-/g, '_')}`;\n headBlankMap.set(term.value, label);\n }\n return blankNode(label);\n }\n if (term.type === 'triple') {\n const s = instantiateHeadTerm(term.s, binding, headBlankLabels, headBlankMap, skolemKey);\n const p = instantiateHeadTerm(term.p, binding, headBlankLabels, headBlankMap, skolemKey);\n const o = instantiateHeadTerm(term.o, binding, headBlankLabels, headBlankMap, skolemKey);\n if (!s || !p || !o) return null;\n return tripleTerm(s, p, o);\n }\n return term;\n}\n\nfunction collectHeadBlankLabels(head) {\n const labels = new Set();\n for (const triple of head || []) {\n collectBlankLabelsFromTerm(triple.s, labels);\n collectBlankLabelsFromTerm(triple.p, labels);\n collectBlankLabelsFromTerm(triple.o, labels);\n }\n return labels;\n}\n\nfunction collectBlankLabelsFromTerm(term, labels) {\n if (!term) return;\n if (term.type === 'blank') {\n labels.add(term.value);\n return;\n }\n if (term.type === 'triple') {\n collectBlankLabelsFromTerm(term.s, labels);\n collectBlankLabelsFromTerm(term.p, labels);\n collectBlankLabelsFromTerm(term.o, labels);\n }\n}\n\nfunction skolemizationKey(ruleIndex, binding) {\n let out = `rule:${ruleIndex}`;\n for (const name of Object.keys(binding).sort()) {\n const value = binding[name];\n out += `|${name}=${value ? termKey(value) : 'unbound'}`;\n }\n return out;\n}\n\nfunction deterministicSkolemIdFromKey(key) {\n let h1 = 0x811c9dc5;\n let h2 = 0x811c9dc5;\n let h3 = 0x811c9dc5;\n let h4 = 0x811c9dc5;\n for (let i = 0; i < key.length; i += 1) {\n const c = key.charCodeAt(i);\n h1 ^= c; h1 = Math.imul(h1, 0x01000193) >>> 0;\n h2 ^= c + 1; h2 = Math.imul(h2, 0x01000193) >>> 0;\n h3 ^= c + 2; h3 = Math.imul(h3, 0x01000193) >>> 0;\n h4 ^= c + 3; h4 = Math.imul(h4, 0x01000193) >>> 0;\n }\n return [h1, h2, h3, h4].map((h) => h.toString(16).padStart(8, '0')).join('');\n}\n\n\nfunction evaluateBody(clauses, store, initialBinding = {}, options = {}) {\n const bindings = [];\n const seen = new Set();\n for (const binding of evaluateBodyStream(clauses, store, initialBinding, options)) {\n const key = bindingKey(binding);\n if (seen.has(key)) continue;\n seen.add(key);\n bindings.push(binding);\n }\n return bindings;\n}\n\nfunction* evaluateBodyStream(clauses, store, initialBinding = {}, options = {}, index = 0) {\n if (index >= clauses.length) {\n yield initialBinding;\n return;\n }\n\n const clause = clauses[index];\n if (clause.type === 'triple') {\n const seen = new Set();\n for (const matched of store.match(clause.triple, initialBinding)) {\n const key = bindingKey(matched);\n seen.add(key);\n yield* evaluateBodyStream(clauses, store, matched, options, index + 1);\n }\n if (shouldUseBackwardForTriple(clause.triple, initialBinding, options)) {\n for (const matched of options.backwardProver.solveTriple(clause.triple, initialBinding)) {\n const key = bindingKey(matched);\n if (seen.has(key)) continue;\n seen.add(key);\n yield* evaluateBodyStream(clauses, store, matched, options, index + 1);\n }\n }\n return;\n }\n\n if (clause.type === 'path') {\n for (const matched of store.matchPath(clause.triple, initialBinding)) {\n yield* evaluateBodyStream(clauses, store, matched, options, index + 1);\n }\n return;\n }\n\n if (clause.type === 'filter') {\n try {\n if (booleanValue(evalExpression(clause.expr, initialBinding, options))) {\n yield* evaluateBodyStream(clauses, store, initialBinding, options, index + 1);\n }\n } catch (_) {\n // SPARQL-style FILTER errors reject the current solution.\n }\n return;\n }\n\n if (clause.type === 'set' || clause.type === 'bind') {\n try {\n const value = asTerm(evalExpression(clause.expr, initialBinding, options));\n if (!initialBinding[clause.variable]) {\n yield* evaluateBodyStream(clauses, store, { ...initialBinding, [clause.variable]: value }, options, index + 1);\n } else if (termEquals(initialBinding[clause.variable], value)) {\n yield* evaluateBodyStream(clauses, store, initialBinding, options, index + 1);\n }\n } catch (_) {\n // The SRL evaluation sketch drops a solution when assignment evaluation errors.\n }\n return;\n }\n\n if (clause.type === 'not') {\n if (!bodyHasAny(clause.body, store, initialBinding, options)) {\n yield* evaluateBodyStream(clauses, store, initialBinding, options, index + 1);\n }\n return;\n }\n\n throw new Error(`Unsupported body clause ${clause.type}`);\n}\n\nfunction shouldUseBackwardForTriple(pattern, binding, options = {}) {\n if (!options.backwardProver || !options.hybridBackwardPredicates || options.hybridBackwardPredicates.size === 0) return false;\n const predicate = instantiateTerm(pattern.p, binding);\n return !!(predicate && predicate.type === 'iri' && options.hybridBackwardPredicates.has(predicate.value));\n}\n\nfunction bodyHasAny(clauses, store, initialBinding, options) {\n for (const _ of evaluateBodyStream(clauses, store, initialBinding, options)) return true;\n return false;\n}\n\nfunction uniqueBindings(bindings) {\n const seen = new Set();\n const out = [];\n for (const binding of bindings) {\n const key = bindingKey(binding);\n if (!seen.has(key)) {\n seen.add(key);\n out.push(binding);\n }\n }\n return out;\n}\n\nmodule.exports = { evaluate, evaluateBody, uniqueBindings };\n","src/store.js":"'use strict';\n\nconst { tripleKey, termKey, termEquals } = require('./term.js');\n\nclass TripleStore {\n constructor(triples = []) {\n this.map = new Map();\n this.byPredicate = new Map();\n this.byPredicateSubject = new Map();\n this.byPredicateObject = new Map();\n this.version = 0;\n for (const triple of triples) this.add(triple);\n }\n\n add(triple) {\n const normalized = normalizeTriple(triple);\n const key = tripleKey(normalized);\n if (this.map.has(key)) return false;\n this.map.set(key, normalized);\n const predicate = termKey(normalized.p);\n const subject = termKey(normalized.s);\n const object = termKey(normalized.o);\n addIndex(this.byPredicate, predicate, key, normalized);\n addNestedIndex(this.byPredicateSubject, predicate, subject, key, normalized);\n addNestedIndex(this.byPredicateObject, predicate, object, key, normalized);\n this.version += 1;\n return true;\n }\n\n has(triple) {\n return this.map.has(tripleKey(normalizeTriple(triple)));\n }\n\n values() {\n return Array.from(this.map.values());\n }\n\n size() {\n return this.map.size;\n }\n\n candidates(pattern, binding = {}) {\n const p = instantiateTerm(pattern.p, binding);\n if (p && p.type !== 'var') {\n const predicate = termKey(p);\n const s = instantiateTerm(pattern.s, binding);\n const o = instantiateTerm(pattern.o, binding);\n const bySubject = s && s.type !== 'var' ? nestedLookup(this.byPredicateSubject, predicate, termKey(s)) : null;\n const byObject = o && o.type !== 'var' ? nestedLookup(this.byPredicateObject, predicate, termKey(o)) : null;\n if (bySubject && byObject) return smallerValues(bySubject, byObject);\n if (bySubject) return Array.from(bySubject.values());\n if (byObject) return Array.from(byObject.values());\n const indexed = this.byPredicate.get(predicate);\n return indexed ? Array.from(indexed.values()) : [];\n }\n return this.values();\n }\n\n match(pattern, binding = {}) {\n const out = [];\n for (const triple of this.candidates(pattern, binding)) {\n const matched = matchTriple(pattern, triple, binding);\n if (matched) out.push(matched);\n }\n return out;\n }\n\n matchPath(pattern, binding = {}) {\n const prefix = `__path_${pathCallCounter++}_`;\n const tempVars = [];\n const bindings = matchPathExpression(this, pattern.p, pattern.s, pattern.o, binding, prefix, tempVars);\n if (tempVars.length === 0) return bindings;\n return bindings.map((matched) => {\n let cleaned = matched;\n for (const name of tempVars) {\n if (Object.prototype.hasOwnProperty.call(cleaned, name)) {\n if (cleaned === matched) cleaned = { ...matched };\n delete cleaned[name];\n }\n }\n return cleaned;\n });\n }\n}\n\nlet pathCallCounter = 0;\n\nfunction addIndex(index, key, tripleKeyValue, triple) {\n if (!index.has(key)) index.set(key, new Map());\n index.get(key).set(tripleKeyValue, triple);\n}\n\nfunction addNestedIndex(index, outerKey, innerKey, tripleKeyValue, triple) {\n if (!index.has(outerKey)) index.set(outerKey, new Map());\n const inner = index.get(outerKey);\n if (!inner.has(innerKey)) inner.set(innerKey, new Map());\n inner.get(innerKey).set(tripleKeyValue, triple);\n}\n\nfunction nestedLookup(index, outerKey, innerKey) {\n const inner = index.get(outerKey);\n return inner ? inner.get(innerKey) || null : null;\n}\n\nfunction smallerValues(left, right) {\n const small = left.size <= right.size ? left : right;\n const large = small === left ? right : left;\n const out = [];\n for (const [key, triple] of small) if (large.has(key)) out.push(triple);\n return out;\n}\n\nfunction normalizeTriple(triple) {\n return { s: triple.s, p: triple.p, o: triple.o };\n}\n\nfunction bindingKey(binding) {\n return Object.keys(binding).sort().map((name) => `${name}=${termKey(binding[name])}`).join(';');\n}\n\nfunction mergeBindingTerm(binding, patternTerm, dataTerm) {\n if (!patternTerm || !dataTerm) return null;\n if (patternTerm.type === 'var') {\n const name = patternTerm.value;\n if (!binding[name]) return { ...binding, [name]: dataTerm };\n return termEquals(binding[name], dataTerm) ? binding : null;\n }\n if (patternTerm.type === 'triple') {\n if (dataTerm.type !== 'triple') return null;\n let next = mergeBindingTerm(binding, patternTerm.s, dataTerm.s);\n if (!next) return null;\n next = mergeBindingTerm(next, patternTerm.p, dataTerm.p);\n if (!next) return null;\n return mergeBindingTerm(next, patternTerm.o, dataTerm.o);\n }\n return termEquals(patternTerm, dataTerm) ? binding : null;\n}\n\nfunction matchTriple(pattern, triple, binding = {}) {\n let next = mergeBindingTerm(binding, pattern.s, triple.s);\n if (!next) return null;\n next = mergeBindingTerm(next, pattern.p, triple.p);\n if (!next) return null;\n next = mergeBindingTerm(next, pattern.o, triple.o);\n return next;\n}\n\nfunction instantiateTerm(term, binding) {\n if (term.type === 'var') return binding[term.value] || null;\n if (term.type === 'triple') {\n const s = instantiateTerm(term.s, binding);\n const p = instantiateTerm(term.p, binding);\n const o = instantiateTerm(term.o, binding);\n if (!s || !p || !o) return null;\n return { type: 'triple', s, p, o };\n }\n return term;\n}\n\nfunction instantiateTriple(pattern, binding) {\n const s = instantiateTerm(pattern.s, binding);\n const p = instantiateTerm(pattern.p, binding);\n const o = instantiateTerm(pattern.o, binding);\n if (!s || !p || !o) return null;\n if (p.type !== 'iri') return null;\n return { s, p, o };\n}\n\nfunction matchPathExpression(store, path, start, end, binding, tempPrefix, tempVars) {\n if (!path || path.type !== 'path') return store.match({ s: start, p: path, o: end }, binding);\n\n if (path.kind === 'iri') return store.match({ s: start, p: path.iri, o: end }, binding);\n\n if (path.kind === 'inverse') {\n return matchPathExpression(store, path.path, end, start, binding, tempPrefix, tempVars);\n }\n\n if (path.kind === 'sequence') {\n let bindings = [binding];\n let currentStart = start;\n for (let index = 0; index < path.parts.length; index += 1) {\n let currentEnd = end;\n if (index + 1 < path.parts.length) {\n const tempName = `${tempPrefix}${tempVars.length}`;\n currentEnd = { type: 'var', value: tempName };\n tempVars.push(tempName);\n }\n const next = [];\n for (const candidate of bindings) {\n for (const matched of matchPathExpression(store, path.parts[index], currentStart, currentEnd, candidate, tempPrefix, tempVars)) {\n next.push(matched);\n }\n }\n if (next.length === 0) return [];\n bindings = next;\n currentStart = currentEnd;\n }\n return bindings;\n }\n\n throw new Error(`Unsupported path kind ${path.kind}`);\n}\n\nfunction pathPairs(store, path) {\n if (!path || path.type !== 'path') {\n return store.match({ s: { type: 'var', value: '__s' }, p: path, o: { type: 'var', value: '__o' } })\n .map((binding) => ({ s: binding.__s, o: binding.__o }));\n }\n\n if (path.kind === 'iri') {\n return pathPairs(store, path.iri);\n }\n\n if (path.kind === 'inverse') {\n return pathPairs(store, path.path).map((pair) => ({ s: pair.o, o: pair.s }));\n }\n\n if (path.kind === 'sequence') {\n let pairs = pathPairs(store, path.parts[0]);\n for (const part of path.parts.slice(1)) {\n const right = pathPairs(store, part);\n const joined = [];\n for (const leftPair of pairs) {\n for (const rightPair of right) {\n if (termEquals(leftPair.o, rightPair.s)) joined.push({ s: leftPair.s, o: rightPair.o });\n }\n }\n pairs = uniquePairs(joined);\n }\n return pairs;\n }\n\n throw new Error(`Unsupported path kind ${path.kind}`);\n}\n\nfunction uniquePairs(pairs) {\n const seen = new Set();\n const out = [];\n for (const pair of pairs) {\n const key = `${termKey(pair.s)} ${termKey(pair.o)}`;\n if (!seen.has(key)) {\n seen.add(key);\n out.push(pair);\n }\n }\n return out;\n}\n\nmodule.exports = {\n TripleStore,\n normalizeTriple,\n bindingKey,\n matchTriple,\n instantiateTerm,\n instantiateTriple,\n pathPairs,\n};\n","src/analyze.js":"'use strict';\n\nconst { compactIRI, iri, variable, termEquals } = require('./term.js');\nconst { tripleHasBlankNode } = require('./assignments.js');\n\nfunction analyze(program, options = {}) {\n const diagnostics = [];\n const dependency = dependencyGraph(program, options);\n const hasTermGeneratingRules = dependency.rules.some((rule) => rule.createsTerms);\n const recursiveIndexes = hasTermGeneratingRules ? recursiveRuleIndexes(dependency) : new Set();\n\n program.rules.forEach((rule, index) => {\n const name = ruleName(rule, index);\n const bound = boundVariables(rule.body);\n const head = new Set();\n for (const triple of rule.head) collectTripleVars(triple, head);\n\n for (const variable of head) {\n if (!bound.has(variable)) {\n diagnostics.push({\n code: 'unsafe-head-variable',\n severity: 'error',\n rule: name,\n message: `${displayRuleName(name, program.prefixes || {})} has unbound head variable ?${variable}`,\n });\n }\n }\n\n for (const triple of rule.head) {\n if (triple.p.type !== 'iri' && triple.p.type !== 'var') {\n diagnostics.push({\n code: 'invalid-head-predicate',\n severity: 'error',\n rule: name,\n message: `${displayRuleName(name, program.prefixes || {})} has a non-IRI/non-variable predicate in the head`,\n });\n }\n }\n\n diagnostics.push(...sequentialWellFormednessDiagnostics(rule.body, name, program.prefixes || {}));\n\n const depRule = dependency.rules[index] || {};\n if (depRule.createsTerms && recursiveIndexes.has(index)) {\n diagnostics.push({\n code: 'recursive-assignment-rule',\n severity: 'warning',\n rule: name,\n message: `${displayRuleName(name, program.prefixes || {})} creates terms in a recursive dependency cycle; relaxed mode allows this but termination is not guaranteed (use --strict to reject it)`,\n });\n }\n\n });\n\n for (const cycle of dependency.unstratifiedCycles) {\n diagnostics.push({\n code: 'unstratified-negation',\n severity: 'error',\n rules: cycle.rules,\n message: `Unstratified negation through ${cycle.rules.map((name) => displayRuleName(name, program.prefixes || {})).join(' -> ')} using ${cycle.predicate ? compactIRI(cycle.predicate, program.prefixes || {}) : '*'}`,\n });\n }\n\n return {\n warnings: diagnostics.filter((diagnostic) => diagnostic.severity === 'warning'),\n errors: diagnostics.filter((diagnostic) => diagnostic.severity === 'error'),\n diagnostics,\n dependency,\n };\n}\n\nfunction ruleName(rule, index) {\n return rule.name || `rule#${index + 1}`;\n}\n\nfunction displayRuleName(name, prefixes = {}) {\n return /^https?:/.test(name) ? compactIRI(name, prefixes) : name;\n}\n\nfunction dependencyGraph(program, options = {}) {\n const rules = program.rules.map((rule, index) => {\n const positivePatterns = bodyTriplePatterns(rule.body, false);\n const negativePatterns = bodyTriplePatterns(rule.body, true);\n const headTemplates = effectiveHeadTemplates(rule);\n return {\n index,\n name: ruleName(rule, index),\n headTemplates,\n positivePatterns,\n negativePatterns,\n headPredicates: new Set(headTemplates.map((triple) => predicateIRI(triple)).filter(Boolean)),\n positivePredicates: new Set(positivePatterns.flatMap((triple) => predicateIRIs(triple))),\n negativePredicates: new Set(negativePatterns.flatMap((triple) => predicateIRIs(triple))),\n runOnce: !!rule.runOnce,\n hasAssignment: ruleHasAssignment(rule, options),\n hasTermGeneratingAssignment: ruleHasTermGeneratingAssignment(rule, options),\n headHasBlankNode: ruleHeadHasBlankNode(rule),\n createsTerms: ruleCreatesTerms(rule, options),\n };\n });\n\n const edgeMap = new Map();\n function addEdge(from, to, kind, predicate) {\n const label = predicate || '*';\n const key = `${from.index}->${to.index}:${label}`;\n const negated = kind === 'negated';\n const termGeneration = kind === 'term-generation';\n const existing = edgeMap.get(key);\n if (existing) {\n existing.negated = existing.negated || negated;\n existing.termGeneration = existing.termGeneration || termGeneration;\n existing.negative = existing.negated || existing.termGeneration;\n return;\n }\n edgeMap.set(key, {\n from: from.index,\n to: to.index,\n negative: negated || termGeneration,\n negated,\n termGeneration,\n predicate,\n });\n }\n\n const headIndex = buildHeadTemplateIndex(rules);\n\n for (const from of rules) {\n const forceClosed = from.createsTerms;\n for (const pattern of from.positivePatterns) {\n for (const candidate of candidateHeadTemplates(headIndex, pattern)) {\n if (canPossiblyGenerate(candidate.template, pattern)) addEdge(from, rules[candidate.ruleIndex], forceClosed ? 'term-generation' : 'positive', dependencyPredicateLabel(pattern));\n }\n }\n for (const pattern of from.negativePatterns) {\n for (const candidate of candidateHeadTemplates(headIndex, pattern)) {\n if (canPossiblyGenerate(candidate.template, pattern)) addEdge(from, rules[candidate.ruleIndex], 'negated', dependencyPredicateLabel(pattern));\n }\n }\n }\n\n const edges = Array.from(edgeMap.values()).sort((a, b) => a.from - b.from || a.to - b.to || String(a.predicate || '').localeCompare(String(b.predicate || '')));\n\n const components = stronglyConnectedComponents(rules.length, edges);\n const componentOf = new Map();\n components.forEach((component, index) => {\n for (const ruleIndex of component) componentOf.set(ruleIndex, index);\n });\n\n const unstratifiedCycles = [];\n const seen = new Set();\n for (const edge of edges) {\n if (!edge.negated) continue;\n if (edge.from === edge.to && rules[edge.from].runOnce && !rules[edge.from].headHasBlankNode) continue;\n if (componentOf.get(edge.from) !== componentOf.get(edge.to)) continue;\n const component = components[componentOf.get(edge.from)];\n const key = `${component.slice().sort((a, b) => a - b).join(',')}|${edge.predicate || '*'}`;\n if (seen.has(key)) continue;\n seen.add(key);\n unstratifiedCycles.push({\n predicate: edge.predicate,\n rules: component.map((ruleIndex) => rules[ruleIndex].name),\n });\n }\n\n const layers = stratificationLayers(rules.length, components, componentOf, edges);\n\n return {\n rules: rules.map((rule) => ({\n index: rule.index,\n name: rule.name,\n headPredicates: Array.from(rule.headPredicates),\n positivePredicates: Array.from(rule.positivePredicates),\n negativePredicates: Array.from(rule.negativePredicates),\n runOnce: rule.runOnce,\n hasAssignment: rule.hasAssignment,\n hasTermGeneratingAssignment: rule.hasTermGeneratingAssignment,\n headHasBlankNode: rule.headHasBlankNode,\n createsTerms: rule.createsTerms,\n })),\n edges,\n components: components.map((component) => component.map((ruleIndex) => rules[ruleIndex].name)),\n layers: layers.map((layer) => layer.map((ruleIndex) => rules[ruleIndex].name)),\n layerIndexes: layers,\n unstratifiedCycles,\n };\n}\n\nfunction buildHeadTemplateIndex(rules) {\n const templates = [];\n const positions = ['s', 'p', 'o'];\n const byPosition = {\n s: new Map(),\n p: new Map(),\n o: new Map(),\n };\n const flexibleByPosition = {\n s: new Set(),\n p: new Set(),\n o: new Set(),\n };\n\n for (const rule of rules) {\n for (const template of rule.headTemplates) {\n const entry = { id: templates.length, ruleIndex: rule.index, template };\n templates.push(entry);\n for (const position of positions) {\n const key = fixedTermIndexKey(template[position]);\n if (key === null) flexibleByPosition[position].add(entry.id);\n else {\n let bucket = byPosition[position].get(key);\n if (!bucket) {\n bucket = new Set();\n byPosition[position].set(key, bucket);\n }\n bucket.add(entry.id);\n }\n }\n }\n }\n\n return { templates, byPosition, flexibleByPosition };\n}\n\nfunction candidateHeadTemplates(index, pattern) {\n const positions = ['s', 'p', 'o'];\n let selected = null;\n\n for (const position of positions) {\n const key = fixedTermIndexKey(pattern[position]);\n if (key === null) continue;\n const exact = index.byPosition[position].get(key) || null;\n const flexible = index.flexibleByPosition[position];\n const estimatedSize = (exact ? exact.size : 0) + flexible.size;\n if (selected === null || estimatedSize < selected.estimatedSize) selected = { exact, flexible, estimatedSize };\n if (estimatedSize === 0) break;\n }\n\n if (selected === null) return index.templates;\n const ids = [];\n if (selected.exact) for (const id of selected.exact) ids.push(id);\n for (const id of selected.flexible) ids.push(id);\n const out = [];\n const seen = new Set();\n for (const id of ids) {\n if (seen.has(id)) continue;\n seen.add(id);\n out.push(index.templates[id]);\n }\n return out;\n}\n\nfunction fixedTermIndexKey(term) {\n if (!term) return null;\n if (term.type === 'var') return null;\n if (term.type === 'path') return null;\n if (term.type === 'triple' && containsVariableTerm(term)) return null;\n return termIndexKey(term);\n}\n\nfunction containsVariableTerm(term) {\n if (!term) return false;\n if (term.type === 'var') return true;\n if (term.type === 'triple') return containsVariableTerm(term.s) || containsVariableTerm(term.p) || containsVariableTerm(term.o);\n if (term.type === 'path') {\n if (term.kind === 'inverse') return containsVariableTerm(term.path);\n if (term.kind === 'sequence') return term.parts.some(containsVariableTerm);\n }\n return false;\n}\n\nfunction literalIndexValue(value) {\n if (typeof value === 'bigint') return `${value.toString()}n`;\n return JSON.stringify(value);\n}\n\nfunction termIndexKey(term) {\n if (!term) return 'null';\n if (term.type === 'iri') return `I:${term.value}`;\n if (term.type === 'blank') return `B:${term.value}`;\n if (term.type === 'literal') return `L:${literalIndexValue(term.value)}^^${term.datatype || ''}@${term.lang || ''}--${term.langDir || ''}`;\n if (term.type === 'triple') return `T:${termIndexKey(term.s)} ${termIndexKey(term.p)} ${termIndexKey(term.o)}`;\n return JSON.stringify(term);\n}\n\nfunction unionSets(a, b) {\n const out = new Set();\n if (a) for (const value of a) out.add(value);\n if (b) for (const value of b) out.add(value);\n return out;\n}\n\nfunction allTemplateIds(length) {\n const out = new Set();\n for (let i = 0; i < length; i += 1) out.add(i);\n return out;\n}\n\nfunction stratificationLayers(ruleCount, components, componentOf, edges) {\n if (ruleCount === 0) return [];\n const outgoing = Array.from({ length: components.length }, () => new Set());\n const indegree = Array(components.length).fill(0);\n\n for (const edge of edges) {\n const dependent = componentOf.get(edge.from);\n const dependency = componentOf.get(edge.to);\n if (dependent === dependency) continue;\n // Rule edge means \"from depends on to\". Evaluation must run dependency before dependent.\n if (!outgoing[dependency].has(dependent)) {\n outgoing[dependency].add(dependent);\n indegree[dependent] += 1;\n }\n }\n\n let ready = [];\n for (let i = 0; i < indegree.length; i += 1) if (indegree[i] === 0) ready.push(i);\n const layers = [];\n const emitted = new Set();\n while (ready.length > 0) {\n ready.sort((a, b) => componentMin(components[a]) - componentMin(components[b]));\n const layerComponents = ready;\n ready = [];\n const layer = [];\n for (const componentIndex of layerComponents) {\n emitted.add(componentIndex);\n layer.push(...components[componentIndex]);\n for (const next of outgoing[componentIndex]) {\n indegree[next] -= 1;\n if (indegree[next] === 0) ready.push(next);\n }\n }\n layers.push(layer.sort((a, b) => a - b));\n }\n\n if (emitted.size !== components.length) return [Array.from({ length: ruleCount }, (_, i) => i)];\n return layers;\n}\n\n\nfunction componentMin(component) {\n let min = Infinity;\n for (const value of component) if (value < min) min = value;\n return min;\n}\n\nfunction recursiveRuleIndexes(dependency) {\n const out = new Set();\n const ruleByName = new Map(dependency.rules.map((rule) => [rule.name, rule]));\n const componentOf = new Map();\n\n dependency.components.forEach((component, componentIndex) => {\n for (const name of component) {\n const rule = ruleByName.get(name);\n if (rule) componentOf.set(rule.index, componentIndex);\n }\n });\n\n for (const component of dependency.components) {\n if (component.length <= 1) continue;\n for (const name of component) {\n const rule = ruleByName.get(name);\n if (rule) out.add(rule.index);\n }\n }\n\n for (const edge of dependency.edges) {\n const rule = (dependency.rules || []).find((candidate) => candidate.index === edge.from);\n if (edge.from === edge.to && edge.negated && rule && rule.runOnce && !rule.headHasBlankNode) continue;\n if (edge.from === edge.to || componentOf.get(edge.from) === componentOf.get(edge.to)) out.add(edge.from);\n }\n return out;\n}\n\nfunction stronglyConnectedComponents(size, edges) {\n const adjacency = Array.from({ length: size }, () => []);\n const reverse = Array.from({ length: size }, () => []);\n for (const edge of edges) {\n adjacency[edge.from].push(edge.to);\n reverse[edge.to].push(edge.from);\n }\n\n const visited = Array(size).fill(false);\n const order = [];\n for (let start = 0; start < size; start += 1) {\n if (visited[start]) continue;\n const stack = [[start, 0]];\n visited[start] = true;\n while (stack.length > 0) {\n const frame = stack[stack.length - 1];\n const v = frame[0];\n let nextIndex = frame[1];\n if (nextIndex < adjacency[v].length) {\n const w = adjacency[v][nextIndex];\n frame[1] = nextIndex + 1;\n if (!visited[w]) {\n visited[w] = true;\n stack.push([w, 0]);\n }\n } else {\n order.push(v);\n stack.pop();\n }\n }\n }\n\n const assigned = Array(size).fill(false);\n const components = [];\n for (let i = order.length - 1; i >= 0; i -= 1) {\n const start = order[i];\n if (assigned[start]) continue;\n const component = [];\n const stack = [start];\n assigned[start] = true;\n while (stack.length > 0) {\n const v = stack.pop();\n component.push(v);\n for (const w of reverse[v]) {\n if (!assigned[w]) {\n assigned[w] = true;\n stack.push(w);\n }\n }\n }\n components.push(component.sort((a, b) => a - b));\n }\n return components;\n}\n\nfunction sequentialWellFormednessDiagnostics(clauses, ruleNameValue, prefixes = {}) {\n const diagnostics = [];\n\n function visit(items, initialBound, scopeLabel) {\n const bound = new Set(initialBound);\n for (const clause of items) {\n if (clause.type === 'triple' || clause.type === 'path') {\n collectTripleVars(clause.triple, bound);\n } else if (clause.type === 'filter') {\n for (const variable of expressionVariables(clause.expr)) {\n if (!bound.has(variable)) {\n diagnostics.push({\n code: 'unbound-filter-variable',\n severity: 'error',\n rule: ruleNameValue,\n message: `${displayRuleName(ruleNameValue, prefixes)} FILTER uses ?${variable} before it is bound${scopeLabel}`,\n });\n }\n }\n } else if ((clause.type === 'set' || clause.type === 'bind')) {\n if (bound.has(clause.variable)) {\n diagnostics.push({\n code: 'assignment-variable-already-bound',\n severity: 'error',\n rule: ruleNameValue,\n message: `${displayRuleName(ruleNameValue, prefixes)} SET assigns ?${clause.variable}, but that variable is already bound${scopeLabel}`,\n });\n }\n for (const variable of expressionVariables(clause.expr)) {\n if (!bound.has(variable)) {\n diagnostics.push({\n code: 'unbound-assignment-variable',\n severity: 'error',\n rule: ruleNameValue,\n message: `${displayRuleName(ruleNameValue, prefixes)} SET expression uses ?${variable} before it is bound${scopeLabel}`,\n });\n }\n }\n bound.add(clause.variable);\n } else if (clause.type === 'not') {\n visit(clause.body, bound, ' inside NOT');\n }\n }\n return bound;\n }\n\n visit(clauses, new Set(), '');\n return diagnostics;\n}\n\nfunction bodyTriplePatterns(clauses, wantNegative, inNegativeContext = false) {\n const out = [];\n for (const clause of clauses) {\n if ((clause.type === 'triple' || clause.type === 'path') && wantNegative === inNegativeContext) {\n if (clause.type === 'path') out.push(...pathTriplePatterns(clause.triple));\n else out.push(clause.triple);\n } else if (clause.type === 'not') {\n out.push(...bodyTriplePatterns(clause.body, wantNegative, true));\n }\n }\n return out;\n}\n\nfunction pathTriplePatterns(triple) {\n const predicates = predicateIRIs(triple);\n if (predicates.length === 0) return [];\n return predicates.map((predicate, index) => ({\n s: variable(`__path_s_${index}`),\n p: iri(predicate),\n o: variable(`__path_o_${index}`),\n }));\n}\n\nfunction dependencyPredicateLabel(pattern) {\n return pattern && pattern.p && pattern.p.type === 'iri' ? pattern.p.value : null;\n}\n\nfunction canPossiblyGenerate(template, pattern) {\n if (!template || !pattern) return false;\n if (!compatibleTerm(template.s, pattern.s)) return false;\n if (!compatibleTerm(template.p, pattern.p)) return false;\n if (!compatibleTerm(template.o, pattern.o)) return false;\n\n const constraints = new Map();\n if (!recordTemplateVariableConstraints(template.s, pattern.s, constraints)) return false;\n if (!recordTemplateVariableConstraints(template.p, pattern.p, constraints)) return false;\n if (!recordTemplateVariableConstraints(template.o, pattern.o, constraints)) return false;\n return true;\n}\n\nfunction compatibleTerm(templateTerm, patternTerm) {\n if (!templateTerm || !patternTerm) return false;\n if (templateTerm.type === 'var' || patternTerm.type === 'var') return true;\n if (templateTerm.type === 'triple' || patternTerm.type === 'triple') {\n if (templateTerm.type !== 'triple' || patternTerm.type !== 'triple') return false;\n return compatibleTerm(templateTerm.s, patternTerm.s)\n && compatibleTerm(templateTerm.p, patternTerm.p)\n && compatibleTerm(templateTerm.o, patternTerm.o);\n }\n return termEquals(templateTerm, patternTerm);\n}\n\nfunction recordTemplateVariableConstraints(templateTerm, patternTerm, constraints) {\n if (!templateTerm || !patternTerm) return false;\n if (templateTerm.type === 'var') {\n const existing = constraints.get(templateTerm.value);\n if (!existing) {\n constraints.set(templateTerm.value, patternTerm);\n return true;\n }\n return possiblySameTerm(existing, patternTerm);\n }\n if (templateTerm.type === 'triple' && patternTerm.type === 'triple') {\n return recordTemplateVariableConstraints(templateTerm.s, patternTerm.s, constraints)\n && recordTemplateVariableConstraints(templateTerm.p, patternTerm.p, constraints)\n && recordTemplateVariableConstraints(templateTerm.o, patternTerm.o, constraints);\n }\n return true;\n}\n\nfunction possiblySameTerm(a, b) {\n if (!a || !b) return false;\n if (a.type === 'var' || b.type === 'var') return true;\n if (a.type === 'triple' || b.type === 'triple') {\n if (a.type !== 'triple' || b.type !== 'triple') return false;\n return possiblySameTerm(a.s, b.s) && possiblySameTerm(a.p, b.p) && possiblySameTerm(a.o, b.o);\n }\n return termEquals(a, b);\n}\n\nfunction bodyPredicates(clauses, wantNegative, inNegativeContext = false) {\n const out = [];\n for (const clause of clauses) {\n if ((clause.type === 'triple' || clause.type === 'path') && wantNegative === inNegativeContext) {\n out.push(...predicateIRIs(clause.triple));\n } else if (clause.type === 'not') {\n out.push(...bodyPredicates(clause.body, wantNegative, true));\n }\n }\n return out;\n}\n\nfunction predicateIRI(triple) {\n return triple && triple.p && triple.p.type === 'iri' ? triple.p.value : null;\n}\n\nfunction predicateIRIs(triple) {\n if (!triple || !triple.p) return [];\n if (triple.p.type === 'iri') return [triple.p.value];\n if (triple.p.type === 'path') return pathPredicateIRIs(triple.p);\n return [];\n}\n\nfunction pathPredicateIRIs(path) {\n if (!path) return [];\n if (path.type === 'iri') return [path.value];\n if (path.type !== 'path') return [];\n if (path.kind === 'inverse') return pathPredicateIRIs(path.path);\n if (path.kind === 'sequence') return path.parts.flatMap(pathPredicateIRIs);\n if (path.kind === 'iri') return pathPredicateIRIs(path.iri);\n return [];\n}\n\nfunction effectiveHeadTemplates(rule) {\n const constants = assignmentConstantTerms(rule.body || []);\n if (constants.size === 0) return rule.head.slice();\n return rule.head.map((triple) => ({\n s: substituteAssignedConstant(triple.s, constants),\n p: substituteAssignedConstant(triple.p, constants),\n o: substituteAssignedConstant(triple.o, constants),\n }));\n}\n\nfunction assignmentConstantTerms(clauses) {\n const constants = new Map();\n const bound = new Set();\n for (const clause of clauses) {\n if (clause.type === 'triple' || clause.type === 'path') {\n collectTripleVars(clause.triple, bound);\n continue;\n }\n if (clause.type === 'filter') continue;\n if (clause.type === 'not') continue;\n if (clause.type === 'set' || clause.type === 'bind') {\n const value = constantExpressionTerm(clause.expr);\n if (value && !bound.has(clause.variable)) constants.set(clause.variable, value);\n bound.add(clause.variable);\n }\n }\n return constants;\n}\n\nfunction constantExpressionTerm(expr) {\n if (!expr || expressionVariables(expr).size > 0) return null;\n if (expr.type === 'term') return expr.value;\n return null;\n}\n\nfunction substituteAssignedConstant(term, constants) {\n if (!term) return term;\n if (term.type === 'var' && constants.has(term.value)) return constants.get(term.value);\n if (term.type === 'triple') {\n return {\n type: 'triple',\n s: substituteAssignedConstant(term.s, constants),\n p: substituteAssignedConstant(term.p, constants),\n o: substituteAssignedConstant(term.o, constants),\n };\n }\n return term;\n}\n\nfunction ruleHasAssignment(rule, options = {}) {\n return (rule.body || []).some((clause) => clause.type === 'set' || clause.type === 'bind');\n}\n\nfunction ruleHasTermGeneratingAssignment(rule, options = {}) {\n return (rule.body || []).some((clause) => (clause.type === 'set' || clause.type === 'bind') && assignmentMayCreateNewTerm(clause.expr));\n}\n\nfunction ruleCreatesTerms(rule, options = {}) {\n return ruleHeadHasBlankNode(rule) || ruleHasTermGeneratingAssignment(rule, options);\n}\n\nfunction assignmentMayCreateNewTerm(expr) {\n // Simple aliases and constants are not term generators: they select from a\n // fixed term or from already-bound terms. Expressions that compute values\n // through operators or functions may create an unbounded sequence when used\n // recursively, e.g. SET(?v1 := ?v + 1), so they remain part of the strict\n // recursive-new-terms check.\n if (!expr) return false;\n if (expr.type === 'var' || expr.type === 'term' || expr.type === 'literal') return false;\n return true;\n}\n\nfunction ruleHeadHasBlankNode(rule) {\n return (rule.head || []).some(tripleHasBlankNode);\n}\n\nfunction boundVariables(clauses) {\n const vars = new Set();\n for (const clause of clauses) {\n if (clause.type === 'triple' || clause.type === 'path') collectTripleVars(clause.triple, vars);\n if ((clause.type === 'set' || clause.type === 'bind')) vars.add(clause.variable);\n }\n return vars;\n}\n\nfunction positiveVariables(clauses) {\n const vars = new Set();\n for (const clause of clauses) {\n if (clause.type === 'triple' || clause.type === 'path') collectTripleVars(clause.triple, vars);\n if ((clause.type === 'set' || clause.type === 'bind')) vars.add(clause.variable);\n if (clause.type === 'filter') for (const v of expressionVariables(clause.expr)) vars.add(v);\n }\n return vars;\n}\n\nfunction bodyVariables(clauses) {\n const vars = new Set();\n for (const clause of clauses) {\n if (clause.type === 'triple' || clause.type === 'path') collectTripleVars(clause.triple, vars);\n if ((clause.type === 'set' || clause.type === 'bind')) {\n vars.add(clause.variable);\n for (const v of expressionVariables(clause.expr)) vars.add(v);\n }\n if (clause.type === 'filter') for (const v of expressionVariables(clause.expr)) vars.add(v);\n if (clause.type === 'not') for (const v of bodyVariables(clause.body)) vars.add(v);\n }\n return vars;\n}\n\nfunction collectTripleVars(triple, vars) {\n for (const term of [triple.s, triple.p, triple.o]) collectTermVars(term, vars);\n}\n\nfunction collectTermVars(term, vars) {\n if (!term) return;\n if (term.type === 'var') vars.add(term.value);\n if (term.type === 'triple') {\n collectTermVars(term.s, vars);\n collectTermVars(term.p, vars);\n collectTermVars(term.o, vars);\n }\n if (term.type === 'path') {\n if (term.kind === 'inverse') collectTermVars(term.path, vars);\n if (term.kind === 'sequence') for (const part of term.parts) collectTermVars(part, vars);\n }\n}\n\nfunction expressionVariables(expr, vars = new Set()) {\n if (!expr) return vars;\n if (expr.type === 'var') vars.add(expr.name);\n else if (expr.type === 'unary') expressionVariables(expr.expr, vars);\n else if (expr.type === 'binary') {\n expressionVariables(expr.left, vars);\n expressionVariables(expr.right, vars);\n } else if (expr.type === 'call') {\n for (const arg of expr.args) expressionVariables(arg, vars);\n } else if (expr.type === 'list') {\n for (const item of expr.items) expressionVariables(item, vars);\n } else if (expr.type === 'term') {\n collectTermVars(expr.value, vars);\n }\n return vars;\n}\n\nmodule.exports = {\n analyze,\n dependencyGraph,\n stratificationLayers,\n boundVariables,\n positiveVariables,\n bodyVariables,\n collectTripleVars,\n expressionVariables,\n pathPredicateIRIs,\n bodyTriplePatterns,\n canPossiblyGenerate,\n};\n","src/backward.js":"'use strict';\n\nconst { TripleStore, bindingKey } = require('./store.js');\nconst { tripleKey, termKey, termEquals } = require('./term.js');\nconst { evalExpression, booleanValue, asTerm } = require('./builtins.js');\n\nfunction backwardQuery(program, querySpec, options = {}) {\n const planner = planBackwardQuery(program, querySpec, options);\n if (!planner.ok) return { ok: false, reason: planner.reason };\n const prover = new BackwardProver(program, { ...options, allowedRuleIndexes: planner.ruleIndexes });\n const bindings = uniqueBindings(Array.from(prover.solveBody(querySpec.body, {})));\n return { ok: true, bindings, stats: prover.stats, plan: planner };\n}\n\nfunction planBackwardQuery(program, querySpec, options = {}) {\n const clauses = querySpec && Array.isArray(querySpec.body) ? querySpec.body : [];\n if (!bodySupported(clauses, options)) return { ok: false, reason: 'query body contains clauses not supported by the backward prover yet' };\n\n const reachable = reachableBackwardRuleIndexes(program, clauses, options);\n for (const ruleIndex of reachable.ruleIndexes) {\n const rule = (program.rules || [])[ruleIndex];\n if (!ruleSupported(rule, options)) return { ok: false, reason: `reachable rule ${rule.name || '<anonymous>'} is not supported by the backward prover yet` };\n }\n return { ok: true, ruleIndexes: reachable.ruleIndexes, predicates: reachable.predicates };\n}\n\nfunction bodySupported(clauses, options = {}) {\n for (const clause of clauses || []) {\n if (clause.type === 'triple' || clause.type === 'filter' || clause.type === 'set' || clause.type === 'bind') continue;\n if (clause.type === 'not') {\n if (options.backwardNegation === false) return false;\n if (!bodySupported(clause.body, options)) return false;\n continue;\n }\n return false;\n }\n return true;\n}\n\nfunction ruleSupported(rule, options = {}) {\n if (!Array.isArray(rule.head) || rule.head.length === 0) return false;\n for (const head of rule.head) {\n if (!head || !head.p || head.p.type !== 'iri') return false;\n if (containsBlank(head.s) || containsBlank(head.p) || containsBlank(head.o)) return false;\n }\n return bodySupported(rule.body || [], options);\n}\n\nclass BackwardProver {\n constructor(program, options = {}) {\n this.program = program;\n this.options = options;\n this.store = options.store || new TripleStore(program.data || []);\n this.maxDepth = options.backwardMaxDepth || options.maxDepth || 10000;\n this.solutionLimit = options.backwardSolutionLimit || options.solutionLimit || 1000000;\n this.allowedPredicates = normalizePredicateSet(options.allowedPredicates || options.backwardPredicates || null);\n this.allowedRuleIndexes = normalizeRuleIndexSet(options.allowedRuleIndexes || null);\n this.ruleHeads = indexRuleHeads(program.rules || [], { allowedPredicates: this.allowedPredicates, allowedRuleIndexes: this.allowedRuleIndexes });\n this.memo = new Map();\n this.active = new Set();\n this.freshCounter = 0;\n this.solutionCount = 0;\n this.stats = {\n mode: 'backward',\n goals: 0,\n facts: 0,\n rules: 0,\n memoHits: 0,\n memoStores: 0,\n maxDepth: 0,\n };\n }\n\n *solveBody(clauses, binding = {}, depth = 0, index = 0) {\n if (depth > this.maxDepth) throw new Error(`Reached backwardMaxDepth=${this.maxDepth}; backward query may not terminate`);\n this.stats.maxDepth = Math.max(this.stats.maxDepth, depth);\n if (this.solutionCount >= this.solutionLimit) return;\n if (index >= clauses.length) {\n this.solutionCount += 1;\n yield resolveBinding(binding);\n return;\n }\n\n const clause = clauses[index];\n if (clause.type === 'triple') {\n for (const matched of this.solveTriple(clause.triple, binding, depth + 1)) {\n yield* this.solveBody(clauses, matched, depth + 1, index + 1);\n }\n return;\n }\n\n if (clause.type === 'filter') {\n try {\n if (booleanValue(evalExpression(clause.expr, resolveBinding(binding), this.options))) {\n yield* this.solveBody(clauses, binding, depth + 1, index + 1);\n }\n } catch (_) {\n // SPARQL-style FILTER errors reject the current solution.\n }\n return;\n }\n\n if (clause.type === 'set' || clause.type === 'bind') {\n try {\n const resolved = resolveBinding(binding);\n const value = asTerm(evalExpression(clause.expr, resolved, this.options));\n const next = unifyTerms({ type: 'var', value: clause.variable }, value, binding);\n if (next) yield* this.solveBody(clauses, next, depth + 1, index + 1);\n } catch (_) {\n // Assignment errors drop the current solution.\n }\n return;\n }\n\n if (clause.type === 'not') {\n let found = false;\n for (const _ of this.solveBody(clause.body, { ...binding }, depth + 1, 0)) { found = true; break; }\n if (!found) yield* this.solveBody(clauses, binding, depth + 1, index + 1);\n return;\n }\n\n throw new Error(`Unsupported backward body clause ${clause.type}`);\n }\n\n *solveTriple(pattern, binding = {}, depth = 0) {\n if (depth > this.maxDepth) throw new Error(`Reached backwardMaxDepth=${this.maxDepth}; backward query may not terminate`);\n this.stats.goals += 1;\n const resolvedPattern = resolvePattern(pattern, binding);\n const key = `${goalKey(resolvedPattern)}@store:${this.store.version || 0}`;\n const entry = this.memo.get(key);\n if (entry && entry.complete) {\n this.stats.memoHits += 1;\n yield* this.replayAnswers(pattern, binding, entry.answers);\n return;\n }\n if (this.active.has(key)) return;\n\n const answers = [];\n const answerKeys = new Set();\n this.active.add(key);\n try {\n for (const fact of this.factCandidates(resolvedPattern, binding)) {\n const next = unifyTriples(pattern, fact, binding);\n if (!next) continue;\n rememberAnswer(answers, answerKeys, pattern, next);\n this.stats.facts += 1;\n }\n\n for (const item of this.ruleCandidates(resolvedPattern)) {\n const suffix = `__b${++this.freshCounter}_${item.ruleIndex}`;\n const freshHead = freshTriple(item.head, suffix);\n const next = unifyTriples(pattern, freshHead, binding);\n if (!next) continue;\n const freshBody = (item.rule.body || []).map((clause) => freshClause(clause, suffix));\n for (const solved of this.solveBody(freshBody, next, depth + 1, 0)) {\n rememberAnswer(answers, answerKeys, pattern, solved);\n this.stats.rules += 1;\n }\n }\n } finally {\n this.active.delete(key);\n }\n\n this.memo.set(key, { complete: true, answers });\n this.stats.memoStores += 1;\n yield* this.replayAnswers(pattern, binding, answers);\n }\n\n *replayAnswers(pattern, binding, answers) {\n for (const answer of answers) {\n const next = unifyTriples(pattern, answer, binding);\n if (next) yield next;\n }\n }\n\n factCandidates(pattern, binding) {\n return this.store.candidates(pattern, binding);\n }\n\n ruleCandidates(pattern) {\n const predicate = pattern.p && pattern.p.type === 'iri' ? pattern.p.value : null;\n if (predicate) return this.ruleHeads.byPredicate.get(predicate) || [];\n return this.ruleHeads.all;\n }\n}\n\nfunction indexRuleHeads(rules, options = {}) {\n const allowedPredicates = options.allowedPredicates || null;\n const allowedRuleIndexes = options.allowedRuleIndexes || null;\n const byPredicate = new Map();\n const all = [];\n for (let ruleIndex = 0; ruleIndex < rules.length; ruleIndex += 1) {\n if (allowedRuleIndexes && !allowedRuleIndexes.has(ruleIndex)) continue;\n const rule = rules[ruleIndex];\n for (let headIndex = 0; headIndex < (rule.head || []).length; headIndex += 1) {\n const head = rule.head[headIndex];\n if (!head || !head.p || head.p.type !== 'iri') continue;\n if (allowedPredicates && !allowedPredicates.has(head.p.value)) continue;\n const item = { ruleIndex, headIndex, rule, head };\n all.push(item);\n const bucket = byPredicate.get(head.p.value);\n if (bucket) bucket.push(item);\n else byPredicate.set(head.p.value, [item]);\n }\n }\n return { byPredicate, all };\n}\n\nfunction freshClause(clause, suffix) {\n if (clause.type === 'triple') return { ...clause, triple: freshTriple(clause.triple, suffix) };\n if (clause.type === 'filter') return { ...clause, expr: freshExpr(clause.expr, suffix) };\n if (clause.type === 'set' || clause.type === 'bind') return { ...clause, variable: freshVarName(clause.variable, suffix), expr: freshExpr(clause.expr, suffix) };\n if (clause.type === 'not') return { ...clause, body: clause.body.map((item) => freshClause(item, suffix)) };\n return clause;\n}\n\nfunction freshTriple(triple, suffix) {\n return { s: freshTerm(triple.s, suffix), p: freshTerm(triple.p, suffix), o: freshTerm(triple.o, suffix) };\n}\n\nfunction freshTerm(term, suffix) {\n if (!term) return term;\n if (term.type === 'var') return { type: 'var', value: freshVarName(term.value, suffix) };\n if (term.type === 'triple') return { type: 'triple', s: freshTerm(term.s, suffix), p: freshTerm(term.p, suffix), o: freshTerm(term.o, suffix) };\n return term;\n}\n\nfunction freshExpr(expr, suffix) {\n if (!expr) return expr;\n if (expr.type === 'var') return { ...expr, name: freshVarName(expr.name, suffix) };\n if (expr.type === 'term') return { ...expr, value: freshTerm(expr.value, suffix) };\n if (expr.type === 'unary') return { ...expr, expr: freshExpr(expr.expr, suffix) };\n if (expr.type === 'binary') return { ...expr, left: freshExpr(expr.left, suffix), right: freshExpr(expr.right, suffix) };\n if (expr.type === 'call') return { ...expr, args: expr.args.map((arg) => freshExpr(arg, suffix)) };\n if (expr.type === 'list') return { ...expr, items: expr.items.map((item) => freshExpr(item, suffix)) };\n return expr;\n}\n\nfunction freshVarName(name, suffix) {\n return `${name}${suffix}`;\n}\n\nfunction resolvePattern(pattern, binding) {\n return { s: resolveTerm(pattern.s, binding, false), p: resolveTerm(pattern.p, binding, false), o: resolveTerm(pattern.o, binding, false) };\n}\n\nfunction resolveBinding(binding) {\n const out = {};\n for (const name of Object.keys(binding)) out[name] = resolveTerm(binding[name], binding, false);\n return out;\n}\n\nfunction resolveTerm(term, binding, preserveUnbound = true, seen = new Set()) {\n if (!term) return term;\n if (term.type === 'var') {\n const name = term.value;\n if (seen.has(name)) return preserveUnbound ? term : { type: 'var', value: name };\n if (!Object.prototype.hasOwnProperty.call(binding, name)) return preserveUnbound ? term : { type: 'var', value: name };\n seen.add(name);\n return resolveTerm(binding[name], binding, preserveUnbound, seen);\n }\n if (term.type === 'triple') {\n return {\n type: 'triple',\n s: resolveTerm(term.s, binding, preserveUnbound, new Set(seen)),\n p: resolveTerm(term.p, binding, preserveUnbound, new Set(seen)),\n o: resolveTerm(term.o, binding, preserveUnbound, new Set(seen)),\n };\n }\n return term;\n}\n\nfunction unifyTriples(left, right, binding) {\n let next = unifyTerms(left.s, right.s, binding);\n if (!next) return null;\n next = unifyTerms(left.p, right.p, next);\n if (!next) return null;\n return unifyTerms(left.o, right.o, next);\n}\n\nfunction unifyTerms(left, right, binding) {\n const a = resolveTerm(left, binding);\n const b = resolveTerm(right, binding);\n if (a.type === 'var' && b.type === 'var' && a.value === b.value) return binding;\n if (a.type === 'var') return bindVariable(a.value, b, binding);\n if (b.type === 'var') return bindVariable(b.value, a, binding);\n if (a.type === 'triple' || b.type === 'triple') {\n if (a.type !== 'triple' || b.type !== 'triple') return null;\n let next = unifyTerms(a.s, b.s, binding);\n if (!next) return null;\n next = unifyTerms(a.p, b.p, next);\n if (!next) return null;\n return unifyTerms(a.o, b.o, next);\n }\n return termEquals(a, b) ? binding : null;\n}\n\nfunction bindVariable(name, term, binding) {\n const existing = binding[name];\n if (existing) return unifyTerms(existing, term, binding);\n if (term.type === 'var' && term.value === name) return binding;\n return { ...binding, [name]: term };\n}\n\nfunction rememberAnswer(answers, answerKeys, pattern, binding) {\n const triple = {\n s: resolveTerm(pattern.s, binding),\n p: resolveTerm(pattern.p, binding),\n o: resolveTerm(pattern.o, binding),\n };\n if (triple.s.type === 'var' || triple.p.type === 'var' || triple.o.type === 'var') return;\n const key = tripleKey(triple);\n if (answerKeys.has(key)) return;\n answerKeys.add(key);\n answers.push(triple);\n}\n\nfunction goalKey(pattern) {\n return `${safeTermKey(pattern.s)} ${safeTermKey(pattern.p)} ${safeTermKey(pattern.o)}`;\n}\n\nfunction safeTermKey(term) {\n return term && term.type === 'var' ? '_' : termKey(term);\n}\n\nfunction uniqueBindings(bindings) {\n const seen = new Set();\n const out = [];\n for (const binding of bindings) {\n const resolved = resolveBinding(binding);\n const key = bindingKey(resolved);\n if (seen.has(key)) continue;\n seen.add(key);\n out.push(resolved);\n }\n return out;\n}\n\nfunction containsBlank(term) {\n if (!term) return false;\n if (term.type === 'blank') return true;\n if (term.type === 'triple') return containsBlank(term.s) || containsBlank(term.p) || containsBlank(term.o);\n return false;\n}\n\n\n\nfunction reachableBackwardRuleIndexes(program, rootClauses, options = {}) {\n const rules = program.rules || [];\n const headIndex = new Map();\n const allHeadPredicates = new Set();\n const allRuleIndexes = new Set();\n for (let ruleIndex = 0; ruleIndex < rules.length; ruleIndex += 1) {\n const rule = rules[ruleIndex];\n for (const head of rule.head || []) {\n if (!head || !head.p || head.p.type !== 'iri') continue;\n allRuleIndexes.add(ruleIndex);\n allHeadPredicates.add(head.p.value);\n if (!headIndex.has(head.p.value)) headIndex.set(head.p.value, new Set());\n headIndex.get(head.p.value).add(ruleIndex);\n }\n }\n\n const predicates = new Set();\n const ruleIndexes = new Set();\n const work = [];\n const enqueue = (predicate) => {\n if (!predicate) {\n for (const item of allHeadPredicates) enqueue(item);\n return;\n }\n if (predicates.has(predicate)) return;\n predicates.add(predicate);\n work.push(predicate);\n };\n\n for (const predicate of bodyPredicateDemands(rootClauses || [])) enqueue(predicate);\n\n while (work.length > 0) {\n const predicate = work.shift();\n const indexes = headIndex.get(predicate);\n if (!indexes) continue;\n for (const ruleIndex of indexes) {\n if (ruleIndexes.has(ruleIndex)) continue;\n ruleIndexes.add(ruleIndex);\n const rule = rules[ruleIndex];\n for (const needed of bodyPredicateDemands(rule.body || [])) enqueue(needed);\n }\n }\n\n return { ruleIndexes, predicates };\n}\n\nfunction bodyPredicateDemands(clauses) {\n const out = [];\n for (const clause of clauses || []) {\n if (clause.type === 'triple') {\n out.push(predicateDemand(clause.triple.p));\n continue;\n }\n if (clause.type === 'not') {\n out.push(...bodyPredicateDemands(clause.body || []));\n }\n }\n return out;\n}\n\nfunction predicateDemand(term) {\n return term && term.type === 'iri' ? term.value : null;\n}\n\nfunction normalizePredicateSet(value) {\n if (!value) return null;\n if (value instanceof Set) return value;\n if (Array.isArray(value)) return new Set(value);\n return new Set(String(value).split(',').map((item) => item.trim()).filter(Boolean));\n}\n\nfunction normalizeRuleIndexSet(value) {\n if (!value) return null;\n if (value instanceof Set) return value;\n if (Array.isArray(value)) return new Set(value);\n return null;\n}\n\nfunction supportedBackwardPredicates(program, options = {}) {\n const explicit = normalizePredicateSet(options.backwardPredicates || options.hybridPredicates || null);\n const byPredicate = new Map();\n for (const rule of program.rules || []) {\n const predicates = new Set();\n for (const head of rule.head || []) {\n if (head && head.p && head.p.type === 'iri') predicates.add(head.p.value);\n }\n for (const predicate of predicates) {\n if (!byPredicate.has(predicate)) byPredicate.set(predicate, []);\n byPredicate.get(predicate).push(rule);\n }\n }\n\n const supported = new Set();\n for (const [predicate, rules] of byPredicate) {\n if (explicit && !explicit.has(predicate)) continue;\n if (rules.length > 0 && rules.every((rule) => ruleSupported(rule, options))) supported.add(predicate);\n }\n return supported;\n}\n\nfunction preferredBackwardPredicates(program, options = {}) {\n const explicit = normalizePredicateSet(options.hybridPredicates || null);\n if (explicit) return supportedBackwardPredicates(program, { ...options, hybridPredicates: explicit });\n const supported = supportedBackwardPredicates(program, options);\n const preferred = new Set();\n const force = options.hybrid === true || options.hybridMode === 'force';\n const demanded = force ? null : demandedBodyPredicates(program);\n for (const rule of program.rules || []) {\n if (!ruleIsFunctionLike(rule)) continue;\n if (!force && ruleCreatesHeadTerms(rule)) continue;\n for (const head of rule.head || []) {\n if (!head || !head.p || head.p.type !== 'iri' || !supported.has(head.p.value)) continue;\n if (force || demanded.has(head.p.value)) preferred.add(head.p.value);\n }\n }\n return preferred;\n}\n\nfunction demandedBodyPredicates(program) {\n const out = new Set();\n for (const rule of program.rules || []) {\n for (const predicate of bodyPredicateDemands(rule.body || [])) if (predicate) out.add(predicate);\n }\n return out;\n}\n\n\nfunction ruleCreatesHeadTerms(rule) {\n const headVars = new Set();\n for (const triple of rule.head || []) {\n for (const term of [triple.s, triple.p, triple.o]) {\n if (!term) continue;\n if (term.type === 'blank') return true;\n if (term.type === 'var') headVars.add(term.value);\n }\n }\n if (headVars.size === 0) return false;\n for (const clause of rule.body || []) {\n if ((clause.type === 'set' || clause.type === 'bind') && headVars.has(clause.variable) && expressionCreatesTerm(clause.expr)) return true;\n }\n return false;\n}\n\nfunction expressionCreatesTerm(expr) {\n if (!expr) return false;\n if (expr.type === 'call') {\n const name = String(expr.name || '').toUpperCase();\n if (name === 'BNODE' || name === 'IRI' || name === 'URI' || name === 'TRIPLE' || name === 'UUID' || name === 'STRUUID') return true;\n return (expr.args || []).some(expressionCreatesTerm);\n }\n if (expr.type === 'binary') return expressionCreatesTerm(expr.left) || expressionCreatesTerm(expr.right);\n if (expr.type === 'unary') return expressionCreatesTerm(expr.expr);\n if (expr.type === 'in') return expressionCreatesTerm(expr.left) || (expr.values || []).some(expressionCreatesTerm);\n return false;\n}\n\nfunction ruleIsFunctionLike(rule) {\n return (rule.body || []).some((clause) => clause.type === 'set' || clause.type === 'bind');\n}\n\nfunction ruleHeadPredicates(rule) {\n const predicates = new Set();\n for (const head of rule.head || []) {\n if (!head || !head.p || head.p.type !== 'iri') return null;\n predicates.add(head.p.value);\n }\n return predicates;\n}\n\nfunction ruleIsBackwardOriented(rule, predicates) {\n if (!predicates || predicates.size === 0) return false;\n const heads = ruleHeadPredicates(rule);\n if (!heads || heads.size === 0) return false;\n for (const predicate of heads) if (!predicates.has(predicate)) return false;\n return true;\n}\n\nmodule.exports = {\n BackwardProver,\n backwardQuery,\n planBackwardQuery,\n supportedBackwardPredicates,\n preferredBackwardPredicates,\n reachableBackwardRuleIndexes,\n ruleIsBackwardOriented,\n ruleSupported,\n resolveBinding,\n};\n","src/format.js":"'use strict';\n\nconst { formatTriple, formatTerm } = require('./term.js');\n\nfunction sortTriples(triples, prefixes = {}) {\n return triples\n .map((triple) => ({ triple, text: formatTriple(triple, prefixes) }))\n .sort((a, b) => a.text.localeCompare(b.text))\n .map((entry) => entry.triple);\n}\n\nfunction formatTriples(triples, prefixes = {}) {\n return triples\n .map((triple) => formatTriple(triple, prefixes))\n .sort((a, b) => a.localeCompare(b))\n .join('\\n');\n}\n\nfunction formatTrace(trace, prefixes = {}) {\n return trace.map((entry) => `#${entry.iteration} ${entry.rule} => ${formatTriple(entry.triple, prefixes)}`).join('\\n');\n}\n\nfunction formatProof(trace, prefixes = {}) {\n if (!trace.length) return '';\n const lines = ['@prefix pe: <https://eyereasoner.github.io/pe#> .', ''];\n for (const entry of trace) {\n const conclusion = formatTriple(entry.triple, prefixes);\n lines.push(`{ ${conclusion} } pe:why {`);\n lines.push(` { ${conclusion} }`);\n lines.push(` pe:by [ pe:rule ${quoteString(entry.rule)} ]${proofDetails(entry, prefixes)} .`);\n lines.push('}.', '');\n }\n return lines.join('\\n').trimEnd();\n}\n\nfunction proofDetails(entry, prefixes) {\n const details = [];\n const bindings = Object.entries(entry.binding || {}).sort(([a], [b]) => a.localeCompare(b));\n if (bindings.length > 0) {\n details.push(`\\n pe:binding ${bindings.map(([name, value]) => `[ pe:var ${quoteString(name)}; pe:value ${formatTerm(value, prefixes)} ]`).join(', ')}`);\n }\n if (entry.uses && entry.uses.length > 0) {\n details.push(`\\n pe:uses ${entry.uses.map((triple) => `{ ${formatTriple(triple, prefixes)} }`).join(', ')}`);\n }\n return details.length ? `;${details.join(';')}` : '';\n}\n\nfunction quoteString(value) {\n return `\"${String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"').replace(/\\n/g, '\\\\n')}\"`;\n}\n\nfunction formatBindings(bindings, prefixes = {}, select = null) {\n const columns = select && select.length > 0 ? select : inferColumns(bindings);\n return bindings\n .slice()\n .sort((a, b) => formatBinding(a, prefixes, columns).localeCompare(formatBinding(b, prefixes, columns)))\n .map((binding) => formatBinding(binding, prefixes, columns))\n .join('\\n');\n}\n\nfunction formatBinding(binding, prefixes = {}, columns = null) {\n const names = columns || Object.keys(binding).sort();\n if (names.length === 0) return 'true';\n return names.map((name) => `?${name} = ${binding[name] ? formatTerm(binding[name], prefixes) : 'UNDEF'}`).join('; ');\n}\n\nfunction inferColumns(bindings) {\n const columns = new Set();\n for (const binding of bindings) for (const name of Object.keys(binding)) columns.add(name);\n return Array.from(columns).sort();\n}\n\nfunction toJSON(result, options = {}) {\n const triples = options.all ? result.closure : result.inferred;\n const json = {\n baseIRI: result.baseIRI || null,\n iterations: result.iterations,\n ruleApplications: result.ruleApplications,\n perRule: result.perRule,\n prefixes: result.prefixes,\n diagnostics: result.diagnostics || [],\n triples: sortTriples(triples, result.prefixes).map(jsonSafeTriple),\n proof: options.proof ? result.trace : undefined,\n };\n if (result.query) json.query = jsonSafeValue(result.query);\n if (result.analysis && options.analysis) json.analysis = result.analysis;\n return json;\n}\n\n\nfunction jsonSafeTriple(triple) {\n return { s: jsonSafeTerm(triple.s), p: jsonSafeTerm(triple.p), o: jsonSafeTerm(triple.o) };\n}\n\nfunction jsonSafeTerm(term) {\n if (!term || typeof term !== 'object') return jsonSafeValue(term);\n if (term.type === 'triple') return { type: 'triple', s: jsonSafeTerm(term.s), p: jsonSafeTerm(term.p), o: jsonSafeTerm(term.o) };\n if (term.type === 'literal' && typeof term.value === 'bigint') return { ...term, value: term.value.toString() };\n return { ...term };\n}\n\nfunction jsonSafeValue(value) {\n if (typeof value === 'bigint') return value.toString();\n if (Array.isArray(value)) return value.map(jsonSafeValue);\n if (value && typeof value === 'object') {\n if (value.type) return jsonSafeTerm(value);\n return Object.fromEntries(Object.entries(value).map(([key, val]) => [key, jsonSafeValue(val)]));\n }\n return value;\n}\n\nmodule.exports = { sortTriples, formatTriples, formatTrace, formatProof, formatBindings, formatBinding, toJSON };\n","src/query.js":"'use strict';\n\nconst { parseQuery } = require('./parser.js');\nconst { TripleStore, bindingKey } = require('./store.js');\nconst { evaluateBody } = require('./engine.js');\nconst { backwardQuery, planBackwardQuery, preferredBackwardPredicates } = require('./backward.js');\n\nfunction queryResult(result, querySpec, options = {}) {\n const store = new TripleStore(result.closure || []);\n const bindings = evaluateBody(querySpec.body, store, {}, options);\n const select = normalizeSelect(querySpec.select, bindings);\n return {\n baseIRI: result.baseIRI,\n prefixes: result.prefixes,\n select,\n bindings: projectBindings(bindings, select),\n mode: result.hybridStats ? 'hybrid' : 'forward',\n };\n}\n\nfunction queryProgram(program, querySpec, options = {}) {\n const mode = options.queryMode || 'auto';\n if (mode !== 'forward' && mode !== 'hybrid') {\n const planned = planBackwardQuery(program, querySpec, options);\n if (planned.ok) {\n const result = backwardQuery(program, querySpec, options);\n if (result.ok) {\n const select = normalizeSelect(querySpec.select, result.bindings);\n return {\n baseIRI: program.baseIRI,\n prefixes: program.prefixes,\n select,\n bindings: projectBindings(result.bindings, select),\n mode: 'backward',\n stats: result.stats,\n };\n }\n if (mode === 'backward') throw new Error(result.reason || 'Backward query failed');\n } else if (mode === 'backward') {\n throw new Error(`Backward query is not supported for this ruleset: ${planned.reason}`);\n }\n }\n return null;\n}\n\nfunction runQuery(source, querySource = null, options = {}) {\n const { run, compile } = require('./api.js');\n const { program, diagnostics, analysis } = compile(source, options);\n\n let querySpec;\n if (querySource) querySpec = parseQuery(querySource, { ...options, prefixes: program.prefixes, baseIRI: program.baseIRI });\n else throw new Error('No query supplied. Use --query or --query-file with a raw body pattern.');\n\n const direct = queryProgram(program, querySpec, options);\n if (direct) {\n return {\n baseIRI: program.baseIRI,\n version: program.version || null,\n imports: program.imports || [],\n prefixes: program.prefixes,\n input: program.data.slice(),\n inferred: [],\n closure: program.data.slice(),\n iterations: 0,\n layers: [],\n ruleApplications: 0,\n perRule: [],\n trace: [],\n diagnostics,\n analysis,\n query: direct,\n };\n }\n\n const runOptions = queryRunOptions(program, querySpec, options);\n const result = run(program, runOptions);\n result.diagnostics = diagnostics;\n result.query = queryResult(result, querySpec, runOptions);\n return result;\n}\n\nfunction queryRunOptions(program, querySpec, options = {}) {\n const mode = options.queryMode || 'auto';\n if (mode === 'forward') return { ...options, hybrid: false };\n if (shouldUseHybridForQuery(program, querySpec, options)) return { ...options, hybrid: options.hybrid ?? 'auto' };\n return options;\n}\n\nfunction shouldUseHybridForQuery(program, querySpec, options = {}) {\n const mode = options.queryMode || 'auto';\n if (options.hybrid === false) return false;\n if (options.hybrid === true) return true;\n if (mode !== 'auto') return false;\n if (!querySpec) return false;\n return preferredBackwardPredicates(program, options).size > 0;\n}\n\nfunction normalizeSelect(select, bindings) {\n if (select && select.length > 0) return select.slice();\n const vars = new Set();\n for (const binding of bindings) for (const key of Object.keys(binding)) vars.add(key);\n return Array.from(vars).sort().filter((name) => !name.includes('__b'));\n}\n\nfunction projectBindings(bindings, select) {\n const seen = new Set();\n const out = [];\n for (const binding of bindings) {\n const projected = {};\n for (const name of select) if (binding[name]) projected[name] = binding[name];\n const key = bindingKey(projected);\n if (!seen.has(key)) {\n seen.add(key);\n out.push(projected);\n }\n }\n return out;\n}\n\nmodule.exports = { runQuery, queryResult, queryProgram, queryRunOptions, shouldUseHybridForQuery, parseQuery, normalizeSelect, projectBindings };\n","src/output.js":"'use strict';\n\nfunction resultTriples(result, program = {}, options = {}) {\n return options.all ? result.closure : result.inferred;\n}\n\nmodule.exports = { resultTriples };\n"};
490
+ window.__EYELENG_MAPPINGS__ = {"src/tokenizer.js":{},"src/assignments.js":{},"src/term.js":{},"src/rdfSyntax.js":{"./tokenizer.js":"src/tokenizer.js","./assignments.js":"src/assignments.js","./term.js":"src/term.js"},"src/builtins.js":{"./term.js":"src/term.js"},"src/parser.js":{"./tokenizer.js":"src/tokenizer.js","./rdfSyntax.js":"src/rdfSyntax.js","./builtins.js":"src/builtins.js","./assignments.js":"src/assignments.js","./term.js":"src/term.js"},"src/rdfMessages.js":{"./rdfSyntax.js":"src/rdfSyntax.js","./term.js":"src/term.js"},"src/store.js":{"./term.js":"src/term.js"},"src/analyze.js":{"./term.js":"src/term.js","./assignments.js":"src/assignments.js"},"src/backward.js":{"./store.js":"src/store.js","./term.js":"src/term.js","./builtins.js":"src/builtins.js"},"src/engine.js":{"./store.js":"src/store.js","./term.js":"src/term.js","./builtins.js":"src/builtins.js","./analyze.js":"src/analyze.js","./backward.js":"src/backward.js"},"src/format.js":{"./term.js":"src/term.js"},"src/query.js":{"./parser.js":"src/parser.js","./store.js":"src/store.js","./engine.js":"src/engine.js","./backward.js":"src/backward.js","./api.js":"src/api.js"},"src/output.js":{},"src/api.js":{"./parser.js":"src/parser.js","./rdfSyntax.js":"src/rdfSyntax.js","./rdfMessages.js":"src/rdfMessages.js","./engine.js":"src/engine.js","./analyze.js":"src/analyze.js","./format.js":"src/format.js","./query.js":"src/query.js","./output.js":"src/output.js"}};
491
+ window.__EYELENG_EXAMPLES__ = {"family.srl":"PREFIX : <http://example/>\n\nDATA {\n :A :fatherOf :X .\n :B :motherOf :X .\n :C :motherOf :A .\n}\n\nRULE { ?x :childOf ?y } WHERE { ?y :fatherOf ?x }\nRULE { ?x :childOf ?y } WHERE { ?y :motherOf ?x }\nRULE { ?x :descendedFrom ?y } WHERE { ?x :childOf ?y }\nRULE { ?x :descendedFrom ?y } WHERE { ?x :childOf ?z . ?z :descendedFrom ?y }\n","socrates.srl":"PREFIX : <http://example.org/socrates#>\nPREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>\n\nDATA {\n :Socrates a :Human .\n :Human rdfs:subClassOf :Mortal .\n}\n\nRULE { ?individual a ?superclass } WHERE {\n ?individual a ?class .\n ?class rdfs:subClassOf ?superclass .\n}\n","spec-2-2-recursion.srl":"PREFIX : <http://example.com/>\n\nDATA {\n :A :fatherOf :X .\n :B :motherOf :X .\n :C :motherOf :A .\n}\n\nRULE { ?x :childOf ?y } WHERE { ?y :fatherOf ?x }\nRULE { ?x :childOf ?y } WHERE { ?y :motherOf ?x }\nRULE { ?x :descendedFrom ?y } WHERE { ?x :childOf ?y }\nRULE { ?x :descendedFrom ?y } WHERE { ?x :childOf ?z . ?z :descendedFrom ?y }\n","bmi.srl":"# Adapted from eyeling/examples/bmi.n3\n# Normalizes metric input, computes BMI, assigns an adult BMI category, and\n# derives a healthy-weight band for the given height.\n\nPREFIX : <http://example/eyeling/bmi/>\n\nDATA {\n :Input :unitSystem \"metric\" ; :weight 72.0 ; :height 178.0 .\n}\n\nRULE { :Case :weightKg ?weight ; :heightM ?heightM }\nWHERE {\n :Input :unitSystem \"metric\" ; :weight ?weight ; :height ?heightCm .\n SET(?heightM := ?heightCm / 100.0)\n}\n\nRULE {\n :Case :heightSquared ?heightSquared ;\n :bmi ?bmi ;\n :bmiRounded ?bmiRounded ;\n :healthyMinKg ?healthyMin ;\n :healthyMaxKg ?healthyMax ;\n :healthyMinKgRounded ?healthyMinRounded ;\n :healthyMaxKgRounded ?healthyMaxRounded .\n}\nWHERE {\n :Case :weightKg ?weight ; :heightM ?heightM .\n SET(?heightSquared := ?heightM * ?heightM)\n SET(?bmi := ?weight / ?heightSquared)\n SET(?bmiRounded := ROUND(?bmi * 100.0) / 100.0)\n SET(?healthyMin := 18.5 * ?heightSquared)\n SET(?healthyMax := 24.9 * ?heightSquared)\n SET(?healthyMinRounded := ROUND(?healthyMin * 10.0) / 10.0)\n SET(?healthyMaxRounded := ROUND(?healthyMax * 10.0) / 10.0)\n}\n\nRULE { :Decision :category \"Underweight\" }\nWHERE { :Case :bmi ?bmi . FILTER(?bmi < 18.5) }\n\nRULE { :Decision :category \"Normal\" }\nWHERE { :Case :bmi ?bmi . FILTER(?bmi >= 18.5 && ?bmi < 25.0) }\n\nRULE { :Decision :category \"Overweight\" }\nWHERE { :Case :bmi ?bmi . FILTER(?bmi >= 25.0 && ?bmi < 30.0) }\n\nRULE { :Decision :category \"Obesity I\" }\nWHERE { :Case :bmi ?bmi . FILTER(?bmi >= 30.0 && ?bmi < 35.0) }\n\nRULE { :Decision :category \"Obesity II\" }\nWHERE { :Case :bmi ?bmi . FILTER(?bmi >= 35.0 && ?bmi < 40.0) }\n\nRULE { :Decision :category \"Obesity III\" }\nWHERE { :Case :bmi ?bmi . FILTER(?bmi >= 40.0) }\n\nRULE {\n :Answer :bmi ?bmiRounded ;\n :category ?category ;\n :healthyMinKg ?healthyMinRounded ;\n :healthyMaxKg ?healthyMaxRounded .\n}\nWHERE {\n :Case :bmiRounded ?bmiRounded ;\n :healthyMinKgRounded ?healthyMinRounded ;\n :healthyMaxKgRounded ?healthyMaxRounded .\n :Decision :category ?category .\n}\n","stratified-negation.srl":"PREFIX : <http://example/>\n\nDATA {\n :alice a :Person ; :blocked true .\n :bob a :Person .\n :carol a :Person ; :flagged true .\n}\n\n# This rule is intentionally written before the producer of :blocked.\n# Stratified evaluation must still run the :blocked-producing rule first.\nRULE { ?x :eligible true }\nWHERE { ?x a :Person . NOT { ?x :blocked true } }\n\nRULE { ?x :blocked true }\nWHERE { ?x :flagged true }\n","spec-builtins.srl":"PREFIX : <http://example/>\nPREFIX xsd: <http://www.w3.org/2001/XMLSchema#>\n\nDATA {\n :event :when \"2026-05-15T10:20:30Z\"^^xsd:dateTime .\n}\n\nRULE { :event :year ?year ; :month ?month ; :blank ?blank ; :tripleSubject ?subject }\nWHERE {\n :event :when ?when .\n SET(?year := YEAR(?when))\n SET(?month := MONTH(?when))\n SET(?blank := BNODE(\"event\"))\n SET(?triple := TRIPLE(:subject, :predicate, :object))\n SET(?subject := SUBJECT(?triple))\n}\n","spec-4-2-rdf-rules-syntax.ttl":"# Captured from the SHACL 1.2 Rules draft section 4.2.\n# This is RDF Rules syntax in Turtle and is executable by Eyeleng.\n\nPREFIX : <http://example/>\nPREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>\nPREFIX sh: <http://www.w3.org/ns/shacl#>\nPREFIX srl: <http://www.w3.org/ns/shacl-rules#>\nPREFIX sparql: <http://www.w3.org/ns/sparql#>\n\n:ruleSet-1\n rdf:type srl:RuleSet;\n srl:data (\n [ srl:subject :x ; srl:predicate :p ; srl:object 1 ]\n [ srl:subject :x ; srl:predicate :q ; srl:object 2 ]\n );\n srl:rules (\n [\n rdf:type srl:Rule;\n srl:head (\n [ srl:subject [ srl:varName \"x\" ] ; srl:predicate :bothPositive ; srl:object true ]\n ) ;\n srl:body (\n [ srl:subject [ srl:varName \"x\" ]; srl:predicate :p ; srl:object [ srl:varName \"v1\" ] ]\n [ srl:expr [ sparql:greaterThan ( [ srl:varName \"v1\" ] 0 ) ] ]\n [ srl:subject [ srl:varName \"x\" ] ; srl:predicate :q ; srl:object [ srl:varName \"v2\" ] ]\n [ srl:expr [ sparql:greaterThan ( [ srl:varName \"v2\" ] 0 ) ] ]\n );\n ]\n [\n rdf:type srl:Rule;\n srl:head (\n [ srl:subject [ srl:varName \"x\" ] ; srl:predicate :oneIsZero ; srl:object true ]\n ) ;\n srl:body (\n [ srl:subject [ srl:varName \"x\" ] ; srl:predicate :p ; srl:object [ srl:varName \"v1\" ] ]\n [ srl:subject [ srl:varName \"x\" ] ; srl:predicate :q ; srl:object [ srl:varName \"v2\" ] ]\n [ srl:filter [ sparql:function-or (\n [ sparql:equals ( [ srl:varName \"v1\" ] 0 ) ]\n [ sparql:equals ( [ srl:varName \"v2\" ] 0 ) ]\n ) ]\n ]\n );\n ]\n ) .\n","basic-ruleset.ttl":"PREFIX : <http://example/>\nPREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>\nPREFIX srl: <http://www.w3.org/ns/shacl-rules#>\n\n:familyRules a srl:RuleSet ;\n srl:data (\n [ srl:subject :alice ; srl:predicate :parentOf ; srl:object :bob ]\n [ srl:subject :bob ; srl:predicate :parentOf ; srl:object :carol ]\n ) ;\n srl:rules (\n [ a srl:Rule ;\n srl:body (\n [ srl:subject [ srl:varName \"x\" ] ; srl:predicate :parentOf ; srl:object [ srl:varName \"y\" ] ]\n ) ;\n srl:head (\n [ srl:subject [ srl:varName \"y\" ] ; srl:predicate :childOf ; srl:object [ srl:varName \"x\" ] ]\n )\n ]\n [ a srl:Rule ;\n srl:body (\n [ srl:subject [ srl:varName \"x\" ] ; srl:predicate :parentOf ; srl:object [ srl:varName \"y\" ] ]\n ) ;\n srl:head (\n [ srl:subject [ srl:varName \"x\" ] ; srl:predicate :ancestorOf ; srl:object [ srl:varName \"y\" ] ]\n )\n ]\n [ a srl:Rule ;\n srl:body (\n [ srl:subject [ srl:varName \"x\" ] ; srl:predicate :parentOf ; srl:object [ srl:varName \"y\" ] ]\n [ srl:subject [ srl:varName \"y\" ] ; srl:predicate :ancestorOf ; srl:object [ srl:varName \"z\" ] ]\n ) ;\n srl:head (\n [ srl:subject [ srl:varName \"x\" ] ; srl:predicate :ancestorOf ; srl:object [ srl:varName \"z\" ] ]\n )\n ]\n ) .\n"};
491
492
  </script>
492
493
  <script>
493
494
  (() => {
@@ -734,13 +735,55 @@
734
735
  }
735
736
 
736
737
  function populateExamples() {
738
+ setExampleOptions(Object.keys(examples));
739
+ }
740
+
741
+ function setExampleOptions(names) {
742
+ const selected = els.example.value;
737
743
  els.example.innerHTML = '';
738
- for (const name of Object.keys(examples)) {
744
+ for (const name of names.slice().sort((a, b) => a.localeCompare(b))) {
739
745
  const option = document.createElement('option');
740
746
  option.value = name;
741
747
  option.textContent = name;
742
748
  els.example.appendChild(option);
743
749
  }
750
+ if (names.includes(selected)) els.example.value = selected;
751
+ }
752
+
753
+ async function discoverExamples() {
754
+ const apiUrl = 'https://api.github.com/repos/eyereasoner/eyeleng/contents/examples?ref=main';
755
+ try {
756
+ const response = await fetch(apiUrl, { headers: { Accept: 'application/vnd.github+json' } });
757
+ if (!response.ok) throw new Error('HTTP ' + response.status);
758
+ const entries = await response.json();
759
+ const names = entries
760
+ .filter((entry) => entry && entry.type === 'file' && /\.(?:srl|ttl)$/i.test(entry.name || ''))
761
+ .map((entry) => entry.name);
762
+ if (names.length > 0) setExampleOptions(names);
763
+ } catch (_) {
764
+ // Keep the bundled examples available when GitHub cannot be reached.
765
+ }
766
+ }
767
+
768
+ async function loadExample(name) {
769
+ if (!name) return;
770
+ setStatus('Loading example…');
771
+ try {
772
+ if (!examples[name]) {
773
+ const url = 'https://raw.githubusercontent.com/eyereasoner/eyeleng/main/examples/' + encodeURIComponent(name);
774
+ const response = await fetch(url, { headers: { Accept: 'text/plain, text/turtle, */*' } });
775
+ if (!response.ok) throw new Error('HTTP ' + response.status + ' loading ' + name);
776
+ examples[name] = await response.text();
777
+ }
778
+ els.editor.value = examples[name];
779
+ updateEditorHighlight();
780
+ els.syntax.value = name.endsWith('.ttl') ? 'rdf' : 'auto';
781
+ saveState();
782
+ setStatus('Example loaded.', 'ok');
783
+ } catch (error) {
784
+ setStatus('Example load failed.', 'error');
785
+ setOutput(error.message || String(error));
786
+ }
744
787
  }
745
788
 
746
789
  function defaultState() {
@@ -946,7 +989,7 @@
946
989
  if (els.json.checked) {
947
990
  text = JSON.stringify(eyeleng.toJSON(result, {
948
991
  all: els.all.checked,
949
- trace: els.trace.checked,
992
+ proof: els.trace.checked,
950
993
  analysis: els.analysis.checked,
951
994
  }), null, 2);
952
995
  } else if (result.query) {
@@ -955,8 +998,8 @@
955
998
  const triples = els.all.checked ? result.closure : result.inferred;
956
999
  text = eyeleng.formatTriples(triples, result.prefixes);
957
1000
  if (els.trace.checked && result.trace.length > 0) {
958
- const trace = eyeleng.formatTrace(result.trace, result.prefixes);
959
- text = (text ? text + '\n\n' : '') + '# Trace\n' + trace;
1001
+ const proof = eyeleng.formatProof(result.trace, result.prefixes);
1002
+ text = (text ? text + '\n\n' : '') + proof;
960
1003
  }
961
1004
  }
962
1005
  if (!text) text = '(no output)';
@@ -1024,27 +1067,16 @@
1024
1067
  els.loadUri.addEventListener('click', loadFromUrl);
1025
1068
  els.run.addEventListener('click', runProgram);
1026
1069
  els.share.addEventListener('click', copyShareLink);
1027
- els.reset.addEventListener('click', () => {
1070
+ els.reset.addEventListener('click', async () => {
1028
1071
  const name = els.example.value || 'family.srl';
1029
- els.editor.value = examples[name] || examples['family.srl'];
1030
- updateEditorHighlight();
1031
- if (name.endsWith('.ttl')) els.syntax.value = 'rdf';
1032
- else els.syntax.value = 'auto';
1033
- saveState();
1034
- setStatus('Example reset.', 'ok');
1072
+ await loadExample(name);
1073
+ if (examples[name]) setStatus('Example reset.', 'ok');
1035
1074
  });
1036
1075
  els.clearStorage.addEventListener('click', () => {
1037
1076
  localStorage.removeItem(storageKey);
1038
1077
  setStatus('Autosave cleared.', 'ok');
1039
1078
  });
1040
- els.example.addEventListener('change', () => {
1041
- const name = els.example.value;
1042
- els.editor.value = examples[name] || '';
1043
- updateEditorHighlight();
1044
- els.syntax.value = name.endsWith('.ttl') ? 'rdf' : 'auto';
1045
- saveState();
1046
- setStatus('Example loaded.', 'ok');
1047
- });
1079
+ els.example.addEventListener('change', () => loadExample(els.example.value));
1048
1080
  els.clearBackground.addEventListener('click', () => {
1049
1081
  backgroundSource = '';
1050
1082
  updateBackgroundStatus();
@@ -1088,6 +1120,7 @@
1088
1120
 
1089
1121
  function init() {
1090
1122
  populateExamples();
1123
+ discoverExamples();
1091
1124
  const hashState = loadHashState();
1092
1125
  const storedState = loadStoredState();
1093
1126
  applyState(hashState || storedState || defaultState());