feat: Index behaviors by return typing strategy
* Adds the concept of, and options for, the return typing strategy * adds the strategy at the beginning of every behavior index * adds the strategy as an additional argument to resolve * No actual use of return type strategy so far * Sets up eslint * Fixes eslint errors
This commit is contained in:
parent
aad62df8ac
commit
47370cec9e
16 changed files with 602 additions and 59 deletions
|
@ -1,6 +1,6 @@
|
|||
import {BooleanT} from './BooleanT.js'
|
||||
import {match} from '#core/TypePatterns.js'
|
||||
import {Returns, Type, TypeOfTypes, Undefined} from '#core/Type.js'
|
||||
import {Returns, TypeOfTypes, Undefined} from '#core/Type.js'
|
||||
import {NumberT} from '#number/NumberT.js'
|
||||
|
||||
const bool = f => Returns(BooleanT, f)
|
||||
|
|
|
@ -11,7 +11,7 @@ function complexSpecialize(ComponentType) {
|
|||
const typeName = `Complex(${ComponentType})`
|
||||
if (ComponentType.concrete) {
|
||||
const fromSpec = [match(
|
||||
ComponentType, math => r => ({re: r, im: ComponentType.zero}))]
|
||||
ComponentType, () => r => ({re: r, im: ComponentType.zero}))]
|
||||
for (const {pattern, does} of ComponentType.from) {
|
||||
fromSpec.push(match(
|
||||
this.specialize(pattern.type),
|
||||
|
|
|
@ -66,7 +66,7 @@ export const Undefined = new Type(
|
|||
t => typeof t === 'undefined',
|
||||
{zero: undefined, one: undefined, nan: undefined})
|
||||
export const TypeOfTypes = new Type(t => t instanceof Type)
|
||||
export const NotAType = new Type(t => true) // Danger, do not merge!
|
||||
export const NotAType = new Type(() => true) // Danger, do not merge!
|
||||
NotAType._doNotMerge = true
|
||||
|
||||
export const Returns = (type, f) => (f.returns = type, f)
|
||||
|
@ -84,3 +84,17 @@ export const whichType = typs => Returns(TypeOfTypes, item => {
|
|||
}
|
||||
throw new TypeError(errorMsg)
|
||||
})
|
||||
|
||||
// The return typing strategies
|
||||
// MAKE SURE NONE ARE FALSY, so that code can easily test whether a strategy
|
||||
// has been specified.
|
||||
export const ReturnTyping = Object.freeze({
|
||||
free: 1,
|
||||
conservative: 2,
|
||||
full: 3,
|
||||
name(strat) {
|
||||
for (const key in this) {
|
||||
if (this[key] === strat) return key
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -3,7 +3,7 @@ import ArrayKeyedMap from 'array-keyed-map'
|
|||
import {ResolutionError, isPlainFunction, isPlainObject} from './helpers.js'
|
||||
import {Implementations, ImplementationsGenerator} from './Implementations.js'
|
||||
import {bootstrapTypes} from './type.js'
|
||||
import {Returns, whichType, Type} from './Type.js'
|
||||
import {Returns, ReturnTyping, whichType, Type} from './Type.js'
|
||||
import {
|
||||
matched, needsCollection, Passthru, Matcher, match
|
||||
} from './TypePatterns.js'
|
||||
|
@ -167,14 +167,18 @@ export class TypeDispatcher {
|
|||
|
||||
if (typeof tryValue === 'object') {
|
||||
if (!('resolve' in tryValue)) {
|
||||
tryValue.resolve = types => this.resolve(key, types)
|
||||
tryValue.resolve =
|
||||
(types, strat) => this.resolve(key, types, strat)
|
||||
}
|
||||
const get = () => {
|
||||
const keyDeps = this._dependencies.get(key)
|
||||
if (!keyDeps) return tryValue
|
||||
const watch = Array.from(keyDeps.keys().filter(
|
||||
types => types.length === 0
|
||||
|| this.resolve(key, types) === tryValue
|
||||
bhvix => {
|
||||
if (bhvix.length < 2) return true
|
||||
const types = bhvix.slice(1)
|
||||
return this.resolve(key, types) === tryValue
|
||||
}
|
||||
))
|
||||
if (watch.length) {
|
||||
return DependencyWatcher(tryValue, key, watch, this)
|
||||
|
@ -221,7 +225,7 @@ export class TypeDispatcher {
|
|||
}
|
||||
}
|
||||
|
||||
_addToDeps(key, types) {
|
||||
_addToDeps(key, bhvix) {
|
||||
// Never depend on internal methods:
|
||||
if (key.startsWith('_')) return
|
||||
let depMap = this._dependencies.get(key)
|
||||
|
@ -229,11 +233,11 @@ export class TypeDispatcher {
|
|||
depMap = new ArrayKeyedMap()
|
||||
this._dependencies.set(key, depMap)
|
||||
}
|
||||
if (!depMap.has(types)) depMap.set(types, new Map())
|
||||
const depColl = depMap.get(types)
|
||||
for (const [dkey, types] of this.resolve._genDepsOf) {
|
||||
if (!depMap.has(bhvix)) depMap.set(bhvix, new Map())
|
||||
const depColl = depMap.get(bhvix)
|
||||
for (const [dkey, bhvix] of this.resolve._genDepsOf) {
|
||||
if (!depColl.has(dkey)) depColl.set(dkey, new ArrayKeyedMap())
|
||||
depColl.get(dkey).set(types, true)
|
||||
depColl.get(dkey).set(bhvix, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -259,28 +263,42 @@ export class TypeDispatcher {
|
|||
return args => extractors.map(f => f(args))
|
||||
}
|
||||
|
||||
resolve(key, types) {
|
||||
// key is the identifier of the method being looked up
|
||||
// types is a single argument type or an array of actual argument types
|
||||
// strategy is a ReturnTyping value specifying how to choose the
|
||||
// return type for the operation.
|
||||
resolve(key, types, strategy) {
|
||||
if (!(key in this)) {
|
||||
throw new ReferenceError(`no method or value for key '${key}'`)
|
||||
}
|
||||
if (!Array.isArray(types)) types = [types]
|
||||
if (!strategy) {
|
||||
// Avoid recursing on obtaining config
|
||||
if (key === 'config') strategy = ReturnTyping.free
|
||||
else strategy = this.config.returnTyping
|
||||
}
|
||||
// The "behavior index": the return type strategy followed by the
|
||||
// types:
|
||||
const bhvix = [strategy, ...types]
|
||||
|
||||
const generatingDeps = this.resolve._genDepsOf?.length
|
||||
if (generatingDeps) this._addToDeps(key, types)
|
||||
if (generatingDeps) this._addToDeps(key, bhvix)
|
||||
|
||||
const behave = this._behaviors[key]
|
||||
// Return the cached resolution if it's there
|
||||
if (behave.has(types)) {
|
||||
const result = behave.get(types)
|
||||
if (behave.has(bhvix)) {
|
||||
const result = behave.get(bhvix)
|
||||
if (result === underResolution) {
|
||||
throw new ResolutionError(
|
||||
`recursive resolution of ${key} on ${types}`)
|
||||
`recursive resolution of ${key} on ${types} with return typing `
|
||||
+ ReturnTyping.name(strategy)
|
||||
)
|
||||
}
|
||||
if (generatingDeps
|
||||
&& typeof result === 'object'
|
||||
&& !(result instanceof Type)
|
||||
) {
|
||||
return DependencyRecorder(result, key, this, types)
|
||||
return DependencyRecorder(result, key, this, bhvix)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
@ -315,12 +333,12 @@ export class TypeDispatcher {
|
|||
}
|
||||
// If this key is producing a non-function value, we're done
|
||||
if (!isPlainFunction(item)) {
|
||||
behave.set(types, item)
|
||||
behave.set(bhvix, item)
|
||||
if (generatingDeps
|
||||
&& typeof item === 'object'
|
||||
&& !(item instanceof Type)
|
||||
) {
|
||||
return DependencyRecorder(item, key, this, types)
|
||||
return DependencyRecorder(item, key, this, bhvix)
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
@ -334,8 +352,8 @@ export class TypeDispatcher {
|
|||
|
||||
let theBehavior = () => undefined
|
||||
let finalBehavior
|
||||
this.resolve._genDepsOf.push([key, types])
|
||||
behave.set(types, underResolution)
|
||||
this.resolve._genDepsOf.push([key, bhvix])
|
||||
behave.set(bhvix, underResolution)
|
||||
try { // Used to make sure not to return without popping _genDepsOf
|
||||
if (!('returns' in item)) {
|
||||
// looks like a factory
|
||||
|
@ -345,6 +363,7 @@ export class TypeDispatcher {
|
|||
matched(template, this))
|
||||
} catch (e) {
|
||||
e.message = `Error in factory for ${key} on ${types} `
|
||||
+ `with return typing ${ReturnTyping.name(strategy)} `
|
||||
+ `(match data ${template}): ${e.message}`
|
||||
throw e
|
||||
}
|
||||
|
@ -355,7 +374,8 @@ export class TypeDispatcher {
|
|||
const returning = theBehavior.returns
|
||||
if (!returning) {
|
||||
throw new TypeError(
|
||||
`No return type specified for ${key} on ${types}`)
|
||||
`No return type specified for ${key} on ${types} with`
|
||||
+ ` return typing ${ReturnTyping.name(strategy)}`)
|
||||
}
|
||||
if (needsCollection(template)) {
|
||||
// have to wrap the behavior to collect the actual arguments
|
||||
|
@ -369,7 +389,7 @@ export class TypeDispatcher {
|
|||
} finally {
|
||||
this.resolve._genDepsOf.pop() // OK, now it's safe to return
|
||||
}
|
||||
behave.set(types, finalBehavior)
|
||||
behave.set(bhvix, finalBehavior)
|
||||
finalBehavior.template = template
|
||||
return finalBehavior
|
||||
}
|
||||
|
@ -380,8 +400,8 @@ export class TypeDispatcher {
|
|||
_invalidate(depColl) {
|
||||
if (!depColl) return
|
||||
for (const [key, typeMap] of depColl) {
|
||||
for (const types of typeMap.keys()) {
|
||||
this._behaviors[key].delete(types)
|
||||
for (const bhvix of typeMap.keys()) {
|
||||
this._behaviors[key].delete(bhvix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -389,25 +409,26 @@ export class TypeDispatcher {
|
|||
_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 fallIxes = []
|
||||
const behs = this._behaviors[key]
|
||||
const imps = this._implementations[key]
|
||||
const deps = this._dependencies.get(key)
|
||||
for (const types of behs.keys()) {
|
||||
for (const bhvix of behs.keys()) {
|
||||
let fallsback = true
|
||||
for (const [pattern] of imps) {
|
||||
const types = bhvix.length ? bhvix.slice(1) : bhvix
|
||||
const [finalIndex] = pattern.match(types)
|
||||
if (finalIndex === types.length) {
|
||||
fallsback = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if (fallsback) fallTypes.push(types)
|
||||
if (fallsback) fallIxes.push(bhvix)
|
||||
}
|
||||
for (const types of fallTypes) {
|
||||
const depColl = deps?.get(types)
|
||||
for (const bhvix of fallIxes) {
|
||||
const depColl = deps?.get(bhvix)
|
||||
if (depColl?.size) this._invalidate(depColl)
|
||||
behs.delete(types)
|
||||
behs.delete(bhvix)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -415,20 +436,21 @@ export class TypeDispatcher {
|
|||
// like disengageFallback, just we have the offending pattern:
|
||||
const behs = this._behaviors[key]
|
||||
const deps = this._dependencies.get(key)
|
||||
const patTypes = behs.keys().filter(types => {
|
||||
const patIxes = behs.keys().filter(bhvix => {
|
||||
const types = bhvix.length ? bhvix.slice(1) : bhvix
|
||||
const [finalIndex] = pattern.match(types)
|
||||
return finalIndex === types.length
|
||||
})
|
||||
for (const types of patTypes) {
|
||||
const depColl = deps?.get(types)
|
||||
for (const bhvix of patIxes) {
|
||||
const depColl = deps?.get(bhvix)
|
||||
if (depColl?.size) this._invalidate(depColl)
|
||||
behs.delete(types)
|
||||
behs.delete(bhvix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Proxy that traps accesses and records dependencies on them
|
||||
const DependencyRecorder = (object, path, repo, types) => new Proxy(object, {
|
||||
const DependencyRecorder = (object, path, repo, bhvix) => new Proxy(object, {
|
||||
get(target, prop, receiver) {
|
||||
const result = Reflect.get(target, prop, receiver)
|
||||
// pass internal methods through, as well as resolve calls,
|
||||
|
@ -443,27 +465,27 @@ const DependencyRecorder = (object, path, repo, types) => new Proxy(object, {
|
|||
// 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 ? [path, prop].join('.') : prop
|
||||
repo._addToDeps(newPath, types)
|
||||
repo._addToDeps(newPath, bhvix)
|
||||
// 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, except for types, which must maintain
|
||||
// strict referential identity:
|
||||
if (typeof result === 'object' && !(result instanceof Type)) {
|
||||
return DependencyRecorder(result, newPath, repo, types)
|
||||
return DependencyRecorder(result, newPath, repo, bhvix)
|
||||
} else return result
|
||||
}
|
||||
})
|
||||
|
||||
// The flip side: proxy that traps setting properties and invalidates things
|
||||
// that depend on them:
|
||||
const DependencyWatcher = (object, path, typesList, repo) => new Proxy(object, {
|
||||
const DependencyWatcher = (object, path, ixList, repo) => new Proxy(object, {
|
||||
set(target, prop, value, receiver) {
|
||||
// First see if this setting has any dependencies:
|
||||
const newPath = [path, prop].join('.')
|
||||
const depPerTypes = repo._dependencies.get(newPath)
|
||||
if (depPerTypes && Reflect.get(target, prop, receiver) !== value) {
|
||||
for (const types of typesList) {
|
||||
repo._invalidate(depPerTypes.get(types))
|
||||
for (const bhvix of ixList) {
|
||||
repo._invalidate(depPerTypes.get(bhvix))
|
||||
}
|
||||
}
|
||||
// Now we can just perform the setting
|
||||
|
@ -474,7 +496,7 @@ const DependencyWatcher = (object, path, typesList, repo) => new Proxy(object, {
|
|||
const result = Reflect.get(target, prop, receiver)
|
||||
if (typeof result === 'object' && !(result instanceof Type)) {
|
||||
const newPath = [path, prop].join('.')
|
||||
return DependencyWatcher(result, newPath, typesList, repo)
|
||||
return DependencyWatcher(result, newPath, ixList, repo)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import {Type, Undefined} from './Type.js'
|
|||
import {isPlainFunction} from './helpers.js'
|
||||
|
||||
export class TypePattern {
|
||||
match(typeSequence, options={}) {
|
||||
match(_typeSequence, _options={}) {
|
||||
throw new Error('Specific TypePatterns must implement match')
|
||||
}
|
||||
sampleTypes() {
|
||||
|
|
|
@ -2,7 +2,7 @@ import assert from 'assert'
|
|||
import math from '#nanomath'
|
||||
import {NumberT} from '#number/NumberT.js'
|
||||
|
||||
import {Returns} from '../Type.js'
|
||||
import {Returns, ReturnTyping} from '../Type.js'
|
||||
import {isPlainFunction} from '../helpers.js'
|
||||
|
||||
describe('Core types', () => {
|
||||
|
@ -40,4 +40,7 @@ describe('Core types', () => {
|
|||
assert(isPlainFunction(labeledF))
|
||||
})
|
||||
|
||||
it('provides return typing strategies', () => {
|
||||
assert.strictEqual(ReturnTyping.name(ReturnTyping.full), 'full')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -6,7 +6,7 @@ import * as numbers from '#number/all.js'
|
|||
import {NumberT} from '#number/NumberT.js'
|
||||
import {ResolutionError} from "#core/helpers.js"
|
||||
import {match, Any} from "#core/TypePatterns.js"
|
||||
import {Returns, NotAType} from "#core/Type.js"
|
||||
import {NotAType, Returns, ReturnTyping} from "#core/Type.js"
|
||||
import {plain} from "#number/helpers.js"
|
||||
|
||||
describe('TypeDispatcher', () => {
|
||||
|
@ -57,12 +57,15 @@ describe('TypeDispatcher', () => {
|
|||
it('detects dependencies on conversion operations', () => {
|
||||
const bgn = new TypeDispatcher(booleans, generics, numbers)
|
||||
const {BooleanT, NumberT} = bgn.types
|
||||
assert(!bgn._behaviors.negate.has([BooleanT]))
|
||||
assert(!bgn._behaviors.negate.has([ReturnTyping.free, BooleanT]))
|
||||
assert.strictEqual(bgn.negate(true), -1)
|
||||
assert(bgn._behaviors.negate.has([BooleanT]))
|
||||
const deps = bgn._dependencies.negate
|
||||
assert(bgn._behaviors.negate.has([ReturnTyping.free, BooleanT]))
|
||||
const deps = bgn._dependencies
|
||||
.get('number')
|
||||
.get([ReturnTyping.free, BooleanT])
|
||||
assert(deps.has('negate'))
|
||||
bgn.merge({number: match([BooleanT], Returns(NumberT, b => b ? 2 : 0))})
|
||||
assert(!bgn._behaviors.negate.has([BooleanT]))
|
||||
assert(!bgn._behaviors.negate.has([ReturnTyping.free, BooleanT]))
|
||||
assert.strictEqual(bgn.negate(true), -2)
|
||||
})
|
||||
it('disallows merging NotAType', () => {
|
||||
|
|
11
src/core/config.js
Normal file
11
src/core/config.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import {ImplementationsGenerator} from './Implementations.js'
|
||||
import {ReturnTyping} from './Type.js'
|
||||
import {match, Passthru} from './TypePatterns.js'
|
||||
|
||||
export const config = new ImplementationsGenerator(() => match(Passthru, {
|
||||
// default comparison tolerances:
|
||||
relTol: 1e-12,
|
||||
absTol: 1e-15,
|
||||
// Strategy for choosing operation return types:
|
||||
returnTyping: ReturnTyping.free,
|
||||
}))
|
|
@ -1,3 +1,4 @@
|
|||
import {config} from './config.js'
|
||||
import {ImplementationsGenerator} from './Implementations.js'
|
||||
import {Type, TypeOfTypes, Undefined, whichType} from './Type.js'
|
||||
import {match, Passthru} from './TypePatterns.js'
|
||||
|
@ -22,5 +23,5 @@ export const types = new ImplementationsGenerator(() => match(Passthru, {}))
|
|||
// an explicitly ordered export of implementations for this sake:
|
||||
|
||||
export const bootstrapTypes = {
|
||||
types, Type, Undefined, TypeOfTypes, typeOf
|
||||
types, config, Type, Undefined, TypeOfTypes, typeOf
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
export * as arithmetic from './arithmetic.js'
|
||||
export * as configuration from './config.js'
|
||||
export * as relational from './relational.js'
|
||||
export * as utilities from './utils.js'
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import {ImplementationsGenerator} from '#core/Implementations.js'
|
||||
import {match, Passthru} from '#core/TypePatterns.js'
|
||||
|
||||
export const config = new ImplementationsGenerator(
|
||||
() => match(Passthru, {relTol: 1e-12, absTol: 1e-15}))
|
|
@ -1,4 +1,3 @@
|
|||
import {Returns} from '#core/Type.js'
|
||||
import {match, Optional} from '#core/TypePatterns.js'
|
||||
import {boolnum} from './helpers.js'
|
||||
import {NumberT} from './NumberT.js'
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import {plain, boolnum} from './helpers.js'
|
||||
import {NumberT} from './NumberT.js'
|
||||
|
||||
import {Returns} from '#core/Type.js'
|
||||
import {match} from '#core/TypePatterns.js'
|
||||
|
||||
export const clone = plain(a => a)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue