feat: First working TypeDispatcher, supporting plain functions on Number (#4)
Resolves #1. A hand test showed this code can add two plus two, always a major milestone. So we will skip review on this PR since there is currently no testing framework, and proceed immediately to addressing #3. Reviewed-on: glen/nanomath#4 Co-authored-by: Glen Whitney <glen@studioinfinity.org> Co-committed-by: Glen Whitney <glen@studioinfinity.org>
This commit is contained in:
parent
69ef928b6e
commit
79c6d44fda
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 {
|
export class Type {
|
||||||
constructor(f) {
|
constructor(f) {
|
||||||
this.test = 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 {typeOf, Type, bootstrapTypes} from './Type.js'
|
||||||
import {Implementations, isPlainObject} from './helpers.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 {
|
export class TypeDispatcher {
|
||||||
constructor(...specs) {
|
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)
|
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) {
|
merge(spec) {
|
||||||
if (!spec) return
|
if (!spec) return
|
||||||
if (typeof spec != 'object') {
|
if (typeof spec != 'object') {
|
||||||
|
@ -13,25 +35,271 @@ export class TypeDispatcher {
|
||||||
`TypeDispatcher specifications must be objects, not '${spec}'.`)
|
`TypeDispatcher specifications must be objects, not '${spec}'.`)
|
||||||
}
|
}
|
||||||
for (const key in spec) {
|
for (const key in spec) {
|
||||||
const val = spec[key]
|
let val = spec[key]
|
||||||
if (val instanceof Type) {
|
if (val instanceof Type) {
|
||||||
console.log(`Pretending to install type ${key}: ${val}`)
|
// TODO: Need to wipe out any dependencies on types[key]!
|
||||||
continue
|
this.types[key] = val
|
||||||
}
|
|
||||||
if (val instanceof Implementations) {
|
|
||||||
console.log(`Pretending to install implementations for ${key}`)
|
|
||||||
console.log(` --> ${val}`)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (typeof val === 'function') {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPlainObject(val)) {
|
if (isPlainObject(val)) {
|
||||||
this.merge(val)
|
this.merge(val)
|
||||||
continue
|
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 {Type} from './Type.js'
|
||||||
|
import {pattern} from './TypePatterns.js'
|
||||||
|
|
||||||
export class Implementations {
|
export class Implementations {
|
||||||
constructor(imps) {
|
constructor(imps) {
|
||||||
this.patterns = new Map()
|
this.patterns = new Map()
|
||||||
for (let i = 0; i < imps.length; ++i) {
|
for (let i = 0; i < imps.length; ++i) {
|
||||||
let pattern = imps[i++]
|
this.patterns.set(pattern(imps[i]), 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])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue