eyeleng 1.0.6 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +275 -18
- package/dist/browser/eyeleng.browser.js +123 -13
- package/eyeleng.js +123 -13
- package/package.json +7 -4
- package/playground.html +16 -4
- package/reports/w3c-rdf-earl.ttl +11707 -0
- package/src/parser.js +76 -2
- package/src/rdfManifest.js +13 -0
- package/src/shacl12RulesManifest.js +386 -0
- package/src/tokenizer.js +47 -11
- package/test/api.test.js +32 -0
- package/test/browser-bundle.test.js +8 -0
- package/test/shacl12-rules.test.js +29 -215
- package/test/w3c-rdf.test.js +2 -2
- package/tools/bundle.js +0 -17
- package/tools/w3c-rdf.js +20 -1
- package/tools/w3c-shacl12-rules.js +55 -0
- package/HANDBOOK.md +0 -1070
package/playground.html
CHANGED
|
@@ -383,7 +383,7 @@
|
|
|
383
383
|
<h1>Eyeleng Playground</h1>
|
|
384
384
|
<p class="lede">Edit an SRL or RDF Rules program below, load it from a URL, autosave locally, or copy a share link with the program encoded in the fragment.</p>
|
|
385
385
|
</div>
|
|
386
|
-
<div class="badge">Powered by <a href="https://github.com/eyereasoner/eyeleng" target="_blank" rel="noreferrer">Eyeleng</a> — running version <span id="version-label"
|
|
386
|
+
<div class="badge">Powered by <a href="https://github.com/eyereasoner/eyeleng" target="_blank" rel="noreferrer">Eyeleng</a> — running version <span id="version-label">v…</span></div>
|
|
387
387
|
</header>
|
|
388
388
|
|
|
389
389
|
<section class="workspace" aria-label="Eyeleng playground workspace">
|
|
@@ -488,7 +488,6 @@
|
|
|
488
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
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
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"};
|
|
491
|
-
window.__EYELENG_VERSION__ = "1.0.6-eyereasoner.2";
|
|
492
491
|
</script>
|
|
493
492
|
<script>
|
|
494
493
|
(() => {
|
|
@@ -511,7 +510,6 @@
|
|
|
511
510
|
|
|
512
511
|
const eyeleng = requireModule('src/api.js');
|
|
513
512
|
const examples = window.__EYELENG_EXAMPLES__;
|
|
514
|
-
const version = window.__EYELENG_VERSION__;
|
|
515
513
|
const storageKey = 'eyeleng-playground-state-v1';
|
|
516
514
|
let backgroundSource = '';
|
|
517
515
|
let lastResult = null;
|
|
@@ -549,7 +547,21 @@
|
|
|
549
547
|
summaryTab: $('summary-tab'),
|
|
550
548
|
};
|
|
551
549
|
|
|
552
|
-
|
|
550
|
+
async function loadPackageVersion() {
|
|
551
|
+
els.versionLabel.textContent = 'v…';
|
|
552
|
+
try {
|
|
553
|
+
const response = await fetch(new URL('package.json', window.location.href), { cache: 'no-store' });
|
|
554
|
+
if (!response.ok) throw new Error('HTTP ' + response.status);
|
|
555
|
+
const metadata = await response.json();
|
|
556
|
+
if (!metadata || !metadata.version) throw new Error('package.json has no version');
|
|
557
|
+
els.versionLabel.textContent = 'v' + metadata.version;
|
|
558
|
+
} catch (err) {
|
|
559
|
+
els.versionLabel.textContent = 'version unavailable';
|
|
560
|
+
els.versionLabel.title = 'Could not load package.json: ' + (err && err.message ? err.message : String(err));
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
loadPackageVersion();
|
|
553
565
|
|
|
554
566
|
function setStatus(text, kind = '') {
|
|
555
567
|
els.runStatus.textContent = text;
|