A Notation3 (N3) reasoner in JavaScript.
eyeling is:
- a single self-contained file (
eyeling.js, no external deps) - a practical N3/Turtle superset (enough for lots of real rulesets)
- supports forward (
=>) + backward (<=) chaining over Horn-style rules - prints only newly derived forward facts, optionally preceded by compact proof comments
- “pass-only-new” style output (we never want to leak raw input data; backward rules can act like “functions” over raw data)
- works fully client-side (browser) and in Node.js
Try it here:
The playground runs eyeling client-side. You can:
- edit an N3 program directly
- load an N3 program from a URL (in the "Load N3 from URL" box or as ?url=...)
- share a link with the program encoded in the URL fragment (
#...)
- Node.js >= 18 (anything modern with
BigIntsupport is fine)
npm i eyelingRun on a file:
npx eyeling examples/socrates.n3(Or install globally: npm i -g eyeling and run eyeling ....)
const { reason } = require("eyeling");
const input = `
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
@prefix : <http://example.org/socrates#>.
:Socrates a :Human.
:Human rdfs:subClassOf :Mortal.
{ ?S a ?A. ?A rdfs:subClassOf ?B } => { ?S a ?B }.
`;
const output = reason({ proofComments: false }, input);
console.log(output);ESM:
import eyeling from "eyeling";
const output = eyeling.reason({ proofComments: false }, input);
console.log(output);Note: the API currently shells out to the bundled eyeling.js CLI under the hood (simple + robust).
From a repo checkout:
npm testOr run individual suites:
npm run test:api
npm run test:examples
npm run test:package
npm run test:packlisttest:apiruns an independent JS API test suite (does not rely onexamples/).test:examplesruns the examples in theexamplesdirectory and compares against the golden outputs inexamples/output.test:packagedoes a “real consumer” smoke test:npm pack→ install tarball into a temp project → run API + CLI + examples.test:packlistsanity-checks what will be published in the npm tarball (and the CLI shebang/bin wiring).
Usage: eyeling [options] <file.n3>
Options:
-h, --help Show this help and exit.
-v, --version Print version and exit.
-p, --proof-comments Enable proof explanations.
-n, --no-proof-comments Disable proof explanations (default).
-s, --super-restricted Disable all builtins except => and <=.
-a, --ast Print parsed AST as JSON and exit.
--strings Print log:outputString strings (ordered by key) instead of N3 output.
By default, eyeling:
- parses the input (facts + rules)
- runs forward chaining to a fixpoint
- prints only newly derived forward facts (not the original input facts)
- prints a compact per-triple explanation as
#comments (can be disabled)
For each newly derived triple, eyeling prints:
- a proof-style comment block explaining why the triple holds (unless
-n), and then - the triple itself in N3/Turtle syntax.
The proof comments are compact “local justifications” per derived triple (not a single exported global proof tree).
- Forward chaining to fixpoint for forward rules written as
{ P } => { C } . - Backward chaining (SLD-style) for backward rules written as
{ H } <= { B } .and for built-ins.
Forward rule premises are proved using:
- ground facts (input + derived)
- backward rules
- built-ins
The CLI prints only newly derived forward facts.
eyeling includes a few key performance mechanisms:
- facts are indexed for matching:
- by predicate, and (when possible) by (predicate, object) (important for type-heavy workloads)
- IRIs/literals are interned to reduce allocations and speed up comparisons/lookups
- parsed numeric literals are cached, and rule standardization reuses unchanged subterms to cut repeated parsing/allocation
- duplicate detection uses a fast key path when a triple is fully IRI/Literal-shaped
- backward rules are indexed by head predicate
- the backward prover is iterative (explicit stack), so deep chains won’t blow the JS call stack
- for very deep backward chains, substitutions may be compactified (semantics-preserving) to avoid quadratic “copy a growing substitution object” behavior
eyeling follows the usual N3 intuition:
- blank nodes in facts are normal RDF blanks (
_:b1,_:b2, … within a run) - blank nodes in rule premises behave like rule-scoped universals (similar to variables)
- blank nodes only in rule conclusions behave like existentials: each rule firing generates fresh Skolem blanks (
_:sk_0,_:sk_1, …)
Equal facts up to renaming of Skolem IDs are treated as duplicates and are not re-added.
eyeling understands the log:implies / log:impliedBy idiom.
Top level:
{ P } log:implies { C } .becomes a forward rule{ P } => { C } .{ H } log:impliedBy { B } .becomes a backward rule{ H } <= { B } .
During reasoning:
- any derived
log:implies/log:impliedBytriple with formula subject/object is turned into a new live forward/backward rule.
Rules whose conclusion is false are treated as hard failures:
:stone :color :black .
:stone :color :white .
{ ?X :color :black . ?X :color :white . } => false.
As soon as the premise is provable, eyeling exits with status code 2.
eyeling’s parser targets (nearly) the full Notation3 Language grammar from the W3C N3 Community Group spec.
In practice this means: it’s a Turtle superset that also accepts quoted formulas, rules, paths, and the N3 “syntax shorthand”
operators (=, =>, <=) described in the spec.
Commonly used N3/Turtle features:
- Prefix/base directives (
@prefix/@base, and SPARQL-stylePREFIX/BASE) - Triples with
;and, - Variables (
?x) - Blank nodes (
[], and[ :p :o; :q :r ]) - Collections
( ... ) - Quoted formulas
{ ... } - Implications (
=>,<=) - Datatyped literals (
^^) and language tags ("..."@en) - Inverse predicate sugar (
<-and keyword forms likeis ... of) - Resource paths (
!and^) #line comments
eyeling implements a pragmatic subset of common N3 builtin families and evaluates them during backward goal proving:
- crypto:
crypto:md5crypto:shacrypto:sha256crypto:sha512 - list:
list:appendlist:firstlist:firstRestlist:inlist:iteratelist:lastlist:lengthlist:maplist:memberlist:memberAtlist:notMemberlist:removelist:restlist:reverselist:sort - log:
log:collectAllInlog:contentlog:equalTolog:forAllInlog:impliedBylog:implieslog:notEqualTolog:notIncludeslog:semanticslog:skolemlog:uri - math:
math:absoluteValuemath:acosmath:asinmath:atanmath:cosmath:coshmath:degreesmath:differencemath:equalTomath:exponentiationmath:greaterThanmath:integerQuotientmath:lessThanmath:negationmath:notEqualTomath:notGreaterThanmath:notLessThanmath:productmath:quotientmath:remaindermath:roundedmath:sinmath:sinhmath:summath:tanmath:tanh - string:
string:concatenationstring:containsstring:containsIgnoringCasestring:endsWithstring:equalIgnoringCasestring:formatstring:greaterThanstring:jsonPointerstring:lessThanstring:matchesstring:notEqualIgnoringCasestring:notGreaterThanstring:notLessThanstring:notMatchesstring:replacestring:scrapestring:startsWith - time:
time:localTime
MIT (see LICENSE).