feat: First working TypeDispatcher, supporting plain functions on Number
This commit is contained in:
parent
69ef928b6e
commit
db82634aed
4 changed files with 426 additions and 18 deletions
|
@ -1,5 +1,32 @@
|
|||
import {Returns} from './helpers.js'
|
||||
|
||||
// Order matters here, because these items will merged into a new
|
||||
// dispatcher in the order they are listed
|
||||
|
||||
const typeObject = {}
|
||||
|
||||
export const types = () => typeObject
|
||||
|
||||
export class Type {
|
||||
constructor(f) {
|
||||
this.test = f
|
||||
}
|
||||
}
|
||||
|
||||
export const Undefined = new Type(t => typeof t === 'undefined')
|
||||
export const TypeOfTypes = new Type(t => t instanceof Type)
|
||||
|
||||
export const typeOf = Returns(TypeOfTypes, item => {
|
||||
for (const type of Object.values(typeObject)) {
|
||||
if (!(type instanceof Type)) continue
|
||||
if (type.test(item)) return type
|
||||
}
|
||||
})
|
||||
|
||||
// bootstrapping order matters, but order of exports in a module isn't
|
||||
// simply the order that the items are listed in the module. So we make
|
||||
// an explicitly ordered export of implementations for this sake:
|
||||
|
||||
export const bootstrapTypes = {
|
||||
types, Type, Undefined, TypeOfTypes, typeOf
|
||||
}
|
||||
|
|
|
@ -1,11 +1,33 @@
|
|||
import {Type} from './Type.js'
|
||||
import {Implementations, isPlainObject} from './helpers.js'
|
||||
import {typeOf, Type, bootstrapTypes} from './Type.js'
|
||||
|
||||
import {Implementations, isPlainObject, onType} from './helpers.js'
|
||||
import {alwaysMatches, Any, Multiple} from './TypePatterns.js'
|
||||
|
||||
// helper that organizes a list into the same chunks as a pattern is
|
||||
const collectLike = (list, pattern, state = {pos: 0}) => {
|
||||
const result = []
|
||||
for (const elt of pattern) {
|
||||
if (Array.isArray(elt)) result.push(collectLike(list, elt, state))
|
||||
else result.push(list[state.pos++])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export class TypeDispatcher {
|
||||
constructor(...specs) {
|
||||
this._implementations = {} // maps key to list of [pattern, result] pairs
|
||||
this._dependencies = {} // maps key to a map from type vectors to...
|
||||
// ...a set of [key, types] that depend on it.
|
||||
this._behaviors = {} // maps key to a map from type vectors to results
|
||||
this._fallbacks = {} // maps key to a catchall result
|
||||
this.merge(bootstrapTypes) // bootstrap the instance
|
||||
for (const spec of specs) this.merge(spec)
|
||||
}
|
||||
|
||||
// Only modify a TypeDispatcher via merge! Otherwise dependencies will
|
||||
// not necessarily be updated properly. Actually I may have it set up so
|
||||
// that's the only way to modify a TypeDispatcher... not sure, should
|
||||
// probably test it.
|
||||
merge(spec) {
|
||||
if (!spec) return
|
||||
if (typeof spec != 'object') {
|
||||
|
@ -13,25 +35,271 @@ export class TypeDispatcher {
|
|||
`TypeDispatcher specifications must be objects, not '${spec}'.`)
|
||||
}
|
||||
for (const key in spec) {
|
||||
const val = spec[key]
|
||||
let val = spec[key]
|
||||
if (val instanceof Type) {
|
||||
console.log(`Pretending to install type ${key}: ${val}`)
|
||||
continue
|
||||
}
|
||||
if (val instanceof Implementations) {
|
||||
console.log(`Pretending to install implementations for ${key}`)
|
||||
console.log(` --> ${val}`)
|
||||
// TODO: Need to wipe out any dependencies on types[key]!
|
||||
this.types[key] = val
|
||||
continue
|
||||
}
|
||||
if (typeof val === 'function') {
|
||||
console.log(`Pretend install of catchall implementation for ${key}`)
|
||||
val = onType(Multiple(Any), val)
|
||||
}
|
||||
if (val instanceof Implementations) {
|
||||
if (!(key in this)) {
|
||||
// need to set up the item
|
||||
Object.defineProperty(this, key, {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get: () => {
|
||||
let tryValue
|
||||
let tryTypes = []
|
||||
try {
|
||||
tryValue = this.resolve(key, [])
|
||||
} catch {
|
||||
// Has no value for the empty type list, so
|
||||
// find a type list it will have a value for
|
||||
tryTypes =
|
||||
this._implementations[key][0][0].sampleTypes()
|
||||
tryValue = this.resolve(key, tryTypes)
|
||||
}
|
||||
// Redefine the property according to what sort of
|
||||
// entity it is:
|
||||
if (typeof tryValue === 'function') {
|
||||
// the usual case: a method of the dispatcher
|
||||
const standard = (...args) => {
|
||||
const types = args.map(typeOf)
|
||||
return this.resolve(key, types)(...args)
|
||||
}
|
||||
standard.resolve = (types) => this.resolve(key, types)
|
||||
standard.isDispatcher = true
|
||||
Object.defineProperty(this, key, {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get: () => standard
|
||||
})
|
||||
return standard
|
||||
}
|
||||
if (tryTypes.length) tryValue = undefined
|
||||
if (typeof tryValue === 'object') {
|
||||
if (!('resolve' in tryValue)) {
|
||||
tryValue.resolve = types => this.resolve(key, types)
|
||||
}
|
||||
const get = () => {
|
||||
if (this._dependencies[key]?.get([])?.size) {
|
||||
return DependencyWatcher(tryValue, [key], this)
|
||||
}
|
||||
return tryValue
|
||||
}
|
||||
Object.defineProperty(this, key, {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get
|
||||
})
|
||||
return get()
|
||||
}
|
||||
Object.defineProperty(this, key, {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get: () => tryValue
|
||||
})
|
||||
return tryValue
|
||||
}
|
||||
})
|
||||
this._implementations[key] = []
|
||||
this._behaviors[key] = new Map()
|
||||
this._dependencies[key] = new Map()
|
||||
}
|
||||
// Now add all of the patterns of this implementation:
|
||||
for (const [pattern, result] of val.patterns) {
|
||||
if (alwaysMatches(pattern)) {
|
||||
if (key in this._fallbacks) this._disengageFallback(key)
|
||||
this._fallbacks[key] = result
|
||||
} else {
|
||||
this._clearBehaviorsMatching(key, pattern)
|
||||
// if it happens the same pattern is already in the
|
||||
// implementations, remove it
|
||||
const imps = this._implementations[key]
|
||||
const have = imps.findIndex(elt => pattern.equal(elt[0]))
|
||||
if (have >= 0) imps.splice(have, 1)
|
||||
this._implementations[key].unshift([pattern, result])
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (isPlainObject(val)) {
|
||||
this.merge(val)
|
||||
continue
|
||||
}
|
||||
console.log(`Pretend install of catchall value for ${key}: ${val}`)
|
||||
|
||||
// install value as a catchall value
|
||||
this.merge({[key]: onType(Multiple(Any), val)})
|
||||
}
|
||||
}
|
||||
|
||||
_addToDeps(key, subkey) {
|
||||
const depMap = this._dependencies[key]
|
||||
if (!depMap.has(subkey)) depMap.set(subkey, new Set())
|
||||
const depSet = depMap.get(subkey)
|
||||
for (const pair of this.resolve._genDepsOf) depSet.add(pair)
|
||||
}
|
||||
|
||||
resolve(key, types) {
|
||||
if (!(key in this)) {
|
||||
throw new ReferenceError(`no method or value for key '${key}'`)
|
||||
}
|
||||
|
||||
if (this.resolve._genDepsOf?.length) _addToDeps(key, types)
|
||||
|
||||
const behave = this._behaviors[key]
|
||||
if (behave.has(types)) return behave.get(types)
|
||||
const imps = this._implementations[key]
|
||||
let needItem = true
|
||||
let item
|
||||
let pattern
|
||||
let template
|
||||
if (imps.length) {
|
||||
for ([pattern, item] of imps) {
|
||||
let finalIndex
|
||||
;[finalIndex, template] = pattern.match(types, 0)
|
||||
if (finalIndex === types.length) {
|
||||
needItem = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (needItem && key in this._fallbacks) {
|
||||
needItem = false
|
||||
item = this._fallbacks[key]
|
||||
template = [types]
|
||||
}
|
||||
if (needItem) {
|
||||
throw new TypeError(`no matching definition of '${key}' on '${types}'`)
|
||||
}
|
||||
if (typeof item !== 'function' || 'returns' in item) {
|
||||
behave.set(types, item)
|
||||
return item
|
||||
}
|
||||
// item is a Factory. We have to use it to build the behavior
|
||||
// First set up to record dependencies
|
||||
if (!('_genDepsOf' in this.resolve)) {
|
||||
this.resolve._genDepsOf = []
|
||||
}
|
||||
this.resolve._genDepsOf.push([key, types])
|
||||
let theBehavior = item(DependencyRecorder(this, [], this), pattern)
|
||||
this.resolve._genDepsOf.pop()
|
||||
if (typeof theBehavior === 'function'
|
||||
&& theBehavior.length
|
||||
&& template.some(elt => Array.isArray(elt))
|
||||
) {
|
||||
// have to wrap the behavior to collect the actual arguments
|
||||
// in the way corresponding to the template
|
||||
const wrappedBehavior = (...args) => {
|
||||
const collectedArgs = collectLike(args, template)
|
||||
return theBehavior(...collectedArgs)
|
||||
}
|
||||
theBehavior = wrappedBehavior
|
||||
}
|
||||
behave.set(types, theBehavior)
|
||||
return theBehavior
|
||||
}
|
||||
|
||||
// Method called to invalidate a set of behaviors
|
||||
// I think all it has to do is throw them out; they should be
|
||||
// regenerated properly by resolve, if all is well.
|
||||
_invalidate(behaveSet) {
|
||||
for (const [key, types] of behaveSet) this._behaviors[key].delete(types)
|
||||
}
|
||||
|
||||
_disengageFallback(key) {
|
||||
// We need to find all of the behaviors that currently rely on the
|
||||
// fallback, invalidate their dependencies, and remove them.
|
||||
const fallTypes = []
|
||||
const behs = this._behaviors[key]
|
||||
const imps = this._implementations[key]
|
||||
const deps = this._dependencies[key]
|
||||
for (const types of behs.keys()) {
|
||||
let fallsback = true
|
||||
for (const [pattern] of imps) {
|
||||
const [finalIndex] = pattern.match(types)
|
||||
if (finalIndex === types.length) {
|
||||
fallsback = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if (fallsback) fallTypes.push(types)
|
||||
}
|
||||
for (const types of fallTypes) {
|
||||
const depSet = deps?.get(types)
|
||||
if (depSet?.size) this._invalidate(depSet)
|
||||
behs.delete(types)
|
||||
}
|
||||
}
|
||||
|
||||
_clearBehaviorsMatching(key, pattern) {
|
||||
// like disengageFallback, just we have the offending pattern:
|
||||
const behs = this._behaviors[key]
|
||||
const deps = this._dependencies[key]
|
||||
const patTypes = behs.keys().filter(types => {
|
||||
const [finalIndex] = pattern.match(types)
|
||||
return finalIndex === types.length
|
||||
})
|
||||
for (const types of patTypes) {
|
||||
const depSet = deps?.get(types)
|
||||
if (depSet?.size) this._invalidate(depSet)
|
||||
behs.delete(types)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Proxy that traps accesses and records dependencies on them
|
||||
const DependencyRecorder = (object, path, repo) => new Proxy(object, {
|
||||
get(target, prop, receiver) {
|
||||
const result = Reflect.get(target, prop, receiver)
|
||||
// pass resolve calls through, since we record dependencies therein:
|
||||
if (prop === 'resolve' || 'isDispatcher' in result) return result
|
||||
|
||||
// OK, it's not a method on a TypeDispatcher, it's some other kind of
|
||||
// value. So first record the dependency on prop at this path:
|
||||
const newPath = path.slice()
|
||||
newPath.push(prop)
|
||||
const key = newPath[0]
|
||||
const subpath = newPath.slice(1)
|
||||
repo._addToDeps(key, subpath)
|
||||
// Now, if the result is an object, we may need to record further
|
||||
// dependencies on its properties (e.g. math.config.predictable)
|
||||
// So proxy the return value:
|
||||
if (typeof result === 'object') {
|
||||
return DependencyRecorder(result, newPath, repo)
|
||||
} else return result
|
||||
}
|
||||
})
|
||||
|
||||
// The flip side: proxy that traps setting properties and invalidates things
|
||||
// that depend on them:
|
||||
const DependencyWatcher = (object, path, repo) => new Proxy(object, {
|
||||
set(target, prop, value, receiver) {
|
||||
// First see if this setting has any dependencies:
|
||||
const newPath = path.slice()
|
||||
newPath.push(prop)
|
||||
const key = newPath.unshift()
|
||||
const depSet = repo._dependencies[key]?.get(newPath)
|
||||
if (depSet?.size) {
|
||||
// It does. So if we are changing it, invalidate them:
|
||||
const oldValue = Reflect.get(target, prop, receiver)
|
||||
if (value !== oldValue) repo._invalidate(depSet)
|
||||
}
|
||||
// Now we can just perform the setting
|
||||
return Reflect.set(target, prop, value, receiver)
|
||||
},
|
||||
get(target, prop, receiver) {
|
||||
// Only thing we need to do is push the watching down
|
||||
const result = Reflect.get(target, prop, receiver)
|
||||
if (typeof result === 'object') {
|
||||
const newPath = path.slice()
|
||||
newPath.push(prop)
|
||||
return DependencyWatcher(result, newPath, repo)
|
||||
}
|
||||
return result
|
||||
}
|
||||
})
|
||||
|
|
118
src/core/TypePatterns.js
Normal file
118
src/core/TypePatterns.js
Normal file
|
@ -0,0 +1,118 @@
|
|||
import {Type, Undefined} from './Type.js'
|
||||
|
||||
class TypePattern {
|
||||
match(typeSequence, position) {
|
||||
throw new Error('Specific TypePatterns must implement match')
|
||||
}
|
||||
sampleTypes() {
|
||||
throw new Error('Specific TypePatterns must implement sampleTypes')
|
||||
}
|
||||
equal(other) {return other.constructor === this.constructor}
|
||||
}
|
||||
|
||||
class MatchTypePattern extends TypePattern {
|
||||
constructor(typeToMatch) {
|
||||
super()
|
||||
this.type = typeToMatch
|
||||
}
|
||||
match(typeSequence, position) {
|
||||
if (position < typeSequence.length
|
||||
&& typeSequence[position] === this.type
|
||||
) {
|
||||
return [position + 1, typeSequence[position]]
|
||||
}
|
||||
return [-1, Undefined]
|
||||
}
|
||||
sampleTypes() {return [this.type]}
|
||||
equal(other) {return super.equal(other) && this.type === other.type}
|
||||
}
|
||||
|
||||
class SequencePattern extends TypePattern {
|
||||
constructor(itemsToMatch) {
|
||||
super()
|
||||
this.patterns = itemsToMatch.map(pattern)
|
||||
}
|
||||
match(typeSequence, position) {
|
||||
const matches = []
|
||||
for (const pat of this.patterns) {
|
||||
const [newPos, newMatch] = pat.match(typeSequence, position)
|
||||
if (newPos < 0) return [-1, Undefined]
|
||||
position = newPos
|
||||
matches.push(newMatch)
|
||||
}
|
||||
return [position, matches]
|
||||
}
|
||||
sampleTypes() {
|
||||
return this.patterns.map(pat => pat.sampleTypes()).flat()
|
||||
}
|
||||
equal(other) {
|
||||
return super.equal(other)
|
||||
&& this.patterns.length === other.patterns.length
|
||||
&& this.patterns.every((elt, ix) => elt.equal(other.patterns[ix]))
|
||||
}
|
||||
}
|
||||
|
||||
export const pattern = patternOrSpec => {
|
||||
if (patternOrSpec instanceof TypePattern) return patternOrSpec
|
||||
if (patternOrSpec instanceof Type) {
|
||||
return new MatchTypePattern(patternOrSpec)
|
||||
}
|
||||
if (Array.isArray(patternOrSpec)) return new SequencePattern(patternOrSpec)
|
||||
throw new TypeError(`Can't interpret '${patternOrSpec}' as a type pattern`)
|
||||
}
|
||||
|
||||
class AnyPattern extends TypePattern {
|
||||
match(typeSequence, position) {
|
||||
return position < typeSequence.length
|
||||
? [position + 1, typeSequence[position]]
|
||||
: [-1, Undefined]
|
||||
}
|
||||
sampleTypes() {return [Undefined]}
|
||||
}
|
||||
|
||||
export const Any = new AnyPattern()
|
||||
|
||||
class OptionalPattern extends TypePattern {
|
||||
constructor(item) {
|
||||
this.pattern = pattern(item)
|
||||
}
|
||||
match(typeSequence, position) {
|
||||
const matches = []
|
||||
const [newPos, newMatch] = this.pattern.match(typeSequence, position)
|
||||
if (newPos >= 0) {
|
||||
position = newPos
|
||||
matches.push(newMatch)
|
||||
}
|
||||
return [position, matches]
|
||||
}
|
||||
sampleTypes() {return []}
|
||||
equal(other) {
|
||||
return super.equal(other) && this.pattern.equal(other.pattern)
|
||||
}
|
||||
}
|
||||
|
||||
export const Optional = item => new OptionalPattern(item)
|
||||
|
||||
class MultiPattern extends TypePattern {
|
||||
constructor(item) {
|
||||
super()
|
||||
this.pattern = pattern(item)
|
||||
}
|
||||
match(typeSequence, position) {
|
||||
const matches = []
|
||||
while (true) {
|
||||
const [newPos, newMatch] = this.pattern.match(typeSequence, position)
|
||||
if (newPos < 0) return [position, matches]
|
||||
position = newPos
|
||||
matches.push(newMatch)
|
||||
}
|
||||
}
|
||||
sampleTypes() {return []}
|
||||
equal(other) {
|
||||
return super.equal(other) && this.pattern.equal(other.pattern)
|
||||
}
|
||||
}
|
||||
|
||||
export const Multiple = item => new MultiPattern(item)
|
||||
export const alwaysMatches =
|
||||
pat => pat instanceof MultiPattern && pat.pattern instanceof AnyPattern
|
|
@ -1,16 +1,11 @@
|
|||
import {Type} from './Type.js'
|
||||
import {pattern} from './TypePatterns.js'
|
||||
|
||||
export class Implementations {
|
||||
constructor(imps) {
|
||||
this.patterns = new Map()
|
||||
for (let i = 0; i < imps.length; ++i) {
|
||||
let pattern = imps[i++]
|
||||
if (!Array.isArray(pattern)) pattern = [pattern]
|
||||
if (!pattern.every(item => item instanceof Type)) {
|
||||
throw new TypeError(
|
||||
`Implementation pattern ${pattern} contains non-Type entry`)
|
||||
}
|
||||
this.patterns.set(pattern, imps[i])
|
||||
this.patterns.set(pattern(imps[i]), imps[++i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue