@dialog-db/query
Version:
Datalog query engine inspired by Datomic
1,952 lines (1,785 loc) • 47.6 kB
JavaScript
import * as API from './api.js'
import * as Syntax from './syntax.js'
import * as Task from './task.js'
import { $, _ } from './$.js'
import * as Variable from './variable.js'
import { Callable } from './syntax/callable.js'
import * as Selector from './selector.js'
import * as Link from './data/link.js'
import { toDebugString } from './debug.js'
import * as JSON from './json.js'
import { base58btc } from 'multiformats/bases/base58'
/**
* @param {unknown} descriptor
* @returns {API.Type|undefined}
*/
const fromConstructor = (descriptor) => {
switch (descriptor) {
case globalThis.Boolean:
return { Boolean: {} }
case globalThis.String:
return { String: {} }
case globalThis.Symbol:
return { Name: {} }
case globalThis.Number:
return { Integer: {} }
case globalThis.BigInt:
return { Integer: {} }
case globalThis.Uint8Array:
return { Bytes: {} }
case globalThis.Object:
return { Entity: {} }
case null:
return { Null: {} }
}
}
/**
* @param {Record<string, unknown>|null|undefined} descriptor
* @returns {API.Type|undefined}
*/
const fromDescriptor = (descriptor) => {
if (descriptor?.Null) {
return { Null: {} }
} else if (descriptor?.Boolean) {
return { Boolean: {} }
} else if (descriptor?.String) {
return { String: {} }
} else if (descriptor?.Integer) {
return { Integer: {} }
} else if (descriptor?.Float) {
return { Float: {} }
} else if (descriptor?.Bytes) {
return { Bytes: {} }
} else if (descriptor?.Entity) {
return { Entity: {} }
} else if (descriptor?.Name) {
return { Name: {} }
} else if (descriptor?.Position) {
return { Position: {} }
} else if (descriptor?.Reference) {
return { Reference: {} }
} else if (descriptor?.Unknown) {
return { Unknown: {} }
}
}
/**
*
* @param {unknown} source
* @returns {API.Scalar|undefined}
*/
const fromScalar = (source) => {
switch (typeof source) {
case 'string':
case 'number':
case 'bigint':
case 'boolean':
return source
case 'object':
return source == null || source instanceof Uint8Array ? source : undefined
default:
return undefined
}
}
/**
*
* @param {object} schema
*/
const deriveThe = (schema) =>
base58btc.baseEncode(Link.of(schema)['/'].subarray(-32))
/**
* @template {string} The
* @template {API.RuleDescriptor} Schema
* @param {Schema & {the?: The, this?: ObjectConstructor | { Entity: {} }, _?: never}} source
* @returns {API.Claim<API.FactView<The, Omit<Schema, 'the'> & { this: ObjectConstructor }>, The, Omit<Schema, 'the'> & { this: ObjectConstructor }, {}>}
*/
export const fact = ({ the, ...source }) => {
const members = []
for (const [name, member] of Object.entries({ this: Object, ...source })) {
const descriptor =
fromConstructor(member) ??
fromDescriptor(/** @type {{}|null|undefined} */ (member)) ??
fromScalar(member)
if (name === '_') {
throw new TypeError(`Schema may no have reserved "_" property`)
}
if (descriptor === undefined) {
throw new TypeError(
`Unsupported schema member ${toDebugString(/** @type {{}} */ (member))}`
)
} else {
members.push([name, descriptor])
}
}
if (members.length === 1 && typeof the !== 'string') {
throw new TypeError(
`Schema must contain at least one property. To tag entities you could use "the" discriminant`
)
}
const schema = Object.fromEntries(members)
the = typeof the === 'string' ? the : /** @type {The} */ (deriveThe(schema))
if (!schema.this.Entity) {
throw new TypeError(
`Schema may not have "this" property that is not an entity`
)
}
/** @type {API.Premise<The, Omit<Schema, 'the'> & { this: ObjectConstructor }>} */
const premise = {
the,
schema,
attributes: deriveAttributes(schema),
}
const conclusion = Fact.for(premise)
const claim = new Claim(premise, conclusion, {})
return claim
}
export const claim = fact
/**
* @template Fact
* @template {string} The
* @template {API.FactSchema} Schema
* @template {API.RuleDescriptor} Context
* @implements {API.Claim<Fact, The, Schema, Context>}
* @extends {Callable<(terms?: API.InferFactTerms<Schema>) => FactMatch<Fact, The, Schema>>}
*/
class Claim extends Callable {
/**
* @param {API.Premise<The, Schema>} premise
* @param {API.Conclusion<Fact, The, Schema>} conclusion
* @param {Context} context
*/
constructor(premise, conclusion, context) {
super((terms) => this.match(terms))
this.premise = premise
this.conclusion = conclusion
this.context = context
this.#cells =
/** @type {API.InferSchemaAttributes<Schema & Context> & {_: API.Variable, this: API.Variable<API.Entity>}} */
(deriveCells({ ...context, ...premise.schema }))
}
get the() {
return this.premise.the
}
/**
* Map of variables corresponding to the fact members.
*
* @type {API.InferSchemaAttributes<Schema>}
*/
get attributes() {
return this.premise.attributes
}
get schema() {
return this.premise.schema
}
/** @type {API.InferSchemaAttributes<Schema & Context> & {_: API.Variable, this: API.Variable<API.Entity>}} */
#cells
get cells() {
return this.#cells
}
/** @type {API.DeductiveRuleSyntax<API.InferSchemaAttributes<Schema>>|undefined} */
#build
/**
* Builds a deduction form for the this fact.
*/
build() {
if (!this.#build) {
const { the, schema, attributes } = this.premise
const where = []
for (const name of Object.keys(schema)) {
if (name !== 'this') {
where.push({
match: {
the: `${the}/${name}`,
of: attributes.this,
is: attributes[name],
},
fact: {},
})
}
}
// If we have no other fields we should still be able to use fact as a
// tag which is why we add a predicate for that case.
if (where.length === 0) {
where.push({
match: {
the: `the/${the}`,
of: attributes.this,
is: the,
},
})
}
this.#build = Syntax.rule({ match: attributes, when: { where } })
}
return this.#build
}
/**
* @param {API.InferSchemaTerms<Schema>} terms
* @returns {API.RuleApplicationSyntax<API.InferSchemaAttributes<Schema>>}
*/
apply(terms) {
return this.build().apply(terms)
}
/**
* @param {API.InferSchemaTerms<Schema>} terms
* @returns {API.MatchView}
*/
recur(terms) {
return [this.apply(terms)]
}
/**
* Creates predicate for this fact that matches given terms.
*
* @param {Partial<API.InferFactTerms<Schema>>} [terms]
* @returns {FactMatch<Fact, The, Schema>}
*/
match(terms = {}) {
return new FactMatch(
this.premise,
this.conclusion,
this,
completeTerms(this.premise.schema, terms)
)
}
/**
* Creates a predicate for this fact that excludes ones that match given
* terms.
*
* @param {Partial<API.InferSchemaTerms<Schema>>} terms
* @returns {Negation<Fact, The, Schema>}
*/
not(terms) {
return new Negation(this.premise, this.conclusion, this, terms)
}
/**
* Asserts this fact with a given data. If data does not conforms this fact
* throws an error.
*
* @param {API.InferAssert<Schema>} fact
* @returns {Fact}
*/
assert(fact) {
return this.conclusion.assert(fact)
}
/**
* Defines local variables so they could be used in the `.when` and `.where`
* methods without makeing those part of the fact.
*
* @template {Exclude<API.RuleDescriptor, Schema & Context>} Extension
* @param {Extension} extension
* @returns {API.Claim<Fact, The, Schema, Context & Extension>}
*/
with(extension) {
return new Claim(this.premise, this.conclusion, {
...extension,
...this.context,
})
}
/**
* Defines a rule that deduces this fact whenever any of the branches are true.
* Takes a `build` function that will be given set of variables corresponding
* to the fact members which must return object where keys represent disjuncts
* and values are arrays representing conjuncts for those disjuncts. In other
* works each member of the returned object represent OR branches where each
* branch is an AND joined predicates by passed variables.
*
* @param {API.SomeBuilder<Schema & Context>} compile
* @returns {API.Deduction<Fact, The, Schema, Context>}
*/
when(compile) {
return new Deduction(this.premise, this.conclusion, this.context, compile)
}
/**
* Defines a rule that dudces this fact whenever all of the predicates are
* true. This is a shortuct of `.when` which is convinient in cases where
* only one branch is needed.
*
* @param {API.EveryBuilder<Schema & Context>} compile
* @returns {API.Deduction<Fact, The, Schema, Context>}
*/
where(compile) {
return new Deduction(this.premise, this.conclusion, this.context, compile)
}
/**
* This method can be used to project different layout of the selected facts,
* it is maraked deprecated, but it is not deprecated but rather an
* experimental and may be removed at any point. This method can be used as an
* aggregator to group results, however this leads to nested structures which
* are at odds with the facts that are flat records.
*
* @deprecated
*
* @template {API.Selector} Terms
* @param {API.ProjectionBuilder<Schema & Context, Terms>} compile
* @returns {Select<The, Schema, Context, Terms>}
*/
select(compile = (variables) => /** @type {Terms} */ (variables)) {
return new Select(this.premise, this.build(), this.cells, compile)
}
/**
* @template View
* @param {(fact: Fact) => View} mapper
* @returns {API.Claim<View, The, Schema, Context>}
*/
map(mapper) {
return new Claim(
this.premise,
{
assert: (fact) => {
return mapper(this.conclusion.assert(fact))
},
},
this.context
)
}
/**
* @template View, State
* @param {API.Aggregator<View, Fact, State>} aggregator
*/
aggregate(aggregator) {
return new Aggregation(this, aggregator)
}
}
/**
* @template Fact
* @template {string} The
* @template {API.FactSchema} Schema
* @template {API.RuleDescriptor} Context
* @implements {API.Deduction<Fact, The, Schema, Context>}
* @extends {Claim<Fact, The, Schema, Context>}
*/
class Deduction extends Claim {
/**
* @param {API.Premise<The, Schema>} premise
* @param {API.Conclusion<Fact, The, Schema>} conclusion
* @param {Context} context
* @param {API.WhenBuilder<Schema & Context>} compile
*/
constructor(premise, conclusion, context, compile) {
super(premise, conclusion, context)
this.compile = compile
}
/**
* If rule is applied recursively we want to return `Recur` as opposed to
* plain `FactMatch` to accomplish this we set this property to `this`
* during `source` compilation. This works because when/where `build` function
* is called during compilation which in turn ends up calling `match` method
* that looks at `this.#circuit` to decide which one to construct. Furthermore
* we do pass this `#circuit` to it during construction.
*
* @type {Circuit<Schema, Fact>|null}
*/
self = null
/** @type {API.DeductiveRuleSyntax<API.InferSchemaAttributes<Schema>>|undefined} */
#build
build() {
if (!this.#build) {
this.self = this
const when = /** @type {Record<string, API.Every>} */ ({})
for (const [name, disjunct] of iterateDisjuncts(
this.compile(this.cells)
)) {
when[name] = /** @type {[API.Conjunct, ...API.Conjunct[]]} */ ([
...iterateConjuncts(disjunct),
])
}
this.#build = Syntax.rule({
match: this.premise.attributes,
when: /** @type {API.Some} */ (when),
})
this.self = null
}
return this.#build
}
/**
* @param {API.InferSchemaTerms<Schema>} terms
* @returns {API.RuleApplicationSyntax<API.InferSchemaAttributes<Schema>>}
*/
apply(terms) {
return this.build().apply(terms)
}
/**
* @param {API.InferSchemaTerms<Schema>} terms
* @returns {API.MatchView}
*/
recur(terms) {
return [
/** @type {API.Recur} */ ({
recur: terms,
}),
]
}
/**
* @param {API.InferClaimTerms<Schema>} terms
*/
induce(terms) {
const { premise, cells } = this
const { schema, the } = premise
const predicates = []
for (const [name, member] of Object.entries(schema)) {
const [term, cell] = [terms[name], cells[name]]
// If `this` was not provided we derive one from the data itself.
if (name === 'this' && !term) {
predicates.push(
Data.Fact({
.../** @type {Record<string, API.Term>} */ (terms),
the,
this: cells.this,
})
)
} else {
predicates.push(
...same({ this: /** @type {API.Scalar} */ (term), as: cell })
)
}
}
return new Constraint(predicates)
}
/**
* @param {Partial<API.InferFactTerms<Schema>>} [terms]
* @returns {FactMatch<Fact, The, Schema>}
*/
match(terms) {
// 🤔 If it is recursion we can't just generate new variables
// we need to reuse ones from the current context.
const { premise, conclusion, self } = this
const match =
self ?
new Recur(premise, conclusion, self ?? this, {
...this.attributes,
...terms,
})
: new FactMatch(
premise,
conclusion,
self ?? this,
completeTerms(premise.schema, terms)
)
return match
}
/**
* @param {API.InferClaimTerms<Schema>} fact
*/
claim(fact) {
return this.induce(fact)
}
/** @type {Induction<Fact, The, Schema, Context>|undefined} */
#induction
get inductive() {
if (!this.#induction) {
const { self } = this
const induction = new Induction(
this.premise,
this.conclusion,
this.context,
this.compile
)
this.self = induction
// Force induction compilaction so that it will be the self in the given
// context.
induction.build()
// Then we reset the self so that it continues to behave as intended.
this.self = self
// cache the instance
this.#induction = induction
}
return this.#induction
}
/**
* @template View
* @param {(fact: Fact) => View} mapper
* @returns {API.Deduction<View, The, Schema, Context>}
*/
map(mapper) {
return new Deduction(
this.premise,
{
assert: (fact) => {
return mapper(this.conclusion.assert(fact))
},
},
this.context,
this.compile
)
}
}
/**
* @template Fact
* @template {string} The
* @template {API.RuleDescriptor & {this: ObjectConstructor}} Schema
* @template {API.RuleDescriptor} Locals
* @extends {Deduction<Fact, The, Schema, Locals>}
*/
class Induction extends Deduction {
/**
* @param {API.InferSchemaTerms<Schema>} terms
* @returns {API.MatchView}
*/
recur(terms) {
return this.induce(terms)
}
}
/**
* @template View
* @template Fact
* @template State
* @template {string} The
* @template {API.FactSchema} Schema
* @template {API.RuleDescriptor} Context
* @implements {API.Aggregation<View, Fact, The, Schema>}
* @extends {Callable<(terms?: API.InferFactTerms<Schema>) => API.Aggregate<View>>}
*/
class Aggregation extends Callable {
/**
* @param {Claim<Fact, The, Schema, Context>} rule
* @param {API.Aggregator<View, Fact, State>} aggregator
*/
constructor(rule, aggregator) {
super((terms) => this.match(terms))
this.rule = rule
this.aggregator = aggregator
}
get the() {
return this.rule.the
}
get attributes() {
return this.rule.attributes
}
get schema() {
return this.rule.schema
}
get cells() {
return this.rule.cells
}
/**
* @param {API.InferSchemaTerms<Schema>} terms
*/
apply(terms) {
return this.rule.apply(terms)
}
/**
* @param {API.InferSchemaTerms<Schema>} terms
*/
recur(terms) {
return this.rule.recur(terms)
}
/**
* Creates a predicate for this fact that excludes ones that match given
* terms.
*
* @param {Partial<API.InferSchemaTerms<Schema>>} terms
* @returns {Negation<Fact, The, Schema>}
*/
not(terms) {
return this.rule.not(terms)
}
/**
* Asserts this fact with a given data. If data does not conforms this fact
* throws an error.
*
* @param {API.InferAssert<Schema>} fact
* @returns {Fact}
*/
assert(fact) {
return this.rule.assert(fact)
}
/**
* Creates predicate for this fact that matches given terms.
*
* @param {Partial<API.InferFactTerms<Schema>>} [terms]
* @returns {Aggregate<View, Fact, State, The, Schema>}
*/
match(terms = {}) {
return new Aggregate(
this.rule.premise,
this.rule.conclusion,
this,
this.aggregator,
completeTerms(this.schema, terms)
)
}
}
/**
*
* @template {API.FactSchema} Schema
* @template Fact
* @typedef {object} Circuit
* @property {API.InferSchemaAttributes<Schema>} cells
* @property {(terms: API.InferSchemaTerms<Schema>) => API.RuleApplicationSyntax<API.InferSchemaAttributes<Schema>>} apply
* @property {(terms: API.InferSchemaTerms<Schema>) => API.MatchView} recur
* @property {(claim: API.InferAssert<Schema>) => Fact} assert
*/
/**
* @template Fact
* @template {string} The
* @template {API.RuleDescriptor & {this: ObjectConstructor}} Schema
* @implements {API.MatchView<unknown>}
*/
class Negation {
/**
* @param {API.Premise<The, Schema>} premise
* @param {API.Conclusion<Fact, The, Schema>} conclusion
* @param {Circuit<Schema, Fact>} rule
* @param {Partial<API.InferSchemaTerms<Schema>>} terms
*/
constructor(premise, conclusion, rule, terms) {
this.premise = premise
this.conclusion = conclusion
this.rule = rule
this.terms = terms
}
/** @type {API.NegationSyntax|undefined} */
#build
build() {
if (!this.#build) {
this.#build = this.rule
.apply(
// This not true, but apply does not actually need all terms we only
// type it this way to make lower level API less error prone. Perhaps
// we need to revise it to having to lie to it.
/** @type {API.InferSchemaTerms<Schema>} */ (this.terms)
)
.negate()
}
return this.#build
}
toJSON() {
return this.build().toJSON()
}
*[Symbol.iterator]() {
yield this.build()
}
}
/**
* @template {API.RuleDescriptor} Schema
*/
class Match {
/**
* @param {API.DeductiveRuleSyntax<API.InferSchemaAttributes<Schema>>} rule
* @param {API.InferSchemaTerms<Schema>} terms
*/
constructor(rule, terms) {
this.rule = rule
this.terms = terms
}
/** @type {API.RuleApplicationSyntax<API.InferSchemaAttributes<Schema>>|undefined} */
#form
get form() {
if (!this.#form) {
this.#form = this.rule.apply(this.terms)
}
return this.#form
}
*[Symbol.iterator]() {
yield this.form
}
/** @type {API.RuleApplicationPlan<API.InferSchemaAttributes<Schema>>|undefined} */
#plan
get plan() {
if (!this.#plan) {
this.#plan = this.form.prepare()
}
return this.#plan
}
toJSON() {
return this.form.toJSON()
}
/**
* @param {{ from: API.Querier }} source
*/
*execute(source) {
const selection = yield* this.plan.query(source)
return Selector.select(this.terms, selection)
}
/**
* @param {{ from: API.Querier }} source
*/
query(source) {
// 😵💫 Here we force plan compilation because we want to get planning error
// before we get a
this.plan
return Task.perform(this.execute(source))
}
}
/**
* @template Fact
* @template {string} The
* @template {API.RuleDescriptor & {this: ObjectConstructor}} Schema
* @implements {API.MatchView<unknown>}
*/
class FactMatch {
/**
* @param {API.Premise<The, Schema>} premise
* @param {API.Conclusion<Fact, The, Schema>} conclusion
* @param {Circuit<Schema, Fact>} rule
* @param {API.InferSchemaTerms<Schema>} terms
*/
constructor(premise, conclusion, rule, terms) {
this.premise = premise
this.conclusion = conclusion
this.rule = rule
this.terms = terms
}
/**
* @returns {Negation<Fact, The, Schema>}
*/
negate() {
return new Negation(this.premise, this.conclusion, this.rule, this.terms)
}
/** @type {API.RuleApplicationSyntax<API.InferSchemaAttributes<Schema>>|undefined} */
#build
build() {
if (!this.#build) {
this.#build = this.rule.apply(this.terms)
}
return this.#build
}
/** @type {API.RuleApplicationPlan<API.InferSchemaAttributes<Schema>>|undefined} */
#plan
plan() {
if (!this.#plan) {
this.#plan = this.rule.apply(this.terms).prepare()
}
return this.#plan
}
/** @returns {Iterator<API.Conjunct|API.Recur>} */
*[Symbol.iterator]() {
yield this.build()
}
toJSON() {
return this.build().toJSON()
}
/**
* @param {API.Task<API.MatchFrame[], Error>} query
*/
*execute(query) {
const { terms } = this
const selection = yield* query
const facts = []
for (const match of selection) {
/** @type {Record<string, API.Scalar>} */
const model = {}
for (const [key, term] of Object.entries(terms)) {
model[key] = /** @type {API.Scalar} */ (
Variable.is(term) ? match.get(term) : term
)
}
model.the = this.premise.the
const fact = this.conclusion.assert(
/** @type {API.InferFact<Schema> & { this: API.Entity, the: The }} */ (
model
)
)
facts.push(fact)
}
return facts
}
/**
* @param {{ from: API.Querier }} source
*/
query(source) {
return Task.perform(this.execute(this.plan().query(source)))
}
}
/**
* @template View
* @template Fact
* @template State
* @template {string} The
* @template {API.RuleDescriptor & {this: ObjectConstructor}} Schema
*/
class Aggregate {
/**
* @param {API.Premise<The, Schema>} premise
* @param {API.Conclusion<Fact, The, Schema>} conclusion
* @param {Circuit<Schema, Fact>} rule
* @param {API.Aggregator<View, Fact, State>} aggregator
* @param {API.InferSchemaTerms<Schema>} terms
*/
constructor(premise, conclusion, rule, aggregator, terms) {
this.premise = premise
this.conclusion = conclusion
this.rule = rule
this.aggregator = aggregator
this.terms = terms
}
/**
* @returns {Negation<Fact, The, Schema>}
*/
negate() {
return new Negation(this.premise, this.conclusion, this.rule, this.terms)
}
/** @type {API.RuleApplicationSyntax<API.InferSchemaAttributes<Schema>>|undefined} */
#build
build() {
if (!this.#build) {
this.#build = this.rule.apply(this.terms)
}
return this.#build
}
/** @type {API.RuleApplicationPlan<API.InferSchemaAttributes<Schema>>|undefined} */
#plan
plan() {
if (!this.#plan) {
this.#plan = this.rule.apply(this.terms).prepare()
}
return this.#plan
}
/** @returns {Iterator<API.Conjunct|API.Recur>} */
*[Symbol.iterator]() {
yield this.build()
}
toJSON() {
return this.build().toJSON()
}
/**
* @param {API.Task<API.MatchFrame[], Error>} query
* @returns {API.Task<View, Error>}
*/
*execute(query) {
const { terms, aggregator } = this
const selection = yield* query
let aggregate = aggregator.open()
for (const match of selection) {
/** @type {Record<string, API.Scalar>} */
const model = {}
for (const [key, term] of Object.entries(terms)) {
model[key] = /** @type {API.Scalar} */ (
Variable.is(term) ? match.get(term) : term
)
}
model.the = this.premise.the
const fact = this.conclusion.assert(
/** @type {API.InferFact<Schema> & { this: API.Entity, the: The }} */ (
model
)
)
aggregate = aggregator.merge(aggregate, fact)
}
return aggregator.close(aggregate)
}
/**
* @param {{ from: API.Querier }} source
*/
query(source) {
return Task.perform(this.execute(this.plan().query(source)))
}
}
/**
* Subclass of {@link FactMatch} that represents a recursive rule application.
* @template {string} The
* @template {API.RuleDescriptor & {this: ObjectConstructor}} Schema
* @template Fact
* @extends {FactMatch<Fact, The, Schema>}
*/
class Recur extends FactMatch {
/**
* We override the itertor to yield recursion form as opposed to application
* from.
*/
*[Symbol.iterator]() {
yield* this.rule.recur(this.terms)
}
}
/**
* @template {string} The
* @template {API.FactSchema} Schema
*/
class Fact {
/**
* @template {string} The
* @template {API.FactSchema} Schema
* @param {API.Premise<The, Schema>} premise
* @returns {API.Conclusion<API.FactView<The, Schema>, The, Schema>}
*/
static for({ the, schema }) {
const This = this
/**
* @extends {This<The, Schema>}
*/
class Fact extends this {
static the = /** @type {The} */ (the)
static schema = schema
}
return Fact
}
/**
* @template {string} The
* @template {API.FactSchema} Schema
* @this {{the: The, schema: Schema} & typeof Fact} this
* @param {Record<string, API.Scalar>} claim
* @returns {API.FactView<The, Schema>}
*/
static assert({ the, this: entity, ...attributes }) {
if (the != null && the !== this.the) {
throw new TypeError(
`Optional attribute "the", if set must match the schema vaule "${this.the}"`
)
}
// Validate that all attributes have being provided
// TODO: Do actual schema validation to ensure tha attributes do
// conform to the schema
for (const name of Object.keys(this.schema)) {
const value = attributes[name]
if (value === undefined && name !== 'this') {
throw new TypeError(`Required attribute "${name}" is missing`)
}
}
const fact = new this(
this.the,
/** @type {API.Entity} */ (
entity ?? Link.of({ ...attributes, the: this.the })
),
/** @type {Omit<API.InferAssert<Schema>, 'this'|'the'>} */ (attributes)
)
return /** @type {API.FactView<The, Schema>} */ (fact)
}
/**
* @param {The} the
* @param {API.Entity} self
* @param {Omit<API.InferAssert<Schema>, 'this'|'the'>} attributes
*/
constructor(the, self, attributes) {
this.#the = the
this.#attributes = attributes
this.this = self
Object.assign(this, attributes)
}
#attributes
get attributes() {
return this.#attributes
}
#the
get the() {
return this.#the
}
*[Symbol.iterator]() {
const { the, this: of } = this
yield {
assert: { the: `the/${the}`, of, is: the },
}
for (const [name, is] of Object.entries(this.#attributes)) {
yield {
assert: {
the: `${the}/${name}`,
of,
is,
},
}
}
}
/**
* @returns {IterableIterator<{retract: API.Fact}>}
*/
*retract() {
const { the, this: of } = this
yield {
retract: { the: `the/${the}`, of: of, is: the },
}
for (const [name, value] of Object.entries(this.#attributes)) {
if (name !== 'this' && name !== 'the') {
yield {
retract: { the: `${the}/${name}`, of, is: value },
}
}
}
}
toJSON() {
return { ...this, the: this.the, this: JSON.from(this.this) }
}
}
/**
* @implements {API.MatchView<unknown>}
*/
class Constraint {
/**
* @param {API.Conjunct[]} predicates
*/
constructor(predicates) {
this.predicates = predicates
}
*[Symbol.iterator]() {
yield* this.predicates
}
}
/**
* @template {string} The
* @template {API.FactSchema} Schema
* @template {API.RuleDescriptor} Context
* @template {API.Selector} Selector
* @implements {API.Projection<Schema, Selector>}
* @extends {Callable<(terms?: API.InferFactTerms<Schema>) => GroupedSelection<The, Schema, Selector>>}
*/
class Select extends Callable {
/**
* @param {API.Premise<The, Schema>} premise
* @param {API.DeductiveRuleSyntax<API.InferSchemaAttributes<Schema>>} rule
* @param {API.InferSchemaAttributes<Schema & Context> & {_: API.Variable; this: API.Variable<API.Entity>}} cells
}}
* @param {API.ProjectionBuilder<Schema & Context, Selector>} compile
*/
constructor(premise, rule, cells, compile) {
super((terms) => this.match(terms))
this.premise = premise
this.cells = cells
this.rule = rule
this.compile = compile
}
/** @type {Selector|undefined} */
#build
build() {
if (!this.#build) {
this.#build = this.compile(this.cells)
}
return this.#build
}
/**
* @param {Partial<API.InferFactTerms<Schema>>} [terms]
* @returns {GroupedSelection<The, Schema, Selector>}
*/
match(terms) {
return new GroupedSelection(this.premise, this.build(), this.rule, {
...this.cells,
...terms,
})
}
}
/**
* @template {string} The
* @template {API.RuleDescriptor & {this: ObjectConstructor}} Schema
* @template {API.Selector} Selector
*/
class GroupedSelection {
/**
* @param {API.Premise<The, Schema>} premise
* @param {Selector} selector
* @param {API.DeductiveRuleSyntax<API.InferSchemaAttributes<Schema>>} rule
* @param {API.InferSchemaTerms<Schema>} terms
*/
constructor(premise, selector, rule, terms) {
this.premise = premise
this.selector = selector
this.rule = rule
this.terms = terms
}
/** @type {API.RuleApplicationSyntax<API.InferSchemaAttributes<Schema>>|undefined} */
#form
get form() {
if (!this.#form) {
this.#form = this.rule.apply(
/** @type {API.InferSchemaTerms<Schema>} */ (this.terms)
)
}
return this.#form
}
*[Symbol.iterator]() {
yield this.form
}
/** @type {API.RuleApplicationPlan<API.InferSchemaAttributes<Schema>>|undefined} */
#plan
plan() {
if (!this.#plan) {
this.#plan = this.rule.apply(this.terms).prepare()
}
return this.#plan
}
toJSON() {
return this.form.toJSON()
}
/**
* @param {API.Task<API.MatchFrame[], Error>} query
*/
*execute(query) {
const selection = yield* query
return Selector.select(this.selector, selection)
}
/**
* @param {{ from: API.Querier }} source
*/
query(source) {
return Task.perform(this.execute(this.plan().query(source)))
}
}
export { $, _ }
/**
* @template Terms
* @template {(terms: any) => API.Constraint} F
* @extends {Callable<F>}
*/
export class Operator extends Callable {
/**
* @template Terms
* @template {(terms: Terms) => API.Constraint} Formula
* @param {Formula} match
* @returns {Operator<Terms, Formula>}
*/
static for(match) {
return new this(match)
}
/**
* @param {F} match
*/
constructor(match) {
super(match)
this.match = match
}
/**
* @param {Terms} terms
* @returns {API.Negation}
*/
not(terms) {
return { not: this.match(terms) }
}
}
export const Collection = Operator.for(
/**
* @template {API.Scalar} Member
* @param {object} terms
* @param {API.Term<API.Entity>} terms.this
* @param {API.Term<Member>} terms.of
* @param {API.Term<string>} [terms.at]
*/
(terms) => ({
match: { the: terms.at, of: terms.this, is: terms.of },
fact: {},
})
)
/**
* @param {API.Term<string>} term
*/
export const text = (term) => new TextVariable(term)
class TextVariable {
#this
/**
* @param {API.Term<string>} term
*/
constructor(term) {
this.#this = term
}
/**
* @param {API.Term<string>} pattern
*/
like(pattern) {
return Text.match({ this: this.#this, pattern: pattern })
}
/**
* @param {API.Term<string>} slice
*/
includes(slice) {
return Text.includes({ this: this.#this, slice })
}
/**
* @param {object} terms
* @param {API.Term<string>} terms.with
* @param {API.Term<string>} terms.is
*/
concat(terms) {
return Text.Concat({ of: [this.#this, terms.with], is: terms.is })
}
words() {
const of = this.#this
return {
/**
* @param {API.Term<string>} is
*/
is(is) {
return Text.Words({ of, is })
},
}
}
lines() {
const of = this.#this
return {
/**
* @param {API.Term<string>} is
*/
is(is) {
return Text.Lines({ of, is })
},
}
}
toUpperCase() {
const of = this.#this
return {
/**
* @param {API.Term<string>} is
*/
is(is) {
return Text.UpperCase({ of, is })
},
/**
* @param {API.Term<string>} is
*/
not(is) {
return { not: Text.UpperCase({ of, is }) }
},
}
}
/**
*/
toLowerCase() {
const of = this.#this
return {
/**
* @param {API.Term<string>} is
*/
is(is) {
return Text.LowerCase({ of, is })
},
/**
* @param {API.Term<string>} is
*/
not(is) {
return { not: Text.LowerCase({ of, is }) }
},
}
}
/**
* @param {API.Term<string>} is
*/
trim(is) {
return Text.Trim({ of: this.#this, is })
}
}
export class Text {
#this
/**
* @param {API.Term<string>} source
*/
constructor(source) {
this.#this = source
}
static match = Operator.for(
/**
* @param {object} terms
* @param {API.Term<string>} terms.this
* @param {API.Term<string>} terms.pattern
*/
({ this: text, pattern: like }) => ({
match: { text, pattern: like },
operator: /** @type {const} */ ('text/like'),
})
)
/**
* @param {object} terms
* @param {API.Term<string>} terms.this
* @param {API.Term<string>} terms.pattern
*/
static not(terms) {
return { not: this.match(terms) }
}
static includes = Operator.for(
/**
* @param {object} terms
* @param {API.Term<string>} terms.this
* @param {API.Term<string>} terms.slice
*/
({ this: source, slice }) => {
return {
match: { this: source, slice },
operator: /** @type {const} */ ('text/includes'),
}
}
)
static Concat = Operator.for(
/**
* @param {object} terms
* @param {[left:API.Term<string>, right: API.Term<string>]} terms.of
* @param {API.Term<string>} [terms.is]
* @returns {API.SystemOperator}
*/
({ of: [left, right], is }) => {
return {
match: { of: left, with: right, is },
operator: /** @type {const} */ ('text/concat'),
}
}
)
static Words = Operator.for(
/**
* @param {object} terms
* @param {API.Term<string>} terms.of
* @param {API.Term<string>} [terms.is]
*/
({ of, is }) => {
return {
match: { of, is },
operator: /** @type {const} */ ('text/words'),
}
}
)
static Lines = Operator.for(
/**
* @param {object} terms
* @param {API.Term<string>} terms.of
* @param {API.Term<string>} [terms.is]
*/
({ of, is }) => {
return {
match: { of, is },
operator: /** @type {const} */ ('text/lines'),
}
}
)
static UpperCase = Operator.for(
/**
* @param {object} terms
* @param {API.Term<string>} terms.of
* @param {API.Term<string>} [terms.is]
*/
({ of, is }) => {
return {
match: { of, is },
operator: /** @type {const} */ ('text/case/upper'),
}
}
)
static LowerCase = Operator.for(
/**
* @param {object} terms
* @param {API.Term<string>} terms.of
* @param {API.Term<string>} [terms.is]
*
*/
({ of, is }) => {
return {
match: { of, is },
operator: /** @type {const} */ ('text/case/lower'),
}
}
)
static Trim = Operator.for(
/**
* @param {object} terms
* @param {API.Term<string>} terms.of
* @param {API.Term<string>} [terms.is]
*/
({ of, is }) => {
return {
match: { of, is },
operator: /** @type {const} */ ('text/trim'),
}
}
)
static TrimStart = Operator.for(
/**
* @param {object} terms
* @param {API.Term<string>} terms.of
* @param {API.Term<string>} [terms.is]
*/
({ of, is }) => {
return {
match: { of, is },
operator: /** @type {const} */ ('text/trim/start'),
}
}
)
static TrimEnd = Operator.for(
/**
* @param {object} terms
* @param {API.Term<string>} terms.of
* @param {API.Term<string>} [terms.is]
*/
({ of, is }) => {
return {
match: { of, is },
operator: /** @type {const} */ ('text/trim/end'),
}
}
)
static Length = Operator.for(
/**
* @param {object} terms
* @param {API.Term<string>} terms.of
* @param {API.Term<number>} [terms.is]
*/
({ of, is }) => {
return {
match: { of, is },
operator: /** @type {const} */ ('text/length'),
}
}
)
}
export class UTF8 {
static ToText = Operator.for(
/**
* @param {object} terms
* @param {API.Term<Uint8Array>} terms.of
* @param {API.Term<string>} [terms.is]
* @returns {API.SystemOperator}
*/
({ of, is }) => {
return {
match: { of, is },
operator: /** @type {const} */ ('utf8/to/text'),
}
}
)
static FromText = Operator.for(
/**
* @param {object} terms
* @param {API.Term<string>} terms.of
* @param {API.Term<Uint8Array>} [terms.is]
* @returns {API.SystemOperator}
*/
({ of, is }) => {
return {
match: { of, is },
operator: /** @type {const} */ ('text/to/utf8'),
}
}
)
}
export class Data {
static same = Object.assign(
/**
* @template {API.Scalar} This
* @template {API.Scalar} As
* @param {object} terms
* @param {API.Term<This>} terms.this
* @param {API.Term<As>} [terms.as]
* @returns {API.SystemOperator}
*/
({ this: of, as }) => {
return /** @type {API.SystemOperator} */ ({
match: { of, is: as },
operator: /** @type {const} */ ('=='),
})
},
{
/**
* @template {API.Scalar} This
* @template {API.Scalar} As
* @param {object} terms
* @param {API.Term<This>} terms.this
* @param {API.Term<As>} [terms.as]
* @returns {API.Negation}
*/
not: (terms) => ({ not: Data.same(terms) }),
}
)
static not = Operator.for(
/**
* @param {object} terms
* @param {API.Term<boolean>} terms.of
* @param {API.Term<boolean>} terms.is
* @returns {API.SystemOperator}
*/
(terms) => {
return /** @type {API.SystemOperator} */ ({
match: terms,
operator: /** @type {const} */ ('!'),
})
}
)
static ['!'] = this.not
static greater = Operator.for(
/**
* @template {number|string} T
* @param {object} terms
* @param {API.Term<T>} terms.this
* @param {API.Term<T>} terms.than
* @returns {API.SystemOperator}
*/
(terms) => {
return {
match: terms,
operator: /** @type {const} */ ('>'),
}
}
)
static ['>'] = this.greater
static greaterOrEqual = Operator.for(
/**
* @template {number|string} T
* @param {object} terms
* @param {API.Term<T>} terms.this
* @param {API.Term<T>} terms.than
* @returns {API.SystemOperator}
*/
(terms) => {
return {
match: terms,
operator: /** @type {const} */ ('>='),
}
}
)
static ['>='] = this.greaterOrEqual
static less = Operator.for(
/**
* @template {number|string} T
* @param {object} terms
* @param {API.Term<T>} terms.this
* @param {API.Term<T>} terms.than
* @returns {API.SystemOperator}
*/
(terms) => {
return {
match: terms,
operator: /** @type {const} */ ('<'),
}
}
)
static ['<'] = this.less
static lessOrEqual = Operator.for(
/**
* @template {number|string} T
* @param {object} terms
* @param {API.Term<T>} terms.this
* @param {API.Term<T>} terms.than
* @returns {API.SystemOperator}
*/
(terms) => {
return {
match: terms,
operator: /** @type {const} */ ('<='),
}
}
)
static ['<='] = this.lessOrEqual
static Type = Operator.for(
/**
* @param {object} terms
* @param {API.Term<API.Scalar>} terms.of
* @param {API.Term<API.TypeName>|API.Term<string>} [terms.is]
* @returns {API.SystemOperator}
*/
({ of, is }) => {
return /** @type {API.SystemOperator} */ ({
match: { of, is },
operator: /** @type {const} */ ('data/type'),
})
}
)
static Reference = Operator.for(
/**
* @param {object} terms
* @param {API.Term<any>} terms.of
* @param {API.Term<API.Entity>} [terms.is]
* @returns {{match: { of: API.Term, is?: API.Term<API.Entity> }, operator: 'data/refer' }}
*/
({ of, is }) => {
return {
match: { of, is },
operator: /** @type {const} */ ('data/refer'),
}
}
)
static Fact = Operator.for(
/**
* @template {Record<string, API.Term> & {this?: API.Term<API.Entity>}} Terms
* @param {Terms} terms
* @returns {{match: Omit<Terms, 'this'> & { is?: API.Term<API.Entity> }, operator: 'data/refer' }}
*/
({ this: is, ...of }) => {
return {
match: { ...of, is },
operator: 'data/refer',
}
}
)
}
export class Math {
static Sum = Operator.for(
/**
* @param {object} terms
* @param {API.Term<API.Numeric>} terms.of
* @param {API.Term<API.Numeric>} terms.with
* @param {API.Term<API.Numeric>} [terms.is]
* @returns {API.SystemOperator}
*/
({ of, with: by, is }) => {
return /** @type {API.SystemOperator} */ ({
match: { of, with: by, is },
operator: /** @type {const} */ ('+'),
})
}
)
static ['+'] = this.Sum
static Subtraction = Operator.for(
/**
* @param {object} terms
* @param {API.Term<API.Numeric>} terms.of
* @param {API.Term<API.Numeric>} terms.by
* @param {API.Term<API.Numeric>} [terms.is]
* @returns {API.SystemOperator}
*/
(terms) => {
return /** @type {API.SystemOperator} */ ({
match: terms,
operator: /** @type {const} */ ('-'),
})
}
)
static ['-'] = this.Subtraction
static Multiplication = Operator.for(
/**
* @param {object} terms
* @param {API.Term<API.Numeric>} terms.of
* @param {API.Term<API.Numeric>} terms.by
* @param {API.Term<API.Numeric>} [terms.is]
* @returns {API.SystemOperator}
*/
(terms) => {
return /** @type {API.SystemOperator} */ ({
match: terms,
operator: /** @type {const} */ ('*'),
})
}
)
static ['*'] = this.Multiplication
static Division = Operator.for(
/**
* @param {object} terms
* @param {API.Term<API.Numeric>} terms.of
* @param {API.Term<API.Numeric>} terms.by
* @param {API.Term<API.Numeric>} [terms.is]
* @returns {API.SystemOperator}
*/
(terms) => {
return /** @type {API.SystemOperator} */ ({
match: terms,
operator: /** @type {const} */ ('/'),
})
}
)
static ['/'] = this.Division
static Modulo = Operator.for(
/**
* @param {object} terms
* @param {API.Term<API.Numeric>} terms.of
* @param {API.Term<API.Numeric>} terms.by
* @param {API.Term<API.Numeric>} [terms.is]
* @returns {API.SystemOperator}
*/
(terms) => {
return /** @type {API.SystemOperator} */ ({
match: terms,
operator: /** @type {const} */ ('%'),
})
}
)
static ['%'] = this.Modulo
static Power = Operator.for(
/**
* @param {object} terms
* @param {API.Term<API.Numeric>} terms.of
* @param {API.Term<API.Numeric>} terms.exponent
* @param {API.Term<API.Numeric>} [terms.is]
* @returns {API.SystemOperator}
*/
({ of, exponent, is }) => {
return /** @type {API.SystemOperator} */ ({
match: { of, by: exponent, is },
operator: /** @type {const} */ ('**'),
})
}
)
static ['**'] = this.Power
static Absolute = Operator.for(
/**
* @param {object} terms
* @param {API.Term<API.Numeric>} terms.of
* @param {API.Term<API.Numeric>} [terms.is]
* @returns {API.SystemOperator}
*/
({ of, is }) => {
return /** @type {API.SystemOperator} */ ({
match: { of, is },
operator: /** @type {const} */ ('math/absolute'),
})
}
)
}
const Same = Syntax.rule({ match: { this: $.this, as: $.this } })
const NotSame = Syntax.rule({
match: { this: $.this, as: $.as },
when: {
where: [{ not: Same.apply({ this: $.this, as: $.as }) }],
},
})
export const same = Object.assign(
/**
* @template {API.Scalar} This
* @template {API.Scalar} As
* @param {{this: API.Term<This>, as: API.Term<As>}} terms
*/
(terms) => new Match(Same, terms),
{
/**
* @template {API.Scalar} This
* @template {API.Scalar} As
* @param {{this: API.Term<This>, as: API.Term<As>}} terms
*/
not(terms) {
return new Match(NotSame, terms)
},
}
)
/**
* @template {API.RuleDescriptor} Schema
* @param {Schema} schema
* @returns {API.InferAttributes<Schema>}
*/
function deriveAttributes(schema) {
const match = /** @type {Record<string, API.Variable>} */ ({})
for (const [key, type] of Object.entries(schema)) {
match[key] = $[key]
}
match.this = $.this
return /** @type {API.InferSchemaAttributes<Schema>} */ (match)
}
/**
* @template {API.RuleDescriptor} Schema
* @param {Schema} schema
* @returns {API.InferSchemaAttributes<Schema> & {_: API.Variable}}
*/
function deriveCells(schema) {
return Object.assign(deriveAttributes(schema), { _: $._ })
}
/**
* @template {API.FactSchema} Schema
* @param {Schema} schema
* @param {Partial<API.InferFactTerms<Schema>>} [input]
* @returns {API.InferSchemaTerms<Schema> & { this: API.Term<API.Entity> }}
*/
const completeTerms = (schema, input = {}) => {
const terms = /** @type {Record<String, API.Term>} */ ({})
for (const key of Object.keys(schema)) {
const value = input[key]
terms[key] = value === undefined ? $[Symbol(key)] : value
}
if (terms.this === undefined) {
terms.this = $[Symbol('this')]
}
return /** @type {API.InferSchemaTerms<Schema> & { this: API.Term<API.Entity> }} */ (
terms
)
}
/**
* @param {API.EveryView} source
* @returns {Iterable<API.Conjunct|API.Recur>}
*/
function* iterateConjuncts(source) {
for (const member of source) {
if (member === undefined) {
continue
} else if (Symbol.iterator in member) {
for (const conjunct of member) {
yield conjunct
}
} else {
yield member
}
}
}
/**
* @param {API.WhenView} source
* @returns {Iterable<[string, API.EveryView]>}
*/
function* iterateDisjuncts(source) {
if (Array.isArray(source)) {
yield ['where', source]
} else {
yield* Object.entries(source)
}
}