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 }