@dialog-db/query
Version:
Datalog query engine inspired by Datomic
177 lines (166 loc) • 4.91 kB
JavaScript
import * as API from './api.js'
import * as Constant from './constant.js'
import * as Variable from './variable.js'
import { _ } from './$.js'
export const NOTHING = /** @type {never} */ (Symbol('NOTHING'))
/**
* Returns true if the variable is bound in this context.
*
* @param {API.MatchFrame} selection
* @param {API.Cursor} scope
* @param {API.Variable} variable
*/
export const has = (selection, scope, variable) => {
for (const candidate of resolve(scope, variable)) {
if (selection.has(candidate)) {
return true
}
}
return false
}
/**
* Attempts to resolve the variable in this scope. If variable is a reference
* it will return the variable it refers to, otherwise it will return provided
* variable essentially treating it as local.
*
* @template {API.Scalar} T
* @param {API.Cursor} scope
* @param {API.Variable<T>} variable
* @returns {Iterable<API.Variable<T>>}
*/
export const resolve = function* (scope, variable) {
const variables =
/** @type {Set<API.Variable<T>>|undefined} */
(scope.get(variable))
if (variables) {
yield* variables
} else {
yield variable
}
}
/**
* Enumerates all the variables that are equivalent to the given one in this
* scope.
*
* @template {API.Scalar} T
* @param {API.Cursor} scope
* @param {API.Variable<T>} variable
*/
export const enumerate = function* (scope, variable) {
for (const target of resolve(scope, variable)) {
let found = 0
for (const variables of scope.values()) {
if (variables.has(target)) {
found += variables.size
yield* variables
}
}
if (found == 0) {
yield target
}
}
}
/**
* Creates a reference from `local` variable to a `remote`
* variable in the given `scope`.
*
* @template {API.Scalar} T
* @param {API.Cursor} scope
* @param {API.Variable<T>} local
* @param {API.Variable<T>} remote
*/
export const link = (scope, local, remote) => {
const variables = scope.get(local)
if (variables == undefined) {
// Note that we store a reference even if `local === remote` so that we
// can iterate over the scope variables directly as opposed to having to
// iterate over variables and then resolve them through scope.
scope.set(local, new Set([remote]))
} else {
variables.add(remote)
}
}
/**
* @template {API.Scalar} T
* @param {API.MatchFrame} selection
* @param {API.Cursor} scope
* @param {API.Variable<T>} variable
* @return {T|undefined}
*/
export const read = (selection, scope, variable) => {
for (const candidate of resolve(scope, variable)) {
const value = /** @type {T|undefined} */ (selection.get(candidate))
if (value !== undefined && value !== NOTHING) {
return value
}
}
return undefined
}
/**
* Gets a value associated with the given term in the given selection.
*
* @template {API.Scalar} T
* @param {API.Cursor} scope
* @param {API.Term<T>} term
* @param {API.MatchFrame} selection
* @returns {T|undefined}
*/
export const get = (selection, scope, term) =>
Variable.is(term) ? read(selection, scope, term) : term
/**
* @template {API.Scalar} T
* @param {API.MatchFrame} selection
* @param {API.Cursor} scope
* @param {API.Variable<T>} variable
* @param {T} value
*/
export const set = (selection, scope, variable, value) => {
for (const target of resolve(scope, variable)) {
// We ignore assignments to `_` because that is discard variable.
if (target !== _) {
const current = selection.get(target)
// If currently variable is not set we set to a given value.
if (current === undefined) {
selection.set(target, value)
}
// If we do however have a value already we need to check that it is
// consistent with value being assigned if it is not we throw an error.
else if (!Constant.equal(current, value)) {
throw new RangeError(
`Can not bind ${Variable.toDebugString(
variable
)} to ${Constant.toDebugString(
value
)} because it is already bound to ${Constant.toDebugString(current)}`
)
}
}
}
}
/**
* @param {API.MatchFrame} selection
* @param {API.Cursor} scope
* @param {API.Variable} variable
*/
export const markBound = (selection, scope, variable) =>
set(selection, scope, variable, NOTHING)
/**
* @param {API.MatchFrame} selection
* @param {API.Cursor} scope
* @param {API.Term} term
* @param {API.Scalar} value
*/
export const merge = (selection, scope, term, value) => {
// If term is a variable we assign the value to it.
if (Variable.is(term)) {
set(selection, scope, term, value)
}
// Otherwise we ensure that term is consistent with value being assigned.
else if (!Constant.equal(term, value)) {
throw new RangeError(
`Can not unify ${Constant.toDebugString(
term
)} with ${Constant.toDebugString(value)}`
)
}
}