From 79c6d44fda4f130b5dac5d0e49bcd58617e08298 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 7 Apr 2025 05:11:50 +0000 Subject: [PATCH] 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: https://code.studioinfinity.org/glen/nanomath/pulls/4 Co-authored-by: Glen Whitney Co-committed-by: Glen Whitney --- src/core/Type.js | 27 ++++ src/core/TypeDispatcher.js | 290 +++++++++++++++++++++++++++++++++++-- src/core/TypePatterns.js | 118 +++++++++++++++ src/core/helpers.js | 9 +- 4 files changed, 426 insertions(+), 18 deletions(-) create mode 100644 src/core/TypePatterns.js diff --git a/src/core/Type.js b/src/core/Type.js index 79ebce2..25acbbc 100644 --- a/src/core/Type.js +++ b/src/core/Type.js @@ -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 +} diff --git a/src/core/TypeDispatcher.js b/src/core/TypeDispatcher.js index fe3334d..b0a28ec 100644 --- a/src/core/TypeDispatcher.js +++ b/src/core/TypeDispatcher.js @@ -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 + } +}) diff --git a/src/core/TypePatterns.js b/src/core/TypePatterns.js new file mode 100644 index 0000000..1561827 --- /dev/null +++ b/src/core/TypePatterns.js @@ -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 diff --git a/src/core/helpers.js b/src/core/helpers.js index 107eccd..a703fef 100644 --- a/src/core/helpers.js +++ b/src/core/helpers.js @@ -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]) } } }