/ src / cursor.js
cursor.js
  1  import * as API from './api.js'
  2  import * as Constant from './constant.js'
  3  import * as Variable from './variable.js'
  4  import { _ } from './$.js'
  5  export const NOTHING = /** @type {never} */ (Symbol('NOTHING'))
  6  
  7  /**
  8   * Returns true if the variable is bound in this context.
  9   *
 10   * @param {API.MatchFrame} selection
 11   * @param {API.Cursor} scope
 12   * @param {API.Variable} variable
 13   */
 14  export const has = (selection, scope, variable) => {
 15    for (const candidate of resolve(scope, variable)) {
 16      if (selection.has(candidate)) {
 17        return true
 18      }
 19    }
 20    return false
 21  }
 22  
 23  /**
 24   * Attempts to resolve the variable in this scope. If variable is a reference
 25   * it will return the variable it refers to, otherwise it will return provided
 26   * variable essentially treating it as local.
 27   *
 28   * @template {API.Scalar} T
 29   * @param {API.Cursor} scope
 30   * @param {API.Variable<T>} variable
 31   * @returns {Iterable<API.Variable<T>>}
 32   */
 33  export const resolve = function* (scope, variable) {
 34    const variables =
 35      /** @type {Set<API.Variable<T>>|undefined} */
 36      (scope.get(variable))
 37  
 38    if (variables) {
 39      yield* variables
 40    } else {
 41      yield variable
 42    }
 43  }
 44  
 45  /**
 46   * Enumerates all the variables that are equivalent to the given one in this
 47   * scope.
 48   *
 49   * @template {API.Scalar} T
 50   * @param {API.Cursor} scope
 51   * @param {API.Variable<T>} variable
 52   */
 53  export const enumerate = function* (scope, variable) {
 54    for (const target of resolve(scope, variable)) {
 55      let found = 0
 56      for (const variables of scope.values()) {
 57        if (variables.has(target)) {
 58          found += variables.size
 59          yield* variables
 60        }
 61      }
 62      if (found == 0) {
 63        yield target
 64      }
 65    }
 66  }
 67  
 68  /**
 69   * Creates a reference from `local` variable to a `remote`
 70   * variable in the given `scope`.
 71   *
 72   * @template {API.Scalar} T
 73   * @param {API.Cursor} scope
 74   * @param {API.Variable<T>} local
 75   * @param {API.Variable<T>} remote
 76   */
 77  export const link = (scope, local, remote) => {
 78    const variables = scope.get(local)
 79    if (variables == undefined) {
 80      // Note that we store a reference even if `local === remote` so that we
 81      // can iterate over the scope variables directly as opposed to having to
 82      // iterate over variables and then resolve them through scope.
 83      scope.set(local, new Set([remote]))
 84    } else {
 85      variables.add(remote)
 86    }
 87  }
 88  
 89  /**
 90   * @template {API.Scalar} T
 91   * @param {API.MatchFrame} selection
 92   * @param {API.Cursor} scope
 93   * @param {API.Variable<T>} variable
 94   * @return {T|undefined}
 95   */
 96  export const read = (selection, scope, variable) => {
 97    for (const candidate of resolve(scope, variable)) {
 98      const value = /** @type {T|undefined} */ (selection.get(candidate))
 99      if (value !== undefined && value !== NOTHING) {
100        return value
101      }
102    }
103    return undefined
104  }
105  
106  /**
107   * Gets a value associated with the given term in the given selection.
108   *
109   * @template {API.Scalar} T
110   * @param {API.Cursor} scope
111   * @param {API.Term<T>} term
112   * @param {API.MatchFrame} selection
113   * @returns {T|undefined}
114   */
115  export const get = (selection, scope, term) =>
116    Variable.is(term) ? read(selection, scope, term) : term
117  
118  /**
119   * @template {API.Scalar} T
120   * @param {API.MatchFrame} selection
121   * @param {API.Cursor} scope
122   * @param {API.Variable<T>} variable
123   * @param {T} value
124   */
125  export const set = (selection, scope, variable, value) => {
126    for (const target of resolve(scope, variable)) {
127      // We ignore assignments to `_` because that is discard variable.
128      if (target !== _) {
129        const current = selection.get(target)
130        // If currently variable is not set we set to a given value.
131        if (current === undefined) {
132          selection.set(target, value)
133        }
134        // If we do however have a value already we need to check that it is
135        // consistent with value being assigned if it is not we throw an error.
136        else if (!Constant.equal(current, value)) {
137          throw new RangeError(
138            `Can not bind ${Variable.toDebugString(
139              variable
140            )} to ${Constant.toDebugString(
141              value
142            )} because it is already bound to ${Constant.toDebugString(current)}`
143          )
144        }
145      }
146    }
147  }
148  
149  /**
150   * @param {API.MatchFrame} selection
151   * @param {API.Cursor} scope
152   * @param {API.Variable} variable
153   */
154  export const markBound = (selection, scope, variable) =>
155    set(selection, scope, variable, NOTHING)
156  
157  /**
158   * @param {API.MatchFrame} selection
159   * @param {API.Cursor} scope
160   * @param {API.Term} term
161   * @param {API.Scalar} value
162   */
163  export const merge = (selection, scope, term, value) => {
164    // If term is a variable we assign the value to it.
165    if (Variable.is(term)) {
166      set(selection, scope, term, value)
167    }
168    // Otherwise we ensure that term is consistent with value being assigned.
169    else if (!Constant.equal(term, value)) {
170      throw new RangeError(
171        `Can not unify ${Constant.toDebugString(
172          term
173        )} with ${Constant.toDebugString(value)}`
174      )
175    }
176  }